From 0d174a3fe6d377a06ed8fcc661abecd40e82564b Mon Sep 17 00:00:00 2001 From: "Raziel K. Crowe" <84860158+CWDSYSTEMS@users.noreply.github.com> Date: Sat, 5 Mar 2022 22:41:29 +0500 Subject: [PATCH] Plasma desktop workspace tweaks --- plasma/workspace/.kde-ci.yml | 64 + plasma/workspace/CMakeLists.txt | 298 ++ plasma/workspace/ConfigureChecks.cmake | 9 + plasma/workspace/ExtraDesktop.sh | 4 + plasma/workspace/LICENSES/BSD-2-Clause.txt | 22 + plasma/workspace/LICENSES/BSD-3-Clause.txt | 26 + plasma/workspace/LICENSES/CC0-1.0.txt | 121 + plasma/workspace/LICENSES/GPL-2.0-only.txt | 311 +++ .../workspace/LICENSES/GPL-2.0-or-later.txt | 311 +++ plasma/workspace/LICENSES/GPL-3.0-only.txt | 604 +++++ plasma/workspace/LICENSES/LGPL-2.0-only.txt | 444 +++ .../workspace/LICENSES/LGPL-2.0-or-later.txt | 444 +++ plasma/workspace/LICENSES/LGPL-2.1-only.txt | 462 ++++ .../workspace/LICENSES/LGPL-2.1-or-later.txt | 462 ++++ plasma/workspace/LICENSES/LGPL-3.0-only.txt | 144 + .../workspace/LICENSES/LGPL-3.0-or-later.txt | 71 + .../LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + .../LICENSES/LicenseRef-KDE-Accepted-LGPL.txt | 12 + plasma/workspace/LICENSES/MIT.txt | 20 + plasma/workspace/applets/CMakeLists.txt | 25 + plasma/workspace/applets/Mainpage.dox | 10 + .../workspace/applets/activitybar/Messages.sh | 2 + .../applets/activitybar/contents/ui/main.qml | 80 + .../applets/activitybar/metadata.json | 166 ++ .../applets/analog-clock/Messages.sh | 2 + .../analog-clock/contents/config/config.qml | 17 + .../analog-clock/contents/config/main.xml | 17 + .../applets/analog-clock/contents/ui/Hand.qml | 67 + .../analog-clock/contents/ui/analogclock.qml | 229 ++ .../contents/ui/configGeneral.qml | 30 + .../applets/analog-clock/metadata.json | 181 ++ .../workspace/applets/appmenu/CMakeLists.txt | 6 + plasma/workspace/applets/appmenu/Messages.sh | 2 + .../applets/appmenu/lib/CMakeLists.txt | 9 + .../applets/appmenu/lib/appmenuapplet.cpp | 292 ++ .../applets/appmenu/lib/appmenuapplet.h | 74 + .../package/contents/config/config.qml | 17 + .../appmenu/package/contents/config/main.xml | 15 + .../package/contents/ui/MenuDelegate.qml | 70 + .../package/contents/ui/configGeneral.qml | 30 + .../appmenu/package/contents/ui/main.qml | 145 + .../applets/appmenu/package/metadata.json | 136 + .../applets/appmenu/plugin/CMakeLists.txt | 19 + .../applets/appmenu/plugin/appmenumodel.cpp | 327 +++ .../applets/appmenu/plugin/appmenumodel.h | 95 + .../applets/appmenu/plugin/appmenuplugin.cpp | 15 + .../applets/appmenu/plugin/appmenuplugin.h | 18 + .../workspace/applets/appmenu/plugin/qmldir | 3 + .../applets/batterymonitor/CMakeLists.txt | 1 + .../applets/batterymonitor/Messages.sh | 2 + .../applets/batterymonitor/README.txt | 32 + .../package/contents/config/main.xml | 16 + .../package/contents/ui/BadgeOverlay.qml | 38 + .../package/contents/ui/BatteryItem.qml | 199 ++ .../package/contents/ui/BrightnessItem.qml | 69 + .../contents/ui/CompactRepresentation.qml | 81 + .../package/contents/ui/InhibitionHint.qml | 34 + .../package/contents/ui/PopupDialog.qml | 170 ++ .../contents/ui/PowerManagementItem.qml | 104 + .../package/contents/ui/PowerProfileItem.qml | 190 ++ .../package/contents/ui/logic.js | 57 + .../package/contents/ui/main.qml | 348 +++ .../batterymonitor/package/metadata.json | 202 ++ .../workspace/applets/calendar/CMakeLists.txt | 6 + plasma/workspace/applets/calendar/Messages.sh | 2 + .../applets/calendar/calendarapplet.cpp | 27 + .../applets/calendar/calendarapplet.h | 22 + .../package/contents/config/config.qml | 17 + .../calendar/package/contents/config/main.xml | 22 + .../contents/images/mini-calendar.svgz | Bin 0 -> 3859 bytes .../package/contents/ui/configGeneral.qml | 49 + .../calendar/package/contents/ui/main.qml | 119 + .../applets/calendar/package/metadata.json | 171 ++ .../workspace/applets/clipboard/Messages.sh | 2 + .../clipboard/contents/ui/BarcodePage.qml | 122 + .../contents/ui/ClipboardItemDelegate.qml | 137 + .../clipboard/contents/ui/ClipboardPage.qml | 202 ++ .../contents/ui/DelegateToolButtons.qml | 60 + .../clipboard/contents/ui/EditPage.qml | 87 + .../contents/ui/ImageItemDelegate.qml | 30 + .../applets/clipboard/contents/ui/Menu.qml | 82 + .../contents/ui/TextItemDelegate.qml | 43 + .../clipboard/contents/ui/UrlItemDelegate.qml | 111 + .../clipboard/contents/ui/clipboard.qml | 112 + .../workspace/applets/clipboard/metadata.json | 144 + .../applets/devicenotifier/CMakeLists.txt | 3 + .../applets/devicenotifier/Messages.sh | 2 + .../package/contents/config/main.xml | 27 + .../package/contents/ui/DeviceItem.qml | 283 ++ .../contents/ui/FullRepresentation.qml | 173 ++ .../package/contents/ui/devicenotifier.qml | 347 +++ .../devicenotifier/package/metadata.json | 133 + .../test-predicate-openinwindow.desktop | 78 + .../applets/digital-clock/CMakeLists.txt | 3 + .../applets/digital-clock/Messages.sh | 2 + .../package/contents/config/config.qml | 45 + .../package/contents/config/main.xml | 95 + .../package/contents/ui/CalendarView.qml | 633 +++++ .../package/contents/ui/DigitalClock.qml | 720 +++++ .../package/contents/ui/MonthMenu.qml | 9 + .../package/contents/ui/Tooltip.qml | 93 + .../package/contents/ui/configAppearance.qml | 288 ++ .../package/contents/ui/configCalendar.qml | 90 + .../package/contents/ui/configTimeZones.qml | 235 ++ .../package/contents/ui/main.qml | 136 + .../digital-clock/package/metadata.json | 179 ++ .../digital-clock/plugin/CMakeLists.txt | 31 + .../plugin/applicationintegration.cpp | 33 + .../plugin/applicationintegration.h | 22 + .../digital-clock/plugin/clipboardmenu.cpp | 148 + .../digital-clock/plugin/clipboardmenu.h | 40 + .../plugin/digitalclockplugin.cpp | 46 + .../digital-clock/plugin/digitalclockplugin.h | 18 + .../applets/digital-clock/plugin/qmldir | 3 + .../digital-clock/plugin/timezonedata.h | 21 + .../digital-clock/plugin/timezonemodel.cpp | 235 ++ .../digital-clock/plugin/timezonemodel.h | 82 + .../digital-clock/plugin/timezonesi18n.cpp | 307 +++ .../digital-clock/plugin/timezonesi18n.h | 30 + .../plugin/timezonesi18n_generate.rb | 113 + .../plugin/timezonesi18n_generated.h | 547 ++++ .../plugin/timezonesi18n_generated.h.erb | 31 + plasma/workspace/applets/icon/CMakeLists.txt | 14 + plasma/workspace/applets/icon/Messages.sh | 2 + plasma/workspace/applets/icon/iconapplet.cpp | 580 ++++ plasma/workspace/applets/icon/iconapplet.h | 95 + .../icon/package/contents/config/main.xml | 15 + .../applets/icon/package/contents/ui/main.qml | 169 ++ .../applets/icon/package/metadata.json | 172 ++ .../workspace/applets/kicker/CMakeLists.txt | 88 + plasma/workspace/applets/kicker/Messages.sh | 2 + .../applets/kicker/plugin/abstractentry.cpp | 96 + .../applets/kicker/plugin/abstractentry.h | 73 + .../applets/kicker/plugin/abstractmodel.cpp | 146 + .../applets/kicker/plugin/abstractmodel.h | 68 + .../applets/kicker/plugin/actionlist.cpp | 466 ++++ .../applets/kicker/plugin/actionlist.h | 61 + .../applets/kicker/plugin/appentry.cpp | 334 +++ .../applets/kicker/plugin/appentry.h | 83 + .../applets/kicker/plugin/appsmodel.cpp | 717 +++++ .../applets/kicker/plugin/appsmodel.h | 143 + .../kicker/plugin/autotests/CMakeLists.txt | 29 + .../kicker/plugin/autotests/qmltest.cpp | 8 + .../plugin/autotests/tst_triangleFilter.qml | 70 + .../applets/kicker/plugin/computermodel.cpp | 287 ++ .../applets/kicker/plugin/computermodel.h | 104 + .../applets/kicker/plugin/contactentry.cpp | 134 + .../applets/kicker/plugin/contactentry.h | 43 + .../kicker/plugin/containmentinterface.cpp | 239 ++ .../kicker/plugin/containmentinterface.h | 50 + .../applets/kicker/plugin/dashboardwindow.cpp | 231 ++ .../applets/kicker/plugin/dashboardwindow.h | 65 + .../applets/kicker/plugin/draghelper.cpp | 104 + .../applets/kicker/plugin/draghelper.h | 53 + .../applets/kicker/plugin/fileentry.cpp | 112 + .../applets/kicker/plugin/fileentry.h | 40 + .../applets/kicker/plugin/forwardingmodel.cpp | 226 ++ .../applets/kicker/plugin/forwardingmodel.h | 61 + .../applets/kicker/plugin/funnelmodel.cpp | 86 + .../applets/kicker/plugin/funnelmodel.h | 20 + .../kicker/plugin/kastatsfavoritesmodel.cpp | 682 +++++ .../kicker/plugin/kastatsfavoritesmodel.h | 102 + .../applets/kicker/plugin/kickerplugin.cpp | 52 + .../applets/kicker/plugin/kickerplugin.h | 19 + .../applets/kicker/plugin/menuentryeditor.cpp | 55 + .../applets/kicker/plugin/menuentryeditor.h | 23 + .../kicker/plugin/placeholdermodel.cpp | 358 +++ .../applets/kicker/plugin/placeholdermodel.h | 78 + .../applets/kicker/plugin/processrunner.cpp | 23 + .../applets/kicker/plugin/processrunner.h | 20 + plasma/workspace/applets/kicker/plugin/qmldir | 2 + .../kicker/plugin/recentcontactsmodel.cpp | 232 ++ .../kicker/plugin/recentcontactsmodel.h | 44 + .../kicker/plugin/recentusagemodel.cpp | 564 ++++ .../applets/kicker/plugin/recentusagemodel.h | 117 + .../applets/kicker/plugin/rootmodel.cpp | 466 ++++ .../applets/kicker/plugin/rootmodel.h | 116 + .../kicker/plugin/runnermatchesmodel.cpp | 256 ++ .../kicker/plugin/runnermatchesmodel.h | 54 + .../applets/kicker/plugin/runnermodel.cpp | 352 +++ .../applets/kicker/plugin/runnermodel.h | 93 + .../kicker/plugin/simplefavoritesmodel.cpp | 323 +++ .../kicker/plugin/simplefavoritesmodel.h | 73 + .../applets/kicker/plugin/submenu.cpp | 87 + .../workspace/applets/kicker/plugin/submenu.h | 42 + .../applets/kicker/plugin/systementry.cpp | 366 +++ .../applets/kicker/plugin/systementry.h | 69 + .../applets/kicker/plugin/systemmodel.cpp | 110 + .../applets/kicker/plugin/systemmodel.h | 35 + .../applets/kicker/plugin/systemsettings.cpp | 34 + .../applets/kicker/plugin/systemsettings.h | 20 + .../kicker/plugin/trianglemousefilter.cpp | 132 + .../kicker/plugin/trianglemousefilter.h | 69 + .../kicker/plugin/wheelinterceptor.cpp | 60 + .../applets/kicker/plugin/wheelinterceptor.h | 36 + .../applets/kicker/plugin/windowsystem.cpp | 77 + .../applets/kicker/plugin/windowsystem.h | 37 + .../workspace/applets/lock_logout/Messages.sh | 2 + .../lock_logout/contents/config/config.qml | 17 + .../lock_logout/contents/config/main.xml | 39 + .../lock_logout/contents/ui/ConfigGeneral.qml | 71 + .../applets/lock_logout/contents/ui/data.js | 49 + .../lock_logout/contents/ui/lockout.qml | 127 + .../applets/lock_logout/metadata.json | 225 ++ .../applets/manage-inputmethod/Messages.sh | 2 + .../contents/ui/manage-inputmethod.qml | 98 + .../applets/manage-inputmethod/metadata.json | 128 + .../applets/mediacontroller/Messages.sh | 2 + .../contents/ui/ExpandedRepresentation.qml | 604 +++++ .../mediacontroller/contents/ui/main.qml | 333 +++ .../applets/mediacontroller/metadata.json | 151 ++ .../applets/notifications/CMakeLists.txt | 32 + .../applets/notifications/Messages.sh | 2 + .../applets/notifications/fileinfo.cpp | 187 ++ .../applets/notifications/fileinfo.h | 82 + .../applets/notifications/filemenu.cpp | 233 ++ .../applets/notifications/filemenu.h | 49 + .../applets/notifications/globalshortcuts.cpp | 48 + .../applets/notifications/globalshortcuts.h | 28 + .../notifications/notificationapplet.cpp | 181 ++ .../notifications/notificationapplet.h | 63 + .../contents/ui/CompactRepresentation.qml | 195 ++ .../package/contents/ui/DraggableDelegate.qml | 42 + .../package/contents/ui/DraggableFileArea.qml | 58 + .../package/contents/ui/EditContextMenu.qml | 65 + .../contents/ui/FullRepresentation.qml | 591 ++++ .../package/contents/ui/JobDetails.qml | 151 ++ .../package/contents/ui/JobItem.qml | 302 +++ .../contents/ui/NotificationHeader.qml | 252 ++ .../package/contents/ui/NotificationItem.qml | 458 ++++ .../package/contents/ui/NotificationPopup.qml | 238 ++ .../contents/ui/NotificationReplyField.qml | 60 + .../package/contents/ui/SelectableLabel.qml | 113 + .../package/contents/ui/ThumbnailStrip.qml | 168 ++ .../package/contents/ui/global/Globals.qml | 692 +++++ .../package/contents/ui/global/PulseAudio.qml | 39 + .../package/contents/ui/global/qmldir | 1 + .../package/contents/ui/main.qml | 180 ++ .../notifications/package/metadata.json | 182 ++ .../notifications/texteditclickhandler.cpp | 58 + .../notifications/texteditclickhandler.h | 44 + .../applets/notifications/thumbnailer.cpp | 157 ++ .../applets/notifications/thumbnailer.h | 79 + .../applets/panelspacer/CMakeLists.txt | 5 + .../workspace/applets/panelspacer/Messages.sh | 2 + .../package/contents/config/main.xml | 19 + .../panelspacer/package/contents/ui/main.qml | 144 + .../applets/panelspacer/package/metadata.json | 154 ++ .../applets/panelspacer/plugin/CMakeLists.txt | 7 + .../panelspacer/plugin/panelspacer.cpp | 131 + .../applets/panelspacer/plugin/panelspacer.h | 59 + .../applets/systemmonitor/CMakeLists.txt | 10 + .../coreusage/contents/config/faceproperties | 10 + .../systemmonitor/coreusage/metadata.json | 127 + .../cpu/contents/config/faceproperties | 11 + .../applets/systemmonitor/cpu/metadata.json | 127 + .../contents/config/faceproperties | 8 + .../systemmonitor/diskactivity/metadata.json | 134 + .../diskusage/contents/config/faceproperties | 11 + .../systemmonitor/diskusage/metadata.json | 130 + .../memory/contents/config/faceproperties | 11 + .../systemmonitor/memory/metadata.json | 130 + .../net/contents/config/faceproperties | 7 + .../applets/systemmonitor/net/metadata.json | 130 + .../systemmonitor/CMakeLists.txt | 20 + .../systemmonitor/systemmonitor/Messages.sh | 2 + .../package/contents/config/config.qml | 25 + .../package/contents/config/main.xml | 25 + .../contents/ui/CompactRepresentation.qml | 68 + .../contents/ui/FullRepresentation.qml | 72 + .../contents/ui/config/ConfigAppearance.qml | 41 + .../contents/ui/config/ConfigSensors.qml | 41 + .../contents/ui/config/FaceDetails.qml | 34 + .../package/contents/ui/main.qml | 57 + .../systemmonitor/package/metadata.json | 128 + .../systemmonitor/systemmonitor.cpp | 92 + .../systemmonitor/systemmonitor.h | 52 + .../applets/systemtray/CMakeLists.txt | 56 + .../workspace/applets/systemtray/Messages.sh | 2 + .../systemtray/autotests/CMakeLists.txt | 6 + .../data/devicenotifier/metadata.json | 31 + .../data/mediacontroller/metadata.json | 33 + .../autotests/systemtraymodeltest.cpp | 186 ++ .../systemtray/container/CMakeLists.txt | 20 + .../container/package/contents/ui/main.qml | 57 + .../container/package/metadata.json | 178 ++ .../container/systemtraycontainer.cpp | 145 + .../container/systemtraycontainer.h | 41 + .../systemtray/dbusserviceobserver.cpp | 175 ++ .../applets/systemtray/dbusserviceobserver.h | 54 + .../package/contents/applet/CompactApplet.qml | 70 + .../package/contents/config/config.qml | 22 + .../package/contents/config/main.xml | 43 + .../package/contents/ui/ConfigEntries.qml | 284 ++ .../package/contents/ui/ConfigGeneral.qml | 85 + .../contents/ui/CurrentItemHighLight.qml | 175 ++ .../contents/ui/ExpandedRepresentation.qml | 211 ++ .../package/contents/ui/ExpanderArrow.qml | 118 + .../package/contents/ui/HiddenItemsView.qml | 89 + .../contents/ui/PlasmoidPopupsContainer.qml | 145 + .../package/contents/ui/SystemTrayState.qml | 88 + .../contents/ui/items/AbstractItem.qml | 202 ++ .../package/contents/ui/items/ItemLoader.qml | 29 + .../contents/ui/items/PlasmoidItem.qml | 137 + .../contents/ui/items/PulseAnimation.qml | 40 + .../contents/ui/items/StatusNotifierItem.qml | 108 + .../systemtray/package/contents/ui/main.qml | 279 ++ .../applets/systemtray/package/metadata.json | 148 + .../applets/systemtray/plasmoidregistry.cpp | 167 ++ .../applets/systemtray/plasmoidregistry.h | 79 + .../systemtray/sortedsystemtraymodel.cpp | 101 + .../systemtray/sortedsystemtraymodel.h | 33 + .../systemtray/statusnotifieritemhost.cpp | 189 ++ .../systemtray/statusnotifieritemhost.h | 51 + .../systemtray/statusnotifieritemjob.cpp | 72 + .../systemtray/statusnotifieritemjob.h | 40 + .../systemtray/statusnotifieritemservice.cpp | 27 + .../systemtray/statusnotifieritemservice.h | 33 + .../systemtray/statusnotifieritemsource.cpp | 552 ++++ .../systemtray/statusnotifieritemsource.h | 97 + .../applets/systemtray/systemtray.cpp | 387 +++ .../workspace/applets/systemtray/systemtray.h | 98 + .../applets/systemtray/systemtraymodel.cpp | 476 ++++ .../applets/systemtray/systemtraymodel.h | 168 ++ .../applets/systemtray/systemtraysettings.cpp | 164 ++ .../applets/systemtray/systemtraysettings.h | 54 + .../applets/systemtray/systemtraytypes.cpp | 114 + .../applets/systemtray/systemtraytypes.h | 20 + .../applets/systemtray/tests/CMakeLists.txt | 1 + .../tests/statusnotifier/CMakeLists.txt | 23 + .../systemtray/tests/statusnotifier/main.cpp | 31 + .../tests/statusnotifier/pumpjob.cpp | 125 + .../systemtray/tests/statusnotifier/pumpjob.h | 40 + .../statusnotifier/statusnotifiertest.cpp | 240 ++ .../tests/statusnotifier/statusnotifiertest.h | 45 + .../statusnotifier/statusnotifiertest.ui | 283 ++ plasma/workspace/appmenu/CMakeLists.txt | 45 + plasma/workspace/appmenu/appmenu.cpp | 263 ++ plasma/workspace/appmenu/appmenu.desktop | 117 + plasma/workspace/appmenu/appmenu.h | 94 + plasma/workspace/appmenu/appmenu.json | 95 + plasma/workspace/appmenu/appmenu_dbus.cpp | 50 + plasma/workspace/appmenu/appmenu_dbus.h | 66 + .../com.canonical.AppMenu.Registrar.xml | 56 + plasma/workspace/appmenu/kdbusimporter.h | 33 + plasma/workspace/appmenu/menuimporter.cpp | 99 + plasma/workspace/appmenu/menuimporter.h | 71 + plasma/workspace/appmenu/org.kde.kappmenu.xml | 28 + plasma/workspace/appmenu/verticalmenu.cpp | 22 + plasma/workspace/appmenu/verticalmenu.h | 41 + .../cmake/FindAppMenuGtkModule.cmake | 41 + plasma/workspace/cmake/FindKIOExtras.cmake | 8 + plasma/workspace/cmake/FindKIOFuse.cmake | 8 + plasma/workspace/cmake/FindLibdrm.cmake | 105 + plasma/workspace/cmake/FindQalculate.cmake | 79 + plasma/workspace/components/CMakeLists.txt | 9 + plasma/workspace/components/Messages.sh | 4 + .../containmentlayoutmanager/CMakeLists.txt | 32 + .../abstractlayoutmanager.cpp | 134 + .../abstractlayoutmanager.h | 123 + .../appletcontainer.cpp | 155 ++ .../appletcontainer.h | 60 + .../appletslayout.cpp | 755 ++++++ .../containmentlayoutmanager/appletslayout.h | 231 ++ .../configoverlay.cpp | 119 + .../containmentlayoutmanager/configoverlay.h | 79 + .../containmentlayoutmanagerplugin.cpp | 31 + .../containmentlayoutmanagerplugin.h | 21 + .../gridlayoutmanager.cpp | 552 ++++ .../gridlayoutmanager.h | 98 + .../itemcontainer.cpp | 768 ++++++ .../containmentlayoutmanager/itemcontainer.h | 233 ++ .../qml/BasicAppletContainer.qml | 196 ++ .../qml/ConfigOverlayWithHandles.qml | 152 ++ .../qml/PlaceHolder.qml | 26 + .../qml/private/BasicResizeHandle.qml | 57 + .../containmentlayoutmanager/qml/qmldir | 7 + .../containmentlayoutmanager/resizehandle.cpp | 248 ++ .../containmentlayoutmanager/resizehandle.h | 69 + .../components/dialogs/SystemDialog.qml | 138 + .../components/dialogs/examples/test.qml | 360 +++ plasma/workspace/components/dialogs/qmldir | 3 + .../components/keyboardlayout/CMakeLists.txt | 29 + .../keyboardlayout/keyboardlayout.cpp | 88 + .../keyboardlayout/keyboardlayout.h | 53 + .../keyboardlayout/keyboardlayoutplugin.cpp | 21 + .../keyboardlayout/keyboardlayoutplugin.h | 18 + .../components/keyboardlayout/layoutnames.cpp | 24 + .../components/keyboardlayout/layoutnames.h | 25 + .../org.kde.KeyboardLayouts.xml | 26 + .../components/keyboardlayout/qmldir | 3 + .../keyboardlayout/virtualkeyboard.cpp | 12 + .../keyboardlayout/virtualkeyboard.h | 20 + .../components/lookandfeelqml/CMakeLists.txt | 15 + .../lookandfeelqml/kpackageinterface.cpp | 18 + .../lookandfeelqml/kpackageinterface.h | 22 + .../lookandfeelqml/lookandfeelqmlplugin.cpp | 25 + .../lookandfeelqml/lookandfeelqmlplugin.h | 18 + .../components/lookandfeelqml/qmldir | 2 + .../components/sessionsprivate/CMakeLists.txt | 27 + .../kscreenlockersettings.kcfg | 47 + .../kscreensaversettings.kcfgc | 4 + .../components/sessionsprivate/qmldir | 2 + .../sessionsprivate/sessionsmodel.cpp | 291 ++ .../sessionsprivate/sessionsmodel.h | 105 + .../sessionsprivate/sessionsprivateplugin.cpp | 20 + .../sessionsprivate/sessionsprivateplugin.h | 18 + .../components/shellprivate/CMakeLists.txt | 41 + .../shellprivate/config-shellprivate.h.cmake | 1 + .../workspace/components/shellprivate/qmldir | 2 + .../shellprivate/shellprivateplugin.cpp | 21 + .../shellprivate/shellprivateplugin.h | 18 + .../shellprivate/wallpaperplugin.knsrc | 49 + .../kcategorizeditemsviewmodels.cpp | 227 ++ .../kcategorizeditemsviewmodels_p.h | 168 ++ .../widgetexplorer/openwidgetassistant.cpp | 72 + .../widgetexplorer/openwidgetassistant_p.h | 33 + .../widgetexplorer/plasmaappletitemmodel.cpp | 420 +++ .../widgetexplorer/plasmaappletitemmodel_p.h | 104 + .../widgetexplorer/plasmoids.knsrc | 57 + .../widgetexplorer/widgetexplorer.cpp | 517 ++++ .../widgetexplorer/widgetexplorer.h | 159 ++ .../workspace/components/tests/sessions.qml | 15 + .../components/workspace/BatteryIcon.qml | 125 + .../workspace/KeyboardLayoutSwitcher.qml | 38 + plasma/workspace/components/workspace/qmldir | 4 + plasma/workspace/config-X11.h.cmake | 8 + plasma/workspace/config-appstream.h.cmake | 1 + plasma/workspace/config-unix.h.cmake | 3 + plasma/workspace/config-workspace.h.cmake | 14 + .../containmentactions/CMakeLists.txt | 6 + .../applauncher/CMakeLists.txt | 10 + .../applauncher/Messages.sh | 3 + .../containmentactions/applauncher/config.ui | 25 + .../containmentactions/applauncher/launch.cpp | 111 + .../containmentactions/applauncher/launch.h | 46 + ...plasma-containmentactions-applauncher.json | 164 ++ .../contextmenu/CMakeLists.txt | 25 + .../contextmenu/Messages.sh | 2 + .../containmentactions/contextmenu/menu.cpp | 336 +++ .../containmentactions/contextmenu/menu.h | 49 + ...plasma-containmentactions-contextmenu.json | 150 + .../containmentactions/paste/CMakeLists.txt | 13 + .../containmentactions/paste/paste.cpp | 63 + .../containmentactions/paste/paste.h | 26 + .../plasma-containmentactions-paste.json | 149 + .../switchactivity/CMakeLists.txt | 8 + .../switchactivity/Messages.sh | 2 + ...sma-containmentactions-switchactivity.json | 146 + .../switchactivity/switch.cpp | 91 + .../switchactivity/switch.h | 36 + .../switchdesktop/CMakeLists.txt | 8 + .../switchdesktop/Messages.sh | 2 + .../switchdesktop/desktop.cpp | 117 + .../switchdesktop/desktop.h | 37 + ...asma-containmentactions-switchdesktop.json | 148 + .../switchwindow/CMakeLists.txt | 14 + .../switchwindow/Messages.sh | 3 + .../containmentactions/switchwindow/config.ui | 38 + ...lasma-containmentactions-switchwindow.json | 146 + .../switchwindow/switch.cpp | 267 ++ .../containmentactions/switchwindow/switch.h | 61 + plasma/workspace/dataengines/CMakeLists.txt | 31 + plasma/workspace/dataengines/Mainpage.dox | 10 + .../dataengines/activities/ActivityData.cpp | 74 + .../dataengines/activities/ActivityData.h | 32 + .../dataengines/activities/CMakeLists.txt | 28 + .../activities/activities.operations | 38 + .../dataengines/activities/activityengine.cpp | 245 ++ .../dataengines/activities/activityengine.h | 63 + .../dataengines/activities/activityjob.cpp | 87 + .../dataengines/activities/activityjob.h | 35 + .../activities/activityservice.cpp | 20 + .../dataengines/activities/activityservice.h | 32 + ...rg.kde.ActivityManager.ActivityRanking.xml | 24 + .../plasma-dataengine-activities.json | 143 + .../applicationjobs/CMakeLists.txt | 21 + .../dataengines/applicationjobs/Messages.sh | 2 + .../applicationjobs.operations | 8 + .../dataengines/applicationjobs/jobaction.cpp | 34 + .../dataengines/applicationjobs/jobaction.h | 37 + .../applicationjobs/jobcontrol.cpp | 24 + .../dataengines/applicationjobs/jobcontrol.h | 27 + .../applicationjobs/kuiserverengine.cpp | 262 ++ .../applicationjobs/kuiserverengine.h | 77 + .../plasma-dataengine-applicationjobs.json | 117 + .../workspace/dataengines/apps/CMakeLists.txt | 18 + plasma/workspace/dataengines/apps/appjob.cpp | 31 + plasma/workspace/dataengines/apps/appjob.h | 28 + .../dataengines/apps/apps.operations | 5 + .../workspace/dataengines/apps/appsengine.cpp | 76 + .../workspace/dataengines/apps/appsengine.h | 47 + .../workspace/dataengines/apps/appservice.cpp | 26 + .../workspace/dataengines/apps/appservice.h | 32 + .../workspace/dataengines/apps/appsource.cpp | 93 + plasma/workspace/dataengines/apps/appsource.h | 42 + .../apps/plasma-dataengine-apps.json | 147 + .../devicenotifications/CMakeLists.txt | 20 + .../devicenotifications/Messages.sh | 2 + .../devicenotifications.notifyrc | 121 + .../devicenotificationsengine.cpp | 44 + .../devicenotificationsengine.h | 31 + .../devicenotifications/ksolidnotify.cpp | 238 ++ .../devicenotifications/ksolidnotify.h | 57 + ...plasma-dataengine-devicenotifications.json | 142 + .../workspace/dataengines/dict/CMakeLists.txt | 16 + plasma/workspace/dataengines/dict/Messages.sh | 2 + plasma/workspace/dataengines/dict/buggywords | 1 + .../workspace/dataengines/dict/dictengine.cpp | 250 ++ .../workspace/dataengines/dict/dictengine.h | 46 + .../dict/plasma-dataengine-dict.json | 128 + .../dataengines/executable/CMakeLists.txt | 11 + .../dataengines/executable/executable.cpp | 65 + .../dataengines/executable/executable.h | 36 + .../plasma-dataengine-executable.json | 123 + .../dataengines/favicons/CMakeLists.txt | 11 + .../dataengines/favicons/faviconprovider.cpp | 88 + .../dataengines/favicons/faviconprovider.h | 70 + .../dataengines/favicons/favicons.cpp | 58 + .../workspace/dataengines/favicons/favicons.h | 37 + .../favicons/plasma-dataengine-favicons.json | 121 + .../dataengines/filebrowser/CMakeLists.txt | 10 + .../filebrowser/filebrowserengine.cpp | 148 + .../filebrowser/filebrowserengine.h | 51 + .../plasma-dataengine-filebrowser.json | 124 + .../dataengines/geolocation/CMakeLists.txt | 47 + .../dataengines/geolocation/geolocation.cpp | 137 + .../dataengines/geolocation/geolocation.h | 44 + .../geolocation/geolocationprovider.cpp | 123 + .../geolocation/geolocationprovider.h | 66 + .../dataengines/geolocation/location_gps.cpp | 116 + .../dataengines/geolocation/location_gps.h | 52 + .../dataengines/geolocation/location_ip.cpp | 195 ++ .../dataengines/geolocation/location_ip.h | 23 + .../plasma-dataengine-geolocation.json | 145 + .../geolocation/plasma-geolocation-gps.json | 110 + .../geolocation/plasma-geolocation-ip.json | 110 + .../dataengines/hotplug/CMakeLists.txt | 23 + .../workspace/dataengines/hotplug/Messages.sh | 2 + .../dataengines/hotplug/deviceaction.cpp | 36 + .../dataengines/hotplug/deviceaction.h | 31 + .../hotplug/deviceserviceaction.cpp | 157 ++ .../dataengines/hotplug/deviceserviceaction.h | 27 + .../dataengines/hotplug/hotplug.operations | 10 + .../dataengines/hotplug/hotplugengine.cpp | 272 ++ .../dataengines/hotplug/hotplugengine.h | 51 + .../dataengines/hotplug/hotplugjob.cpp | 42 + .../dataengines/hotplug/hotplugjob.h | 30 + .../dataengines/hotplug/hotplugservice.cpp | 22 + .../dataengines/hotplug/hotplugservice.h | 26 + .../hotplug/plasma-dataengine-hotplug.json | 149 + .../dataengines/keystate/CMakeLists.txt | 18 + .../dataengines/keystate/Messages.sh | 2 + .../dataengines/keystate/keyservice.cpp | 65 + .../dataengines/keystate/keyservice.h | 53 + .../dataengines/keystate/keystate.cpp | 129 + .../workspace/dataengines/keystate/keystate.h | 44 + .../keystate/modifierkeystate.operations | 15 + .../keystate/plasma-dataengine-keystate.json | 150 + .../dataengines/mouse/CMakeLists.txt | 22 + .../mouse/cursornotificationhandler.cpp | 90 + .../mouse/cursornotificationhandler.h | 39 + .../dataengines/mouse/mouseengine.cpp | 82 + .../workspace/dataengines/mouse/mouseengine.h | 43 + .../mouse/plasma-dataengine-mouse.json | 120 + .../dataengines/mpris2/CMakeLists.txt | 39 + .../workspace/dataengines/mpris2/Messages.sh | 2 + plasma/workspace/dataengines/mpris2/TODO | 2 + .../dataengines/mpris2/mpris2.operations | 58 + .../dataengines/mpris2/mpris2engine.cpp | 186 ++ .../dataengines/mpris2/mpris2engine.h | 45 + .../dataengines/mpris2/multiplexedservice.cpp | 149 + .../dataengines/mpris2/multiplexedservice.h | 37 + .../dataengines/mpris2/multiplexer.cpp | 234 ++ .../dataengines/mpris2/multiplexer.h | 53 + .../org.freedesktop.DBus.Properties.xml | 27 + .../mpris2/org.mpris.MediaPlayer2.Player.xml | 108 + .../mpris2/org.mpris.MediaPlayer2.xml | 41 + .../mpris2/plasma-dataengine-mpris2.json | 103 + .../dataengines/mpris2/playeractionjob.cpp | 180 ++ .../dataengines/mpris2/playeractionjob.h | 57 + .../dataengines/mpris2/playercontainer.cpp | 374 +++ .../dataengines/mpris2/playercontainer.h | 95 + .../dataengines/mpris2/playercontrol.cpp | 123 + .../dataengines/mpris2/playercontrol.h | 65 + .../dataengines/notifications/CMakeLists.txt | 28 + .../dataengines/notifications/Messages.sh | 2 + .../notifications/notificationaction.cpp | 80 + .../notifications/notificationaction.h | 32 + .../notifications/notifications.operations | 57 + .../notifications/notificationsengine.cpp | 238 ++ .../notifications/notificationsengine.h | 84 + .../notifications/notificationservice.cpp | 22 + .../notifications/notificationservice.h | 25 + .../plasma-dataengine-notifications.json | 123 + .../dataengines/packagekit/CMakeLists.txt | 13 + .../packagekit/packagekit.operations | 10 + .../packagekit/packagekitengine.cpp | 50 + .../dataengines/packagekit/packagekitengine.h | 24 + .../dataengines/packagekit/packagekitjob.cpp | 40 + .../dataengines/packagekit/packagekitjob.h | 22 + .../packagekit/packagekitservice.cpp | 19 + .../packagekit/packagekitservice.h | 17 + .../plasma-dataengine-packagekit.json | 130 + .../dataengines/places/CMakeLists.txt | 22 + plasma/workspace/dataengines/places/TODO | 2 + plasma/workspace/dataengines/places/jobs.h | 84 + .../workspace/dataengines/places/modeljob.h | 26 + .../places/org.kde.places.operations | 59 + .../dataengines/places/placesengine.cpp | 39 + .../dataengines/places/placesengine.h | 28 + .../dataengines/places/placeservice.cpp | 51 + .../dataengines/places/placeservice.h | 26 + .../dataengines/places/placesproxymodel.cpp | 67 + .../dataengines/places/placesproxymodel.h | 33 + .../places/plasma-dataengine-places.json | 156 ++ .../dataengines/places/setupdevicejob.cpp | 24 + .../dataengines/places/setupdevicejob.h | 30 + .../powermanagement/CMakeLists.txt | 25 + .../dataengines/powermanagement/Messages.sh | 2 + .../dataengines/powermanagement/README.txt | 13 + .../plasma-dataengine-powermanagement.json | 78 + .../powermanagement/powermanagementengine.cpp | 846 ++++++ .../powermanagement/powermanagementengine.h | 81 + .../powermanagement/powermanagementjob.cpp | 190 ++ .../powermanagement/powermanagementjob.h | 31 + .../powermanagementservice.cpp | 19 + .../powermanagement/powermanagementservice.h | 21 + .../powermanagementservice.operations | 57 + .../dataengines/soliddevice/CMakeLists.txt | 24 + .../dataengines/soliddevice/Messages.sh | 2 + .../soliddevice/devicesignalmapmanager.cpp | 71 + .../soliddevice/devicesignalmapmanager.h | 30 + .../soliddevice/devicesignalmapper.cpp | 61 + .../soliddevice/devicesignalmapper.h | 69 + .../dataengines/soliddevice/hddtemp.cpp | 91 + .../dataengines/soliddevice/hddtemp.h | 40 + .../plasma-dataengine-soliddevice.json | 155 ++ .../soliddevice/soliddevice.operations | 14 + .../soliddevice/soliddeviceengine.cpp | 678 +++++ .../soliddevice/soliddeviceengine.h | 87 + .../soliddevice/soliddevicejob.cpp | 46 + .../dataengines/soliddevice/soliddevicejob.h | 34 + .../soliddevice/soliddeviceservice.cpp | 27 + .../soliddevice/soliddeviceservice.h | 26 + .../statusnotifieritem/CMakeLists.txt | 41 + .../plasma-dataengine-statusnotifieritem.json | 138 + .../statusnotifieritem.operations | 20 + .../statusnotifieritem_engine.cpp | 156 ++ .../statusnotifieritem_engine.h | 41 + .../statusnotifieritemjob.cpp | 69 + .../statusnotifieritemjob.h | 40 + .../statusnotifieritemservice.cpp | 27 + .../statusnotifieritemservice.h | 33 + .../statusnotifieritemsource.cpp | 536 ++++ .../statusnotifieritemsource.h | 71 + .../statusnotifieritem/systemtraytypes.cpp | 114 + .../statusnotifieritem/systemtraytypes.h | 20 + .../dataengines/systemmonitor/CMakeLists.txt | 14 + .../dataengines/systemmonitor/Messages.sh | 2 + .../plasma-dataengine-systemmonitor.json | 131 + .../systemmonitor/systemmonitor.cpp | 179 ++ .../dataengines/systemmonitor/systemmonitor.h | 46 + .../workspace/dataengines/time/CMakeLists.txt | 22 + plasma/workspace/dataengines/time/Messages.sh | 2 + .../time/plasma-dataengine-time.json | 158 ++ .../dataengines/time/solarsystem.cpp | 316 +++ .../workspace/dataengines/time/solarsystem.h | 136 + .../workspace/dataengines/time/timeengine.cpp | 136 + .../workspace/dataengines/time/timeengine.h | 42 + .../workspace/dataengines/time/timesource.cpp | 245 ++ .../workspace/dataengines/time/timesource.h | 45 + .../dataengines/weather/CMakeLists.txt | 31 + .../workspace/dataengines/weather/Messages.sh | 10 + .../dataengines/weather/ions/CMakeLists.txt | 33 + .../weather/ions/bbcukmet/CMakeLists.txt | 17 + .../weather/ions/bbcukmet/ion-bbcukmet.json | 79 + .../weather/ions/bbcukmet/ion_bbcukmet.cpp | 1053 +++++++ .../weather/ions/bbcukmet/ion_bbcukmet.h | 170 ++ .../weather/ions/data/bbcukmet_i18n.dat | 105 + .../weather/ions/data/envcan_i18n.dat | 342 +++ .../weather/ions/data/noaa_i18n.dat | 356 +++ .../weather/ions/dwd/CMakeLists.txt | 17 + .../dataengines/weather/ions/dwd/ion-dwd.json | 70 + .../dataengines/weather/ions/dwd/ion_dwd.cpp | 733 +++++ .../dataengines/weather/ions/dwd/ion_dwd.h | 156 ++ .../weather/ions/envcan/CMakeLists.txt | 17 + .../weather/ions/envcan/ion-envcan.json | 110 + .../weather/ions/envcan/ion_envcan.cpp | 1627 +++++++++++ .../weather/ions/envcan/ion_envcan.h | 227 ++ .../dataengines/weather/ions/includes/Ion | 1 + .../dataengines/weather/ions/ion.cpp | 214 ++ .../workspace/dataengines/weather/ions/ion.h | 241 ++ .../weather/ions/noaa/CMakeLists.txt | 17 + .../weather/ions/noaa/ion-noaa.json | 109 + .../weather/ions/noaa/ion_noaa.cpp | 909 +++++++ .../dataengines/weather/ions/noaa/ion_noaa.h | 163 ++ .../weather/ions/wetter.com/CMakeLists.txt | 17 + .../ions/wetter.com/ion-wettercom.json | 109 + .../weather/ions/wetter.com/ion_wettercom.cpp | 782 ++++++ .../weather/ions/wetter.com/ion_wettercom.h | 162 ++ .../weather/plasma-dataengine-weather.json | 153 ++ .../dataengines/weather/weatherengine.cpp | 213 ++ .../dataengines/weather/weatherengine.h | 107 + plasma/workspace/doc/CMakeLists.txt | 3 + .../doc/PolicyKit-kde/CMakeLists.txt | 1 + .../doc/PolicyKit-kde/authdialog_1.png | Bin 0 -> 35345 bytes .../doc/PolicyKit-kde/authdialog_2.png | Bin 0 -> 27268 bytes .../doc/PolicyKit-kde/authdialog_3.png | Bin 0 -> 30019 bytes .../doc/PolicyKit-kde/authdialog_4.png | Bin 0 -> 33101 bytes .../doc/PolicyKit-kde/authdialog_5.png | Bin 0 -> 29978 bytes .../doc/PolicyKit-kde/authdialog_6.png | Bin 0 -> 28962 bytes .../doc/PolicyKit-kde/authorization.docbook | 112 + .../doc/PolicyKit-kde/authorization_1.png | Bin 0 -> 101385 bytes .../doc/PolicyKit-kde/authorization_2.png | Bin 0 -> 49686 bytes .../PolicyKit-kde/authorizationagent.docbook | 120 + .../doc/PolicyKit-kde/howitworks.docbook | 53 + .../workspace/doc/PolicyKit-kde/index.docbook | 88 + .../doc/PolicyKit-kde/introduction.docbook | 28 + .../doc/config_update_tool/extract_config.py | 72 + plasma/workspace/doc/kcontrol/CMakeLists.txt | 11 + .../doc/kcontrol/autostart/CMakeLists.txt | 3 + .../doc/kcontrol/autostart/index.docbook | 158 ++ .../doc/kcontrol/colors/CMakeLists.txt | 2 + .../doc/kcontrol/colors/index.docbook | 367 +++ .../desktopthemedetails/CMakeLists.txt | 3 + .../desktopthemedetails/edit-delete.png | Bin 0 -> 147 bytes .../desktopthemedetails/edit-undo.png | Bin 0 -> 374 bytes .../desktopthemedetails/get-new-theme.png | Bin 0 -> 42836 bytes .../desktopthemedetails/index.docbook | 142 + .../doc/kcontrol/desktopthemedetails/main.png | Bin 0 -> 31266 bytes .../doc/kcontrol/fontinst/CMakeLists.txt | 2 + .../doc/kcontrol/fontinst/edit-delete.png | Bin 0 -> 147 bytes .../doc/kcontrol/fontinst/index.docbook | 112 + .../doc/kcontrol/fonts/CMakeLists.txt | 2 + .../doc/kcontrol/fonts/adjust-all.png | Bin 0 -> 19197 bytes .../doc/kcontrol/fonts/index.docbook | 158 ++ plasma/workspace/doc/kcontrol/fonts/main.png | Bin 0 -> 20694 bytes .../doc/kcontrol/formats/CMakeLists.txt | 2 + .../doc/kcontrol/formats/index.docbook | 63 + .../doc/kcontrol/icons/CMakeLists.txt | 2 + .../doc/kcontrol/icons/edit-delete.png | Bin 0 -> 147 bytes .../doc/kcontrol/icons/edit-undo.png | Bin 0 -> 374 bytes .../doc/kcontrol/icons/get-new-theme.png | Bin 0 -> 40803 bytes .../doc/kcontrol/icons/index.docbook | 134 + plasma/workspace/doc/kcontrol/icons/main.png | Bin 0 -> 34341 bytes .../doc/kcontrol/icons/use-of-icons.png | Bin 0 -> 9031 bytes .../doc/kcontrol/kcmstyle/CMakeLists.txt | 2 + .../doc/kcontrol/kcmstyle/index.docbook | 98 + .../doc/kcontrol/notifications/CMakeLists.txt | 2 + .../doc/kcontrol/notifications/index.docbook | 209 ++ .../doc/kcontrol/screenlocker/CMakeLists.txt | 2 + .../doc/kcontrol/screenlocker/index.docbook | 53 + .../doc/kcontrol/translations/CMakeLists.txt | 2 + .../doc/kcontrol/translations/go-top.png | Bin 0 -> 418 bytes .../doc/kcontrol/translations/index.docbook | 81 + .../doc/kcontrol/translations/list-remove.png | Bin 0 -> 340 bytes plasma/workspace/doc/klipper/CMakeLists.txt | 4 + plasma/workspace/doc/klipper/index.docbook | 475 ++++ .../workspace/doc/klipper/klipper-widget.png | Bin 0 -> 10082 bytes plasma/workspace/doc/klipper/screenshot.png | Bin 0 -> 6073 bytes .../freespacenotifier/CMakeLists.txt | 26 + .../workspace/freespacenotifier/Messages.sh | 3 + plasma/workspace/freespacenotifier/README | 3 + .../freespacenotifier/freespacenotifier.cpp | 154 ++ .../freespacenotifier/freespacenotifier.h | 46 + .../freespacenotifier/freespacenotifier.json | 103 + .../freespacenotifier/freespacenotifier.kcfg | 19 + .../freespacenotifier.notifyrc | 436 +++ .../freespacenotifier_prefs_base.ui | 91 + plasma/workspace/freespacenotifier/module.cpp | 79 + plasma/workspace/freespacenotifier/module.h | 24 + .../freespacenotifier/settings.kcfgc | 6 + .../gmenu-dbusmenu-proxy/CMakeLists.txt | 49 + .../gmenu-dbusmenu-proxy/actions.cpp | 198 ++ .../workspace/gmenu-dbusmenu-proxy/actions.h | 45 + .../gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp | 119 + .../gmenu-dbusmenu-proxy/gdbusmenutypes_p.h | 89 + .../gmenudbusmenuproxy.desktop | 50 + .../workspace/gmenu-dbusmenu-proxy/icons.cpp | 308 +++ plasma/workspace/gmenu-dbusmenu-proxy/icons.h | 15 + .../workspace/gmenu-dbusmenu-proxy/main.cpp | 37 + .../workspace/gmenu-dbusmenu-proxy/menu.cpp | 328 +++ plasma/workspace/gmenu-dbusmenu-proxy/menu.h | 67 + .../gmenu-dbusmenu-proxy/menuproxy.cpp | 384 +++ .../gmenu-dbusmenu-proxy/menuproxy.h | 63 + .../plasma-gmenudbusmenuproxy.service.in | 11 + .../workspace/gmenu-dbusmenu-proxy/utils.cpp | 29 + plasma/workspace/gmenu-dbusmenu-proxy/utils.h | 19 + .../workspace/gmenu-dbusmenu-proxy/window.cpp | 655 +++++ .../workspace/gmenu-dbusmenu-proxy/window.h | 127 + .../interactiveconsole/CMakeLists.txt | 8 + .../interactiveconsole/interactiveconsole.cpp | 591 ++++ .../interactiveconsole/interactiveconsole.h | 108 + plasma/workspace/interactiveconsole/main.cpp | 36 + plasma/workspace/kcms/CMakeLists.txt | 27 + .../workspace/kcms/autostart/CMakeLists.txt | 19 + plasma/workspace/kcms/autostart/Messages.sh | 3 + plasma/workspace/kcms/autostart/autostart.cpp | 43 + plasma/workspace/kcms/autostart/autostart.h | 31 + .../kcms/autostart/autostartmodel.cpp | 413 +++ .../workspace/kcms/autostart/autostartmodel.h | 77 + .../kcms/autostart/kcm_autostart.desktop | 43 + .../kcms/autostart/kcm_autostart.json | 97 + .../autostart/package/contents/ui/main.qml | 185 ++ plasma/workspace/kcms/colors/CMakeLists.txt | 75 + plasma/workspace/kcms/colors/Messages.sh | 4 + plasma/workspace/kcms/colors/README.i18n | 54 + plasma/workspace/kcms/colors/colors.cpp | 411 +++ plasma/workspace/kcms/colors/colors.h | 112 + .../kcms/colors/colorsapplicator.cpp | 196 ++ .../workspace/kcms/colors/colorsapplicator.h | 86 + .../workspace/kcms/colors/colorschemes.knsrc | 48 + plasma/workspace/kcms/colors/colorsmodel.cpp | 249 ++ plasma/workspace/kcms/colors/colorsmodel.h | 77 + .../workspace/kcms/colors/colorssettings.kcfg | 17 + .../kcms/colors/colorssettings.kcfgc | 7 + .../kcms/colors/editor/CMakeLists.txt | 32 + .../kcms/colors/editor/kcolorschemeeditor.cpp | 81 + .../editor/org.kde.kcolorschemeeditor.desktop | 125 + .../workspace/kcms/colors/editor/preview.ui | 360 +++ .../kcms/colors/editor/previewwidget.cpp | 145 + .../kcms/colors/editor/previewwidget.h | 32 + .../kcms/colors/editor/scmeditorcolors.cpp | 492 ++++ .../kcms/colors/editor/scmeditorcolors.h | 93 + .../kcms/colors/editor/scmeditorcolors.ui | 523 ++++ .../kcms/colors/editor/scmeditordialog.cpp | 210 ++ .../kcms/colors/editor/scmeditordialog.h | 61 + .../kcms/colors/editor/scmeditordialog.ui | 49 + .../kcms/colors/editor/scmeditoreffects.cpp | 153 ++ .../kcms/colors/editor/scmeditoreffects.h | 50 + .../kcms/colors/editor/scmeditoreffects.ui | 265 ++ .../kcms/colors/editor/scmeditoroptions.cpp | 110 + .../kcms/colors/editor/scmeditoroptions.h | 47 + .../kcms/colors/editor/scmeditoroptions.ui | 131 + .../kcms/colors/editor/setpreview.ui | 347 +++ .../kcms/colors/editor/setpreviewwidget.cpp | 118 + .../kcms/colors/editor/setpreviewwidget.h | 33 + .../kcms/colors/filterproxymodel.cpp | 116 + .../workspace/kcms/colors/filterproxymodel.h | 54 + .../workspace/kcms/colors/kcm_colors.desktop | 48 + plasma/workspace/kcms/colors/kcm_colors.json | 156 ++ .../kcms/colors/package/contents/ui/main.qml | 523 ++++ .../kcms/colors/plasma-apply-colorscheme.cpp | 150 + .../workspace/kcms/cursortheme/CMakeLists.txt | 101 + plasma/workspace/kcms/cursortheme/Messages.sh | 3 + .../kcms/cursortheme/cursorthemesettings.kcfg | 17 + .../cursortheme/cursorthemesettings.kcfgc | 7 + .../delete_cursor_old_default_size.pl | 10 + .../delete_cursor_old_default_size.upd | 8 + .../kcms/cursortheme/kcm_cursortheme.desktop | 48 + .../kcms/cursortheme/kcm_cursortheme.json | 107 + .../kcms/cursortheme/kcmcursortheme.cpp | 497 ++++ .../kcms/cursortheme/kcmcursortheme.h | 126 + .../package/contents/ui/Delegate.qml | 74 + .../cursortheme/package/contents/ui/main.qml | 171 ++ .../cursortheme/plasma-apply-cursortheme.cpp | 110 + .../kcms/cursortheme/xcursor/cursortheme.cpp | 146 + .../kcms/cursortheme/xcursor/cursortheme.h | 185 ++ .../cursortheme/xcursor/previewwidget.cpp | 289 ++ .../kcms/cursortheme/xcursor/previewwidget.h | 62 + .../cursortheme/xcursor/sortproxymodel.cpp | 42 + .../kcms/cursortheme/xcursor/sortproxymodel.h | 67 + .../cursortheme/xcursor/themeapplicator.cpp | 103 + .../cursortheme/xcursor/themeapplicator.h | 17 + .../kcms/cursortheme/xcursor/thememodel.cpp | 406 +++ .../kcms/cursortheme/xcursor/thememodel.h | 103 + .../kcms/cursortheme/xcursor/xcursor.knsrc | 50 + .../kcms/cursortheme/xcursor/xcursortheme.cpp | 207 ++ .../kcms/cursortheme/xcursor/xcursortheme.h | 65 + .../kcms/desktoptheme/CMakeLists.txt | 52 + .../workspace/kcms/desktoptheme/Messages.sh | 4 + .../desktoptheme/desktopthemesettings.kcfg | 13 + .../desktoptheme/desktopthemesettings.kcfgc | 6 + .../kcms/desktoptheme/filterproxymodel.cpp | 120 + .../kcms/desktoptheme/filterproxymodel.h | 59 + plasma/workspace/kcms/desktoptheme/kcm.cpp | 254 ++ plasma/workspace/kcms/desktoptheme/kcm.h | 91 + .../desktoptheme/kcm_desktoptheme.desktop | 47 + .../kcms/desktoptheme/kcm_desktoptheme.json | 114 + .../desktoptheme/package/contents/ui/Hand.qml | 67 + .../package/contents/ui/ThemePreview.qml | 178 ++ .../desktoptheme/package/contents/ui/main.qml | 206 ++ .../plasma-apply-desktoptheme.cpp | 93 + .../kcms/desktoptheme/plasma-themes.knsrc | 49 + .../kcms/desktoptheme/themesmodel.cpp | 238 ++ .../workspace/kcms/desktoptheme/themesmodel.h | 85 + plasma/workspace/kcms/feedback/CMakeLists.txt | 31 + plasma/workspace/kcms/feedback/Messages.sh | 2 + plasma/workspace/kcms/feedback/feedback.cpp | 139 + plasma/workspace/kcms/feedback/feedback.h | 43 + .../kcms/feedback/feedbacksettings.kcfg | 12 + .../kcms/feedback/feedbacksettings.kcfgc | 7 + .../kcms/feedback/kcm_feedback.desktop | 49 + .../workspace/kcms/feedback/kcm_feedback.json | 115 + .../feedback/package/contents/ui/main.qml | 184 ++ plasma/workspace/kcms/fonts/CMakeLists.txt | 42 + plasma/workspace/kcms/fonts/Messages.sh | 3 + plasma/workspace/kcms/fonts/fontinit.cpp | 35 + plasma/workspace/kcms/fonts/fonts.cpp | 250 ++ plasma/workspace/kcms/fonts/fonts.h | 74 + .../workspace/kcms/fonts/fontsaasettings.cpp | 386 +++ plasma/workspace/kcms/fonts/fontsaasettings.h | 67 + .../kcms/fonts/fontsaasettingsbase.kcfg | 17 + .../kcms/fonts/fontsaasettingsbase.kcfgc | 6 + .../workspace/kcms/fonts/fontssettings.kcfg | 83 + .../workspace/kcms/fonts/fontssettings.kcfgc | 7 + plasma/workspace/kcms/fonts/kcm_fonts.desktop | 47 + plasma/workspace/kcms/fonts/kcm_fonts.json | 117 + plasma/workspace/kcms/fonts/kxftconfig.cpp | 884 ++++++ plasma/workspace/kcms/fonts/kxftconfig.h | 232 ++ .../fonts/package/contents/ui/FontWidget.qml | 53 + .../kcms/fonts/package/contents/ui/main.qml | 419 +++ .../kcms/fonts/previewimageprovider.cpp | 120 + .../kcms/fonts/previewimageprovider.h | 20 + .../kcms/fonts/previewrenderengine.cpp | 117 + .../kcms/fonts/previewrenderengine.h | 25 + plasma/workspace/kcms/formats/CMakeLists.txt | 28 + plasma/workspace/kcms/formats/Messages.sh | 5 + .../workspace/kcms/formats/exampleutility.cpp | 66 + .../kcms/formats/formatssettings.kcfg | 61 + .../kcms/formats/formatssettings.kcfgc | 6 + .../kcms/formats/kcm_formats.desktop | 44 + .../workspace/kcms/formats/kcm_formats.json | 113 + plasma/workspace/kcms/formats/kcmformats.cpp | 79 + plasma/workspace/kcms/formats/kcmformats.h | 34 + .../kcms/formats/localelistmodel.cpp | 182 ++ .../workspace/kcms/formats/localelistmodel.h | 58 + .../workspace/kcms/formats/optionsmodel.cpp | 140 + plasma/workspace/kcms/formats/optionsmodel.h | 34 + .../kcms/formats/package/contents/ui/main.qml | 126 + plasma/workspace/kcms/icons/CMakeLists.txt | 53 + plasma/workspace/kcms/icons/Messages.sh | 3 + plasma/workspace/kcms/icons/changeicons.cpp | 38 + plasma/workspace/kcms/icons/icons.knsrc | 51 + .../kcms/icons/icons_remove_effects.upd | 105 + .../kcms/icons/iconsizecategorymodel.cpp | 63 + .../kcms/icons/iconsizecategorymodel.h | 47 + plasma/workspace/kcms/icons/iconsmodel.cpp | 164 ++ plasma/workspace/kcms/icons/iconsmodel.h | 59 + plasma/workspace/kcms/icons/iconssettings.cpp | 46 + plasma/workspace/kcms/icons/iconssettings.h | 23 + .../kcms/icons/iconssettingsbase.kcfg | 49 + .../kcms/icons/iconssettingsbase.kcfgc | 7 + plasma/workspace/kcms/icons/kcm_icons.desktop | 48 + plasma/workspace/kcms/icons/kcm_icons.json | 115 + plasma/workspace/kcms/icons/main.cpp | 477 ++++ plasma/workspace/kcms/icons/main.h | 110 + .../package/contents/ui/IconSizePopup.qml | 161 ++ .../kcms/icons/package/contents/ui/main.qml | 292 ++ plasma/workspace/kcms/kcms-common.cpp | 15 + plasma/workspace/kcms/kcms-common_p.h | 37 + .../workspace/kcms/kfontinst/CMakeLists.txt | 37 + plasma/workspace/kcms/kfontinst/ChangeLog | 562 ++++ plasma/workspace/kcms/kfontinst/Messages.sh | 4 + .../kcms/kfontinst/apps/16-apps-kfontview.png | Bin 0 -> 719 bytes .../kcms/kfontinst/apps/22-apps-kfontview.png | Bin 0 -> 1047 bytes .../kcms/kfontinst/apps/32-apps-kfontview.png | Bin 0 -> 1545 bytes .../kcms/kfontinst/apps/48-apps-kfontview.png | Bin 0 -> 2701 bytes .../kcms/kfontinst/apps/64-apps-kfontview.png | Bin 0 -> 4025 bytes .../kcms/kfontinst/apps/CMakeLists.txt | 54 + .../kcms/kfontinst/apps/CreateParent.h | 48 + .../kcms/kfontinst/apps/Installer.cpp | 141 + .../workspace/kcms/kfontinst/apps/Installer.h | 33 + .../workspace/kcms/kfontinst/apps/Printer.cpp | 403 +++ .../workspace/kcms/kfontinst/apps/Printer.h | 75 + .../workspace/kcms/kfontinst/apps/Viewer.cpp | 168 ++ plasma/workspace/kcms/kfontinst/apps/Viewer.h | 39 + .../kfontinst/apps/hisc-apps-kfontview.svgz | Bin 0 -> 9292 bytes .../kcms/kfontinst/apps/installfont.desktop | 48 + .../kcms/kfontinst/apps/kfontviewui.rc | 4 + .../kfontinst/apps/org.kde.kfontview.desktop | 89 + .../kcms/kfontinst/config-fontinst.h.cmake | 6 + .../kcms/kfontinst/dbus/CMakeLists.txt | 33 + .../kcms/kfontinst/dbus/FcConfig.cpp | 174 ++ .../workspace/kcms/kfontinst/dbus/FcConfig.h | 17 + .../workspace/kcms/kfontinst/dbus/Folder.cpp | 362 +++ plasma/workspace/kcms/kfontinst/dbus/Folder.h | 125 + .../kcms/kfontinst/dbus/FontInst.cpp | 889 ++++++ .../workspace/kcms/kfontinst/dbus/FontInst.h | 124 + .../kcms/kfontinst/dbus/FontinstIface.cpp | 25 + .../kcms/kfontinst/dbus/FontinstIface.h | 148 + .../workspace/kcms/kfontinst/dbus/Helper.cpp | 406 +++ plasma/workspace/kcms/kfontinst/dbus/Helper.h | 39 + plasma/workspace/kcms/kfontinst/dbus/Main.cpp | 22 + .../workspace/kcms/kfontinst/dbus/Utils.cpp | 203 ++ plasma/workspace/kcms/kfontinst/dbus/Utils.h | 31 + .../kcms/kfontinst/dbus/fontinst.actions | 121 + .../kcms/kfontinst/dbus/fontinst_x11 | 37 + .../dbus/org.kde.fontinst.service.cmake | 4 + .../org.kde.fontinst.system-service.cmake | 5 + .../kcms/kfontinst/dbus/org.kde.fontinst.xml | 105 + .../kcmfontinst/16-actions-addfont.png | Bin 0 -> 704 bytes .../kcmfontinst/16-actions-font-disable.png | Bin 0 -> 731 bytes .../kcmfontinst/16-actions-font-enable.png | Bin 0 -> 736 bytes .../kcmfontinst/16-actions-fontstatus.png | Bin 0 -> 744 bytes .../kcmfontinst/22-actions-addfont.png | Bin 0 -> 1003 bytes .../kcmfontinst/22-actions-font-disable.png | Bin 0 -> 1050 bytes .../kcmfontinst/22-actions-font-enable.png | Bin 0 -> 1042 bytes .../kcmfontinst/22-actions-fontstatus.png | Bin 0 -> 1096 bytes .../kfontinst/kcmfontinst/ActionLabel.cpp | 91 + .../kcms/kfontinst/kcmfontinst/ActionLabel.h | 33 + .../kcms/kfontinst/kcmfontinst/CMakeLists.txt | 35 + .../kcmfontinst/DuplicatesDialog.cpp | 631 +++++ .../kfontinst/kcmfontinst/DuplicatesDialog.h | 171 ++ .../kcms/kfontinst/kcmfontinst/FcQuery.cpp | 98 + .../kcms/kfontinst/kcmfontinst/FcQuery.h | 54 + .../kcms/kfontinst/kcmfontinst/FontFilter.cpp | 346 +++ .../kcms/kfontinst/kcmfontinst/FontFilter.h | 76 + .../kcmfontinst/FontFilterProxyStyle.cpp | 151 ++ .../kcmfontinst/FontFilterProxyStyle.h | 54 + .../kfontinst/kcmfontinst/FontInstInterface.h | 17 + .../kcms/kfontinst/kcmfontinst/FontList.cpp | 1900 +++++++++++++ .../kcms/kfontinst/kcmfontinst/FontList.h | 458 ++++ .../kfontinst/kcmfontinst/FontsPackage.cpp | 69 + .../kcms/kfontinst/kcmfontinst/FontsPackage.h | 20 + .../kcms/kfontinst/kcmfontinst/GroupList.cpp | 973 +++++++ .../kcms/kfontinst/kcmfontinst/GroupList.h | 270 ++ .../kcms/kfontinst/kcmfontinst/JobRunner.cpp | 753 +++++ .../kcms/kfontinst/kcmfontinst/JobRunner.h | 114 + .../kfontinst/kcmfontinst/KCmFontInst.cpp | 1202 ++++++++ .../kcms/kfontinst/kcmfontinst/KCmFontInst.h | 112 + .../kfontinst/kcmfontinst/PreviewList.cpp | 200 ++ .../kcms/kfontinst/kcmfontinst/PreviewList.h | 105 + .../kfontinst/kcmfontinst/PrintDialog.cpp | 61 + .../kcms/kfontinst/kcmfontinst/PrintDialog.h | 28 + .../kcms/kfontinst/kcmfontinst/fontinst.json | 118 + .../kcmfontinst/kcm_fontinst.desktop | 47 + .../kfontinst/kcmfontinst/kfontinst.knsrc | 47 + .../kcmfontinst/sc-actions-addfont.svgz | Bin 0 -> 55400 bytes .../kcmfontinst/sc-actions-disablefont.svgz | Bin 0 -> 55550 bytes .../kcmfontinst/sc-actions-enablefont.svgz | Bin 0 -> 55250 bytes .../kcmfontinst/sc-actions-fontstatus.svgz | Bin 0 -> 55218 bytes .../kio/128-mimetypes-fonts-package.png | Bin 0 -> 5266 bytes .../kio/16-mimetypes-fonts-package.png | Bin 0 -> 638 bytes .../kio/22-mimetypes-fonts-package.png | Bin 0 -> 873 bytes .../kio/32-mimetypes-fonts-package.png | Bin 0 -> 1273 bytes .../kio/48-mimetypes-fonts-package.png | Bin 0 -> 1896 bytes .../kio/64-mimetypes-fonts-package.png | Bin 0 -> 2613 bytes .../kcms/kfontinst/kio/CMakeLists.txt | 17 + .../kcms/kfontinst/kio/FontInstInterface.cpp | 142 + .../kcms/kfontinst/kio/FontInstInterface.h | 49 + .../workspace/kcms/kfontinst/kio/KioFonts.cpp | 741 +++++ .../workspace/kcms/kfontinst/kio/KioFonts.h | 59 + .../kcms/kfontinst/kio/fonts.desktop | 48 + .../workspace/kcms/kfontinst/kio/fonts.json | 32 + .../kio/oxsc-mimetypes-fonts-package.svgz | Bin 0 -> 3498 bytes .../kcms/kfontinst/lib/CMakeLists.txt | 23 + .../workspace/kcms/kfontinst/lib/Family.cpp | 132 + plasma/workspace/kcms/kfontinst/lib/Family.h | 91 + plasma/workspace/kcms/kfontinst/lib/Fc.cpp | 628 +++++ plasma/workspace/kcms/kfontinst/lib/Fc.h | 110 + .../workspace/kcms/kfontinst/lib/FcEngine.cpp | 1503 ++++++++++ .../workspace/kcms/kfontinst/lib/FcEngine.h | 160 ++ plasma/workspace/kcms/kfontinst/lib/File.cpp | 72 + plasma/workspace/kcms/kfontinst/lib/File.h | 70 + .../kcms/kfontinst/lib/KfiConstants.h | 106 + plasma/workspace/kcms/kfontinst/lib/Misc.cpp | 452 +++ plasma/workspace/kcms/kfontinst/lib/Misc.h | 138 + plasma/workspace/kcms/kfontinst/lib/Style.cpp | 165 ++ plasma/workspace/kcms/kfontinst/lib/Style.h | 103 + .../kcms/kfontinst/lib/WritingSystems.cpp | 152 ++ .../kcms/kfontinst/lib/WritingSystems.h | 30 + .../workspace/kcms/kfontinst/lib/XmlStrings.h | 24 + .../kcms/kfontinst/lib/config-paths.h.cmake | 3 + .../kcms/kfontinst/lib/kfontinst_export.h | 25 + ...ps-preferences-desktop-font-installer.svgz | Bin 0 -> 54135 bytes .../kcms/kfontinst/thumbnail/CMakeLists.txt | 14 + .../kfontinst/thumbnail/FontThumbnail.cpp | 94 + .../kcms/kfontinst/thumbnail/FontThumbnail.h | 29 + .../kfontinst/thumbnail/fontthumbnail.desktop | 46 + .../kcms/kfontinst/viewpart/CMakeLists.txt | 15 + .../kcms/kfontinst/viewpart/COPYING.UNICODE | 31 + .../kcms/kfontinst/viewpart/CharTip.cpp | 282 ++ .../kcms/kfontinst/viewpart/CharTip.h | 47 + .../kcms/kfontinst/viewpart/FontPreview.cpp | 160 ++ .../kcms/kfontinst/viewpart/FontPreview.h | 72 + .../kcms/kfontinst/viewpart/FontViewPart.cpp | 468 ++++ .../kcms/kfontinst/viewpart/FontViewPart.h | 92 + .../viewpart/PreviewSelectAction.cpp | 80 + .../kfontinst/viewpart/PreviewSelectAction.h | 44 + .../kcms/kfontinst/viewpart/UnicodeBlocks.h | 170 ++ .../kfontinst/viewpart/UnicodeCategories.h | 2120 +++++++++++++++ .../kcms/kfontinst/viewpart/UnicodeScripts.h | 1284 +++++++++ .../viewpart/download-unicode-files.sh | 26 + .../viewpart/generate-unicode-tables.pl | 842 ++++++ .../kfontinst/viewpart/kfontviewpart.json | 54 + .../kcms/kfontinst/viewpart/kfontviewpart.rc | 11 + plasma/workspace/kcms/krdb/AUTHORS | 9 + plasma/workspace/kcms/krdb/CMakeLists.txt | 7 + plasma/workspace/kcms/krdb/Messages.sh | 2 + plasma/workspace/kcms/krdb/krdb.cpp | 610 +++++ plasma/workspace/kcms/krdb/krdb.h | 17 + .../workspace/kcms/lookandfeel/CMakeLists.txt | 96 + plasma/workspace/kcms/lookandfeel/Messages.sh | 3 + .../kcms/lookandfeel/autotests/CMakeLists.txt | 10 + .../kcms/lookandfeel/autotests/kcmtest.cpp | 282 ++ .../autotests/lookandfeel/contents/defaults | 20 + .../autotests/lookandfeel/metadata.desktop | 84 + .../kcms/lookandfeel/config-kcm.h.cmake | 7 + plasma/workspace/kcms/lookandfeel/kcm.cpp | 475 ++++ plasma/workspace/kcms/lookandfeel/kcm.h | 98 + .../kcms/lookandfeel/kcm_lookandfeel.desktop | 48 + .../kcms/lookandfeel/kcm_lookandfeel.json | 108 + plasma/workspace/kcms/lookandfeel/kcmmain.cpp | 14 + plasma/workspace/kcms/lookandfeel/lnftool.cpp | 106 + .../kcms/lookandfeel/lookandfeel.knsrc | 48 + .../kcms/lookandfeel/lookandfeelmanager.cpp | 557 ++++ .../kcms/lookandfeel/lookandfeelmanager.h | 122 + .../kcms/lookandfeel/lookandfeelsettings.kcfg | 13 + .../lookandfeel/lookandfeelsettings.kcfgc | 6 + .../lookandfeel/package/contents/ui/main.qml | 128 + .../workspace/kcms/nightcolor/CMakeLists.txt | 32 + plasma/workspace/kcms/nightcolor/Messages.sh | 2 + plasma/workspace/kcms/nightcolor/enum.h | 40 + plasma/workspace/kcms/nightcolor/kcm.cpp | 35 + plasma/workspace/kcms/nightcolor/kcm.h | 31 + .../kcms/nightcolor/kcm_nightcolor.desktop | 44 + .../kcms/nightcolor/kcm_nightcolor.json | 110 + .../kcms/nightcolor/nightcolorsettings.kcfg | 59 + .../kcms/nightcolor/nightcolorsettings.kcfgc | 11 + .../contents/ui/LocationsFixedView.qml | 74 + .../package/contents/ui/NumberField.qml | 38 + .../package/contents/ui/TimeField.qml | 62 + .../package/contents/ui/TimingsView.qml | 41 + .../nightcolor/package/contents/ui/main.qml | 350 +++ .../kcms/notifications/CMakeLists.txt | 28 + .../workspace/kcms/notifications/Messages.sh | 2 + .../kcms/notifications/filterproxymodel.cpp | 47 + .../kcms/notifications/filterproxymodel.h | 31 + plasma/workspace/kcms/notifications/kcm.cpp | 354 +++ plasma/workspace/kcms/notifications/kcm.h | 116 + .../notifications/kcm_notifications.desktop | 95 + .../kcms/notifications/kcm_notifications.json | 135 + .../kcms/notifications/notificationsdata.cpp | 108 + .../kcms/notifications/notificationsdata.h | 52 + .../contents/ui/ApplicationConfiguration.qml | 139 + .../package/contents/ui/PopupPositionPage.qml | 22 + .../contents/ui/ScreenPositionSelector.qml | 225 ++ .../package/contents/ui/SourcesPage.qml | 155 ++ .../package/contents/ui/main.qml | 338 +++ .../kcms/notifications/sourcesmodel.cpp | 381 +++ .../kcms/notifications/sourcesmodel.h | 85 + plasma/workspace/kcms/style/CMakeLists.txt | 61 + plasma/workspace/kcms/style/Messages.sh | 3 + plasma/workspace/kcms/style/gtk_themes.knsrc | 46 + plasma/workspace/kcms/style/gtkpage.cpp | 104 + plasma/workspace/kcms/style/gtkpage.h | 53 + .../workspace/kcms/style/gtkthemesmodel.cpp | 153 ++ plasma/workspace/kcms/style/gtkthemesmodel.h | 58 + plasma/workspace/kcms/style/kcm_style.desktop | 45 + plasma/workspace/kcms/style/kcm_style.json | 110 + plasma/workspace/kcms/style/kcmstyle.cpp | 345 +++ plasma/workspace/kcms/style/kcmstyle.h | 99 + .../kcms/style/org.kde.GtkConfig.xml | 15 + .../contents/ui/EffectSettingsPopup.qml | 90 + .../package/contents/ui/GtkStylePage.qml | 129 + .../kcms/style/package/contents/ui/main.qml | 149 + plasma/workspace/kcms/style/previewitem.cpp | 294 ++ plasma/workspace/kcms/style/previewitem.h | 64 + .../style/style_widgetstyle_default_breeze.pl | 10 + .../style_widgetstyle_default_breeze.upd | 8 + .../workspace/kcms/style/styleconfdialog.cpp | 62 + plasma/workspace/kcms/style/styleconfdialog.h | 34 + plasma/workspace/kcms/style/stylepreview.ui | 142 + .../workspace/kcms/style/stylesettings.kcfg | 31 + .../workspace/kcms/style/stylesettings.kcfgc | 7 + plasma/workspace/kcms/style/stylesmodel.cpp | 204 ++ plasma/workspace/kcms/style/stylesmodel.h | 60 + .../kcms/translations/CMakeLists.txt | 49 + .../workspace/kcms/translations/Messages.sh | 2 + .../translations/kcm_translations.desktop | 52 + .../kcms/translations/kcm_translations.json | 119 + .../workspace/kcms/translations/language.cpp | 191 ++ plasma/workspace/kcms/translations/language.h | 32 + .../translations/package/contents/ui/main.qml | 312 +++ .../kcms/translations/translations.cpp | 86 + .../kcms/translations/translations.h | 53 + .../kcms/translations/translationsmodel.cpp | 195 ++ .../kcms/translations/translationsmodel.h | 72 + .../translations/translationssettings.cpp | 27 + .../kcms/translations/translationssettings.h | 24 + .../translationssettingsbase.kcfg | 28 + .../translationssettingsbase.kcfgc | 6 + plasma/workspace/kcms/users/CMakeLists.txt | 7 + plasma/workspace/kcms/users/Messages.sh | 2 + .../kcms/users/avatars/Artist Konqi.png | Bin 0 -> 95594 bytes .../kcms/users/avatars/Bookworm Konqi.png | Bin 0 -> 109449 bytes .../kcms/users/avatars/Boss Konqi.png | Bin 0 -> 87114 bytes .../kcms/users/avatars/Bug Catcher Konqi.png | Bin 0 -> 86821 bytes .../kcms/users/avatars/Card Shark Konqi.png | Bin 0 -> 85015 bytes .../kcms/users/avatars/Hacker Konqi.png | Bin 0 -> 132244 bytes .../kcms/users/avatars/Journalist Konqi.png | Bin 0 -> 97689 bytes plasma/workspace/kcms/users/avatars/Katie.png | Bin 0 -> 73816 bytes plasma/workspace/kcms/users/avatars/Konqi.png | Bin 0 -> 87527 bytes .../kcms/users/avatars/Mechanic Konqi.png | Bin 0 -> 122232 bytes .../kcms/users/avatars/Messenger Konqi.png | Bin 0 -> 100421 bytes .../kcms/users/avatars/Musician Konqi.png | Bin 0 -> 115850 bytes .../users/avatars/Office Worker Konqi.png | Bin 0 -> 158410 bytes .../kcms/users/avatars/PC Builder Konqi.png | Bin 0 -> 117572 bytes .../kcms/users/avatars/Scientist Konqi.png | Bin 0 -> 105636 bytes .../kcms/users/avatars/Teacher Konqi.png | Bin 0 -> 68665 bytes .../users/avatars/Virtual Reality Konqi.png | Bin 0 -> 110059 bytes .../kcms/users/avatars/photos/Air Balloon.png | Bin 0 -> 291547 bytes .../avatars/photos/Air Balloon.png.license | 5 + .../kcms/users/avatars/photos/Astronaut.png | Bin 0 -> 540348 bytes .../avatars/photos/Astronaut.png.license | 5 + .../kcms/users/avatars/photos/Books.png | Bin 0 -> 360957 bytes .../users/avatars/photos/Books.png.license | 5 + .../kcms/users/avatars/photos/Brushes.png | Bin 0 -> 369506 bytes .../users/avatars/photos/Brushes.png.license | 5 + .../kcms/users/avatars/photos/Bulb.png | Bin 0 -> 288250 bytes .../users/avatars/photos/Bulb.png.license | 5 + .../kcms/users/avatars/photos/Car.png | Bin 0 -> 343436 bytes .../kcms/users/avatars/photos/Car.png.license | 5 + .../kcms/users/avatars/photos/Cat.png | Bin 0 -> 432202 bytes .../kcms/users/avatars/photos/Cat.png.license | 5 + .../kcms/users/avatars/photos/Chameleon.png | Bin 0 -> 387778 bytes .../users/avatars/photos/Chamelon.png.license | 5 + .../kcms/users/avatars/photos/Cocktail.png | Bin 0 -> 345370 bytes .../users/avatars/photos/Cocktail.png.license | 5 + .../kcms/users/avatars/photos/Dog.png | Bin 0 -> 431574 bytes .../kcms/users/avatars/photos/Dog.png.license | 5 + .../kcms/users/avatars/photos/Fish.png | Bin 0 -> 346140 bytes .../users/avatars/photos/Fish.png.license | 5 + .../kcms/users/avatars/photos/Gamepad.png | Bin 0 -> 396134 bytes .../users/avatars/photos/Gamepad.png.license | 5 + .../kcms/users/avatars/photos/Owl.png | Bin 0 -> 501674 bytes .../kcms/users/avatars/photos/Owl.png.license | 5 + .../kcms/users/avatars/photos/Pancakes.png | Bin 0 -> 370971 bytes .../users/avatars/photos/Pancakes.png.license | 5 + .../kcms/users/avatars/photos/Parrot.png | Bin 0 -> 362440 bytes .../users/avatars/photos/Parrot.png.license | 5 + .../kcms/users/avatars/photos/Pencils.png | Bin 0 -> 407428 bytes .../users/avatars/photos/Pencils.png.license | 5 + .../kcms/users/avatars/photos/Shuttle.png | Bin 0 -> 500498 bytes .../users/avatars/photos/Shuttle.png.license | 5 + .../kcms/users/avatars/photos/Soccer.png | Bin 0 -> 430100 bytes .../users/avatars/photos/Soccer.png.license | 5 + .../kcms/users/avatars/photos/Sunflower.png | Bin 0 -> 395640 bytes .../avatars/photos/Sunflower.png.license | 5 + .../kcms/users/avatars/photos/Sushi.png | Bin 0 -> 346691 bytes .../users/avatars/photos/Sushi.png.license | 5 + plasma/workspace/kcms/users/kcm_users.desktop | 37 + .../package/contents/ui/ChangePassword.qml | 84 + .../contents/ui/ChangeWalletPassword.qml | 74 + .../users/package/contents/ui/CreateUser.qml | 77 + .../package/contents/ui/FingerprintDialog.qml | 269 ++ .../contents/ui/FingerprintProgressCircle.qml | 131 + .../package/contents/ui/UserDetailsPage.qml | 552 ++++ .../kcms/users/package/contents/ui/main.qml | 138 + .../workspace/kcms/users/src/CMakeLists.txt | 74 + .../kcms/users/src/fingerprintmodel.cpp | 346 +++ .../kcms/users/src/fingerprintmodel.h | 154 ++ .../workspace/kcms/users/src/fprintdevice.cpp | 134 + .../workspace/kcms/users/src/fprintdevice.h | 63 + plasma/workspace/kcms/users/src/kcm.cpp | 138 + plasma/workspace/kcms/users/src/kcm.h | 47 + .../workspace/kcms/users/src/kcm_users.json | 94 + .../src/net.reactivated.Fprint.Device.xml | 645 +++++ .../src/net.reactivated.Fprint.Manager.xml | 48 + .../src/org.freedesktop.Accounts.User.xml | 84 + .../users/src/org.freedesktop.Accounts.xml | 50 + .../src/org.freedesktop.login1.Manager.xml | 18 + plasma/workspace/kcms/users/src/user.cpp | 371 +++ plasma/workspace/kcms/users/src/user.h | 127 + plasma/workspace/kcms/users/src/usermodel.cpp | 152 ++ plasma/workspace/kcms/users/src/usermodel.h | 47 + .../workspace/kcms/users/src/usersessions.h | 42 + plasma/workspace/kioslave/CMakeLists.txt | 2 + .../kioslave/applications/CMakeLists.txt | 6 + .../kioslave/applications/Messages.sh | 2 + .../kioslave/applications/applications.json | 52 + .../applications/kio_applications.cpp | 196 ++ .../workspace/kioslave/desktop/CMakeLists.txt | 26 + .../kioslave/desktop/ExtraDesktop.sh | 4 + plasma/workspace/kioslave/desktop/Messages.sh | 2 + .../workspace/kioslave/desktop/desktop.json | 30 + .../kioslave/desktop/desktopnotifier.cpp | 89 + .../kioslave/desktop/desktopnotifier.h | 35 + .../kioslave/desktop/desktopnotifier.json | 91 + .../kioslave/desktop/directory.desktop | 4 + .../kioslave/desktop/directory.trash | 114 + .../kioslave/desktop/kio_desktop.cpp | 251 ++ .../workspace/kioslave/desktop/kio_desktop.h | 30 + .../kioslave/desktop/tests/CMakeLists.txt | 6 + .../desktop/tests/kio_desktop_test.cpp | 194 ++ plasma/workspace/klipper/CMakeLists.txt | 78 + plasma/workspace/klipper/Messages.sh | 5 + plasma/workspace/klipper/actionsconfig.ui | 118 + .../workspace/klipper/actionstreewidget.cpp | 49 + plasma/workspace/klipper/actionstreewidget.h | 59 + .../klipper/autotests/CMakeLists.txt | 28 + .../klipper/autotests/historymodeltest.cpp | 236 ++ .../klipper/autotests/historytest.cpp | 348 +++ .../workspace/klipper/autotests/utilstest.cpp | 31 + plasma/workspace/klipper/clipboardengine.cpp | 50 + plasma/workspace/klipper/clipboardengine.h | 23 + plasma/workspace/klipper/clipboardjob.cpp | 189 ++ plasma/workspace/klipper/clipboardjob.h | 25 + plasma/workspace/klipper/clipboardservice.cpp | 21 + plasma/workspace/klipper/clipboardservice.h | 25 + .../workspace/klipper/clipcommandprocess.cpp | 70 + plasma/workspace/klipper/clipcommandprocess.h | 34 + .../workspace/klipper/config-klipper.h.cmake | 1 + plasma/workspace/klipper/configdialog.cpp | 553 ++++ plasma/workspace/klipper/configdialog.h | 130 + plasma/workspace/klipper/editactiondialog.cpp | 368 +++ plasma/workspace/klipper/editactiondialog.h | 56 + plasma/workspace/klipper/editactiondialog.ui | 220 ++ plasma/workspace/klipper/history.cpp | 226 ++ plasma/workspace/klipper/history.h | 138 + plasma/workspace/klipper/historyimageitem.cpp | 63 + plasma/workspace/klipper/historyimageitem.h | 49 + plasma/workspace/klipper/historyitem.cpp | 121 + plasma/workspace/klipper/historyitem.h | 127 + plasma/workspace/klipper/historymodel.cpp | 198 ++ plasma/workspace/klipper/historymodel.h | 76 + .../workspace/klipper/historystringitem.cpp | 27 + plasma/workspace/klipper/historystringitem.h | 50 + plasma/workspace/klipper/historyurlitem.cpp | 72 + plasma/workspace/klipper/historyurlitem.h | 38 + plasma/workspace/klipper/klipper.cpp | 1057 ++++++++ plasma/workspace/klipper/klipper.desktop | 273 ++ plasma/workspace/klipper/klipper.h | 225 ++ plasma/workspace/klipper/klipper.kcfg | 93 + plasma/workspace/klipper/klipperpopup.cpp | 268 ++ plasma/workspace/klipper/klipperpopup.h | 129 + plasma/workspace/klipper/klipperrc.desktop | 2416 +++++++++++++++++ .../workspace/klipper/klippersettings.kcfgc | 9 + plasma/workspace/klipper/main.cpp | 69 + .../workspace/klipper/org.kde.klipper.desktop | 274 ++ .../org.kde.plasma.clipboard.operations | 39 + .../klipper/plasma-dataengine-clipboard.json | 135 + plasma/workspace/klipper/popupproxy.cpp | 154 ++ plasma/workspace/klipper/popupproxy.h | 74 + plasma/workspace/klipper/tray.cpp | 50 + plasma/workspace/klipper/tray.h | 25 + plasma/workspace/klipper/urlgrabber.cpp | 433 +++ plasma/workspace/klipper/urlgrabber.h | 224 ++ plasma/workspace/klipper/utils.cpp | 41 + plasma/workspace/klipper/utils.h | 18 + plasma/workspace/krunner/CMakeLists.txt | 48 + .../KRunnerAppDBusInterfaceConfig.cmake.in | 4 + plasma/workspace/krunner/Messages.sh | 2 + .../krunner/dbus/org.kde.krunner.App.xml | 23 + plasma/workspace/krunner/main.cpp | 116 + .../krunner/org.kde.krunner.desktop.cmake | 110 + .../krunner/plasma-krunner.service.in | 12 + .../workspace/krunner/update/CMakeLists.txt | 13 + .../krunner/update/krunnerglobalshortcuts.cpp | 76 + .../update/krunnerglobalshortcuts2.upd | 3 + .../krunner/update/krunnerhistory.cpp | 48 + .../krunner/update/krunnerhistory.upd | 3 + plasma/workspace/krunner/view.cpp | 386 +++ plasma/workspace/krunner/view.h | 113 + plasma/workspace/ksmserver/CMakeLists.txt | 73 + plasma/workspace/ksmserver/Copyright.txt | 56 + .../KSMServerDBusInterfaceConfig.cmake.in | 3 + plasma/workspace/ksmserver/LICENSE | 16 + plasma/workspace/ksmserver/Messages.sh | 2 + plasma/workspace/ksmserver/README | 161 ++ plasma/workspace/ksmserver/client.cpp | 162 ++ plasma/workspace/ksmserver/client.h | 52 + .../ksmserver/config-ksmserver.h.cmake | 3 + plasma/workspace/ksmserver/global.h | 12 + plasma/workspace/ksmserver/legacy.cpp | 365 +++ plasma/workspace/ksmserver/logout.cpp | 580 ++++ plasma/workspace/ksmserver/main.cpp | 308 +++ .../ksmserver/org.kde.KSMServerInterface.xml | 50 + .../ksmserver/org.kde.KWin.Session.xml | 26 + .../ksmserver/org.kde.LogoutPrompt.xml | 8 + .../ksmserver/plasma-ksmserver.service.in | 12 + .../plasma-restoresession.service.in | 9 + plasma/workspace/ksmserver/server.cpp | 1053 +++++++ plasma/workspace/ksmserver/server.h | 231 ++ .../workspace/ksmserver/tests/CMakeLists.txt | 9 + .../ksmserver/tests/autostarttest.cpp | 44 + plasma/workspace/ksplash/CMakeLists.txt | 2 + plasma/workspace/ksplash/README | 62 + .../ksplash/ksplashqml/CMakeLists.txt | 26 + plasma/workspace/ksplash/ksplashqml/main.cpp | 63 + .../ksplash/ksplashqml/org.kde.KSplash.xml | 9 + .../ksplashqml/plasma-ksplash.service.in | 10 + .../ksplash/ksplashqml/splashapp.cpp | 153 ++ .../workspace/ksplash/ksplashqml/splashapp.h | 44 + .../ksplash/ksplashqml/splashwindow.cpp | 116 + .../ksplash/ksplashqml/splashwindow.h | 33 + .../ksplash/ksplashqml/themes/CMakeLists.txt | 2 + .../ksplashqml/themes/Classic/CMakeLists.txt | 9 + .../ksplashqml/themes/Classic/FadeIn.qml | 13 + .../ksplashqml/themes/Classic/Preview.png | Bin 0 -> 101141 bytes .../ksplashqml/themes/Classic/Theme.rc | 6 + .../themes/Classic/images/background.png | Bin 0 -> 1201431 bytes .../themes/Classic/images/icon1.png | Bin 0 -> 4748 bytes .../themes/Classic/images/icon2.png | Bin 0 -> 8531 bytes .../themes/Classic/images/icon3.png | Bin 0 -> 9428 bytes .../themes/Classic/images/icon4.png | Bin 0 -> 5468 bytes .../themes/Classic/images/icon5.png | Bin 0 -> 20953 bytes .../themes/Classic/images/rectangle.png | Bin 0 -> 8120 bytes .../ksplashqml/themes/Classic/main.qml | 114 + .../themes/Minimalistic/CMakeLists.txt | 7 + .../themes/Minimalistic/Preview.png | Bin 0 -> 3935 bytes .../ksplashqml/themes/Minimalistic/Theme.rc | 9 + .../themes/Minimalistic/images/kdegear.png | Bin 0 -> 4375 bytes .../themes/Minimalistic/images/kdeletter.png | Bin 0 -> 2947 bytes .../Minimalistic/images/kdelogo-contrast.png | Bin 0 -> 22780 bytes .../themes/Minimalistic/images/kdelogo.png | Bin 0 -> 20156 bytes .../themes/Minimalistic/images/kdemask.png | Bin 0 -> 1090 bytes .../ksplashqml/themes/Minimalistic/main.qml | 168 ++ plasma/workspace/ksplash/none/CMakeLists.txt | 1 + plasma/workspace/ksplash/none/Theme.rc | 7 + plasma/workspace/ktimezoned/CMakeLists.txt | 17 + plasma/workspace/ktimezoned/ktimezoned.cpp | 173 ++ plasma/workspace/ktimezoned/ktimezoned.h | 36 + plasma/workspace/ktimezoned/ktimezoned.json | 110 + .../workspace/ktimezoned/ktimezoned_win.cpp | 131 + plasma/workspace/ktimezoned/ktimezoned_win.h | 28 + plasma/workspace/ktimezoned/ktimezonedbase.h | 51 + .../ktimezoned/org.kde.KTimeZoned.xml | 11 + .../workspace/libcolorcorrect/CMakeLists.txt | 58 + .../LibColorCorrectConfig.cmake.in | 8 + plasma/workspace/libcolorcorrect/README | 5 + .../libcolorcorrect/colorcorrectconstants.h | 17 + .../compositorcoloradaptor.cpp | 111 + .../libcolorcorrect/compositorcoloradaptor.h | 97 + .../declarative/CMakeLists.txt | 10 + .../declarative/colorcorrectplugin.cpp | 24 + .../declarative/colorcorrectplugin.h | 22 + .../libcolorcorrect/declarative/qmldir | 3 + .../workspace/libcolorcorrect/geolocator.cpp | 71 + plasma/workspace/libcolorcorrect/geolocator.h | 58 + .../libcolorcorrect/kded/CMakeLists.txt | 8 + .../kded/colorcorrectlocationupdater.json | 80 + .../libcolorcorrect/kded/locationupdater.cpp | 46 + .../libcolorcorrect/kded/locationupdater.h | 31 + plasma/workspace/libcolorcorrect/suncalc.cpp | 165 ++ plasma/workspace/libcolorcorrect/suncalc.h | 43 + plasma/workspace/libdbusmenuqt/CMakeLists.txt | 27 + plasma/workspace/libdbusmenuqt/README | 2 + .../libdbusmenuqt/com.canonical.dbusmenu.xml | 49 + .../libdbusmenuqt/dbusmenuimporter.cpp | 536 ++++ .../libdbusmenuqt/dbusmenuimporter.h | 94 + .../libdbusmenuqt/dbusmenushortcut_p.cpp | 69 + .../libdbusmenuqt/dbusmenushortcut_p.h | 22 + .../libdbusmenuqt/dbusmenutypes_p.cpp | 122 + .../workspace/libdbusmenuqt/dbusmenutypes_p.h | 80 + .../libdbusmenuqt/test/CMakeLists.txt | 3 + plasma/workspace/libdbusmenuqt/test/README | 2 + plasma/workspace/libdbusmenuqt/test/main.cpp | 71 + plasma/workspace/libdbusmenuqt/utils.cpp | 50 + plasma/workspace/libdbusmenuqt/utils_p.h | 14 + plasma/workspace/libkworkspace/CMakeLists.txt | 107 + .../LibKWorkspaceConfig.cmake.in | 6 + plasma/workspace/libkworkspace/Messages.sh | 2 + .../autostartscriptdesktopfile.cpp | 34 + .../autostartscriptdesktopfile.h | 32 + .../libkworkspace/autotests/CMakeLists.txt | 5 + .../autotests/testPlatformDetection.cpp | 108 + .../config-libkworkspace.h.cmake | 2 + .../libkworkspace/kdisplaymanager.cpp | 884 ++++++ .../workspace/libkworkspace/kdisplaymanager.h | 102 + plasma/workspace/libkworkspace/kworkspace.cpp | 139 + plasma/workspace/libkworkspace/kworkspace.h | 153 ++ .../login1_manager_interface.cpp | 54 + .../workspace/libkworkspace/loginddbustypes.h | 172 ++ .../org.freedesktop.ConsoleKit.Manager.xml | 299 ++ .../libkworkspace/org.freedesktop.UPower.xml | 395 +++ .../org.freedesktop.login1.Manager.xml | 220 ++ .../org.freedesktop.login1.Seat.xml | 43 + .../org.freedesktop.login1.Session.xml | 132 + .../org.freedesktop.login1.User.xml | 56 + .../libkworkspace/sessionmanagement.cpp | 214 ++ .../libkworkspace/sessionmanagement.h | 112 + .../sessionmanagementbackend.cpp | 267 ++ .../libkworkspace/sessionmanagementbackend.h | 187 ++ .../libkworkspace/tests/CMakeLists.txt | 5 + .../libkworkspace/tests/sessiontest.cpp | 33 + .../libkworkspace/tests/syncdbusenvtest.cpp | 19 + .../libkworkspace/updatelaunchenvjob.cpp | 160 ++ .../libkworkspace/updatelaunchenvjob.h | 42 + .../libnotificationmanager/CMakeLists.txt | 133 + .../LibNotificationManagerConfig.cmake.in | 9 + .../libnotificationmanager/Messages.sh | 2 + .../abstractnotificationsmodel.cpp | 471 ++++ .../abstractnotificationsmodel.h | 72 + .../abstractnotificationsmodel_p.h | 47 + .../autotests/CMakeLists.txt | 8 + .../autotests/notifications_test.cpp | 146 + .../dbus/org.freedesktop.Notifications.xml | 69 + .../dbus/org.kde.JobViewServer.xml | 17 + .../dbus/org.kde.JobViewServerV2.xml | 23 + .../dbus/org.kde.JobViewV2.xml | 57 + .../dbus/org.kde.JobViewV3.xml | 39 + .../dbus/org.kde.kuiserver.xml | 28 + .../dbus/org.kde.notificationmanager.xml | 11 + .../declarative/CMakeLists.txt | 9 + .../declarative/notificationmanagerplugin.cpp | 35 + .../declarative/notificationmanagerplugin.h | 22 + .../libnotificationmanager/declarative/qmldir | 2 + .../workspace/libnotificationmanager/job.cpp | 291 ++ plasma/workspace/libnotificationmanager/job.h | 243 ++ .../libnotificationmanager/job_p.cpp | 472 ++++ .../workspace/libnotificationmanager/job_p.h | 176 ++ .../libnotificationmanager/jobsmodel.cpp | 238 ++ .../libnotificationmanager/jobsmodel.h | 86 + .../libnotificationmanager/jobsmodel_p.cpp | 486 ++++ .../libnotificationmanager/jobsmodel_p.h | 87 + .../kcfg/badgesettings.kcfg | 14 + .../kcfg/badgesettings.kcfgc | 10 + .../kcfg/behaviorsettings.kcfg | 24 + .../kcfg/behaviorsettings.kcfgc | 10 + .../kcfg/donotdisturbsettings.kcfg | 25 + .../kcfg/donotdisturbsettings.kcfgc | 10 + .../kcfg/jobsettings.kcfg | 20 + .../kcfg/jobsettings.kcfgc | 10 + .../kcfg/notificationsettings.kcfg | 38 + .../kcfg/notificationsettings.kcfgc | 12 + .../limitedrowcountproxymodel.cpp | 64 + .../limitedrowcountproxymodel_p.h | 32 + .../mirroredscreenstracker.cpp | 88 + .../mirroredscreenstracker_p.h | 51 + .../libnotificationmanager/notification.cpp | 792 ++++++ .../libnotificationmanager/notification.h | 143 + .../libnotificationmanager/notification_p.h | 103 + .../notificationfilterproxymodel.cpp | 181 ++ .../notificationfilterproxymodel_p.h | 69 + .../notificationgroupcollapsingproxymodel.cpp | 215 ++ .../notificationgroupcollapsingproxymodel_p.h | 58 + .../notificationgroupingproxymodel.cpp | 519 ++++ .../notificationgroupingproxymodel_p.h | 51 + .../libnotificationmanager/notifications.cpp | 834 ++++++ .../libnotificationmanager/notifications.h | 562 ++++ .../notificationsmodel.cpp | 166 ++ .../notificationsmodel.h | 33 + .../notificationsortproxymodel.cpp | 105 + .../notificationsortproxymodel_p.h | 41 + .../libnotificationmanager/plasmanotifyrc | 21 + .../libnotificationmanager/server.cpp | 143 + .../workspace/libnotificationmanager/server.h | 220 ++ .../libnotificationmanager/server_p.cpp | 547 ++++ .../libnotificationmanager/server_p.h | 131 + .../libnotificationmanager/serverinfo.cpp | 154 ++ .../libnotificationmanager/serverinfo.h | 67 + .../libnotificationmanager/settings.cpp | 616 +++++ .../libnotificationmanager/settings.h | 342 +++ .../libnotificationmanager/utils.cpp | 127 + .../libnotificationmanager/utils_p.h | 31 + .../watchednotificationsmodel.cpp | 168 ++ .../watchednotificationsmodel.h | 41 + .../workspace/libtaskmanager/CMakeLists.txt | 128 + .../LibTaskManagerConfig.cmake.in | 9 + plasma/workspace/libtaskmanager/TODO.txt | 7 + .../libtaskmanager/abstracttasksmodel.cpp | 125 + .../libtaskmanager/abstracttasksmodel.h | 292 ++ .../libtaskmanager/abstracttasksmodeliface.h | 202 ++ .../abstracttasksproxymodeliface.cpp | 237 ++ .../abstracttasksproxymodeliface.h | 207 ++ .../abstractwindowtasksmodel.cpp | 43 + .../libtaskmanager/abstractwindowtasksmodel.h | 35 + .../workspace/libtaskmanager/activityinfo.cpp | 108 + .../workspace/libtaskmanager/activityinfo.h | 84 + .../libtaskmanager/autotests/CMakeLists.txt | 7 + .../data/applications/org.kde.dolphin.desktop | 15 + .../applications/org.kde.konversation.desktop | 15 + .../autotests/launchertasksmodeltest.cpp | 171 ++ .../autotests/tasktoolstest.cpp | 199 ++ .../concatenatetasksproxymodel.cpp | 25 + .../concatenatetasksproxymodel.h | 38 + .../libtaskmanager/declarative/CMakeLists.txt | 41 + .../declarative/pipewirecore.cpp | 100 + .../libtaskmanager/declarative/pipewirecore.h | 37 + .../declarative/pipewiresourceitem.cpp | 298 ++ .../declarative/pipewiresourceitem.h | 60 + .../declarative/pipewiresourcestream.cpp | 371 +++ .../declarative/pipewiresourcestream.h | 91 + .../libtaskmanager/declarative/qmldir | 2 + .../declarative/screencasting.cpp | 129 + .../declarative/screencasting.h | 77 + .../declarative/screencastingrequest.cpp | 135 + .../declarative/screencastingrequest.h | 59 + .../declarative/taskmanagerplugin.cpp | 43 + .../declarative/taskmanagerplugin.h | 23 + .../flattentaskgroupsproxymodel.cpp | 45 + .../flattentaskgroupsproxymodel.h | 45 + .../libtaskmanager/launchertasksmodel.cpp | 555 ++++ .../libtaskmanager/launchertasksmodel.h | 173 ++ .../libtaskmanager/launchertasksmodel_p.h | 62 + .../libtaskmanager/startuptasksmodel.cpp | 95 + .../libtaskmanager/startuptasksmodel.h | 48 + .../libtaskmanager/taskfilterproxymodel.cpp | 352 +++ .../libtaskmanager/taskfilterproxymodel.h | 324 +++ .../libtaskmanager/taskgroupingproxymodel.cpp | 1227 +++++++++ .../libtaskmanager/taskgroupingproxymodel.h | 392 +++ .../libtaskmanager/taskmanagerrulesrc | 16 + .../workspace/libtaskmanager/tasksmodel.cpp | 1910 +++++++++++++ plasma/workspace/libtaskmanager/tasksmodel.h | 897 ++++++ plasma/workspace/libtaskmanager/tasktools.cpp | 809 ++++++ plasma/workspace/libtaskmanager/tasktools.h | 177 ++ .../libtaskmanager/virtualdesktopinfo.cpp | 518 ++++ .../libtaskmanager/virtualdesktopinfo.h | 140 + .../waylandstartuptasksmodel.cpp | 210 ++ .../libtaskmanager/waylandstartuptasksmodel.h | 29 + .../libtaskmanager/waylandtasksmodel.cpp | 767 ++++++ .../libtaskmanager/waylandtasksmodel.h | 224 ++ .../libtaskmanager/windowtasksmodel.cpp | 94 + .../libtaskmanager/windowtasksmodel.h | 48 + .../libtaskmanager/xstartuptasksmodel.cpp | 264 ++ .../libtaskmanager/xstartuptasksmodel.h | 29 + .../xwindowsystemeventbatcher.cpp | 61 + .../xwindowsystemeventbatcher.h | 37 + .../libtaskmanager/xwindowtasksmodel.cpp | 1127 ++++++++ .../libtaskmanager/xwindowtasksmodel.h | 229 ++ .../workspace/login-sessions/CMakeLists.txt | 27 + .../login-sessions/install-sessions.sh.cmake | 28 + .../plasmawayland-dev.desktop.cmake | 99 + .../plasmawayland.desktop.cmake | 104 + .../plasmax11-dev.desktop.cmake | 100 + .../login-sessions/plasmax11.desktop.cmake | 100 + .../login-sessions/startplasma-dev.sh.cmake | 15 + .../workspace/logout-greeter/CMakeLists.txt | 31 + plasma/workspace/logout-greeter/greeter.cpp | 127 + plasma/workspace/logout-greeter/greeter.h | 46 + plasma/workspace/logout-greeter/main.cpp | 63 + .../workspace/logout-greeter/shutdowndlg.cpp | 275 ++ plasma/workspace/logout-greeter/shutdowndlg.h | 50 + .../logout-greeter/tests/CMakeLists.txt | 5 + .../logout-greeter/tests/config.h.cmake | 1 + .../workspace/logout-greeter/tests/main.cpp | 100 + .../lookandfeel.dark/contents/defaults | 29 + .../contents/previews/fullscreenpreview.jpg | Bin 0 -> 177059 bytes .../contents/previews/preview.png | Bin 0 -> 183021 bytes .../workspace/lookandfeel.dark/metadata.json | 114 + .../lookandfeel.twilight/contents/defaults | 29 + .../contents/previews/fullscreenpreview.jpg | Bin 0 -> 182221 bytes .../contents/previews/preview.png | Bin 0 -> 182256 bytes .../lookandfeel.twilight/metadata.json | 112 + plasma/workspace/lookandfeel/Messages.sh | 2 + .../contents/components/ActionButton.qml | 116 + .../contents/components/Battery.qml | 54 + .../lookandfeel/contents/components/Clock.qml | 38 + .../components/SessionManagementScreen.qml | 116 + .../contents/components/UserDelegate.qml | 178 ++ .../contents/components/UserList.qml | 90 + .../contents/components/VirtualKeyboard.qml | 71 + .../components/VirtualKeyboard_wayland.qml | 25 + .../contents/components/WallpaperFader.qml | 162 ++ .../contents/components/artwork/README.txt | 1 + .../components/artwork/logout_primary.svgz | Bin 0 -> 2514 bytes .../components/artwork/restart_primary.svgz | Bin 0 -> 1860 bytes .../components/artwork/shutdown_primary.svgz | Bin 0 -> 1738 bytes .../workspace/lookandfeel/contents/defaults | 29 + .../desktopswitcher/DesktopSwitcher.qml | 186 ++ .../contents/lockscreen/LockOsd.qml | 64 + .../contents/lockscreen/LockScreen.qml | 63 + .../contents/lockscreen/LockScreenUi.qml | 570 ++++ .../contents/lockscreen/MainBlock.qml | 103 + .../contents/lockscreen/MediaControls.qml | 163 ++ .../contents/lockscreen/config.qml | 32 + .../contents/lockscreen/config.xml | 19 + .../lookandfeel/contents/logout/Logout.qml | 250 ++ .../contents/logout/LogoutButton.qml | 43 + .../logout/dummydata/screenGeometry.qml | 11 + .../lookandfeel/contents/logout/timer.js | 26 + .../lookandfeel/contents/osd/Osd.qml | 27 + .../lookandfeel/contents/osd/OsdItem.qml | 109 + .../contents/previews/desktopswitcher.png | 0 .../contents/previews/fullscreenpreview.jpg | Bin 0 -> 187129 bytes .../contents/previews/lockscreen.png | Bin 0 -> 135936 bytes .../contents/previews/loginmanager.png | 0 .../lookandfeel/contents/previews/preview.png | Bin 0 -> 182218 bytes .../contents/previews/runcommand.png | 0 .../lookandfeel/contents/previews/splash.png | Bin 0 -> 1231 bytes .../contents/previews/userswitcher.png | 0 .../contents/previews/windowdecoration.png | 0 .../contents/previews/windowswitcher.png | 0 .../contents/runcommand/RunCommand.qml | 402 +++ .../lookandfeel/contents/splash/Splash.qml | 100 + .../contents/splash/images/busywidget.svgz | Bin 0 -> 1338 bytes .../contents/splash/images/kde.svgz | Bin 0 -> 1789 bytes .../contents/splash/images/plasma.svgz | Bin 0 -> 1150 bytes .../contents/systemdialog/SystemDialog.qml | 115 + .../windowswitcher/WindowSwitcher.qml | 133 + plasma/workspace/lookandfeel/metadata.json | 179 ++ plasma/workspace/menu/CMakeLists.txt | 1 + plasma/workspace/menu/desktop/CMakeLists.txt | 43 + .../workspace/menu/desktop/hidden.directory | 60 + .../kf5-development-translation.directory | 95 + .../kf5-development-webdevelopment.directory | 94 + .../menu/desktop/kf5-development.directory | 97 + .../menu/desktop/kf5-editors.directory | 96 + .../menu/desktop/kf5-edu-languages.directory | 91 + .../desktop/kf5-edu-mathematics.directory | 91 + .../desktop/kf5-edu-miscellaneous.directory | 91 + .../menu/desktop/kf5-edu-science.directory | 92 + .../menu/desktop/kf5-edu-tools.directory | 89 + .../menu/desktop/kf5-education.directory | 63 + .../menu/desktop/kf5-games-arcade.directory | 94 + .../menu/desktop/kf5-games-board.directory | 94 + .../menu/desktop/kf5-games-card.directory | 95 + .../menu/desktop/kf5-games-kids.directory | 95 + .../menu/desktop/kf5-games-logic.directory | 83 + .../desktop/kf5-games-roguelikes.directory | 91 + .../menu/desktop/kf5-games-strategy.directory | 93 + .../menu/desktop/kf5-games.directory | 97 + .../menu/desktop/kf5-graphics.directory | 97 + .../desktop/kf5-internet-terminal.directory | 60 + .../menu/desktop/kf5-internet.directory | 97 + .../workspace/menu/desktop/kf5-main.directory | 95 + .../workspace/menu/desktop/kf5-more.directory | 60 + .../menu/desktop/kf5-multimedia.directory | 98 + .../menu/desktop/kf5-network.directory | 47 + .../menu/desktop/kf5-office.directory | 98 + .../menu/desktop/kf5-science.directory | 94 + .../menu/desktop/kf5-settingsmenu.directory | 95 + .../desktop/kf5-system-terminal.directory | 60 + .../menu/desktop/kf5-system.directory | 63 + .../workspace/menu/desktop/kf5-toys.directory | 95 + .../menu/desktop/kf5-unknown.directory | 93 + .../kf5-utilities-accessibility.directory | 189 ++ .../desktop/kf5-utilities-desktop.directory | 118 + .../menu/desktop/kf5-utilities-file.directory | 123 + .../kf5-utilities-peripherals.directory | 181 ++ .../menu/desktop/kf5-utilities-pim.directory | 177 ++ .../desktop/kf5-utilities-xutils.directory | 151 ++ .../menu/desktop/kf5-utilities.directory | 119 + plasma/workspace/metainfo.yaml | 31 + plasma/workspace/phonon/CMakeLists.txt | 8 + .../phonon/platform_kde/CMakeLists.txt | 11 + .../workspace/phonon/platform_kde/Messages.sh | 17 + .../workspace/phonon/platform_kde/debug.cpp | 3 + plasma/workspace/phonon/platform_kde/debug.h | 5 + .../phonon/platform_kde/kdeplatformplugin.cpp | 144 + .../phonon/platform_kde/kdeplatformplugin.h | 42 + .../phonon/platform_kde/kiomediastream.cpp | 246 ++ .../phonon/platform_kde/kiomediastream.h | 42 + .../phonon/platform_kde/kiomediastream_p.h | 61 + .../phonon/platform_kde/phonon.notifyrc | 271 ++ .../phonon/platform_kde/phononbackend.desktop | 116 + .../phonon/platform_kde/phononbackend.json | 85 + .../workspace/plasma-windowed/CMakeLists.txt | 30 + plasma/workspace/plasma-windowed/main.cpp | 63 + .../org.kde.plasmawindowed.desktop.cmake | 111 + .../plasma-windowed/plasmawindowedcorona.cpp | 165 ++ .../plasma-windowed/plasmawindowedcorona.h | 30 + .../plasma-windowed/plasmawindowedview.cpp | 313 +++ .../plasma-windowed/plasmawindowedview.h | 54 + plasma/workspace/plasma-workspace.categories | 5 + .../plasmacalendarintegration/CMakeLists.txt | 15 + .../HolidaysConfig.qml | 102 + .../plasmacalendarintegration/Messages.sh | 2 + .../holidayeventsplugin.json | 104 + .../holidaysevents.cpp | 72 + .../holidaysevents.h | 33 + .../qmlhelper/CMakeLists.txt | 10 + .../qmlhelper/holidayeventshelperplugin.cpp | 68 + .../qmlhelper/holidayeventshelperplugin.h | 17 + .../qmlhelper/qmldir | 3 + plasma/workspace/runners/CMakeLists.txt | 24 + .../runners/appstream/CMakeLists.txt | 14 + .../workspace/runners/appstream/Messages.sh | 2 + .../runners/appstream/appstreamrunner.cpp | 166 ++ .../runners/appstream/appstreamrunner.h | 29 + .../appstream/plasma-runner-appstream.json | 125 + plasma/workspace/runners/baloo/CMakeLists.txt | 35 + plasma/workspace/runners/baloo/Messages.sh | 2 + .../runners/baloo/baloosearchrunner.cpp | 163 ++ .../runners/baloo/baloosearchrunner.h | 31 + plasma/workspace/runners/baloo/dbusutils_p.h | 81 + .../runners/baloo/org.kde.krunner1.xml | 55 + .../baloo/plasma-baloorunner.service.in | 12 + .../baloo/plasma-runner-baloosearch.desktop | 120 + plasma/workspace/runners/bookmarks/.gitignore | 1 + .../runners/bookmarks/CMakeLists.txt | 44 + .../workspace/runners/bookmarks/Messages.sh | 2 + .../bookmarks/autotests/CMakeLists.txt | 16 + .../autotests/bookmarksmatchtest.cpp | 79 + .../.config/chromium/Local State | 65 + .../Chrome-Bookmarks-Sample.json | 55 + .../Chrome-Bookmarks-SecondProfile.json | 20 + .../autotests/chrome/testchromebookmarks.cpp | 106 + .../autotests/chrome/testchromebookmarks.h | 52 + .../atnsd8ae.testmekde/favicons.sqlite | Bin 0 -> 5242880 bytes .../favicons.sqlite.license | 2 + .../atnsd8ae.testmekde/places.sqlite | Bin 0 -> 5242880 bytes .../atnsd8ae.testmekde/places.sqlite.license | 2 + .../firefox/firefox-config-home/profiles.ini | 16 + .../firefox/testfirefoxbookmarks.cpp | 70 + .../runners/bookmarks/bookmarkmatch.cpp | 73 + .../runners/bookmarks/bookmarkmatch.h | 45 + .../runners/bookmarks/bookmarksrunner.cpp | 115 + .../runners/bookmarks/bookmarksrunner.h | 40 + .../runners/bookmarks/bookmarksrunner_defs.h | 10 + .../runners/bookmarks/browserfactory.cpp | 46 + .../runners/bookmarks/browserfactory.h | 23 + .../runners/bookmarks/browsers/browser.h | 99 + .../runners/bookmarks/browsers/chrome.cpp | 128 + .../runners/bookmarks/browsers/chrome.h | 37 + .../bookmarks/browsers/chromefindprofile.cpp | 53 + .../bookmarks/browsers/chromefindprofile.h | 23 + .../runners/bookmarks/browsers/falkon.cpp | 52 + .../runners/bookmarks/browsers/falkon.h | 28 + .../runners/bookmarks/browsers/findprofile.h | 62 + .../runners/bookmarks/browsers/firefox.cpp | 192 ++ .../runners/bookmarks/browsers/firefox.h | 35 + .../runners/bookmarks/browsers/konqueror.cpp | 93 + .../runners/bookmarks/browsers/konqueror.h | 38 + .../runners/bookmarks/browsers/opera.cpp | 86 + .../runners/bookmarks/browsers/opera.h | 28 + .../workspace/runners/bookmarks/favicon.cpp | 14 + plasma/workspace/runners/bookmarks/favicon.h | 50 + .../runners/bookmarks/faviconfromblob.cpp | 122 + .../runners/bookmarks/faviconfromblob.h | 35 + .../runners/bookmarks/fetchsqlite.cpp | 114 + .../workspace/runners/bookmarks/fetchsqlite.h | 34 + .../bookmarks/plasma-runner-bookmarks.json | 109 + .../runners/calculator/CMakeLists.txt | 33 + .../workspace/runners/calculator/Messages.sh | 2 + .../calculator/autotests/CMakeLists.txt | 5 + .../autotests/calculatorrunnertest.cpp | 88 + .../runners/calculator/calculatorrunner.cpp | 192 ++ .../runners/calculator/calculatorrunner.h | 42 + .../calculator/plasma-runner-calculator.json | 160 ++ .../runners/calculator/qalculate_engine.cpp | 124 + .../runners/calculator/qalculate_engine.h | 38 + .../runners/helprunner/CMakeLists.txt | 2 + .../runners/helprunner/helprunner.cpp | 113 + .../workspace/runners/helprunner/helprunner.h | 29 + .../runners/helprunner/helprunner.json | 99 + plasma/workspace/runners/kill/CMakeLists.txt | 28 + plasma/workspace/runners/kill/Messages.sh | 3 + plasma/workspace/runners/kill/TODO | 0 plasma/workspace/runners/kill/config_keys.h | 19 + plasma/workspace/runners/kill/killrunner.cpp | 172 ++ plasma/workspace/runners/kill/killrunner.h | 61 + .../runners/kill/killrunner_config.cpp | 83 + .../runners/kill/killrunner_config.h | 38 + .../runners/kill/killrunner_config.ui | 141 + .../runners/kill/plasma-runner-kill.json | 146 + .../runners/locations/CMakeLists.txt | 18 + .../workspace/runners/locations/Messages.sh | 2 + .../locations/autotests/CMakeLists.txt | 2 + .../autotests/locationsrunnertest.cpp | 127 + .../runners/locations/locationrunner.cpp | 112 + .../runners/locations/locationrunner.h | 21 + .../locations/plasma-runner-locations.json | 158 ++ .../workspace/runners/places/CMakeLists.txt | 14 + plasma/workspace/runners/places/Messages.sh | 2 + .../workspace/runners/places/placesrunner.cpp | 160 ++ .../workspace/runners/places/placesrunner.h | 46 + .../runners/places/plasma-runner-places.json | 155 ++ .../runners/powerdevil/CMakeLists.txt | 12 + .../workspace/runners/powerdevil/Messages.sh | 3 + .../runners/powerdevil/PowerDevilRunner.cpp | 233 ++ .../runners/powerdevil/PowerDevilRunner.h | 47 + .../powerdevil/plasma-runner-powerdevil.json | 129 + .../runners/recentdocuments/CMakeLists.txt | 11 + .../runners/recentdocuments/Messages.sh | 2 + .../plasma-runner-recentdocuments.json | 87 + .../recentdocuments/recentdocuments.cpp | 117 + .../runners/recentdocuments/recentdocuments.h | 27 + .../workspace/runners/services/CMakeLists.txt | 31 + plasma/workspace/runners/services/Messages.sh | 2 + .../runners/services/autotests/CMakeLists.txt | 11 + .../autotests/fixtures/chrome-signal.desktop | 9 + .../autotests/fixtures/google-chrome.desktop | 22 + .../fixtures/kdesystemsettings.desktop | 13 + .../fixtures/org.kde.konsole.desktop | 25 + .../fixtures/org.kde.systemsettings.desktop | 15 + .../fixtures/org.kde.virtthings.desktop | 14 + .../fixtures/org.kde.yakuake.desktop | 12 + .../autotests/fixtures/virt-manager.desktop | 8 + .../services/autotests/servicerunnertest.cpp | 290 ++ .../services/plasma-runner-services.json | 171 ++ plasma/workspace/runners/services/plugin.cpp | 13 + .../runners/services/servicerunner.cpp | 488 ++++ .../runners/services/servicerunner.h | 37 + .../workspace/runners/sessions/CMakeLists.txt | 10 + plasma/workspace/runners/sessions/Messages.sh | 2 + .../sessions/plasma-runner-sessions.json | 156 ++ .../runners/sessions/sessionrunner.cpp | 233 ++ .../runners/sessions/sessionrunner.h | 43 + plasma/workspace/runners/shell/CMakeLists.txt | 17 + plasma/workspace/runners/shell/Messages.sh | 4 + .../runners/shell/autotests/CMakeLists.txt | 6 + .../shell/autotests/shellrunnertest.cpp | 126 + .../runners/shell/plasma-runner-shell.json | 159 ++ .../workspace/runners/shell/shellrunner.cpp | 104 + plasma/workspace/runners/shell/shellrunner.h | 31 + .../runners/webshortcuts/CMakeLists.txt | 9 + .../runners/webshortcuts/Messages.sh | 2 + .../plasma-runner-webshortcuts.json | 107 + .../webshortcuts/webshortcutrunner.cpp | 188 ++ .../runners/webshortcuts/webshortcutrunner.h | 38 + .../runners/windowedwidgets/CMakeLists.txt | 4 + .../runners/windowedwidgets/Messages.sh | 2 + .../plasma-runner-windowedwidgets.json | 142 + .../windowedwidgets/windowedwidgetsrunner.cpp | 106 + .../windowedwidgets/windowedwidgetsrunner.h | 40 + plasma/workspace/sddm-theme/Background.qml | 56 + .../workspace/sddm-theme/BreezeMenuStyle.qml | 25 + .../workspace/sddm-theme/KeyboardButton.qml | 38 + plasma/workspace/sddm-theme/Login.qml | 129 + plasma/workspace/sddm-theme/Main.qml | 606 +++++ plasma/workspace/sddm-theme/SessionButton.qml | 43 + plasma/workspace/sddm-theme/components | 1 + plasma/workspace/sddm-theme/default-logo.svg | 4 + .../workspace/sddm-theme/dummydata/config.qml | 8 + .../sddm-theme/dummydata/keyboard.qml | 9 + .../sddm-theme/dummydata/screenModel.qml | 32 + .../workspace/sddm-theme/dummydata/sddm.qml | 50 + .../sddm-theme/dummydata/sessionModel.qml | 11 + .../sddm-theme/dummydata/userModel.qml | 35 + plasma/workspace/sddm-theme/faces/.face.icon | 14 + plasma/workspace/sddm-theme/metadata.desktop | 108 + plasma/workspace/sddm-theme/preview.png | Bin 0 -> 717013 bytes plasma/workspace/sddm-theme/theme.conf.cmake | 8 + .../sddm-wayland-session/plasma-wayland.conf | 7 + plasma/workspace/shell/CMakeLists.txt | 126 + plasma/workspace/shell/Messages.sh | 2 + plasma/workspace/shell/alternativeshelper.cpp | 80 + plasma/workspace/shell/alternativeshelper.h | 32 + .../workspace/shell/autotests/CMakeLists.txt | 38 + .../workspace/shell/autotests/desktopview.cpp | 38 + .../workspace/shell/autotests/desktopview.h | 31 + .../shell/autotests/mockserver/CMakeLists.txt | 47 + .../autotests/mockserver/corecompositor.cpp | 136 + .../autotests/mockserver/corecompositor.h | 228 ++ .../autotests/mockserver/coreprotocol.cpp | 89 + .../shell/autotests/mockserver/coreprotocol.h | 117 + .../autotests/mockserver/mockcompositor.cpp | 47 + .../autotests/mockserver/mockcompositor.h | 98 + .../autotests/mockserver/primaryoutput.cpp | 53 + .../autotests/mockserver/primaryoutput.h | 57 + .../autotests/mockserver/xdgoutputv1.cpp | 59 + .../shell/autotests/mockserver/xdgoutputv1.h | 89 + .../shell/autotests/screenpooltest.cpp | 416 +++ .../shell/config-ktexteditor.h.cmake | 2 + plasma/workspace/shell/config-plasma.h.cmake | 1 + .../workspace/shell/containmentconfigview.cpp | 239 ++ .../workspace/shell/containmentconfigview.h | 68 + plasma/workspace/shell/coronatesthelper.cpp | 100 + plasma/workspace/shell/coronatesthelper.h | 33 + .../shell/currentcontainmentactionsmodel.cpp | 260 ++ .../shell/currentcontainmentactionsmodel.h | 60 + .../shell/dbus/org.kde.PlasmaShell.xml | 28 + plasma/workspace/shell/desktopview.cpp | 354 +++ plasma/workspace/shell/desktopview.h | 99 + plasma/workspace/shell/futureutil.h | 17 + plasma/workspace/shell/main.cpp | 242 ++ .../shell/org.kde.plasmashell.desktop.cmake | 70 + plasma/workspace/shell/osd.cpp | 256 ++ plasma/workspace/shell/osd.h | 70 + .../shell/packageplugins/CMakeLists.txt | 5 + .../layouttemplate/CMakeLists.txt | 10 + .../layouttemplate/layouttemplate.cpp | 21 + .../layouttemplate/layouttemplate.h | 20 + ...lasma-packagestructure-layouttemplate.json | 96 + .../packageplugins/lookandfeel/CMakeLists.txt | 9 + .../lookandfeel/lookandfeel.cpp | 99 + .../packageplugins/lookandfeel/lookandfeel.h | 21 + .../plasma-packagestructure-lookandfeel.json | 101 + .../qmlWallpaper/CMakeLists.txt | 11 + .../plasma-packagestructure-wallpaper.json | 99 + .../packageplugins/qmlWallpaper/wallpaper.cpp | 58 + .../packageplugins/qmlWallpaper/wallpaper.h | 20 + .../shell/packageplugins/shell/CMakeLists.txt | 10 + .../shell/packageplugins/shell/Messages.sh | 2 + .../plasma-packagestructure-plasma-shell.json | 97 + .../packageplugins/shell/shellpackage.cpp | 92 + .../shell/packageplugins/shell/shellpackage.h | 17 + .../wallpaperimages/CMakeLists.txt | 10 + ...asma-packagestructure-wallpaperimages.json | 95 + .../wallpaperimages/wallpaperpackage.cpp | 68 + .../wallpaperimages/wallpaperpackage.h | 20 + plasma/workspace/shell/panelconfigview.cpp | 297 ++ plasma/workspace/shell/panelconfigview.h | 87 + plasma/workspace/shell/panelshadows.cpp | 288 ++ plasma/workspace/shell/panelshadows_p.h | 32 + plasma/workspace/shell/panelview.cpp | 1447 ++++++++++ plasma/workspace/shell/panelview.h | 266 ++ .../shell/plasma-plasmashell.service.in | 15 + .../workspace/shell/primaryoutputwatcher.cpp | 159 ++ plasma/workspace/shell/primaryoutputwatcher.h | 54 + plasma/workspace/shell/screenpool.cpp | 559 ++++ plasma/workspace/shell/screenpool.h | 90 + .../shell/scripting/appinterface.cpp | 215 ++ .../workspace/shell/scripting/appinterface.h | 89 + plasma/workspace/shell/scripting/applet.cpp | 266 ++ plasma/workspace/shell/scripting/applet.h | 69 + .../workspace/shell/scripting/configgroup.cpp | 207 ++ .../workspace/shell/scripting/configgroup.h | 58 + .../workspace/shell/scripting/containment.cpp | 276 ++ .../workspace/shell/scripting/containment.h | 80 + plasma/workspace/shell/scripting/panel.cpp | 297 ++ plasma/workspace/shell/scripting/panel.h | 93 + .../scripting/plasma-layouttemplate.desktop | 9 + .../shell/scripting/scriptengine.cpp | 424 +++ .../workspace/shell/scripting/scriptengine.h | 86 + .../shell/scripting/scriptengine_v1.cpp | 856 ++++++ .../shell/scripting/scriptengine_v1.h | 70 + plasma/workspace/shell/scripting/widget.cpp | 190 ++ plasma/workspace/shell/scripting/widget.h | 69 + .../shell/shellcontainmentconfig.cpp | 483 ++++ .../workspace/shell/shellcontainmentconfig.h | 145 + plasma/workspace/shell/shellcorona.cpp | 2313 ++++++++++++++++ plasma/workspace/shell/shellcorona.h | 274 ++ .../shell/softwarerendernotifier.cpp | 49 + .../workspace/shell/softwarerendernotifier.h | 26 + .../workspace/shell/standaloneappcorona.cpp | 229 ++ plasma/workspace/shell/standaloneappcorona.h | 54 + plasma/workspace/shell/strutmanager.cpp | 100 + plasma/workspace/shell/strutmanager.h | 41 + plasma/workspace/shell/tests/CMakeLists.txt | 30 + .../org.kde.plasmashelltest/metadata.desktop | 9 + .../workspace/shell/tests/screenpooltest.cpp | 78 + plasma/workspace/shell/userfeedback.cpp | 79 + plasma/workspace/shell/userfeedback.h | 29 + .../workspace/solidautoeject/CMakeLists.txt | 9 + plasma/workspace/solidautoeject/Messages.sh | 2 + .../solidautoeject/solidautoeject.cpp | 48 + .../workspace/solidautoeject/solidautoeject.h | 30 + .../solidautoeject/solidautoeject.json | 104 + plasma/workspace/soliduiserver/CMakeLists.txt | 20 + plasma/workspace/soliduiserver/Messages.sh | 3 + .../workspace/soliduiserver/soliduiserver.cpp | 161 ++ .../workspace/soliduiserver/soliduiserver.h | 39 + .../soliduiserver/soliduiserver.json | 92 + plasma/workspace/startkde/CMakeLists.txt | 57 + plasma/workspace/startkde/README | 9 + .../startkde/config-startplasma.h.cmake | 9 + .../startkde/kcheckrunning/kcheckrunning.cpp | 21 + .../startkde/kcheckrunning/kcheckrunning.h | 15 + .../workspace/startkde/kcminit/CMakeLists.txt | 24 + plasma/workspace/startkde/kcminit/Messages.sh | 2 + plasma/workspace/startkde/kcminit/main.cpp | 181 ++ plasma/workspace/startkde/kcminit/main.h | 28 + .../kcminit/plasma-kcminit-phase1.service.in | 10 + .../kcminit/plasma-kcminit.service.in | 13 + .../plasma-dbus-run-session-if-needed | 10 + .../startkde/plasma-session/CMakeLists.txt | 33 + .../workspace/startkde/plasma-session/README | 3 + .../startkde/plasma-session/autostart.cpp | 141 + .../startkde/plasma-session/autostart.h | 46 + .../startkde/plasma-session/main.cpp | 17 + .../plasma-session/org.kde.Startup.xml | 9 + .../plasma-autostart-list/CMakeLists.txt | 2 + .../plasma-autostart-list/main.cpp | 37 + .../startkde/plasma-session/sessiontrack.cpp | 47 + .../startkde/plasma-session/sessiontrack.h | 22 + .../startkde/plasma-session/signalhandler.cpp | 62 + .../startkde/plasma-session/signalhandler.h | 38 + .../startkde/plasma-session/startup.cpp | 450 +++ .../startkde/plasma-session/startup.h | 124 + .../startkde/plasma-shutdown/CMakeLists.txt | 24 + .../startkde/plasma-shutdown/main.cpp | 16 + .../plasma-shutdown/org.kde.Shutdown.xml | 8 + .../startkde/plasma-shutdown/shutdown.cpp | 105 + .../startkde/plasma-shutdown/shutdown.h | 30 + plasma/workspace/startkde/plasma-sourceenv.sh | 7 + .../startkde/plasmaautostart/CMakeLists.txt | 2 + .../plasmaautostart/plasmaautostart.cpp | 204 ++ .../plasmaautostart/plasmaautostart.h | 199 ++ .../startkde/startplasma-wayland.cpp | 102 + plasma/workspace/startkde/startplasma-x11.cpp | 99 + plasma/workspace/startkde/startplasma.cpp | 791 ++++++ plasma/workspace/startkde/startplasma.h | 51 + .../workspace/startkde/systemd/CMakeLists.txt | 11 + plasma/workspace/startkde/systemd/README.md | 30 + .../systemd/kde-systemd-start-condition.cpp | 41 + .../startkde/systemd/plasma-core.target | 7 + .../systemd/plasma-ksplash-ready.service.in | 10 + .../systemd/plasma-workspace-wayland.target | 4 + .../systemd/plasma-workspace-x11.target | 4 + .../startkde/systemd/plasma-workspace.target | 8 + .../startkde/waitforname/CMakeLists.txt | 28 + .../workspace/startkde/waitforname/main.cpp | 27 + .../waitforname/org.kde.KSplash.service.in | 3 + .../org.kde.plasma.Notifications.service.in | 3 + .../workspace/startkde/waitforname/waiter.cpp | 81 + .../workspace/startkde/waitforname/waiter.h | 30 + .../statusnotifierwatcher/CMakeLists.txt | 18 + .../statusnotifierwatcher.cpp | 123 + .../statusnotifierwatcher.h | 54 + .../statusnotifierwatcher.json | 103 + .../systemtraytypedefs.h | 32 + plasma/workspace/systemmonitor/CMakeLists.txt | 28 + plasma/workspace/systemmonitor/Messages.sh | 2 + .../workspace/systemmonitor/kdedksysguard.cpp | 63 + .../workspace/systemmonitor/kdedksysguard.h | 23 + plasma/workspace/systemmonitor/ksysguard.json | 93 + .../systemmonitor/ksystemactivitydialog.cpp | 88 + .../systemmonitor/ksystemactivitydialog.h | 48 + plasma/workspace/systemmonitor/main.cpp | 35 + .../org.kde.systemmonitor.desktop | 91 + plasma/workspace/themes/CMakeLists.txt | 22 + plasma/workspace/themes/qtcde.themerc | 172 ++ plasma/workspace/themes/qtcleanlooks.themerc | 137 + plasma/workspace/themes/qtgtk.themerc | 141 + plasma/workspace/themes/qtmotif.themerc | 165 ++ plasma/workspace/themes/qtplastique.themerc | 137 + plasma/workspace/themes/qtwindows.themerc | 168 ++ plasma/workspace/wallpapers/CMakeLists.txt | 3 + plasma/workspace/wallpapers/color/Messages.sh | 2 + .../wallpapers/color/contents/config/main.xml | 15 + .../wallpapers/color/contents/ui/config.qml | 24 + .../wallpapers/color/contents/ui/main.qml | 17 + .../workspace/wallpapers/color/metadata.json | 135 + .../workspace/wallpapers/image/CMakeLists.txt | 59 + plasma/workspace/wallpapers/image/Messages.sh | 2 + .../wallpapers/image/autotests/CMakeLists.txt | 11 + .../autotests/testfindpreferredimage.cpp | 87 + .../wallpapers/image/backgroundlistmodel.cpp | 557 ++++ .../wallpapers/image/backgroundlistmodel.h | 137 + plasma/workspace/wallpapers/image/image.cpp | 917 +++++++ plasma/workspace/wallpapers/image/image.h | 192 ++ .../imagepackage/contents/config/main.xml | 50 + .../contents/ui/WallpaperDelegate.qml | 123 + .../image/imagepackage/contents/ui/config.qml | 500 ++++ .../image/imagepackage/contents/ui/main.qml | 172 ++ .../image/imagepackage/metadata.json | 182 ++ .../imagepackage/setaswallpaper.desktop.in | 39 + .../wallpapers/image/imageplugin.cpp | 17 + .../workspace/wallpapers/image/imageplugin.h | 19 + .../image/plasma-apply-wallpaperimage.cpp | 105 + plasma/workspace/wallpapers/image/qmldir | 5 + .../wallpapers/image/slidefiltermodel.cpp | 190 ++ .../wallpapers/image/slidefiltermodel.h | 50 + .../workspace/wallpapers/image/slidemodel.cpp | 64 + .../workspace/wallpapers/image/slidemodel.h | 28 + .../slideshowpackage/contents/config/main.xml | 50 + .../image/slideshowpackage/metadata.json | 178 ++ .../wallpapers/image/wallpaper-mobile.knsrc | 53 + .../wallpapers/image/wallpaper.knsrc | 53 + .../workspace/xembed-sni-proxy/CMakeLists.txt | 66 + plasma/workspace/xembed-sni-proxy/Readme.md | 30 + .../xembed-sni-proxy/fdoselectionmanager.cpp | 235 ++ .../xembed-sni-proxy/fdoselectionmanager.h | 47 + plasma/workspace/xembed-sni-proxy/main.cpp | 60 + .../org.kde.StatusNotifierItem.xml | 63 + .../org.kde.StatusNotifierWatcher.xml | 42 + .../plasma-xembedsniproxy.service.in | 11 + plasma/workspace/xembed-sni-proxy/snidbus.cpp | 133 + plasma/workspace/xembed-sni-proxy/snidbus.h | 47 + .../workspace/xembed-sni-proxy/sniproxy.cpp | 596 ++++ plasma/workspace/xembed-sni-proxy/sniproxy.h | 153 ++ plasma/workspace/xembed-sni-proxy/xcbutils.h | 121 + .../xembed-sni-proxy/xembedsniproxy.desktop | 55 + .../xembed-sni-proxy/xtestsender.cpp | 19 + .../workspace/xembed-sni-proxy/xtestsender.h | 12 + 2150 files changed, 234244 insertions(+) create mode 100644 plasma/workspace/.kde-ci.yml create mode 100644 plasma/workspace/CMakeLists.txt create mode 100644 plasma/workspace/ConfigureChecks.cmake create mode 100644 plasma/workspace/ExtraDesktop.sh create mode 100644 plasma/workspace/LICENSES/BSD-2-Clause.txt create mode 100644 plasma/workspace/LICENSES/BSD-3-Clause.txt create mode 100644 plasma/workspace/LICENSES/CC0-1.0.txt create mode 100644 plasma/workspace/LICENSES/GPL-2.0-only.txt create mode 100644 plasma/workspace/LICENSES/GPL-2.0-or-later.txt create mode 100644 plasma/workspace/LICENSES/GPL-3.0-only.txt create mode 100644 plasma/workspace/LICENSES/LGPL-2.0-only.txt create mode 100644 plasma/workspace/LICENSES/LGPL-2.0-or-later.txt create mode 100644 plasma/workspace/LICENSES/LGPL-2.1-only.txt create mode 100644 plasma/workspace/LICENSES/LGPL-2.1-or-later.txt create mode 100644 plasma/workspace/LICENSES/LGPL-3.0-only.txt create mode 100644 plasma/workspace/LICENSES/LGPL-3.0-or-later.txt create mode 100644 plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt create mode 100644 plasma/workspace/LICENSES/MIT.txt create mode 100644 plasma/workspace/applets/CMakeLists.txt create mode 100644 plasma/workspace/applets/Mainpage.dox create mode 100644 plasma/workspace/applets/activitybar/Messages.sh create mode 100644 plasma/workspace/applets/activitybar/contents/ui/main.qml create mode 100644 plasma/workspace/applets/activitybar/metadata.json create mode 100644 plasma/workspace/applets/analog-clock/Messages.sh create mode 100644 plasma/workspace/applets/analog-clock/contents/config/config.qml create mode 100644 plasma/workspace/applets/analog-clock/contents/config/main.xml create mode 100644 plasma/workspace/applets/analog-clock/contents/ui/Hand.qml create mode 100644 plasma/workspace/applets/analog-clock/contents/ui/analogclock.qml create mode 100644 plasma/workspace/applets/analog-clock/contents/ui/configGeneral.qml create mode 100644 plasma/workspace/applets/analog-clock/metadata.json create mode 100644 plasma/workspace/applets/appmenu/CMakeLists.txt create mode 100644 plasma/workspace/applets/appmenu/Messages.sh create mode 100644 plasma/workspace/applets/appmenu/lib/CMakeLists.txt create mode 100644 plasma/workspace/applets/appmenu/lib/appmenuapplet.cpp create mode 100644 plasma/workspace/applets/appmenu/lib/appmenuapplet.h create mode 100644 plasma/workspace/applets/appmenu/package/contents/config/config.qml create mode 100644 plasma/workspace/applets/appmenu/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/appmenu/package/contents/ui/MenuDelegate.qml create mode 100644 plasma/workspace/applets/appmenu/package/contents/ui/configGeneral.qml create mode 100644 plasma/workspace/applets/appmenu/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/appmenu/package/metadata.json create mode 100644 plasma/workspace/applets/appmenu/plugin/CMakeLists.txt create mode 100644 plasma/workspace/applets/appmenu/plugin/appmenumodel.cpp create mode 100644 plasma/workspace/applets/appmenu/plugin/appmenumodel.h create mode 100644 plasma/workspace/applets/appmenu/plugin/appmenuplugin.cpp create mode 100644 plasma/workspace/applets/appmenu/plugin/appmenuplugin.h create mode 100644 plasma/workspace/applets/appmenu/plugin/qmldir create mode 100644 plasma/workspace/applets/batterymonitor/CMakeLists.txt create mode 100644 plasma/workspace/applets/batterymonitor/Messages.sh create mode 100644 plasma/workspace/applets/batterymonitor/README.txt create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/BadgeOverlay.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/BatteryItem.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/BrightnessItem.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/CompactRepresentation.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/InhibitionHint.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/PopupDialog.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/PowerManagementItem.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/PowerProfileItem.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/logic.js create mode 100644 plasma/workspace/applets/batterymonitor/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/batterymonitor/package/metadata.json create mode 100644 plasma/workspace/applets/calendar/CMakeLists.txt create mode 100644 plasma/workspace/applets/calendar/Messages.sh create mode 100644 plasma/workspace/applets/calendar/calendarapplet.cpp create mode 100644 plasma/workspace/applets/calendar/calendarapplet.h create mode 100644 plasma/workspace/applets/calendar/package/contents/config/config.qml create mode 100644 plasma/workspace/applets/calendar/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/calendar/package/contents/images/mini-calendar.svgz create mode 100644 plasma/workspace/applets/calendar/package/contents/ui/configGeneral.qml create mode 100644 plasma/workspace/applets/calendar/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/calendar/package/metadata.json create mode 100644 plasma/workspace/applets/clipboard/Messages.sh create mode 100644 plasma/workspace/applets/clipboard/contents/ui/BarcodePage.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/ClipboardItemDelegate.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/ClipboardPage.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/DelegateToolButtons.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/EditPage.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/ImageItemDelegate.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/Menu.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/TextItemDelegate.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/UrlItemDelegate.qml create mode 100644 plasma/workspace/applets/clipboard/contents/ui/clipboard.qml create mode 100644 plasma/workspace/applets/clipboard/metadata.json create mode 100644 plasma/workspace/applets/devicenotifier/CMakeLists.txt create mode 100644 plasma/workspace/applets/devicenotifier/Messages.sh create mode 100644 plasma/workspace/applets/devicenotifier/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/devicenotifier/package/contents/ui/DeviceItem.qml create mode 100644 plasma/workspace/applets/devicenotifier/package/contents/ui/FullRepresentation.qml create mode 100644 plasma/workspace/applets/devicenotifier/package/contents/ui/devicenotifier.qml create mode 100644 plasma/workspace/applets/devicenotifier/package/metadata.json create mode 100644 plasma/workspace/applets/devicenotifier/test-predicate-openinwindow.desktop create mode 100644 plasma/workspace/applets/digital-clock/CMakeLists.txt create mode 100644 plasma/workspace/applets/digital-clock/Messages.sh create mode 100644 plasma/workspace/applets/digital-clock/package/contents/config/config.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/CalendarView.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/DigitalClock.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/MonthMenu.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/Tooltip.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/configAppearance.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/configCalendar.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/configTimeZones.qml create mode 100644 plasma/workspace/applets/digital-clock/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/digital-clock/package/metadata.json create mode 100644 plasma/workspace/applets/digital-clock/plugin/CMakeLists.txt create mode 100644 plasma/workspace/applets/digital-clock/plugin/applicationintegration.cpp create mode 100644 plasma/workspace/applets/digital-clock/plugin/applicationintegration.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/clipboardmenu.cpp create mode 100644 plasma/workspace/applets/digital-clock/plugin/clipboardmenu.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.cpp create mode 100644 plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/qmldir create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonedata.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonemodel.cpp create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonemodel.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonesi18n.cpp create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonesi18n.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generate.rb create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h create mode 100644 plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h.erb create mode 100644 plasma/workspace/applets/icon/CMakeLists.txt create mode 100644 plasma/workspace/applets/icon/Messages.sh create mode 100644 plasma/workspace/applets/icon/iconapplet.cpp create mode 100644 plasma/workspace/applets/icon/iconapplet.h create mode 100644 plasma/workspace/applets/icon/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/icon/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/icon/package/metadata.json create mode 100644 plasma/workspace/applets/kicker/CMakeLists.txt create mode 100644 plasma/workspace/applets/kicker/Messages.sh create mode 100644 plasma/workspace/applets/kicker/plugin/abstractentry.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/abstractentry.h create mode 100644 plasma/workspace/applets/kicker/plugin/abstractmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/abstractmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/actionlist.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/actionlist.h create mode 100644 plasma/workspace/applets/kicker/plugin/appentry.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/appentry.h create mode 100644 plasma/workspace/applets/kicker/plugin/appsmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/appsmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/autotests/CMakeLists.txt create mode 100644 plasma/workspace/applets/kicker/plugin/autotests/qmltest.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/autotests/tst_triangleFilter.qml create mode 100644 plasma/workspace/applets/kicker/plugin/computermodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/computermodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/contactentry.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/contactentry.h create mode 100644 plasma/workspace/applets/kicker/plugin/containmentinterface.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/containmentinterface.h create mode 100644 plasma/workspace/applets/kicker/plugin/dashboardwindow.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/dashboardwindow.h create mode 100644 plasma/workspace/applets/kicker/plugin/draghelper.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/draghelper.h create mode 100644 plasma/workspace/applets/kicker/plugin/fileentry.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/fileentry.h create mode 100644 plasma/workspace/applets/kicker/plugin/forwardingmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/forwardingmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/funnelmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/funnelmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/kickerplugin.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/kickerplugin.h create mode 100644 plasma/workspace/applets/kicker/plugin/menuentryeditor.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/menuentryeditor.h create mode 100644 plasma/workspace/applets/kicker/plugin/placeholdermodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/placeholdermodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/processrunner.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/processrunner.h create mode 100644 plasma/workspace/applets/kicker/plugin/qmldir create mode 100644 plasma/workspace/applets/kicker/plugin/recentcontactsmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/recentcontactsmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/recentusagemodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/recentusagemodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/rootmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/rootmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/runnermatchesmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/runnermatchesmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/runnermodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/runnermodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/submenu.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/submenu.h create mode 100644 plasma/workspace/applets/kicker/plugin/systementry.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/systementry.h create mode 100644 plasma/workspace/applets/kicker/plugin/systemmodel.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/systemmodel.h create mode 100644 plasma/workspace/applets/kicker/plugin/systemsettings.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/systemsettings.h create mode 100644 plasma/workspace/applets/kicker/plugin/trianglemousefilter.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/trianglemousefilter.h create mode 100644 plasma/workspace/applets/kicker/plugin/wheelinterceptor.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/wheelinterceptor.h create mode 100644 plasma/workspace/applets/kicker/plugin/windowsystem.cpp create mode 100644 plasma/workspace/applets/kicker/plugin/windowsystem.h create mode 100644 plasma/workspace/applets/lock_logout/Messages.sh create mode 100644 plasma/workspace/applets/lock_logout/contents/config/config.qml create mode 100644 plasma/workspace/applets/lock_logout/contents/config/main.xml create mode 100644 plasma/workspace/applets/lock_logout/contents/ui/ConfigGeneral.qml create mode 100644 plasma/workspace/applets/lock_logout/contents/ui/data.js create mode 100644 plasma/workspace/applets/lock_logout/contents/ui/lockout.qml create mode 100644 plasma/workspace/applets/lock_logout/metadata.json create mode 100644 plasma/workspace/applets/manage-inputmethod/Messages.sh create mode 100644 plasma/workspace/applets/manage-inputmethod/contents/ui/manage-inputmethod.qml create mode 100644 plasma/workspace/applets/manage-inputmethod/metadata.json create mode 100644 plasma/workspace/applets/mediacontroller/Messages.sh create mode 100644 plasma/workspace/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml create mode 100644 plasma/workspace/applets/mediacontroller/contents/ui/main.qml create mode 100644 plasma/workspace/applets/mediacontroller/metadata.json create mode 100644 plasma/workspace/applets/notifications/CMakeLists.txt create mode 100644 plasma/workspace/applets/notifications/Messages.sh create mode 100644 plasma/workspace/applets/notifications/fileinfo.cpp create mode 100644 plasma/workspace/applets/notifications/fileinfo.h create mode 100644 plasma/workspace/applets/notifications/filemenu.cpp create mode 100644 plasma/workspace/applets/notifications/filemenu.h create mode 100644 plasma/workspace/applets/notifications/globalshortcuts.cpp create mode 100644 plasma/workspace/applets/notifications/globalshortcuts.h create mode 100644 plasma/workspace/applets/notifications/notificationapplet.cpp create mode 100644 plasma/workspace/applets/notifications/notificationapplet.h create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/CompactRepresentation.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/DraggableDelegate.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/DraggableFileArea.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/EditContextMenu.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/FullRepresentation.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/JobDetails.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/JobItem.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/NotificationHeader.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/NotificationItem.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/NotificationPopup.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/NotificationReplyField.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/SelectableLabel.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/ThumbnailStrip.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/global/Globals.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/global/PulseAudio.qml create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/global/qmldir create mode 100644 plasma/workspace/applets/notifications/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/notifications/package/metadata.json create mode 100644 plasma/workspace/applets/notifications/texteditclickhandler.cpp create mode 100644 plasma/workspace/applets/notifications/texteditclickhandler.h create mode 100644 plasma/workspace/applets/notifications/thumbnailer.cpp create mode 100644 plasma/workspace/applets/notifications/thumbnailer.h create mode 100644 plasma/workspace/applets/panelspacer/CMakeLists.txt create mode 100644 plasma/workspace/applets/panelspacer/Messages.sh create mode 100644 plasma/workspace/applets/panelspacer/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/panelspacer/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/panelspacer/package/metadata.json create mode 100644 plasma/workspace/applets/panelspacer/plugin/CMakeLists.txt create mode 100644 plasma/workspace/applets/panelspacer/plugin/panelspacer.cpp create mode 100644 plasma/workspace/applets/panelspacer/plugin/panelspacer.h create mode 100644 plasma/workspace/applets/systemmonitor/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemmonitor/coreusage/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/coreusage/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/cpu/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/cpu/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/diskactivity/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/diskactivity/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/diskusage/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/diskusage/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/memory/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/memory/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/net/contents/config/faceproperties create mode 100644 plasma/workspace/applets/systemmonitor/net/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/Messages.sh create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/config.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/CompactRepresentation.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/FullRepresentation.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigAppearance.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigSensors.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/FaceDetails.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/package/metadata.json create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.cpp create mode 100644 plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.h create mode 100644 plasma/workspace/applets/systemtray/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemtray/Messages.sh create mode 100644 plasma/workspace/applets/systemtray/autotests/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemtray/autotests/data/devicenotifier/metadata.json create mode 100644 plasma/workspace/applets/systemtray/autotests/data/mediacontroller/metadata.json create mode 100644 plasma/workspace/applets/systemtray/autotests/systemtraymodeltest.cpp create mode 100644 plasma/workspace/applets/systemtray/container/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemtray/container/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/systemtray/container/package/metadata.json create mode 100644 plasma/workspace/applets/systemtray/container/systemtraycontainer.cpp create mode 100644 plasma/workspace/applets/systemtray/container/systemtraycontainer.h create mode 100644 plasma/workspace/applets/systemtray/dbusserviceobserver.cpp create mode 100644 plasma/workspace/applets/systemtray/dbusserviceobserver.h create mode 100644 plasma/workspace/applets/systemtray/package/contents/applet/CompactApplet.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/config/config.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/config/main.xml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/ConfigEntries.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/ConfigGeneral.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/CurrentItemHighLight.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/ExpandedRepresentation.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/ExpanderArrow.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/HiddenItemsView.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/PlasmoidPopupsContainer.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/SystemTrayState.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/items/AbstractItem.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/items/ItemLoader.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/items/PlasmoidItem.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/items/PulseAnimation.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml create mode 100644 plasma/workspace/applets/systemtray/package/contents/ui/main.qml create mode 100644 plasma/workspace/applets/systemtray/package/metadata.json create mode 100644 plasma/workspace/applets/systemtray/plasmoidregistry.cpp create mode 100644 plasma/workspace/applets/systemtray/plasmoidregistry.h create mode 100644 plasma/workspace/applets/systemtray/sortedsystemtraymodel.cpp create mode 100644 plasma/workspace/applets/systemtray/sortedsystemtraymodel.h create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemhost.cpp create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemhost.h create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemjob.cpp create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemjob.h create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemservice.cpp create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemservice.h create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemsource.cpp create mode 100644 plasma/workspace/applets/systemtray/statusnotifieritemsource.h create mode 100644 plasma/workspace/applets/systemtray/systemtray.cpp create mode 100644 plasma/workspace/applets/systemtray/systemtray.h create mode 100644 plasma/workspace/applets/systemtray/systemtraymodel.cpp create mode 100644 plasma/workspace/applets/systemtray/systemtraymodel.h create mode 100644 plasma/workspace/applets/systemtray/systemtraysettings.cpp create mode 100644 plasma/workspace/applets/systemtray/systemtraysettings.h create mode 100644 plasma/workspace/applets/systemtray/systemtraytypes.cpp create mode 100644 plasma/workspace/applets/systemtray/systemtraytypes.h create mode 100644 plasma/workspace/applets/systemtray/tests/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/CMakeLists.txt create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/main.cpp create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.cpp create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.h create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.cpp create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.h create mode 100644 plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.ui create mode 100644 plasma/workspace/appmenu/CMakeLists.txt create mode 100644 plasma/workspace/appmenu/appmenu.cpp create mode 100644 plasma/workspace/appmenu/appmenu.desktop create mode 100644 plasma/workspace/appmenu/appmenu.h create mode 100644 plasma/workspace/appmenu/appmenu.json create mode 100644 plasma/workspace/appmenu/appmenu_dbus.cpp create mode 100644 plasma/workspace/appmenu/appmenu_dbus.h create mode 100644 plasma/workspace/appmenu/com.canonical.AppMenu.Registrar.xml create mode 100644 plasma/workspace/appmenu/kdbusimporter.h create mode 100644 plasma/workspace/appmenu/menuimporter.cpp create mode 100644 plasma/workspace/appmenu/menuimporter.h create mode 100644 plasma/workspace/appmenu/org.kde.kappmenu.xml create mode 100644 plasma/workspace/appmenu/verticalmenu.cpp create mode 100644 plasma/workspace/appmenu/verticalmenu.h create mode 100644 plasma/workspace/cmake/FindAppMenuGtkModule.cmake create mode 100644 plasma/workspace/cmake/FindKIOExtras.cmake create mode 100644 plasma/workspace/cmake/FindKIOFuse.cmake create mode 100644 plasma/workspace/cmake/FindLibdrm.cmake create mode 100644 plasma/workspace/cmake/FindQalculate.cmake create mode 100644 plasma/workspace/components/CMakeLists.txt create mode 100644 plasma/workspace/components/Messages.sh create mode 100644 plasma/workspace/components/containmentlayoutmanager/CMakeLists.txt create mode 100644 plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/appletcontainer.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/appletcontainer.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/appletslayout.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/appletslayout.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/configoverlay.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/configoverlay.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/itemcontainer.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/itemcontainer.h create mode 100644 plasma/workspace/components/containmentlayoutmanager/qml/BasicAppletContainer.qml create mode 100644 plasma/workspace/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml create mode 100644 plasma/workspace/components/containmentlayoutmanager/qml/PlaceHolder.qml create mode 100644 plasma/workspace/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml create mode 100644 plasma/workspace/components/containmentlayoutmanager/qml/qmldir create mode 100644 plasma/workspace/components/containmentlayoutmanager/resizehandle.cpp create mode 100644 plasma/workspace/components/containmentlayoutmanager/resizehandle.h create mode 100644 plasma/workspace/components/dialogs/SystemDialog.qml create mode 100644 plasma/workspace/components/dialogs/examples/test.qml create mode 100644 plasma/workspace/components/dialogs/qmldir create mode 100644 plasma/workspace/components/keyboardlayout/CMakeLists.txt create mode 100644 plasma/workspace/components/keyboardlayout/keyboardlayout.cpp create mode 100644 plasma/workspace/components/keyboardlayout/keyboardlayout.h create mode 100644 plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.cpp create mode 100644 plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.h create mode 100644 plasma/workspace/components/keyboardlayout/layoutnames.cpp create mode 100644 plasma/workspace/components/keyboardlayout/layoutnames.h create mode 100644 plasma/workspace/components/keyboardlayout/org.kde.KeyboardLayouts.xml create mode 100644 plasma/workspace/components/keyboardlayout/qmldir create mode 100644 plasma/workspace/components/keyboardlayout/virtualkeyboard.cpp create mode 100644 plasma/workspace/components/keyboardlayout/virtualkeyboard.h create mode 100644 plasma/workspace/components/lookandfeelqml/CMakeLists.txt create mode 100644 plasma/workspace/components/lookandfeelqml/kpackageinterface.cpp create mode 100644 plasma/workspace/components/lookandfeelqml/kpackageinterface.h create mode 100644 plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.cpp create mode 100644 plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.h create mode 100644 plasma/workspace/components/lookandfeelqml/qmldir create mode 100644 plasma/workspace/components/sessionsprivate/CMakeLists.txt create mode 100644 plasma/workspace/components/sessionsprivate/kscreenlockersettings.kcfg create mode 100644 plasma/workspace/components/sessionsprivate/kscreensaversettings.kcfgc create mode 100644 plasma/workspace/components/sessionsprivate/qmldir create mode 100644 plasma/workspace/components/sessionsprivate/sessionsmodel.cpp create mode 100644 plasma/workspace/components/sessionsprivate/sessionsmodel.h create mode 100644 plasma/workspace/components/sessionsprivate/sessionsprivateplugin.cpp create mode 100644 plasma/workspace/components/sessionsprivate/sessionsprivateplugin.h create mode 100644 plasma/workspace/components/shellprivate/CMakeLists.txt create mode 100644 plasma/workspace/components/shellprivate/config-shellprivate.h.cmake create mode 100644 plasma/workspace/components/shellprivate/qmldir create mode 100644 plasma/workspace/components/shellprivate/shellprivateplugin.cpp create mode 100644 plasma/workspace/components/shellprivate/shellprivateplugin.h create mode 100644 plasma/workspace/components/shellprivate/wallpaperplugin.knsrc create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels.cpp create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels_p.h create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant.cpp create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant_p.h create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel.cpp create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel_p.h create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/plasmoids.knsrc create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.cpp create mode 100644 plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.h create mode 100644 plasma/workspace/components/tests/sessions.qml create mode 100644 plasma/workspace/components/workspace/BatteryIcon.qml create mode 100644 plasma/workspace/components/workspace/KeyboardLayoutSwitcher.qml create mode 100644 plasma/workspace/components/workspace/qmldir create mode 100644 plasma/workspace/config-X11.h.cmake create mode 100644 plasma/workspace/config-appstream.h.cmake create mode 100644 plasma/workspace/config-unix.h.cmake create mode 100644 plasma/workspace/config-workspace.h.cmake create mode 100644 plasma/workspace/containmentactions/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/applauncher/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/applauncher/Messages.sh create mode 100644 plasma/workspace/containmentactions/applauncher/config.ui create mode 100644 plasma/workspace/containmentactions/applauncher/launch.cpp create mode 100644 plasma/workspace/containmentactions/applauncher/launch.h create mode 100644 plasma/workspace/containmentactions/applauncher/plasma-containmentactions-applauncher.json create mode 100644 plasma/workspace/containmentactions/contextmenu/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/contextmenu/Messages.sh create mode 100644 plasma/workspace/containmentactions/contextmenu/menu.cpp create mode 100644 plasma/workspace/containmentactions/contextmenu/menu.h create mode 100644 plasma/workspace/containmentactions/contextmenu/plasma-containmentactions-contextmenu.json create mode 100644 plasma/workspace/containmentactions/paste/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/paste/paste.cpp create mode 100644 plasma/workspace/containmentactions/paste/paste.h create mode 100644 plasma/workspace/containmentactions/paste/plasma-containmentactions-paste.json create mode 100644 plasma/workspace/containmentactions/switchactivity/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/switchactivity/Messages.sh create mode 100644 plasma/workspace/containmentactions/switchactivity/plasma-containmentactions-switchactivity.json create mode 100644 plasma/workspace/containmentactions/switchactivity/switch.cpp create mode 100644 plasma/workspace/containmentactions/switchactivity/switch.h create mode 100644 plasma/workspace/containmentactions/switchdesktop/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/switchdesktop/Messages.sh create mode 100644 plasma/workspace/containmentactions/switchdesktop/desktop.cpp create mode 100644 plasma/workspace/containmentactions/switchdesktop/desktop.h create mode 100644 plasma/workspace/containmentactions/switchdesktop/plasma-containmentactions-switchdesktop.json create mode 100644 plasma/workspace/containmentactions/switchwindow/CMakeLists.txt create mode 100644 plasma/workspace/containmentactions/switchwindow/Messages.sh create mode 100644 plasma/workspace/containmentactions/switchwindow/config.ui create mode 100644 plasma/workspace/containmentactions/switchwindow/plasma-containmentactions-switchwindow.json create mode 100644 plasma/workspace/containmentactions/switchwindow/switch.cpp create mode 100644 plasma/workspace/containmentactions/switchwindow/switch.h create mode 100644 plasma/workspace/dataengines/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/Mainpage.dox create mode 100644 plasma/workspace/dataengines/activities/ActivityData.cpp create mode 100644 plasma/workspace/dataengines/activities/ActivityData.h create mode 100644 plasma/workspace/dataengines/activities/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/activities/activities.operations create mode 100644 plasma/workspace/dataengines/activities/activityengine.cpp create mode 100644 plasma/workspace/dataengines/activities/activityengine.h create mode 100644 plasma/workspace/dataengines/activities/activityjob.cpp create mode 100644 plasma/workspace/dataengines/activities/activityjob.h create mode 100644 plasma/workspace/dataengines/activities/activityservice.cpp create mode 100644 plasma/workspace/dataengines/activities/activityservice.h create mode 100644 plasma/workspace/dataengines/activities/org.kde.ActivityManager.ActivityRanking.xml create mode 100644 plasma/workspace/dataengines/activities/plasma-dataengine-activities.json create mode 100644 plasma/workspace/dataengines/applicationjobs/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/applicationjobs/Messages.sh create mode 100644 plasma/workspace/dataengines/applicationjobs/applicationjobs.operations create mode 100644 plasma/workspace/dataengines/applicationjobs/jobaction.cpp create mode 100644 plasma/workspace/dataengines/applicationjobs/jobaction.h create mode 100644 plasma/workspace/dataengines/applicationjobs/jobcontrol.cpp create mode 100644 plasma/workspace/dataengines/applicationjobs/jobcontrol.h create mode 100644 plasma/workspace/dataengines/applicationjobs/kuiserverengine.cpp create mode 100644 plasma/workspace/dataengines/applicationjobs/kuiserverengine.h create mode 100644 plasma/workspace/dataengines/applicationjobs/plasma-dataengine-applicationjobs.json create mode 100644 plasma/workspace/dataengines/apps/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/apps/appjob.cpp create mode 100644 plasma/workspace/dataengines/apps/appjob.h create mode 100644 plasma/workspace/dataengines/apps/apps.operations create mode 100644 plasma/workspace/dataengines/apps/appsengine.cpp create mode 100644 plasma/workspace/dataengines/apps/appsengine.h create mode 100644 plasma/workspace/dataengines/apps/appservice.cpp create mode 100644 plasma/workspace/dataengines/apps/appservice.h create mode 100644 plasma/workspace/dataengines/apps/appsource.cpp create mode 100644 plasma/workspace/dataengines/apps/appsource.h create mode 100644 plasma/workspace/dataengines/apps/plasma-dataengine-apps.json create mode 100644 plasma/workspace/dataengines/devicenotifications/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/devicenotifications/Messages.sh create mode 100644 plasma/workspace/dataengines/devicenotifications/devicenotifications.notifyrc create mode 100644 plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.cpp create mode 100644 plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.h create mode 100644 plasma/workspace/dataengines/devicenotifications/ksolidnotify.cpp create mode 100644 plasma/workspace/dataengines/devicenotifications/ksolidnotify.h create mode 100644 plasma/workspace/dataengines/devicenotifications/plasma-dataengine-devicenotifications.json create mode 100644 plasma/workspace/dataengines/dict/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/dict/Messages.sh create mode 100644 plasma/workspace/dataengines/dict/buggywords create mode 100644 plasma/workspace/dataengines/dict/dictengine.cpp create mode 100644 plasma/workspace/dataengines/dict/dictengine.h create mode 100644 plasma/workspace/dataengines/dict/plasma-dataengine-dict.json create mode 100644 plasma/workspace/dataengines/executable/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/executable/executable.cpp create mode 100644 plasma/workspace/dataengines/executable/executable.h create mode 100644 plasma/workspace/dataengines/executable/plasma-dataengine-executable.json create mode 100644 plasma/workspace/dataengines/favicons/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/favicons/faviconprovider.cpp create mode 100644 plasma/workspace/dataengines/favicons/faviconprovider.h create mode 100644 plasma/workspace/dataengines/favicons/favicons.cpp create mode 100644 plasma/workspace/dataengines/favicons/favicons.h create mode 100644 plasma/workspace/dataengines/favicons/plasma-dataengine-favicons.json create mode 100644 plasma/workspace/dataengines/filebrowser/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/filebrowser/filebrowserengine.cpp create mode 100644 plasma/workspace/dataengines/filebrowser/filebrowserengine.h create mode 100644 plasma/workspace/dataengines/filebrowser/plasma-dataengine-filebrowser.json create mode 100644 plasma/workspace/dataengines/geolocation/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/geolocation/geolocation.cpp create mode 100644 plasma/workspace/dataengines/geolocation/geolocation.h create mode 100644 plasma/workspace/dataengines/geolocation/geolocationprovider.cpp create mode 100644 plasma/workspace/dataengines/geolocation/geolocationprovider.h create mode 100644 plasma/workspace/dataengines/geolocation/location_gps.cpp create mode 100644 plasma/workspace/dataengines/geolocation/location_gps.h create mode 100644 plasma/workspace/dataengines/geolocation/location_ip.cpp create mode 100644 plasma/workspace/dataengines/geolocation/location_ip.h create mode 100644 plasma/workspace/dataengines/geolocation/plasma-dataengine-geolocation.json create mode 100644 plasma/workspace/dataengines/geolocation/plasma-geolocation-gps.json create mode 100644 plasma/workspace/dataengines/geolocation/plasma-geolocation-ip.json create mode 100644 plasma/workspace/dataengines/hotplug/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/hotplug/Messages.sh create mode 100644 plasma/workspace/dataengines/hotplug/deviceaction.cpp create mode 100644 plasma/workspace/dataengines/hotplug/deviceaction.h create mode 100644 plasma/workspace/dataengines/hotplug/deviceserviceaction.cpp create mode 100644 plasma/workspace/dataengines/hotplug/deviceserviceaction.h create mode 100644 plasma/workspace/dataengines/hotplug/hotplug.operations create mode 100644 plasma/workspace/dataengines/hotplug/hotplugengine.cpp create mode 100644 plasma/workspace/dataengines/hotplug/hotplugengine.h create mode 100644 plasma/workspace/dataengines/hotplug/hotplugjob.cpp create mode 100644 plasma/workspace/dataengines/hotplug/hotplugjob.h create mode 100644 plasma/workspace/dataengines/hotplug/hotplugservice.cpp create mode 100644 plasma/workspace/dataengines/hotplug/hotplugservice.h create mode 100644 plasma/workspace/dataengines/hotplug/plasma-dataengine-hotplug.json create mode 100644 plasma/workspace/dataengines/keystate/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/keystate/Messages.sh create mode 100644 plasma/workspace/dataengines/keystate/keyservice.cpp create mode 100644 plasma/workspace/dataengines/keystate/keyservice.h create mode 100644 plasma/workspace/dataengines/keystate/keystate.cpp create mode 100644 plasma/workspace/dataengines/keystate/keystate.h create mode 100644 plasma/workspace/dataengines/keystate/modifierkeystate.operations create mode 100644 plasma/workspace/dataengines/keystate/plasma-dataengine-keystate.json create mode 100644 plasma/workspace/dataengines/mouse/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/mouse/cursornotificationhandler.cpp create mode 100644 plasma/workspace/dataengines/mouse/cursornotificationhandler.h create mode 100644 plasma/workspace/dataengines/mouse/mouseengine.cpp create mode 100644 plasma/workspace/dataengines/mouse/mouseengine.h create mode 100644 plasma/workspace/dataengines/mouse/plasma-dataengine-mouse.json create mode 100644 plasma/workspace/dataengines/mpris2/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/mpris2/Messages.sh create mode 100644 plasma/workspace/dataengines/mpris2/TODO create mode 100644 plasma/workspace/dataengines/mpris2/mpris2.operations create mode 100644 plasma/workspace/dataengines/mpris2/mpris2engine.cpp create mode 100644 plasma/workspace/dataengines/mpris2/mpris2engine.h create mode 100644 plasma/workspace/dataengines/mpris2/multiplexedservice.cpp create mode 100644 plasma/workspace/dataengines/mpris2/multiplexedservice.h create mode 100644 plasma/workspace/dataengines/mpris2/multiplexer.cpp create mode 100644 plasma/workspace/dataengines/mpris2/multiplexer.h create mode 100644 plasma/workspace/dataengines/mpris2/org.freedesktop.DBus.Properties.xml create mode 100644 plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.Player.xml create mode 100644 plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.xml create mode 100644 plasma/workspace/dataengines/mpris2/plasma-dataengine-mpris2.json create mode 100644 plasma/workspace/dataengines/mpris2/playeractionjob.cpp create mode 100644 plasma/workspace/dataengines/mpris2/playeractionjob.h create mode 100644 plasma/workspace/dataengines/mpris2/playercontainer.cpp create mode 100644 plasma/workspace/dataengines/mpris2/playercontainer.h create mode 100644 plasma/workspace/dataengines/mpris2/playercontrol.cpp create mode 100644 plasma/workspace/dataengines/mpris2/playercontrol.h create mode 100644 plasma/workspace/dataengines/notifications/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/notifications/Messages.sh create mode 100644 plasma/workspace/dataengines/notifications/notificationaction.cpp create mode 100644 plasma/workspace/dataengines/notifications/notificationaction.h create mode 100644 plasma/workspace/dataengines/notifications/notifications.operations create mode 100644 plasma/workspace/dataengines/notifications/notificationsengine.cpp create mode 100644 plasma/workspace/dataengines/notifications/notificationsengine.h create mode 100644 plasma/workspace/dataengines/notifications/notificationservice.cpp create mode 100644 plasma/workspace/dataengines/notifications/notificationservice.h create mode 100644 plasma/workspace/dataengines/notifications/plasma-dataengine-notifications.json create mode 100644 plasma/workspace/dataengines/packagekit/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/packagekit/packagekit.operations create mode 100644 plasma/workspace/dataengines/packagekit/packagekitengine.cpp create mode 100644 plasma/workspace/dataengines/packagekit/packagekitengine.h create mode 100644 plasma/workspace/dataengines/packagekit/packagekitjob.cpp create mode 100644 plasma/workspace/dataengines/packagekit/packagekitjob.h create mode 100644 plasma/workspace/dataengines/packagekit/packagekitservice.cpp create mode 100644 plasma/workspace/dataengines/packagekit/packagekitservice.h create mode 100644 plasma/workspace/dataengines/packagekit/plasma-dataengine-packagekit.json create mode 100644 plasma/workspace/dataengines/places/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/places/TODO create mode 100644 plasma/workspace/dataengines/places/jobs.h create mode 100644 plasma/workspace/dataengines/places/modeljob.h create mode 100644 plasma/workspace/dataengines/places/org.kde.places.operations create mode 100644 plasma/workspace/dataengines/places/placesengine.cpp create mode 100644 plasma/workspace/dataengines/places/placesengine.h create mode 100644 plasma/workspace/dataengines/places/placeservice.cpp create mode 100644 plasma/workspace/dataengines/places/placeservice.h create mode 100644 plasma/workspace/dataengines/places/placesproxymodel.cpp create mode 100644 plasma/workspace/dataengines/places/placesproxymodel.h create mode 100644 plasma/workspace/dataengines/places/plasma-dataengine-places.json create mode 100644 plasma/workspace/dataengines/places/setupdevicejob.cpp create mode 100644 plasma/workspace/dataengines/places/setupdevicejob.h create mode 100644 plasma/workspace/dataengines/powermanagement/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/powermanagement/Messages.sh create mode 100644 plasma/workspace/dataengines/powermanagement/README.txt create mode 100644 plasma/workspace/dataengines/powermanagement/plasma-dataengine-powermanagement.json create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementengine.cpp create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementengine.h create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementjob.cpp create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementjob.h create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementservice.cpp create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementservice.h create mode 100644 plasma/workspace/dataengines/powermanagement/powermanagementservice.operations create mode 100644 plasma/workspace/dataengines/soliddevice/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/soliddevice/Messages.sh create mode 100644 plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.h create mode 100644 plasma/workspace/dataengines/soliddevice/devicesignalmapper.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/devicesignalmapper.h create mode 100644 plasma/workspace/dataengines/soliddevice/hddtemp.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/hddtemp.h create mode 100644 plasma/workspace/dataengines/soliddevice/plasma-dataengine-soliddevice.json create mode 100644 plasma/workspace/dataengines/soliddevice/soliddevice.operations create mode 100644 plasma/workspace/dataengines/soliddevice/soliddeviceengine.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/soliddeviceengine.h create mode 100644 plasma/workspace/dataengines/soliddevice/soliddevicejob.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/soliddevicejob.h create mode 100644 plasma/workspace/dataengines/soliddevice/soliddeviceservice.cpp create mode 100644 plasma/workspace/dataengines/soliddevice/soliddeviceservice.h create mode 100644 plasma/workspace/dataengines/statusnotifieritem/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/statusnotifieritem/plasma-dataengine-statusnotifieritem.json create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem.operations create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.cpp create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.h create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.cpp create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.h create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.cpp create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.h create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.cpp create mode 100644 plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.h create mode 100644 plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.cpp create mode 100644 plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.h create mode 100644 plasma/workspace/dataengines/systemmonitor/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/systemmonitor/Messages.sh create mode 100644 plasma/workspace/dataengines/systemmonitor/plasma-dataengine-systemmonitor.json create mode 100644 plasma/workspace/dataengines/systemmonitor/systemmonitor.cpp create mode 100644 plasma/workspace/dataengines/systemmonitor/systemmonitor.h create mode 100644 plasma/workspace/dataengines/time/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/time/Messages.sh create mode 100644 plasma/workspace/dataengines/time/plasma-dataengine-time.json create mode 100644 plasma/workspace/dataengines/time/solarsystem.cpp create mode 100644 plasma/workspace/dataengines/time/solarsystem.h create mode 100644 plasma/workspace/dataengines/time/timeengine.cpp create mode 100644 plasma/workspace/dataengines/time/timeengine.h create mode 100644 plasma/workspace/dataengines/time/timesource.cpp create mode 100644 plasma/workspace/dataengines/time/timesource.h create mode 100644 plasma/workspace/dataengines/weather/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/Messages.sh create mode 100644 plasma/workspace/dataengines/weather/ions/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/bbcukmet/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/bbcukmet/ion-bbcukmet.json create mode 100644 plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.h create mode 100644 plasma/workspace/dataengines/weather/ions/data/bbcukmet_i18n.dat create mode 100644 plasma/workspace/dataengines/weather/ions/data/envcan_i18n.dat create mode 100644 plasma/workspace/dataengines/weather/ions/data/noaa_i18n.dat create mode 100644 plasma/workspace/dataengines/weather/ions/dwd/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/dwd/ion-dwd.json create mode 100644 plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.h create mode 100644 plasma/workspace/dataengines/weather/ions/envcan/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/envcan/ion-envcan.json create mode 100644 plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.h create mode 100644 plasma/workspace/dataengines/weather/ions/includes/Ion create mode 100644 plasma/workspace/dataengines/weather/ions/ion.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/ion.h create mode 100644 plasma/workspace/dataengines/weather/ions/noaa/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/noaa/ion-noaa.json create mode 100644 plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.h create mode 100644 plasma/workspace/dataengines/weather/ions/wetter.com/CMakeLists.txt create mode 100644 plasma/workspace/dataengines/weather/ions/wetter.com/ion-wettercom.json create mode 100644 plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.cpp create mode 100644 plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.h create mode 100644 plasma/workspace/dataengines/weather/plasma-dataengine-weather.json create mode 100644 plasma/workspace/dataengines/weather/weatherengine.cpp create mode 100644 plasma/workspace/dataengines/weather/weatherengine.h create mode 100644 plasma/workspace/doc/CMakeLists.txt create mode 100644 plasma/workspace/doc/PolicyKit-kde/CMakeLists.txt create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_1.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_2.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_3.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_4.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_5.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authdialog_6.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authorization.docbook create mode 100644 plasma/workspace/doc/PolicyKit-kde/authorization_1.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authorization_2.png create mode 100644 plasma/workspace/doc/PolicyKit-kde/authorizationagent.docbook create mode 100644 plasma/workspace/doc/PolicyKit-kde/howitworks.docbook create mode 100644 plasma/workspace/doc/PolicyKit-kde/index.docbook create mode 100644 plasma/workspace/doc/PolicyKit-kde/introduction.docbook create mode 100644 plasma/workspace/doc/config_update_tool/extract_config.py create mode 100644 plasma/workspace/doc/kcontrol/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/autostart/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/autostart/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/colors/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/colors/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/edit-delete.png create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/edit-undo.png create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/get-new-theme.png create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/desktopthemedetails/main.png create mode 100644 plasma/workspace/doc/kcontrol/fontinst/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/fontinst/edit-delete.png create mode 100644 plasma/workspace/doc/kcontrol/fontinst/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/fonts/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/fonts/adjust-all.png create mode 100644 plasma/workspace/doc/kcontrol/fonts/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/fonts/main.png create mode 100644 plasma/workspace/doc/kcontrol/formats/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/formats/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/icons/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/icons/edit-delete.png create mode 100644 plasma/workspace/doc/kcontrol/icons/edit-undo.png create mode 100644 plasma/workspace/doc/kcontrol/icons/get-new-theme.png create mode 100644 plasma/workspace/doc/kcontrol/icons/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/icons/main.png create mode 100644 plasma/workspace/doc/kcontrol/icons/use-of-icons.png create mode 100644 plasma/workspace/doc/kcontrol/kcmstyle/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/kcmstyle/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/notifications/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/notifications/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/screenlocker/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/screenlocker/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/translations/CMakeLists.txt create mode 100644 plasma/workspace/doc/kcontrol/translations/go-top.png create mode 100644 plasma/workspace/doc/kcontrol/translations/index.docbook create mode 100644 plasma/workspace/doc/kcontrol/translations/list-remove.png create mode 100644 plasma/workspace/doc/klipper/CMakeLists.txt create mode 100644 plasma/workspace/doc/klipper/index.docbook create mode 100644 plasma/workspace/doc/klipper/klipper-widget.png create mode 100644 plasma/workspace/doc/klipper/screenshot.png create mode 100644 plasma/workspace/freespacenotifier/CMakeLists.txt create mode 100644 plasma/workspace/freespacenotifier/Messages.sh create mode 100644 plasma/workspace/freespacenotifier/README create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier.cpp create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier.h create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier.json create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier.kcfg create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier.notifyrc create mode 100644 plasma/workspace/freespacenotifier/freespacenotifier_prefs_base.ui create mode 100644 plasma/workspace/freespacenotifier/module.cpp create mode 100644 plasma/workspace/freespacenotifier/module.h create mode 100644 plasma/workspace/freespacenotifier/settings.kcfgc create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/CMakeLists.txt create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/actions.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/actions.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/icons.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/icons.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/main.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/menu.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/menu.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/plasma-gmenudbusmenuproxy.service.in create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/utils.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/utils.h create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/window.cpp create mode 100644 plasma/workspace/gmenu-dbusmenu-proxy/window.h create mode 100644 plasma/workspace/interactiveconsole/CMakeLists.txt create mode 100644 plasma/workspace/interactiveconsole/interactiveconsole.cpp create mode 100644 plasma/workspace/interactiveconsole/interactiveconsole.h create mode 100644 plasma/workspace/interactiveconsole/main.cpp create mode 100644 plasma/workspace/kcms/CMakeLists.txt create mode 100644 plasma/workspace/kcms/autostart/CMakeLists.txt create mode 100644 plasma/workspace/kcms/autostart/Messages.sh create mode 100644 plasma/workspace/kcms/autostart/autostart.cpp create mode 100644 plasma/workspace/kcms/autostart/autostart.h create mode 100644 plasma/workspace/kcms/autostart/autostartmodel.cpp create mode 100644 plasma/workspace/kcms/autostart/autostartmodel.h create mode 100644 plasma/workspace/kcms/autostart/kcm_autostart.desktop create mode 100644 plasma/workspace/kcms/autostart/kcm_autostart.json create mode 100644 plasma/workspace/kcms/autostart/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/colors/CMakeLists.txt create mode 100644 plasma/workspace/kcms/colors/Messages.sh create mode 100644 plasma/workspace/kcms/colors/README.i18n create mode 100644 plasma/workspace/kcms/colors/colors.cpp create mode 100644 plasma/workspace/kcms/colors/colors.h create mode 100644 plasma/workspace/kcms/colors/colorsapplicator.cpp create mode 100644 plasma/workspace/kcms/colors/colorsapplicator.h create mode 100644 plasma/workspace/kcms/colors/colorschemes.knsrc create mode 100644 plasma/workspace/kcms/colors/colorsmodel.cpp create mode 100644 plasma/workspace/kcms/colors/colorsmodel.h create mode 100644 plasma/workspace/kcms/colors/colorssettings.kcfg create mode 100644 plasma/workspace/kcms/colors/colorssettings.kcfgc create mode 100644 plasma/workspace/kcms/colors/editor/CMakeLists.txt create mode 100644 plasma/workspace/kcms/colors/editor/kcolorschemeeditor.cpp create mode 100644 plasma/workspace/kcms/colors/editor/org.kde.kcolorschemeeditor.desktop create mode 100644 plasma/workspace/kcms/colors/editor/preview.ui create mode 100644 plasma/workspace/kcms/colors/editor/previewwidget.cpp create mode 100644 plasma/workspace/kcms/colors/editor/previewwidget.h create mode 100644 plasma/workspace/kcms/colors/editor/scmeditorcolors.cpp create mode 100644 plasma/workspace/kcms/colors/editor/scmeditorcolors.h create mode 100644 plasma/workspace/kcms/colors/editor/scmeditorcolors.ui create mode 100644 plasma/workspace/kcms/colors/editor/scmeditordialog.cpp create mode 100644 plasma/workspace/kcms/colors/editor/scmeditordialog.h create mode 100644 plasma/workspace/kcms/colors/editor/scmeditordialog.ui create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoreffects.cpp create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoreffects.h create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoreffects.ui create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoroptions.cpp create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoroptions.h create mode 100644 plasma/workspace/kcms/colors/editor/scmeditoroptions.ui create mode 100644 plasma/workspace/kcms/colors/editor/setpreview.ui create mode 100644 plasma/workspace/kcms/colors/editor/setpreviewwidget.cpp create mode 100644 plasma/workspace/kcms/colors/editor/setpreviewwidget.h create mode 100644 plasma/workspace/kcms/colors/filterproxymodel.cpp create mode 100644 plasma/workspace/kcms/colors/filterproxymodel.h create mode 100644 plasma/workspace/kcms/colors/kcm_colors.desktop create mode 100644 plasma/workspace/kcms/colors/kcm_colors.json create mode 100644 plasma/workspace/kcms/colors/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/colors/plasma-apply-colorscheme.cpp create mode 100644 plasma/workspace/kcms/cursortheme/CMakeLists.txt create mode 100644 plasma/workspace/kcms/cursortheme/Messages.sh create mode 100644 plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfg create mode 100644 plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfgc create mode 100644 plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.pl create mode 100644 plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.upd create mode 100644 plasma/workspace/kcms/cursortheme/kcm_cursortheme.desktop create mode 100644 plasma/workspace/kcms/cursortheme/kcm_cursortheme.json create mode 100644 plasma/workspace/kcms/cursortheme/kcmcursortheme.cpp create mode 100644 plasma/workspace/kcms/cursortheme/kcmcursortheme.h create mode 100644 plasma/workspace/kcms/cursortheme/package/contents/ui/Delegate.qml create mode 100644 plasma/workspace/kcms/cursortheme/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/cursortheme/plasma-apply-cursortheme.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/cursortheme.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/cursortheme.h create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/previewwidget.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/previewwidget.h create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.h create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.h create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/thememodel.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/thememodel.h create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/xcursor.knsrc create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.cpp create mode 100644 plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.h create mode 100644 plasma/workspace/kcms/desktoptheme/CMakeLists.txt create mode 100644 plasma/workspace/kcms/desktoptheme/Messages.sh create mode 100644 plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfg create mode 100644 plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfgc create mode 100644 plasma/workspace/kcms/desktoptheme/filterproxymodel.cpp create mode 100644 plasma/workspace/kcms/desktoptheme/filterproxymodel.h create mode 100644 plasma/workspace/kcms/desktoptheme/kcm.cpp create mode 100644 plasma/workspace/kcms/desktoptheme/kcm.h create mode 100644 plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.desktop create mode 100644 plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.json create mode 100644 plasma/workspace/kcms/desktoptheme/package/contents/ui/Hand.qml create mode 100644 plasma/workspace/kcms/desktoptheme/package/contents/ui/ThemePreview.qml create mode 100644 plasma/workspace/kcms/desktoptheme/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/desktoptheme/plasma-apply-desktoptheme.cpp create mode 100644 plasma/workspace/kcms/desktoptheme/plasma-themes.knsrc create mode 100644 plasma/workspace/kcms/desktoptheme/themesmodel.cpp create mode 100644 plasma/workspace/kcms/desktoptheme/themesmodel.h create mode 100644 plasma/workspace/kcms/feedback/CMakeLists.txt create mode 100644 plasma/workspace/kcms/feedback/Messages.sh create mode 100644 plasma/workspace/kcms/feedback/feedback.cpp create mode 100644 plasma/workspace/kcms/feedback/feedback.h create mode 100644 plasma/workspace/kcms/feedback/feedbacksettings.kcfg create mode 100644 plasma/workspace/kcms/feedback/feedbacksettings.kcfgc create mode 100644 plasma/workspace/kcms/feedback/kcm_feedback.desktop create mode 100644 plasma/workspace/kcms/feedback/kcm_feedback.json create mode 100644 plasma/workspace/kcms/feedback/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/fonts/CMakeLists.txt create mode 100644 plasma/workspace/kcms/fonts/Messages.sh create mode 100644 plasma/workspace/kcms/fonts/fontinit.cpp create mode 100644 plasma/workspace/kcms/fonts/fonts.cpp create mode 100644 plasma/workspace/kcms/fonts/fonts.h create mode 100644 plasma/workspace/kcms/fonts/fontsaasettings.cpp create mode 100644 plasma/workspace/kcms/fonts/fontsaasettings.h create mode 100644 plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfg create mode 100644 plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfgc create mode 100644 plasma/workspace/kcms/fonts/fontssettings.kcfg create mode 100644 plasma/workspace/kcms/fonts/fontssettings.kcfgc create mode 100644 plasma/workspace/kcms/fonts/kcm_fonts.desktop create mode 100644 plasma/workspace/kcms/fonts/kcm_fonts.json create mode 100644 plasma/workspace/kcms/fonts/kxftconfig.cpp create mode 100644 plasma/workspace/kcms/fonts/kxftconfig.h create mode 100644 plasma/workspace/kcms/fonts/package/contents/ui/FontWidget.qml create mode 100644 plasma/workspace/kcms/fonts/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/fonts/previewimageprovider.cpp create mode 100644 plasma/workspace/kcms/fonts/previewimageprovider.h create mode 100644 plasma/workspace/kcms/fonts/previewrenderengine.cpp create mode 100644 plasma/workspace/kcms/fonts/previewrenderengine.h create mode 100644 plasma/workspace/kcms/formats/CMakeLists.txt create mode 100644 plasma/workspace/kcms/formats/Messages.sh create mode 100644 plasma/workspace/kcms/formats/exampleutility.cpp create mode 100644 plasma/workspace/kcms/formats/formatssettings.kcfg create mode 100644 plasma/workspace/kcms/formats/formatssettings.kcfgc create mode 100644 plasma/workspace/kcms/formats/kcm_formats.desktop create mode 100644 plasma/workspace/kcms/formats/kcm_formats.json create mode 100644 plasma/workspace/kcms/formats/kcmformats.cpp create mode 100644 plasma/workspace/kcms/formats/kcmformats.h create mode 100644 plasma/workspace/kcms/formats/localelistmodel.cpp create mode 100644 plasma/workspace/kcms/formats/localelistmodel.h create mode 100644 plasma/workspace/kcms/formats/optionsmodel.cpp create mode 100644 plasma/workspace/kcms/formats/optionsmodel.h create mode 100644 plasma/workspace/kcms/formats/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/icons/CMakeLists.txt create mode 100644 plasma/workspace/kcms/icons/Messages.sh create mode 100644 plasma/workspace/kcms/icons/changeicons.cpp create mode 100644 plasma/workspace/kcms/icons/icons.knsrc create mode 100644 plasma/workspace/kcms/icons/icons_remove_effects.upd create mode 100644 plasma/workspace/kcms/icons/iconsizecategorymodel.cpp create mode 100644 plasma/workspace/kcms/icons/iconsizecategorymodel.h create mode 100644 plasma/workspace/kcms/icons/iconsmodel.cpp create mode 100644 plasma/workspace/kcms/icons/iconsmodel.h create mode 100644 plasma/workspace/kcms/icons/iconssettings.cpp create mode 100644 plasma/workspace/kcms/icons/iconssettings.h create mode 100644 plasma/workspace/kcms/icons/iconssettingsbase.kcfg create mode 100644 plasma/workspace/kcms/icons/iconssettingsbase.kcfgc create mode 100644 plasma/workspace/kcms/icons/kcm_icons.desktop create mode 100644 plasma/workspace/kcms/icons/kcm_icons.json create mode 100644 plasma/workspace/kcms/icons/main.cpp create mode 100644 plasma/workspace/kcms/icons/main.h create mode 100644 plasma/workspace/kcms/icons/package/contents/ui/IconSizePopup.qml create mode 100644 plasma/workspace/kcms/icons/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/kcms-common.cpp create mode 100644 plasma/workspace/kcms/kcms-common_p.h create mode 100644 plasma/workspace/kcms/kfontinst/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/ChangeLog create mode 100644 plasma/workspace/kcms/kfontinst/Messages.sh create mode 100644 plasma/workspace/kcms/kfontinst/apps/16-apps-kfontview.png create mode 100644 plasma/workspace/kcms/kfontinst/apps/22-apps-kfontview.png create mode 100644 plasma/workspace/kcms/kfontinst/apps/32-apps-kfontview.png create mode 100644 plasma/workspace/kcms/kfontinst/apps/48-apps-kfontview.png create mode 100644 plasma/workspace/kcms/kfontinst/apps/64-apps-kfontview.png create mode 100644 plasma/workspace/kcms/kfontinst/apps/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/apps/CreateParent.h create mode 100644 plasma/workspace/kcms/kfontinst/apps/Installer.cpp create mode 100644 plasma/workspace/kcms/kfontinst/apps/Installer.h create mode 100644 plasma/workspace/kcms/kfontinst/apps/Printer.cpp create mode 100644 plasma/workspace/kcms/kfontinst/apps/Printer.h create mode 100644 plasma/workspace/kcms/kfontinst/apps/Viewer.cpp create mode 100644 plasma/workspace/kcms/kfontinst/apps/Viewer.h create mode 100644 plasma/workspace/kcms/kfontinst/apps/hisc-apps-kfontview.svgz create mode 100644 plasma/workspace/kcms/kfontinst/apps/installfont.desktop create mode 100644 plasma/workspace/kcms/kfontinst/apps/kfontviewui.rc create mode 100644 plasma/workspace/kcms/kfontinst/apps/org.kde.kfontview.desktop create mode 100644 plasma/workspace/kcms/kfontinst/config-fontinst.h.cmake create mode 100644 plasma/workspace/kcms/kfontinst/dbus/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FcConfig.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FcConfig.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Folder.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Folder.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FontInst.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FontInst.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FontinstIface.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/FontinstIface.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Helper.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Helper.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Main.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Utils.cpp create mode 100644 plasma/workspace/kcms/kfontinst/dbus/Utils.h create mode 100644 plasma/workspace/kcms/kfontinst/dbus/fontinst.actions create mode 100644 plasma/workspace/kcms/kfontinst/dbus/fontinst_x11 create mode 100644 plasma/workspace/kcms/kfontinst/dbus/org.kde.fontinst.service.cmake create mode 100644 plasma/workspace/kcms/kfontinst/dbus/org.kde.fontinst.system-service.cmake create mode 100644 plasma/workspace/kcms/kfontinst/dbus/org.kde.fontinst.xml create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/16-actions-addfont.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/16-actions-font-disable.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/16-actions-font-enable.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/16-actions-fontstatus.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/22-actions-addfont.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/22-actions-font-disable.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/22-actions-font-enable.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/22-actions-fontstatus.png create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/ActionLabel.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/ActionLabel.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/DuplicatesDialog.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/DuplicatesDialog.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FcQuery.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FcQuery.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontFilter.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontFilter.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontFilterProxyStyle.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontFilterProxyStyle.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontInstInterface.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontList.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontList.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontsPackage.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/FontsPackage.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/GroupList.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/GroupList.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/JobRunner.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/JobRunner.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/KCmFontInst.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/KCmFontInst.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/PreviewList.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/PreviewList.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/PrintDialog.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/PrintDialog.h create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/fontinst.json create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/kcm_fontinst.desktop create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/kfontinst.knsrc create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/sc-actions-addfont.svgz create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/sc-actions-disablefont.svgz create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/sc-actions-enablefont.svgz create mode 100644 plasma/workspace/kcms/kfontinst/kcmfontinst/sc-actions-fontstatus.svgz create mode 100644 plasma/workspace/kcms/kfontinst/kio/128-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/16-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/22-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/32-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/48-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/64-mimetypes-fonts-package.png create mode 100644 plasma/workspace/kcms/kfontinst/kio/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/kio/FontInstInterface.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kio/FontInstInterface.h create mode 100644 plasma/workspace/kcms/kfontinst/kio/KioFonts.cpp create mode 100644 plasma/workspace/kcms/kfontinst/kio/KioFonts.h create mode 100644 plasma/workspace/kcms/kfontinst/kio/fonts.desktop create mode 100644 plasma/workspace/kcms/kfontinst/kio/fonts.json create mode 100644 plasma/workspace/kcms/kfontinst/kio/oxsc-mimetypes-fonts-package.svgz create mode 100644 plasma/workspace/kcms/kfontinst/lib/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/lib/Family.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/Family.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/Fc.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/Fc.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/FcEngine.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/FcEngine.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/File.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/File.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/KfiConstants.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/Misc.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/Misc.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/Style.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/Style.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/WritingSystems.cpp create mode 100644 plasma/workspace/kcms/kfontinst/lib/WritingSystems.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/XmlStrings.h create mode 100644 plasma/workspace/kcms/kfontinst/lib/config-paths.h.cmake create mode 100644 plasma/workspace/kcms/kfontinst/lib/kfontinst_export.h create mode 100644 plasma/workspace/kcms/kfontinst/sc-apps-preferences-desktop-font-installer.svgz create mode 100644 plasma/workspace/kcms/kfontinst/thumbnail/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/thumbnail/FontThumbnail.cpp create mode 100644 plasma/workspace/kcms/kfontinst/thumbnail/FontThumbnail.h create mode 100644 plasma/workspace/kcms/kfontinst/thumbnail/fontthumbnail.desktop create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/CMakeLists.txt create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/COPYING.UNICODE create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/CharTip.cpp create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/CharTip.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/FontPreview.cpp create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/FontPreview.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/FontViewPart.cpp create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/FontViewPart.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/PreviewSelectAction.cpp create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/PreviewSelectAction.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/UnicodeBlocks.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/UnicodeCategories.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/UnicodeScripts.h create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/download-unicode-files.sh create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/generate-unicode-tables.pl create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/kfontviewpart.json create mode 100644 plasma/workspace/kcms/kfontinst/viewpart/kfontviewpart.rc create mode 100644 plasma/workspace/kcms/krdb/AUTHORS create mode 100644 plasma/workspace/kcms/krdb/CMakeLists.txt create mode 100644 plasma/workspace/kcms/krdb/Messages.sh create mode 100644 plasma/workspace/kcms/krdb/krdb.cpp create mode 100644 plasma/workspace/kcms/krdb/krdb.h create mode 100644 plasma/workspace/kcms/lookandfeel/CMakeLists.txt create mode 100644 plasma/workspace/kcms/lookandfeel/Messages.sh create mode 100644 plasma/workspace/kcms/lookandfeel/autotests/CMakeLists.txt create mode 100644 plasma/workspace/kcms/lookandfeel/autotests/kcmtest.cpp create mode 100644 plasma/workspace/kcms/lookandfeel/autotests/lookandfeel/contents/defaults create mode 100644 plasma/workspace/kcms/lookandfeel/autotests/lookandfeel/metadata.desktop create mode 100644 plasma/workspace/kcms/lookandfeel/config-kcm.h.cmake create mode 100644 plasma/workspace/kcms/lookandfeel/kcm.cpp create mode 100644 plasma/workspace/kcms/lookandfeel/kcm.h create mode 100644 plasma/workspace/kcms/lookandfeel/kcm_lookandfeel.desktop create mode 100644 plasma/workspace/kcms/lookandfeel/kcm_lookandfeel.json create mode 100644 plasma/workspace/kcms/lookandfeel/kcmmain.cpp create mode 100644 plasma/workspace/kcms/lookandfeel/lnftool.cpp create mode 100644 plasma/workspace/kcms/lookandfeel/lookandfeel.knsrc create mode 100644 plasma/workspace/kcms/lookandfeel/lookandfeelmanager.cpp create mode 100644 plasma/workspace/kcms/lookandfeel/lookandfeelmanager.h create mode 100644 plasma/workspace/kcms/lookandfeel/lookandfeelsettings.kcfg create mode 100644 plasma/workspace/kcms/lookandfeel/lookandfeelsettings.kcfgc create mode 100644 plasma/workspace/kcms/lookandfeel/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/nightcolor/CMakeLists.txt create mode 100644 plasma/workspace/kcms/nightcolor/Messages.sh create mode 100644 plasma/workspace/kcms/nightcolor/enum.h create mode 100644 plasma/workspace/kcms/nightcolor/kcm.cpp create mode 100644 plasma/workspace/kcms/nightcolor/kcm.h create mode 100644 plasma/workspace/kcms/nightcolor/kcm_nightcolor.desktop create mode 100644 plasma/workspace/kcms/nightcolor/kcm_nightcolor.json create mode 100644 plasma/workspace/kcms/nightcolor/nightcolorsettings.kcfg create mode 100644 plasma/workspace/kcms/nightcolor/nightcolorsettings.kcfgc create mode 100644 plasma/workspace/kcms/nightcolor/package/contents/ui/LocationsFixedView.qml create mode 100644 plasma/workspace/kcms/nightcolor/package/contents/ui/NumberField.qml create mode 100644 plasma/workspace/kcms/nightcolor/package/contents/ui/TimeField.qml create mode 100644 plasma/workspace/kcms/nightcolor/package/contents/ui/TimingsView.qml create mode 100644 plasma/workspace/kcms/nightcolor/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/notifications/CMakeLists.txt create mode 100644 plasma/workspace/kcms/notifications/Messages.sh create mode 100644 plasma/workspace/kcms/notifications/filterproxymodel.cpp create mode 100644 plasma/workspace/kcms/notifications/filterproxymodel.h create mode 100644 plasma/workspace/kcms/notifications/kcm.cpp create mode 100644 plasma/workspace/kcms/notifications/kcm.h create mode 100644 plasma/workspace/kcms/notifications/kcm_notifications.desktop create mode 100644 plasma/workspace/kcms/notifications/kcm_notifications.json create mode 100644 plasma/workspace/kcms/notifications/notificationsdata.cpp create mode 100644 plasma/workspace/kcms/notifications/notificationsdata.h create mode 100644 plasma/workspace/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml create mode 100644 plasma/workspace/kcms/notifications/package/contents/ui/PopupPositionPage.qml create mode 100644 plasma/workspace/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml create mode 100644 plasma/workspace/kcms/notifications/package/contents/ui/SourcesPage.qml create mode 100644 plasma/workspace/kcms/notifications/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/notifications/sourcesmodel.cpp create mode 100644 plasma/workspace/kcms/notifications/sourcesmodel.h create mode 100644 plasma/workspace/kcms/style/CMakeLists.txt create mode 100644 plasma/workspace/kcms/style/Messages.sh create mode 100644 plasma/workspace/kcms/style/gtk_themes.knsrc create mode 100644 plasma/workspace/kcms/style/gtkpage.cpp create mode 100644 plasma/workspace/kcms/style/gtkpage.h create mode 100644 plasma/workspace/kcms/style/gtkthemesmodel.cpp create mode 100644 plasma/workspace/kcms/style/gtkthemesmodel.h create mode 100644 plasma/workspace/kcms/style/kcm_style.desktop create mode 100644 plasma/workspace/kcms/style/kcm_style.json create mode 100644 plasma/workspace/kcms/style/kcmstyle.cpp create mode 100644 plasma/workspace/kcms/style/kcmstyle.h create mode 100644 plasma/workspace/kcms/style/org.kde.GtkConfig.xml create mode 100644 plasma/workspace/kcms/style/package/contents/ui/EffectSettingsPopup.qml create mode 100644 plasma/workspace/kcms/style/package/contents/ui/GtkStylePage.qml create mode 100644 plasma/workspace/kcms/style/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/style/previewitem.cpp create mode 100644 plasma/workspace/kcms/style/previewitem.h create mode 100644 plasma/workspace/kcms/style/style_widgetstyle_default_breeze.pl create mode 100644 plasma/workspace/kcms/style/style_widgetstyle_default_breeze.upd create mode 100644 plasma/workspace/kcms/style/styleconfdialog.cpp create mode 100644 plasma/workspace/kcms/style/styleconfdialog.h create mode 100644 plasma/workspace/kcms/style/stylepreview.ui create mode 100644 plasma/workspace/kcms/style/stylesettings.kcfg create mode 100644 plasma/workspace/kcms/style/stylesettings.kcfgc create mode 100644 plasma/workspace/kcms/style/stylesmodel.cpp create mode 100644 plasma/workspace/kcms/style/stylesmodel.h create mode 100644 plasma/workspace/kcms/translations/CMakeLists.txt create mode 100644 plasma/workspace/kcms/translations/Messages.sh create mode 100644 plasma/workspace/kcms/translations/kcm_translations.desktop create mode 100644 plasma/workspace/kcms/translations/kcm_translations.json create mode 100644 plasma/workspace/kcms/translations/language.cpp create mode 100644 plasma/workspace/kcms/translations/language.h create mode 100644 plasma/workspace/kcms/translations/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/translations/translations.cpp create mode 100644 plasma/workspace/kcms/translations/translations.h create mode 100644 plasma/workspace/kcms/translations/translationsmodel.cpp create mode 100644 plasma/workspace/kcms/translations/translationsmodel.h create mode 100644 plasma/workspace/kcms/translations/translationssettings.cpp create mode 100644 plasma/workspace/kcms/translations/translationssettings.h create mode 100644 plasma/workspace/kcms/translations/translationssettingsbase.kcfg create mode 100644 plasma/workspace/kcms/translations/translationssettingsbase.kcfgc create mode 100644 plasma/workspace/kcms/users/CMakeLists.txt create mode 100644 plasma/workspace/kcms/users/Messages.sh create mode 100644 plasma/workspace/kcms/users/avatars/Artist Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Bookworm Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Boss Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Bug Catcher Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Card Shark Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Hacker Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Journalist Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Katie.png create mode 100644 plasma/workspace/kcms/users/avatars/Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Mechanic Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Messenger Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Musician Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Office Worker Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/PC Builder Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Scientist Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Teacher Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/Virtual Reality Konqi.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Air Balloon.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Air Balloon.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Astronaut.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Astronaut.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Books.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Books.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Brushes.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Brushes.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Bulb.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Bulb.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Car.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Car.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Cat.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Cat.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Chameleon.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Chamelon.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Cocktail.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Cocktail.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Dog.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Dog.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Fish.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Fish.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Gamepad.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Gamepad.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Owl.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Owl.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Pancakes.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Pancakes.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Parrot.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Parrot.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Pencils.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Pencils.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Shuttle.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Shuttle.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Soccer.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Soccer.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Sunflower.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Sunflower.png.license create mode 100644 plasma/workspace/kcms/users/avatars/photos/Sushi.png create mode 100644 plasma/workspace/kcms/users/avatars/photos/Sushi.png.license create mode 100644 plasma/workspace/kcms/users/kcm_users.desktop create mode 100644 plasma/workspace/kcms/users/package/contents/ui/ChangePassword.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/ChangeWalletPassword.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/CreateUser.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/FingerprintDialog.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/FingerprintProgressCircle.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/UserDetailsPage.qml create mode 100644 plasma/workspace/kcms/users/package/contents/ui/main.qml create mode 100644 plasma/workspace/kcms/users/src/CMakeLists.txt create mode 100644 plasma/workspace/kcms/users/src/fingerprintmodel.cpp create mode 100644 plasma/workspace/kcms/users/src/fingerprintmodel.h create mode 100644 plasma/workspace/kcms/users/src/fprintdevice.cpp create mode 100644 plasma/workspace/kcms/users/src/fprintdevice.h create mode 100644 plasma/workspace/kcms/users/src/kcm.cpp create mode 100644 plasma/workspace/kcms/users/src/kcm.h create mode 100644 plasma/workspace/kcms/users/src/kcm_users.json create mode 100644 plasma/workspace/kcms/users/src/net.reactivated.Fprint.Device.xml create mode 100644 plasma/workspace/kcms/users/src/net.reactivated.Fprint.Manager.xml create mode 100644 plasma/workspace/kcms/users/src/org.freedesktop.Accounts.User.xml create mode 100644 plasma/workspace/kcms/users/src/org.freedesktop.Accounts.xml create mode 100644 plasma/workspace/kcms/users/src/org.freedesktop.login1.Manager.xml create mode 100644 plasma/workspace/kcms/users/src/user.cpp create mode 100644 plasma/workspace/kcms/users/src/user.h create mode 100644 plasma/workspace/kcms/users/src/usermodel.cpp create mode 100644 plasma/workspace/kcms/users/src/usermodel.h create mode 100644 plasma/workspace/kcms/users/src/usersessions.h create mode 100644 plasma/workspace/kioslave/CMakeLists.txt create mode 100644 plasma/workspace/kioslave/applications/CMakeLists.txt create mode 100644 plasma/workspace/kioslave/applications/Messages.sh create mode 100644 plasma/workspace/kioslave/applications/applications.json create mode 100644 plasma/workspace/kioslave/applications/kio_applications.cpp create mode 100644 plasma/workspace/kioslave/desktop/CMakeLists.txt create mode 100644 plasma/workspace/kioslave/desktop/ExtraDesktop.sh create mode 100644 plasma/workspace/kioslave/desktop/Messages.sh create mode 100644 plasma/workspace/kioslave/desktop/desktop.json create mode 100644 plasma/workspace/kioslave/desktop/desktopnotifier.cpp create mode 100644 plasma/workspace/kioslave/desktop/desktopnotifier.h create mode 100644 plasma/workspace/kioslave/desktop/desktopnotifier.json create mode 100644 plasma/workspace/kioslave/desktop/directory.desktop create mode 100644 plasma/workspace/kioslave/desktop/directory.trash create mode 100644 plasma/workspace/kioslave/desktop/kio_desktop.cpp create mode 100644 plasma/workspace/kioslave/desktop/kio_desktop.h create mode 100644 plasma/workspace/kioslave/desktop/tests/CMakeLists.txt create mode 100644 plasma/workspace/kioslave/desktop/tests/kio_desktop_test.cpp create mode 100644 plasma/workspace/klipper/CMakeLists.txt create mode 100644 plasma/workspace/klipper/Messages.sh create mode 100644 plasma/workspace/klipper/actionsconfig.ui create mode 100644 plasma/workspace/klipper/actionstreewidget.cpp create mode 100644 plasma/workspace/klipper/actionstreewidget.h create mode 100644 plasma/workspace/klipper/autotests/CMakeLists.txt create mode 100644 plasma/workspace/klipper/autotests/historymodeltest.cpp create mode 100644 plasma/workspace/klipper/autotests/historytest.cpp create mode 100644 plasma/workspace/klipper/autotests/utilstest.cpp create mode 100644 plasma/workspace/klipper/clipboardengine.cpp create mode 100644 plasma/workspace/klipper/clipboardengine.h create mode 100644 plasma/workspace/klipper/clipboardjob.cpp create mode 100644 plasma/workspace/klipper/clipboardjob.h create mode 100644 plasma/workspace/klipper/clipboardservice.cpp create mode 100644 plasma/workspace/klipper/clipboardservice.h create mode 100644 plasma/workspace/klipper/clipcommandprocess.cpp create mode 100644 plasma/workspace/klipper/clipcommandprocess.h create mode 100644 plasma/workspace/klipper/config-klipper.h.cmake create mode 100644 plasma/workspace/klipper/configdialog.cpp create mode 100644 plasma/workspace/klipper/configdialog.h create mode 100644 plasma/workspace/klipper/editactiondialog.cpp create mode 100644 plasma/workspace/klipper/editactiondialog.h create mode 100644 plasma/workspace/klipper/editactiondialog.ui create mode 100644 plasma/workspace/klipper/history.cpp create mode 100644 plasma/workspace/klipper/history.h create mode 100644 plasma/workspace/klipper/historyimageitem.cpp create mode 100644 plasma/workspace/klipper/historyimageitem.h create mode 100644 plasma/workspace/klipper/historyitem.cpp create mode 100644 plasma/workspace/klipper/historyitem.h create mode 100644 plasma/workspace/klipper/historymodel.cpp create mode 100644 plasma/workspace/klipper/historymodel.h create mode 100644 plasma/workspace/klipper/historystringitem.cpp create mode 100644 plasma/workspace/klipper/historystringitem.h create mode 100644 plasma/workspace/klipper/historyurlitem.cpp create mode 100644 plasma/workspace/klipper/historyurlitem.h create mode 100644 plasma/workspace/klipper/klipper.cpp create mode 100644 plasma/workspace/klipper/klipper.desktop create mode 100644 plasma/workspace/klipper/klipper.h create mode 100644 plasma/workspace/klipper/klipper.kcfg create mode 100644 plasma/workspace/klipper/klipperpopup.cpp create mode 100644 plasma/workspace/klipper/klipperpopup.h create mode 100644 plasma/workspace/klipper/klipperrc.desktop create mode 100644 plasma/workspace/klipper/klippersettings.kcfgc create mode 100644 plasma/workspace/klipper/main.cpp create mode 100644 plasma/workspace/klipper/org.kde.klipper.desktop create mode 100644 plasma/workspace/klipper/org.kde.plasma.clipboard.operations create mode 100644 plasma/workspace/klipper/plasma-dataengine-clipboard.json create mode 100644 plasma/workspace/klipper/popupproxy.cpp create mode 100644 plasma/workspace/klipper/popupproxy.h create mode 100644 plasma/workspace/klipper/tray.cpp create mode 100644 plasma/workspace/klipper/tray.h create mode 100644 plasma/workspace/klipper/urlgrabber.cpp create mode 100644 plasma/workspace/klipper/urlgrabber.h create mode 100644 plasma/workspace/klipper/utils.cpp create mode 100644 plasma/workspace/klipper/utils.h create mode 100644 plasma/workspace/krunner/CMakeLists.txt create mode 100644 plasma/workspace/krunner/KRunnerAppDBusInterfaceConfig.cmake.in create mode 100644 plasma/workspace/krunner/Messages.sh create mode 100644 plasma/workspace/krunner/dbus/org.kde.krunner.App.xml create mode 100644 plasma/workspace/krunner/main.cpp create mode 100644 plasma/workspace/krunner/org.kde.krunner.desktop.cmake create mode 100644 plasma/workspace/krunner/plasma-krunner.service.in create mode 100644 plasma/workspace/krunner/update/CMakeLists.txt create mode 100644 plasma/workspace/krunner/update/krunnerglobalshortcuts.cpp create mode 100644 plasma/workspace/krunner/update/krunnerglobalshortcuts2.upd create mode 100644 plasma/workspace/krunner/update/krunnerhistory.cpp create mode 100644 plasma/workspace/krunner/update/krunnerhistory.upd create mode 100644 plasma/workspace/krunner/view.cpp create mode 100644 plasma/workspace/krunner/view.h create mode 100644 plasma/workspace/ksmserver/CMakeLists.txt create mode 100644 plasma/workspace/ksmserver/Copyright.txt create mode 100644 plasma/workspace/ksmserver/KSMServerDBusInterfaceConfig.cmake.in create mode 100644 plasma/workspace/ksmserver/LICENSE create mode 100644 plasma/workspace/ksmserver/Messages.sh create mode 100644 plasma/workspace/ksmserver/README create mode 100644 plasma/workspace/ksmserver/client.cpp create mode 100644 plasma/workspace/ksmserver/client.h create mode 100644 plasma/workspace/ksmserver/config-ksmserver.h.cmake create mode 100644 plasma/workspace/ksmserver/global.h create mode 100644 plasma/workspace/ksmserver/legacy.cpp create mode 100644 plasma/workspace/ksmserver/logout.cpp create mode 100644 plasma/workspace/ksmserver/main.cpp create mode 100644 plasma/workspace/ksmserver/org.kde.KSMServerInterface.xml create mode 100644 plasma/workspace/ksmserver/org.kde.KWin.Session.xml create mode 100644 plasma/workspace/ksmserver/org.kde.LogoutPrompt.xml create mode 100644 plasma/workspace/ksmserver/plasma-ksmserver.service.in create mode 100644 plasma/workspace/ksmserver/plasma-restoresession.service.in create mode 100644 plasma/workspace/ksmserver/server.cpp create mode 100644 plasma/workspace/ksmserver/server.h create mode 100644 plasma/workspace/ksmserver/tests/CMakeLists.txt create mode 100644 plasma/workspace/ksmserver/tests/autostarttest.cpp create mode 100644 plasma/workspace/ksplash/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/README create mode 100644 plasma/workspace/ksplash/ksplashqml/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/ksplashqml/main.cpp create mode 100644 plasma/workspace/ksplash/ksplashqml/org.kde.KSplash.xml create mode 100644 plasma/workspace/ksplash/ksplashqml/plasma-ksplash.service.in create mode 100644 plasma/workspace/ksplash/ksplashqml/splashapp.cpp create mode 100644 plasma/workspace/ksplash/ksplashqml/splashapp.h create mode 100644 plasma/workspace/ksplash/ksplashqml/splashwindow.cpp create mode 100644 plasma/workspace/ksplash/ksplashqml/splashwindow.h create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/FadeIn.qml create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/Preview.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/Theme.rc create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/background.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/icon1.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/icon2.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/icon3.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/icon4.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/icon5.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/images/rectangle.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Classic/main.qml create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/Preview.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/Theme.rc create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/images/kdegear.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/images/kdeletter.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/images/kdelogo-contrast.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/images/kdelogo.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/images/kdemask.png create mode 100644 plasma/workspace/ksplash/ksplashqml/themes/Minimalistic/main.qml create mode 100644 plasma/workspace/ksplash/none/CMakeLists.txt create mode 100644 plasma/workspace/ksplash/none/Theme.rc create mode 100644 plasma/workspace/ktimezoned/CMakeLists.txt create mode 100644 plasma/workspace/ktimezoned/ktimezoned.cpp create mode 100644 plasma/workspace/ktimezoned/ktimezoned.h create mode 100644 plasma/workspace/ktimezoned/ktimezoned.json create mode 100644 plasma/workspace/ktimezoned/ktimezoned_win.cpp create mode 100644 plasma/workspace/ktimezoned/ktimezoned_win.h create mode 100644 plasma/workspace/ktimezoned/ktimezonedbase.h create mode 100644 plasma/workspace/ktimezoned/org.kde.KTimeZoned.xml create mode 100644 plasma/workspace/libcolorcorrect/CMakeLists.txt create mode 100644 plasma/workspace/libcolorcorrect/LibColorCorrectConfig.cmake.in create mode 100644 plasma/workspace/libcolorcorrect/README create mode 100644 plasma/workspace/libcolorcorrect/colorcorrectconstants.h create mode 100644 plasma/workspace/libcolorcorrect/compositorcoloradaptor.cpp create mode 100644 plasma/workspace/libcolorcorrect/compositorcoloradaptor.h create mode 100644 plasma/workspace/libcolorcorrect/declarative/CMakeLists.txt create mode 100644 plasma/workspace/libcolorcorrect/declarative/colorcorrectplugin.cpp create mode 100644 plasma/workspace/libcolorcorrect/declarative/colorcorrectplugin.h create mode 100644 plasma/workspace/libcolorcorrect/declarative/qmldir create mode 100644 plasma/workspace/libcolorcorrect/geolocator.cpp create mode 100644 plasma/workspace/libcolorcorrect/geolocator.h create mode 100644 plasma/workspace/libcolorcorrect/kded/CMakeLists.txt create mode 100644 plasma/workspace/libcolorcorrect/kded/colorcorrectlocationupdater.json create mode 100644 plasma/workspace/libcolorcorrect/kded/locationupdater.cpp create mode 100644 plasma/workspace/libcolorcorrect/kded/locationupdater.h create mode 100644 plasma/workspace/libcolorcorrect/suncalc.cpp create mode 100644 plasma/workspace/libcolorcorrect/suncalc.h create mode 100644 plasma/workspace/libdbusmenuqt/CMakeLists.txt create mode 100644 plasma/workspace/libdbusmenuqt/README create mode 100644 plasma/workspace/libdbusmenuqt/com.canonical.dbusmenu.xml create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenuimporter.cpp create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenuimporter.h create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenushortcut_p.cpp create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenushortcut_p.h create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenutypes_p.cpp create mode 100644 plasma/workspace/libdbusmenuqt/dbusmenutypes_p.h create mode 100644 plasma/workspace/libdbusmenuqt/test/CMakeLists.txt create mode 100644 plasma/workspace/libdbusmenuqt/test/README create mode 100644 plasma/workspace/libdbusmenuqt/test/main.cpp create mode 100644 plasma/workspace/libdbusmenuqt/utils.cpp create mode 100644 plasma/workspace/libdbusmenuqt/utils_p.h create mode 100644 plasma/workspace/libkworkspace/CMakeLists.txt create mode 100644 plasma/workspace/libkworkspace/LibKWorkspaceConfig.cmake.in create mode 100644 plasma/workspace/libkworkspace/Messages.sh create mode 100644 plasma/workspace/libkworkspace/autostartscriptdesktopfile.cpp create mode 100644 plasma/workspace/libkworkspace/autostartscriptdesktopfile.h create mode 100644 plasma/workspace/libkworkspace/autotests/CMakeLists.txt create mode 100644 plasma/workspace/libkworkspace/autotests/testPlatformDetection.cpp create mode 100644 plasma/workspace/libkworkspace/config-libkworkspace.h.cmake create mode 100644 plasma/workspace/libkworkspace/kdisplaymanager.cpp create mode 100644 plasma/workspace/libkworkspace/kdisplaymanager.h create mode 100644 plasma/workspace/libkworkspace/kworkspace.cpp create mode 100644 plasma/workspace/libkworkspace/kworkspace.h create mode 100644 plasma/workspace/libkworkspace/login1_manager_interface.cpp create mode 100644 plasma/workspace/libkworkspace/loginddbustypes.h create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.ConsoleKit.Manager.xml create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.UPower.xml create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.login1.Manager.xml create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.login1.Seat.xml create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.login1.Session.xml create mode 100644 plasma/workspace/libkworkspace/org.freedesktop.login1.User.xml create mode 100644 plasma/workspace/libkworkspace/sessionmanagement.cpp create mode 100644 plasma/workspace/libkworkspace/sessionmanagement.h create mode 100644 plasma/workspace/libkworkspace/sessionmanagementbackend.cpp create mode 100644 plasma/workspace/libkworkspace/sessionmanagementbackend.h create mode 100644 plasma/workspace/libkworkspace/tests/CMakeLists.txt create mode 100644 plasma/workspace/libkworkspace/tests/sessiontest.cpp create mode 100644 plasma/workspace/libkworkspace/tests/syncdbusenvtest.cpp create mode 100644 plasma/workspace/libkworkspace/updatelaunchenvjob.cpp create mode 100644 plasma/workspace/libkworkspace/updatelaunchenvjob.h create mode 100644 plasma/workspace/libnotificationmanager/CMakeLists.txt create mode 100644 plasma/workspace/libnotificationmanager/LibNotificationManagerConfig.cmake.in create mode 100644 plasma/workspace/libnotificationmanager/Messages.sh create mode 100644 plasma/workspace/libnotificationmanager/abstractnotificationsmodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/abstractnotificationsmodel.h create mode 100644 plasma/workspace/libnotificationmanager/abstractnotificationsmodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/autotests/CMakeLists.txt create mode 100644 plasma/workspace/libnotificationmanager/autotests/notifications_test.cpp create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.freedesktop.Notifications.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.JobViewServer.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.JobViewServerV2.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.JobViewV2.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.JobViewV3.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.kuiserver.xml create mode 100644 plasma/workspace/libnotificationmanager/dbus/org.kde.notificationmanager.xml create mode 100644 plasma/workspace/libnotificationmanager/declarative/CMakeLists.txt create mode 100644 plasma/workspace/libnotificationmanager/declarative/notificationmanagerplugin.cpp create mode 100644 plasma/workspace/libnotificationmanager/declarative/notificationmanagerplugin.h create mode 100644 plasma/workspace/libnotificationmanager/declarative/qmldir create mode 100644 plasma/workspace/libnotificationmanager/job.cpp create mode 100644 plasma/workspace/libnotificationmanager/job.h create mode 100644 plasma/workspace/libnotificationmanager/job_p.cpp create mode 100644 plasma/workspace/libnotificationmanager/job_p.h create mode 100644 plasma/workspace/libnotificationmanager/jobsmodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/jobsmodel.h create mode 100644 plasma/workspace/libnotificationmanager/jobsmodel_p.cpp create mode 100644 plasma/workspace/libnotificationmanager/jobsmodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/kcfg/badgesettings.kcfg create mode 100644 plasma/workspace/libnotificationmanager/kcfg/badgesettings.kcfgc create mode 100644 plasma/workspace/libnotificationmanager/kcfg/behaviorsettings.kcfg create mode 100644 plasma/workspace/libnotificationmanager/kcfg/behaviorsettings.kcfgc create mode 100644 plasma/workspace/libnotificationmanager/kcfg/donotdisturbsettings.kcfg create mode 100644 plasma/workspace/libnotificationmanager/kcfg/donotdisturbsettings.kcfgc create mode 100644 plasma/workspace/libnotificationmanager/kcfg/jobsettings.kcfg create mode 100644 plasma/workspace/libnotificationmanager/kcfg/jobsettings.kcfgc create mode 100644 plasma/workspace/libnotificationmanager/kcfg/notificationsettings.kcfg create mode 100644 plasma/workspace/libnotificationmanager/kcfg/notificationsettings.kcfgc create mode 100644 plasma/workspace/libnotificationmanager/limitedrowcountproxymodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/limitedrowcountproxymodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/mirroredscreenstracker.cpp create mode 100644 plasma/workspace/libnotificationmanager/mirroredscreenstracker_p.h create mode 100644 plasma/workspace/libnotificationmanager/notification.cpp create mode 100644 plasma/workspace/libnotificationmanager/notification.h create mode 100644 plasma/workspace/libnotificationmanager/notification_p.h create mode 100644 plasma/workspace/libnotificationmanager/notificationfilterproxymodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/notificationfilterproxymodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/notificationgroupcollapsingproxymodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/notificationgroupcollapsingproxymodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/notificationgroupingproxymodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/notificationgroupingproxymodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/notifications.cpp create mode 100644 plasma/workspace/libnotificationmanager/notifications.h create mode 100644 plasma/workspace/libnotificationmanager/notificationsmodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/notificationsmodel.h create mode 100644 plasma/workspace/libnotificationmanager/notificationsortproxymodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/notificationsortproxymodel_p.h create mode 100644 plasma/workspace/libnotificationmanager/plasmanotifyrc create mode 100644 plasma/workspace/libnotificationmanager/server.cpp create mode 100644 plasma/workspace/libnotificationmanager/server.h create mode 100644 plasma/workspace/libnotificationmanager/server_p.cpp create mode 100644 plasma/workspace/libnotificationmanager/server_p.h create mode 100644 plasma/workspace/libnotificationmanager/serverinfo.cpp create mode 100644 plasma/workspace/libnotificationmanager/serverinfo.h create mode 100644 plasma/workspace/libnotificationmanager/settings.cpp create mode 100644 plasma/workspace/libnotificationmanager/settings.h create mode 100644 plasma/workspace/libnotificationmanager/utils.cpp create mode 100644 plasma/workspace/libnotificationmanager/utils_p.h create mode 100644 plasma/workspace/libnotificationmanager/watchednotificationsmodel.cpp create mode 100644 plasma/workspace/libnotificationmanager/watchednotificationsmodel.h create mode 100644 plasma/workspace/libtaskmanager/CMakeLists.txt create mode 100644 plasma/workspace/libtaskmanager/LibTaskManagerConfig.cmake.in create mode 100644 plasma/workspace/libtaskmanager/TODO.txt create mode 100644 plasma/workspace/libtaskmanager/abstracttasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/abstracttasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/abstracttasksmodeliface.h create mode 100644 plasma/workspace/libtaskmanager/abstracttasksproxymodeliface.cpp create mode 100644 plasma/workspace/libtaskmanager/abstracttasksproxymodeliface.h create mode 100644 plasma/workspace/libtaskmanager/abstractwindowtasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/abstractwindowtasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/activityinfo.cpp create mode 100644 plasma/workspace/libtaskmanager/activityinfo.h create mode 100644 plasma/workspace/libtaskmanager/autotests/CMakeLists.txt create mode 100644 plasma/workspace/libtaskmanager/autotests/data/applications/org.kde.dolphin.desktop create mode 100644 plasma/workspace/libtaskmanager/autotests/data/applications/org.kde.konversation.desktop create mode 100644 plasma/workspace/libtaskmanager/autotests/launchertasksmodeltest.cpp create mode 100644 plasma/workspace/libtaskmanager/autotests/tasktoolstest.cpp create mode 100644 plasma/workspace/libtaskmanager/concatenatetasksproxymodel.cpp create mode 100644 plasma/workspace/libtaskmanager/concatenatetasksproxymodel.h create mode 100644 plasma/workspace/libtaskmanager/declarative/CMakeLists.txt create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewirecore.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewirecore.h create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewiresourceitem.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewiresourceitem.h create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewiresourcestream.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/pipewiresourcestream.h create mode 100644 plasma/workspace/libtaskmanager/declarative/qmldir create mode 100644 plasma/workspace/libtaskmanager/declarative/screencasting.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/screencasting.h create mode 100644 plasma/workspace/libtaskmanager/declarative/screencastingrequest.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/screencastingrequest.h create mode 100644 plasma/workspace/libtaskmanager/declarative/taskmanagerplugin.cpp create mode 100644 plasma/workspace/libtaskmanager/declarative/taskmanagerplugin.h create mode 100644 plasma/workspace/libtaskmanager/flattentaskgroupsproxymodel.cpp create mode 100644 plasma/workspace/libtaskmanager/flattentaskgroupsproxymodel.h create mode 100644 plasma/workspace/libtaskmanager/launchertasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/launchertasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/launchertasksmodel_p.h create mode 100644 plasma/workspace/libtaskmanager/startuptasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/startuptasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/taskfilterproxymodel.cpp create mode 100644 plasma/workspace/libtaskmanager/taskfilterproxymodel.h create mode 100644 plasma/workspace/libtaskmanager/taskgroupingproxymodel.cpp create mode 100644 plasma/workspace/libtaskmanager/taskgroupingproxymodel.h create mode 100644 plasma/workspace/libtaskmanager/taskmanagerrulesrc create mode 100644 plasma/workspace/libtaskmanager/tasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/tasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/tasktools.cpp create mode 100644 plasma/workspace/libtaskmanager/tasktools.h create mode 100644 plasma/workspace/libtaskmanager/virtualdesktopinfo.cpp create mode 100644 plasma/workspace/libtaskmanager/virtualdesktopinfo.h create mode 100644 plasma/workspace/libtaskmanager/waylandstartuptasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/waylandstartuptasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/waylandtasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/waylandtasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/windowtasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/windowtasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/xstartuptasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/xstartuptasksmodel.h create mode 100644 plasma/workspace/libtaskmanager/xwindowsystemeventbatcher.cpp create mode 100644 plasma/workspace/libtaskmanager/xwindowsystemeventbatcher.h create mode 100644 plasma/workspace/libtaskmanager/xwindowtasksmodel.cpp create mode 100644 plasma/workspace/libtaskmanager/xwindowtasksmodel.h create mode 100644 plasma/workspace/login-sessions/CMakeLists.txt create mode 100644 plasma/workspace/login-sessions/install-sessions.sh.cmake create mode 100644 plasma/workspace/login-sessions/plasmawayland-dev.desktop.cmake create mode 100644 plasma/workspace/login-sessions/plasmawayland.desktop.cmake create mode 100644 plasma/workspace/login-sessions/plasmax11-dev.desktop.cmake create mode 100644 plasma/workspace/login-sessions/plasmax11.desktop.cmake create mode 100644 plasma/workspace/login-sessions/startplasma-dev.sh.cmake create mode 100644 plasma/workspace/logout-greeter/CMakeLists.txt create mode 100644 plasma/workspace/logout-greeter/greeter.cpp create mode 100644 plasma/workspace/logout-greeter/greeter.h create mode 100644 plasma/workspace/logout-greeter/main.cpp create mode 100644 plasma/workspace/logout-greeter/shutdowndlg.cpp create mode 100644 plasma/workspace/logout-greeter/shutdowndlg.h create mode 100644 plasma/workspace/logout-greeter/tests/CMakeLists.txt create mode 100644 plasma/workspace/logout-greeter/tests/config.h.cmake create mode 100644 plasma/workspace/logout-greeter/tests/main.cpp create mode 100644 plasma/workspace/lookandfeel.dark/contents/defaults create mode 100644 plasma/workspace/lookandfeel.dark/contents/previews/fullscreenpreview.jpg create mode 100644 plasma/workspace/lookandfeel.dark/contents/previews/preview.png create mode 100644 plasma/workspace/lookandfeel.dark/metadata.json create mode 100644 plasma/workspace/lookandfeel.twilight/contents/defaults create mode 100644 plasma/workspace/lookandfeel.twilight/contents/previews/fullscreenpreview.jpg create mode 100644 plasma/workspace/lookandfeel.twilight/contents/previews/preview.png create mode 100644 plasma/workspace/lookandfeel.twilight/metadata.json create mode 100644 plasma/workspace/lookandfeel/Messages.sh create mode 100644 plasma/workspace/lookandfeel/contents/components/ActionButton.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/Battery.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/Clock.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/SessionManagementScreen.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/UserDelegate.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/UserList.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/VirtualKeyboard.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/VirtualKeyboard_wayland.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/WallpaperFader.qml create mode 100644 plasma/workspace/lookandfeel/contents/components/artwork/README.txt create mode 100644 plasma/workspace/lookandfeel/contents/components/artwork/logout_primary.svgz create mode 100644 plasma/workspace/lookandfeel/contents/components/artwork/restart_primary.svgz create mode 100644 plasma/workspace/lookandfeel/contents/components/artwork/shutdown_primary.svgz create mode 100644 plasma/workspace/lookandfeel/contents/defaults create mode 100644 plasma/workspace/lookandfeel/contents/desktopswitcher/DesktopSwitcher.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/LockOsd.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/LockScreen.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/LockScreenUi.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/MainBlock.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/MediaControls.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/config.qml create mode 100644 plasma/workspace/lookandfeel/contents/lockscreen/config.xml create mode 100644 plasma/workspace/lookandfeel/contents/logout/Logout.qml create mode 100644 plasma/workspace/lookandfeel/contents/logout/LogoutButton.qml create mode 100644 plasma/workspace/lookandfeel/contents/logout/dummydata/screenGeometry.qml create mode 100644 plasma/workspace/lookandfeel/contents/logout/timer.js create mode 100644 plasma/workspace/lookandfeel/contents/osd/Osd.qml create mode 100644 plasma/workspace/lookandfeel/contents/osd/OsdItem.qml create mode 100644 plasma/workspace/lookandfeel/contents/previews/desktopswitcher.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/fullscreenpreview.jpg create mode 100644 plasma/workspace/lookandfeel/contents/previews/lockscreen.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/loginmanager.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/preview.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/runcommand.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/splash.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/userswitcher.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/windowdecoration.png create mode 100644 plasma/workspace/lookandfeel/contents/previews/windowswitcher.png create mode 100644 plasma/workspace/lookandfeel/contents/runcommand/RunCommand.qml create mode 100644 plasma/workspace/lookandfeel/contents/splash/Splash.qml create mode 100644 plasma/workspace/lookandfeel/contents/splash/images/busywidget.svgz create mode 100644 plasma/workspace/lookandfeel/contents/splash/images/kde.svgz create mode 100644 plasma/workspace/lookandfeel/contents/splash/images/plasma.svgz create mode 100644 plasma/workspace/lookandfeel/contents/systemdialog/SystemDialog.qml create mode 100644 plasma/workspace/lookandfeel/contents/windowswitcher/WindowSwitcher.qml create mode 100644 plasma/workspace/lookandfeel/metadata.json create mode 100644 plasma/workspace/menu/CMakeLists.txt create mode 100644 plasma/workspace/menu/desktop/CMakeLists.txt create mode 100644 plasma/workspace/menu/desktop/hidden.directory create mode 100644 plasma/workspace/menu/desktop/kf5-development-translation.directory create mode 100644 plasma/workspace/menu/desktop/kf5-development-webdevelopment.directory create mode 100644 plasma/workspace/menu/desktop/kf5-development.directory create mode 100644 plasma/workspace/menu/desktop/kf5-editors.directory create mode 100644 plasma/workspace/menu/desktop/kf5-edu-languages.directory create mode 100644 plasma/workspace/menu/desktop/kf5-edu-mathematics.directory create mode 100644 plasma/workspace/menu/desktop/kf5-edu-miscellaneous.directory create mode 100644 plasma/workspace/menu/desktop/kf5-edu-science.directory create mode 100644 plasma/workspace/menu/desktop/kf5-edu-tools.directory create mode 100644 plasma/workspace/menu/desktop/kf5-education.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-arcade.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-board.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-card.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-kids.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-logic.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-roguelikes.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games-strategy.directory create mode 100644 plasma/workspace/menu/desktop/kf5-games.directory create mode 100644 plasma/workspace/menu/desktop/kf5-graphics.directory create mode 100644 plasma/workspace/menu/desktop/kf5-internet-terminal.directory create mode 100644 plasma/workspace/menu/desktop/kf5-internet.directory create mode 100644 plasma/workspace/menu/desktop/kf5-main.directory create mode 100644 plasma/workspace/menu/desktop/kf5-more.directory create mode 100644 plasma/workspace/menu/desktop/kf5-multimedia.directory create mode 100644 plasma/workspace/menu/desktop/kf5-network.directory create mode 100644 plasma/workspace/menu/desktop/kf5-office.directory create mode 100644 plasma/workspace/menu/desktop/kf5-science.directory create mode 100644 plasma/workspace/menu/desktop/kf5-settingsmenu.directory create mode 100644 plasma/workspace/menu/desktop/kf5-system-terminal.directory create mode 100644 plasma/workspace/menu/desktop/kf5-system.directory create mode 100644 plasma/workspace/menu/desktop/kf5-toys.directory create mode 100644 plasma/workspace/menu/desktop/kf5-unknown.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-accessibility.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-desktop.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-file.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-peripherals.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-pim.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities-xutils.directory create mode 100644 plasma/workspace/menu/desktop/kf5-utilities.directory create mode 100644 plasma/workspace/metainfo.yaml create mode 100644 plasma/workspace/phonon/CMakeLists.txt create mode 100644 plasma/workspace/phonon/platform_kde/CMakeLists.txt create mode 100644 plasma/workspace/phonon/platform_kde/Messages.sh create mode 100644 plasma/workspace/phonon/platform_kde/debug.cpp create mode 100644 plasma/workspace/phonon/platform_kde/debug.h create mode 100644 plasma/workspace/phonon/platform_kde/kdeplatformplugin.cpp create mode 100644 plasma/workspace/phonon/platform_kde/kdeplatformplugin.h create mode 100644 plasma/workspace/phonon/platform_kde/kiomediastream.cpp create mode 100644 plasma/workspace/phonon/platform_kde/kiomediastream.h create mode 100644 plasma/workspace/phonon/platform_kde/kiomediastream_p.h create mode 100644 plasma/workspace/phonon/platform_kde/phonon.notifyrc create mode 100644 plasma/workspace/phonon/platform_kde/phononbackend.desktop create mode 100644 plasma/workspace/phonon/platform_kde/phononbackend.json create mode 100644 plasma/workspace/plasma-windowed/CMakeLists.txt create mode 100644 plasma/workspace/plasma-windowed/main.cpp create mode 100644 plasma/workspace/plasma-windowed/org.kde.plasmawindowed.desktop.cmake create mode 100644 plasma/workspace/plasma-windowed/plasmawindowedcorona.cpp create mode 100644 plasma/workspace/plasma-windowed/plasmawindowedcorona.h create mode 100644 plasma/workspace/plasma-windowed/plasmawindowedview.cpp create mode 100644 plasma/workspace/plasma-windowed/plasmawindowedview.h create mode 100644 plasma/workspace/plasma-workspace.categories create mode 100644 plasma/workspace/plasmacalendarintegration/CMakeLists.txt create mode 100644 plasma/workspace/plasmacalendarintegration/HolidaysConfig.qml create mode 100644 plasma/workspace/plasmacalendarintegration/Messages.sh create mode 100644 plasma/workspace/plasmacalendarintegration/holidayeventsplugin.json create mode 100644 plasma/workspace/plasmacalendarintegration/holidaysevents.cpp create mode 100644 plasma/workspace/plasmacalendarintegration/holidaysevents.h create mode 100644 plasma/workspace/plasmacalendarintegration/qmlhelper/CMakeLists.txt create mode 100644 plasma/workspace/plasmacalendarintegration/qmlhelper/holidayeventshelperplugin.cpp create mode 100644 plasma/workspace/plasmacalendarintegration/qmlhelper/holidayeventshelperplugin.h create mode 100644 plasma/workspace/plasmacalendarintegration/qmlhelper/qmldir create mode 100644 plasma/workspace/runners/CMakeLists.txt create mode 100644 plasma/workspace/runners/appstream/CMakeLists.txt create mode 100644 plasma/workspace/runners/appstream/Messages.sh create mode 100644 plasma/workspace/runners/appstream/appstreamrunner.cpp create mode 100644 plasma/workspace/runners/appstream/appstreamrunner.h create mode 100644 plasma/workspace/runners/appstream/plasma-runner-appstream.json create mode 100644 plasma/workspace/runners/baloo/CMakeLists.txt create mode 100644 plasma/workspace/runners/baloo/Messages.sh create mode 100644 plasma/workspace/runners/baloo/baloosearchrunner.cpp create mode 100644 plasma/workspace/runners/baloo/baloosearchrunner.h create mode 100644 plasma/workspace/runners/baloo/dbusutils_p.h create mode 100644 plasma/workspace/runners/baloo/org.kde.krunner1.xml create mode 100644 plasma/workspace/runners/baloo/plasma-baloorunner.service.in create mode 100644 plasma/workspace/runners/baloo/plasma-runner-baloosearch.desktop create mode 100644 plasma/workspace/runners/bookmarks/.gitignore create mode 100644 plasma/workspace/runners/bookmarks/CMakeLists.txt create mode 100644 plasma/workspace/runners/bookmarks/Messages.sh create mode 100644 plasma/workspace/runners/bookmarks/autotests/CMakeLists.txt create mode 100644 plasma/workspace/runners/bookmarks/autotests/bookmarksmatchtest.cpp create mode 100644 plasma/workspace/runners/bookmarks/autotests/chrome/chrome-config-home/.config/chromium/Local State create mode 100644 plasma/workspace/runners/bookmarks/autotests/chrome/chrome-config-home/Chrome-Bookmarks-Sample.json create mode 100644 plasma/workspace/runners/bookmarks/autotests/chrome/chrome-config-home/Chrome-Bookmarks-SecondProfile.json create mode 100644 plasma/workspace/runners/bookmarks/autotests/chrome/testchromebookmarks.cpp create mode 100644 plasma/workspace/runners/bookmarks/autotests/chrome/testchromebookmarks.h create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/firefox-config-home/atnsd8ae.testmekde/favicons.sqlite create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/firefox-config-home/atnsd8ae.testmekde/favicons.sqlite.license create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/firefox-config-home/atnsd8ae.testmekde/places.sqlite create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/firefox-config-home/atnsd8ae.testmekde/places.sqlite.license create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/firefox-config-home/profiles.ini create mode 100644 plasma/workspace/runners/bookmarks/autotests/firefox/testfirefoxbookmarks.cpp create mode 100644 plasma/workspace/runners/bookmarks/bookmarkmatch.cpp create mode 100644 plasma/workspace/runners/bookmarks/bookmarkmatch.h create mode 100644 plasma/workspace/runners/bookmarks/bookmarksrunner.cpp create mode 100644 plasma/workspace/runners/bookmarks/bookmarksrunner.h create mode 100644 plasma/workspace/runners/bookmarks/bookmarksrunner_defs.h create mode 100644 plasma/workspace/runners/bookmarks/browserfactory.cpp create mode 100644 plasma/workspace/runners/bookmarks/browserfactory.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/browser.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/chrome.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/chrome.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/chromefindprofile.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/chromefindprofile.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/falkon.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/falkon.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/findprofile.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/firefox.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/firefox.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/konqueror.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/konqueror.h create mode 100644 plasma/workspace/runners/bookmarks/browsers/opera.cpp create mode 100644 plasma/workspace/runners/bookmarks/browsers/opera.h create mode 100644 plasma/workspace/runners/bookmarks/favicon.cpp create mode 100644 plasma/workspace/runners/bookmarks/favicon.h create mode 100644 plasma/workspace/runners/bookmarks/faviconfromblob.cpp create mode 100644 plasma/workspace/runners/bookmarks/faviconfromblob.h create mode 100644 plasma/workspace/runners/bookmarks/fetchsqlite.cpp create mode 100644 plasma/workspace/runners/bookmarks/fetchsqlite.h create mode 100644 plasma/workspace/runners/bookmarks/plasma-runner-bookmarks.json create mode 100644 plasma/workspace/runners/calculator/CMakeLists.txt create mode 100644 plasma/workspace/runners/calculator/Messages.sh create mode 100644 plasma/workspace/runners/calculator/autotests/CMakeLists.txt create mode 100644 plasma/workspace/runners/calculator/autotests/calculatorrunnertest.cpp create mode 100644 plasma/workspace/runners/calculator/calculatorrunner.cpp create mode 100644 plasma/workspace/runners/calculator/calculatorrunner.h create mode 100644 plasma/workspace/runners/calculator/plasma-runner-calculator.json create mode 100644 plasma/workspace/runners/calculator/qalculate_engine.cpp create mode 100644 plasma/workspace/runners/calculator/qalculate_engine.h create mode 100644 plasma/workspace/runners/helprunner/CMakeLists.txt create mode 100644 plasma/workspace/runners/helprunner/helprunner.cpp create mode 100644 plasma/workspace/runners/helprunner/helprunner.h create mode 100644 plasma/workspace/runners/helprunner/helprunner.json create mode 100644 plasma/workspace/runners/kill/CMakeLists.txt create mode 100644 plasma/workspace/runners/kill/Messages.sh create mode 100644 plasma/workspace/runners/kill/TODO create mode 100644 plasma/workspace/runners/kill/config_keys.h create mode 100644 plasma/workspace/runners/kill/killrunner.cpp create mode 100644 plasma/workspace/runners/kill/killrunner.h create mode 100644 plasma/workspace/runners/kill/killrunner_config.cpp create mode 100644 plasma/workspace/runners/kill/killrunner_config.h create mode 100644 plasma/workspace/runners/kill/killrunner_config.ui create mode 100644 plasma/workspace/runners/kill/plasma-runner-kill.json create mode 100644 plasma/workspace/runners/locations/CMakeLists.txt create mode 100644 plasma/workspace/runners/locations/Messages.sh create mode 100644 plasma/workspace/runners/locations/autotests/CMakeLists.txt create mode 100644 plasma/workspace/runners/locations/autotests/locationsrunnertest.cpp create mode 100644 plasma/workspace/runners/locations/locationrunner.cpp create mode 100644 plasma/workspace/runners/locations/locationrunner.h create mode 100644 plasma/workspace/runners/locations/plasma-runner-locations.json create mode 100644 plasma/workspace/runners/places/CMakeLists.txt create mode 100644 plasma/workspace/runners/places/Messages.sh create mode 100644 plasma/workspace/runners/places/placesrunner.cpp create mode 100644 plasma/workspace/runners/places/placesrunner.h create mode 100644 plasma/workspace/runners/places/plasma-runner-places.json create mode 100644 plasma/workspace/runners/powerdevil/CMakeLists.txt create mode 100644 plasma/workspace/runners/powerdevil/Messages.sh create mode 100644 plasma/workspace/runners/powerdevil/PowerDevilRunner.cpp create mode 100644 plasma/workspace/runners/powerdevil/PowerDevilRunner.h create mode 100644 plasma/workspace/runners/powerdevil/plasma-runner-powerdevil.json create mode 100644 plasma/workspace/runners/recentdocuments/CMakeLists.txt create mode 100644 plasma/workspace/runners/recentdocuments/Messages.sh create mode 100644 plasma/workspace/runners/recentdocuments/plasma-runner-recentdocuments.json create mode 100644 plasma/workspace/runners/recentdocuments/recentdocuments.cpp create mode 100644 plasma/workspace/runners/recentdocuments/recentdocuments.h create mode 100644 plasma/workspace/runners/services/CMakeLists.txt create mode 100644 plasma/workspace/runners/services/Messages.sh create mode 100644 plasma/workspace/runners/services/autotests/CMakeLists.txt create mode 100644 plasma/workspace/runners/services/autotests/fixtures/chrome-signal.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/google-chrome.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/kdesystemsettings.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/org.kde.konsole.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/org.kde.systemsettings.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/org.kde.virtthings.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/org.kde.yakuake.desktop create mode 100644 plasma/workspace/runners/services/autotests/fixtures/virt-manager.desktop create mode 100644 plasma/workspace/runners/services/autotests/servicerunnertest.cpp create mode 100644 plasma/workspace/runners/services/plasma-runner-services.json create mode 100644 plasma/workspace/runners/services/plugin.cpp create mode 100644 plasma/workspace/runners/services/servicerunner.cpp create mode 100644 plasma/workspace/runners/services/servicerunner.h create mode 100644 plasma/workspace/runners/sessions/CMakeLists.txt create mode 100644 plasma/workspace/runners/sessions/Messages.sh create mode 100644 plasma/workspace/runners/sessions/plasma-runner-sessions.json create mode 100644 plasma/workspace/runners/sessions/sessionrunner.cpp create mode 100644 plasma/workspace/runners/sessions/sessionrunner.h create mode 100644 plasma/workspace/runners/shell/CMakeLists.txt create mode 100644 plasma/workspace/runners/shell/Messages.sh create mode 100644 plasma/workspace/runners/shell/autotests/CMakeLists.txt create mode 100644 plasma/workspace/runners/shell/autotests/shellrunnertest.cpp create mode 100644 plasma/workspace/runners/shell/plasma-runner-shell.json create mode 100644 plasma/workspace/runners/shell/shellrunner.cpp create mode 100644 plasma/workspace/runners/shell/shellrunner.h create mode 100644 plasma/workspace/runners/webshortcuts/CMakeLists.txt create mode 100644 plasma/workspace/runners/webshortcuts/Messages.sh create mode 100644 plasma/workspace/runners/webshortcuts/plasma-runner-webshortcuts.json create mode 100644 plasma/workspace/runners/webshortcuts/webshortcutrunner.cpp create mode 100644 plasma/workspace/runners/webshortcuts/webshortcutrunner.h create mode 100644 plasma/workspace/runners/windowedwidgets/CMakeLists.txt create mode 100644 plasma/workspace/runners/windowedwidgets/Messages.sh create mode 100644 plasma/workspace/runners/windowedwidgets/plasma-runner-windowedwidgets.json create mode 100644 plasma/workspace/runners/windowedwidgets/windowedwidgetsrunner.cpp create mode 100644 plasma/workspace/runners/windowedwidgets/windowedwidgetsrunner.h create mode 100644 plasma/workspace/sddm-theme/Background.qml create mode 100644 plasma/workspace/sddm-theme/BreezeMenuStyle.qml create mode 100644 plasma/workspace/sddm-theme/KeyboardButton.qml create mode 100644 plasma/workspace/sddm-theme/Login.qml create mode 100644 plasma/workspace/sddm-theme/Main.qml create mode 100644 plasma/workspace/sddm-theme/SessionButton.qml create mode 100644 plasma/workspace/sddm-theme/components create mode 100644 plasma/workspace/sddm-theme/default-logo.svg create mode 100644 plasma/workspace/sddm-theme/dummydata/config.qml create mode 100644 plasma/workspace/sddm-theme/dummydata/keyboard.qml create mode 100644 plasma/workspace/sddm-theme/dummydata/screenModel.qml create mode 100644 plasma/workspace/sddm-theme/dummydata/sddm.qml create mode 100644 plasma/workspace/sddm-theme/dummydata/sessionModel.qml create mode 100644 plasma/workspace/sddm-theme/dummydata/userModel.qml create mode 100644 plasma/workspace/sddm-theme/faces/.face.icon create mode 100644 plasma/workspace/sddm-theme/metadata.desktop create mode 100644 plasma/workspace/sddm-theme/preview.png create mode 100644 plasma/workspace/sddm-theme/theme.conf.cmake create mode 100644 plasma/workspace/sddm-wayland-session/plasma-wayland.conf create mode 100644 plasma/workspace/shell/CMakeLists.txt create mode 100644 plasma/workspace/shell/Messages.sh create mode 100644 plasma/workspace/shell/alternativeshelper.cpp create mode 100644 plasma/workspace/shell/alternativeshelper.h create mode 100644 plasma/workspace/shell/autotests/CMakeLists.txt create mode 100644 plasma/workspace/shell/autotests/desktopview.cpp create mode 100644 plasma/workspace/shell/autotests/desktopview.h create mode 100644 plasma/workspace/shell/autotests/mockserver/CMakeLists.txt create mode 100644 plasma/workspace/shell/autotests/mockserver/corecompositor.cpp create mode 100644 plasma/workspace/shell/autotests/mockserver/corecompositor.h create mode 100644 plasma/workspace/shell/autotests/mockserver/coreprotocol.cpp create mode 100644 plasma/workspace/shell/autotests/mockserver/coreprotocol.h create mode 100644 plasma/workspace/shell/autotests/mockserver/mockcompositor.cpp create mode 100644 plasma/workspace/shell/autotests/mockserver/mockcompositor.h create mode 100644 plasma/workspace/shell/autotests/mockserver/primaryoutput.cpp create mode 100644 plasma/workspace/shell/autotests/mockserver/primaryoutput.h create mode 100644 plasma/workspace/shell/autotests/mockserver/xdgoutputv1.cpp create mode 100644 plasma/workspace/shell/autotests/mockserver/xdgoutputv1.h create mode 100644 plasma/workspace/shell/autotests/screenpooltest.cpp create mode 100644 plasma/workspace/shell/config-ktexteditor.h.cmake create mode 100644 plasma/workspace/shell/config-plasma.h.cmake create mode 100644 plasma/workspace/shell/containmentconfigview.cpp create mode 100644 plasma/workspace/shell/containmentconfigview.h create mode 100644 plasma/workspace/shell/coronatesthelper.cpp create mode 100644 plasma/workspace/shell/coronatesthelper.h create mode 100644 plasma/workspace/shell/currentcontainmentactionsmodel.cpp create mode 100644 plasma/workspace/shell/currentcontainmentactionsmodel.h create mode 100644 plasma/workspace/shell/dbus/org.kde.PlasmaShell.xml create mode 100644 plasma/workspace/shell/desktopview.cpp create mode 100644 plasma/workspace/shell/desktopview.h create mode 100644 plasma/workspace/shell/futureutil.h create mode 100644 plasma/workspace/shell/main.cpp create mode 100644 plasma/workspace/shell/org.kde.plasmashell.desktop.cmake create mode 100644 plasma/workspace/shell/osd.cpp create mode 100644 plasma/workspace/shell/osd.h create mode 100644 plasma/workspace/shell/packageplugins/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/layouttemplate/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.cpp create mode 100644 plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.h create mode 100644 plasma/workspace/shell/packageplugins/layouttemplate/plasma-packagestructure-layouttemplate.json create mode 100644 plasma/workspace/shell/packageplugins/lookandfeel/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.cpp create mode 100644 plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.h create mode 100644 plasma/workspace/shell/packageplugins/lookandfeel/plasma-packagestructure-lookandfeel.json create mode 100644 plasma/workspace/shell/packageplugins/qmlWallpaper/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/qmlWallpaper/plasma-packagestructure-wallpaper.json create mode 100644 plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.cpp create mode 100644 plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.h create mode 100644 plasma/workspace/shell/packageplugins/shell/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/shell/Messages.sh create mode 100644 plasma/workspace/shell/packageplugins/shell/plasma-packagestructure-plasma-shell.json create mode 100644 plasma/workspace/shell/packageplugins/shell/shellpackage.cpp create mode 100644 plasma/workspace/shell/packageplugins/shell/shellpackage.h create mode 100644 plasma/workspace/shell/packageplugins/wallpaperimages/CMakeLists.txt create mode 100644 plasma/workspace/shell/packageplugins/wallpaperimages/plasma-packagestructure-wallpaperimages.json create mode 100644 plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.cpp create mode 100644 plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.h create mode 100644 plasma/workspace/shell/panelconfigview.cpp create mode 100644 plasma/workspace/shell/panelconfigview.h create mode 100644 plasma/workspace/shell/panelshadows.cpp create mode 100644 plasma/workspace/shell/panelshadows_p.h create mode 100644 plasma/workspace/shell/panelview.cpp create mode 100644 plasma/workspace/shell/panelview.h create mode 100644 plasma/workspace/shell/plasma-plasmashell.service.in create mode 100644 plasma/workspace/shell/primaryoutputwatcher.cpp create mode 100644 plasma/workspace/shell/primaryoutputwatcher.h create mode 100644 plasma/workspace/shell/screenpool.cpp create mode 100644 plasma/workspace/shell/screenpool.h create mode 100644 plasma/workspace/shell/scripting/appinterface.cpp create mode 100644 plasma/workspace/shell/scripting/appinterface.h create mode 100644 plasma/workspace/shell/scripting/applet.cpp create mode 100644 plasma/workspace/shell/scripting/applet.h create mode 100644 plasma/workspace/shell/scripting/configgroup.cpp create mode 100644 plasma/workspace/shell/scripting/configgroup.h create mode 100644 plasma/workspace/shell/scripting/containment.cpp create mode 100644 plasma/workspace/shell/scripting/containment.h create mode 100644 plasma/workspace/shell/scripting/panel.cpp create mode 100644 plasma/workspace/shell/scripting/panel.h create mode 100644 plasma/workspace/shell/scripting/plasma-layouttemplate.desktop create mode 100644 plasma/workspace/shell/scripting/scriptengine.cpp create mode 100644 plasma/workspace/shell/scripting/scriptengine.h create mode 100644 plasma/workspace/shell/scripting/scriptengine_v1.cpp create mode 100644 plasma/workspace/shell/scripting/scriptengine_v1.h create mode 100644 plasma/workspace/shell/scripting/widget.cpp create mode 100644 plasma/workspace/shell/scripting/widget.h create mode 100644 plasma/workspace/shell/shellcontainmentconfig.cpp create mode 100644 plasma/workspace/shell/shellcontainmentconfig.h create mode 100644 plasma/workspace/shell/shellcorona.cpp create mode 100644 plasma/workspace/shell/shellcorona.h create mode 100644 plasma/workspace/shell/softwarerendernotifier.cpp create mode 100644 plasma/workspace/shell/softwarerendernotifier.h create mode 100644 plasma/workspace/shell/standaloneappcorona.cpp create mode 100644 plasma/workspace/shell/standaloneappcorona.h create mode 100644 plasma/workspace/shell/strutmanager.cpp create mode 100644 plasma/workspace/shell/strutmanager.h create mode 100644 plasma/workspace/shell/tests/CMakeLists.txt create mode 100644 plasma/workspace/shell/tests/plasma/shells/org.kde.plasmashelltest/metadata.desktop create mode 100644 plasma/workspace/shell/tests/screenpooltest.cpp create mode 100644 plasma/workspace/shell/userfeedback.cpp create mode 100644 plasma/workspace/shell/userfeedback.h create mode 100644 plasma/workspace/solidautoeject/CMakeLists.txt create mode 100644 plasma/workspace/solidautoeject/Messages.sh create mode 100644 plasma/workspace/solidautoeject/solidautoeject.cpp create mode 100644 plasma/workspace/solidautoeject/solidautoeject.h create mode 100644 plasma/workspace/solidautoeject/solidautoeject.json create mode 100644 plasma/workspace/soliduiserver/CMakeLists.txt create mode 100644 plasma/workspace/soliduiserver/Messages.sh create mode 100644 plasma/workspace/soliduiserver/soliduiserver.cpp create mode 100644 plasma/workspace/soliduiserver/soliduiserver.h create mode 100644 plasma/workspace/soliduiserver/soliduiserver.json create mode 100644 plasma/workspace/startkde/CMakeLists.txt create mode 100644 plasma/workspace/startkde/README create mode 100644 plasma/workspace/startkde/config-startplasma.h.cmake create mode 100644 plasma/workspace/startkde/kcheckrunning/kcheckrunning.cpp create mode 100644 plasma/workspace/startkde/kcheckrunning/kcheckrunning.h create mode 100644 plasma/workspace/startkde/kcminit/CMakeLists.txt create mode 100644 plasma/workspace/startkde/kcminit/Messages.sh create mode 100644 plasma/workspace/startkde/kcminit/main.cpp create mode 100644 plasma/workspace/startkde/kcminit/main.h create mode 100644 plasma/workspace/startkde/kcminit/plasma-kcminit-phase1.service.in create mode 100644 plasma/workspace/startkde/kcminit/plasma-kcminit.service.in create mode 100644 plasma/workspace/startkde/plasma-dbus-run-session-if-needed create mode 100644 plasma/workspace/startkde/plasma-session/CMakeLists.txt create mode 100644 plasma/workspace/startkde/plasma-session/README create mode 100644 plasma/workspace/startkde/plasma-session/autostart.cpp create mode 100644 plasma/workspace/startkde/plasma-session/autostart.h create mode 100644 plasma/workspace/startkde/plasma-session/main.cpp create mode 100644 plasma/workspace/startkde/plasma-session/org.kde.Startup.xml create mode 100644 plasma/workspace/startkde/plasma-session/plasma-autostart-list/CMakeLists.txt create mode 100644 plasma/workspace/startkde/plasma-session/plasma-autostart-list/main.cpp create mode 100644 plasma/workspace/startkde/plasma-session/sessiontrack.cpp create mode 100644 plasma/workspace/startkde/plasma-session/sessiontrack.h create mode 100644 plasma/workspace/startkde/plasma-session/signalhandler.cpp create mode 100644 plasma/workspace/startkde/plasma-session/signalhandler.h create mode 100644 plasma/workspace/startkde/plasma-session/startup.cpp create mode 100644 plasma/workspace/startkde/plasma-session/startup.h create mode 100644 plasma/workspace/startkde/plasma-shutdown/CMakeLists.txt create mode 100644 plasma/workspace/startkde/plasma-shutdown/main.cpp create mode 100644 plasma/workspace/startkde/plasma-shutdown/org.kde.Shutdown.xml create mode 100644 plasma/workspace/startkde/plasma-shutdown/shutdown.cpp create mode 100644 plasma/workspace/startkde/plasma-shutdown/shutdown.h create mode 100644 plasma/workspace/startkde/plasma-sourceenv.sh create mode 100644 plasma/workspace/startkde/plasmaautostart/CMakeLists.txt create mode 100644 plasma/workspace/startkde/plasmaautostart/plasmaautostart.cpp create mode 100644 plasma/workspace/startkde/plasmaautostart/plasmaautostart.h create mode 100644 plasma/workspace/startkde/startplasma-wayland.cpp create mode 100644 plasma/workspace/startkde/startplasma-x11.cpp create mode 100644 plasma/workspace/startkde/startplasma.cpp create mode 100644 plasma/workspace/startkde/startplasma.h create mode 100644 plasma/workspace/startkde/systemd/CMakeLists.txt create mode 100644 plasma/workspace/startkde/systemd/README.md create mode 100644 plasma/workspace/startkde/systemd/kde-systemd-start-condition.cpp create mode 100644 plasma/workspace/startkde/systemd/plasma-core.target create mode 100644 plasma/workspace/startkde/systemd/plasma-ksplash-ready.service.in create mode 100644 plasma/workspace/startkde/systemd/plasma-workspace-wayland.target create mode 100644 plasma/workspace/startkde/systemd/plasma-workspace-x11.target create mode 100644 plasma/workspace/startkde/systemd/plasma-workspace.target create mode 100644 plasma/workspace/startkde/waitforname/CMakeLists.txt create mode 100644 plasma/workspace/startkde/waitforname/main.cpp create mode 100644 plasma/workspace/startkde/waitforname/org.kde.KSplash.service.in create mode 100644 plasma/workspace/startkde/waitforname/org.kde.plasma.Notifications.service.in create mode 100644 plasma/workspace/startkde/waitforname/waiter.cpp create mode 100644 plasma/workspace/startkde/waitforname/waiter.h create mode 100644 plasma/workspace/statusnotifierwatcher/CMakeLists.txt create mode 100644 plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.cpp create mode 100644 plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.h create mode 100644 plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.json create mode 100644 plasma/workspace/statusnotifierwatcher/systemtraytypedefs.h create mode 100644 plasma/workspace/systemmonitor/CMakeLists.txt create mode 100644 plasma/workspace/systemmonitor/Messages.sh create mode 100644 plasma/workspace/systemmonitor/kdedksysguard.cpp create mode 100644 plasma/workspace/systemmonitor/kdedksysguard.h create mode 100644 plasma/workspace/systemmonitor/ksysguard.json create mode 100644 plasma/workspace/systemmonitor/ksystemactivitydialog.cpp create mode 100644 plasma/workspace/systemmonitor/ksystemactivitydialog.h create mode 100644 plasma/workspace/systemmonitor/main.cpp create mode 100644 plasma/workspace/systemmonitor/org.kde.systemmonitor.desktop create mode 100644 plasma/workspace/themes/CMakeLists.txt create mode 100644 plasma/workspace/themes/qtcde.themerc create mode 100644 plasma/workspace/themes/qtcleanlooks.themerc create mode 100644 plasma/workspace/themes/qtgtk.themerc create mode 100644 plasma/workspace/themes/qtmotif.themerc create mode 100644 plasma/workspace/themes/qtplastique.themerc create mode 100644 plasma/workspace/themes/qtwindows.themerc create mode 100644 plasma/workspace/wallpapers/CMakeLists.txt create mode 100644 plasma/workspace/wallpapers/color/Messages.sh create mode 100644 plasma/workspace/wallpapers/color/contents/config/main.xml create mode 100644 plasma/workspace/wallpapers/color/contents/ui/config.qml create mode 100644 plasma/workspace/wallpapers/color/contents/ui/main.qml create mode 100644 plasma/workspace/wallpapers/color/metadata.json create mode 100644 plasma/workspace/wallpapers/image/CMakeLists.txt create mode 100644 plasma/workspace/wallpapers/image/Messages.sh create mode 100644 plasma/workspace/wallpapers/image/autotests/CMakeLists.txt create mode 100644 plasma/workspace/wallpapers/image/autotests/testfindpreferredimage.cpp create mode 100644 plasma/workspace/wallpapers/image/backgroundlistmodel.cpp create mode 100644 plasma/workspace/wallpapers/image/backgroundlistmodel.h create mode 100644 plasma/workspace/wallpapers/image/image.cpp create mode 100644 plasma/workspace/wallpapers/image/image.h create mode 100644 plasma/workspace/wallpapers/image/imagepackage/contents/config/main.xml create mode 100644 plasma/workspace/wallpapers/image/imagepackage/contents/ui/WallpaperDelegate.qml create mode 100644 plasma/workspace/wallpapers/image/imagepackage/contents/ui/config.qml create mode 100644 plasma/workspace/wallpapers/image/imagepackage/contents/ui/main.qml create mode 100644 plasma/workspace/wallpapers/image/imagepackage/metadata.json create mode 100644 plasma/workspace/wallpapers/image/imagepackage/setaswallpaper.desktop.in create mode 100644 plasma/workspace/wallpapers/image/imageplugin.cpp create mode 100644 plasma/workspace/wallpapers/image/imageplugin.h create mode 100644 plasma/workspace/wallpapers/image/plasma-apply-wallpaperimage.cpp create mode 100644 plasma/workspace/wallpapers/image/qmldir create mode 100644 plasma/workspace/wallpapers/image/slidefiltermodel.cpp create mode 100644 plasma/workspace/wallpapers/image/slidefiltermodel.h create mode 100644 plasma/workspace/wallpapers/image/slidemodel.cpp create mode 100644 plasma/workspace/wallpapers/image/slidemodel.h create mode 100644 plasma/workspace/wallpapers/image/slideshowpackage/contents/config/main.xml create mode 100644 plasma/workspace/wallpapers/image/slideshowpackage/metadata.json create mode 100644 plasma/workspace/wallpapers/image/wallpaper-mobile.knsrc create mode 100644 plasma/workspace/wallpapers/image/wallpaper.knsrc create mode 100644 plasma/workspace/xembed-sni-proxy/CMakeLists.txt create mode 100644 plasma/workspace/xembed-sni-proxy/Readme.md create mode 100644 plasma/workspace/xembed-sni-proxy/fdoselectionmanager.cpp create mode 100644 plasma/workspace/xembed-sni-proxy/fdoselectionmanager.h create mode 100644 plasma/workspace/xembed-sni-proxy/main.cpp create mode 100644 plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierItem.xml create mode 100644 plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml create mode 100644 plasma/workspace/xembed-sni-proxy/plasma-xembedsniproxy.service.in create mode 100644 plasma/workspace/xembed-sni-proxy/snidbus.cpp create mode 100644 plasma/workspace/xembed-sni-proxy/snidbus.h create mode 100644 plasma/workspace/xembed-sni-proxy/sniproxy.cpp create mode 100644 plasma/workspace/xembed-sni-proxy/sniproxy.h create mode 100644 plasma/workspace/xembed-sni-proxy/xcbutils.h create mode 100644 plasma/workspace/xembed-sni-proxy/xembedsniproxy.desktop create mode 100644 plasma/workspace/xembed-sni-proxy/xtestsender.cpp create mode 100644 plasma/workspace/xembed-sni-proxy/xtestsender.h diff --git a/plasma/workspace/.kde-ci.yml b/plasma/workspace/.kde-ci.yml new file mode 100644 index 0000000000..645a456e02 --- /dev/null +++ b/plasma/workspace/.kde-ci.yml @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +Dependencies: +- 'on': ['@all'] + 'require': + 'frameworks/attica': '@latest' + 'frameworks/baloo': '@latest' + 'frameworks/extra-cmake-modules': '@latest' + 'frameworks/kactivities': '@latest' + 'frameworks/kactivities-stats': '@latest' + 'frameworks/kauth': '@latest' + 'frameworks/kbookmarks': '@latest' + 'frameworks/kcmutils': '@latest' + 'frameworks/kcodecs': '@latest' + 'frameworks/kcompletion': '@latest' + 'frameworks/kconfig': '@latest' + 'frameworks/kconfigwidgets': '@latest' + 'frameworks/kcoreaddons': '@latest' + 'frameworks/kcrash': '@latest' + 'frameworks/kdbusaddons': '@latest' + 'frameworks/kdeclarative': '@latest' + 'frameworks/kfilemetadata': '@latest' + 'frameworks/kglobalaccel': '@latest' + 'frameworks/kguiaddons': '@latest' + 'frameworks/ki18n': '@latest' + 'frameworks/kiconthemes': '@latest' + 'frameworks/kio': '@latest' + 'frameworks/kitemmodels': '@latest' + 'frameworks/kitemviews': '@latest' + 'frameworks/kjobwidgets': '@latest' + 'frameworks/knewstuff': '@latest' + 'frameworks/knotifications': '@latest' + 'frameworks/knotifyconfig': '@latest' + 'frameworks/kpackage': '@latest' + 'frameworks/kparts': '@latest' + 'frameworks/krunner': '@latest' + 'frameworks/kservice': '@latest' + 'frameworks/ktextwidgets': '@latest' + 'frameworks/kwallet': '@latest' + 'frameworks/kwidgetsaddons': '@latest' + 'frameworks/kwindowsystem': '@latest' + 'frameworks/kxmlgui': '@latest' + 'frameworks/plasma-framework': '@latest' + 'frameworks/solid': '@latest' + 'frameworks/sonnet': '@latest' + 'frameworks/kholidays': '@latest' + 'frameworks/kquickcharts': '@latest' + 'frameworks/kded': '@latest' + 'frameworks/kdesu': '@latest' + 'frameworks/kpeople': '@latest' + 'frameworks/prison': '@latest' + 'frameworks/kinit': '@latest' + 'plasma/layer-shell-qt': '@same' + 'plasma/kscreenlocker': '@same' + 'plasma/libkscreen': '@same' + 'plasma/kwin': '@same' + 'plasma/libksysguard': '@same' + 'libraries/plasma-wayland-protocols': '@latest' + 'libraries/kuserfeedback': '@stable' + +- 'on': ['Linux'] + 'require': + 'frameworks/networkmanager-qt': '@latest' diff --git a/plasma/workspace/CMakeLists.txt b/plasma/workspace/CMakeLists.txt new file mode 100644 index 0000000000..e064c4016d --- /dev/null +++ b/plasma/workspace/CMakeLists.txt @@ -0,0 +1,298 @@ +cmake_minimum_required(VERSION 3.16) + +project(plasma-workspace) +set(PROJECT_VERSION "5.24.80") +set(PROJECT_VERSION_MAJOR 5) + +set(QT_MIN_VERSION "5.15.0") +set(KF5_MIN_VERSION "5.89") +set(KDE_COMPILERSETTINGS_LEVEL "5.82") +set(INSTALL_SDDM_THEME TRUE) +option(INSTALL_SDDM_WAYLAND_SESSION OFF) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Svg Widgets Quick QuickWidgets Concurrent Test Network) +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(ECMMarkNonGuiExecutable) +include(CMakePackageConfigHelpers) +include(WriteBasicConfigVersionFile) +include(CheckIncludeFiles) +include(FeatureSummary) +include(ECMOptionalAddSubdirectory) +include(ECMQtDeclareLoggingCategory) +include(ECMQueryQmake) +include(ECMInstallIcons) +include(KDEClangFormat) +include(KDEGitCommitHooks) +include(ECMConfiguredInstall) +include(ECMGenerateDBusServiceFile) +include(ECMQMLModules) +include(ECMGenerateExportHeader) + +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS + Plasma Runner Notifications NotifyConfig NewStuff Wallet IdleTime + Declarative I18n KCMUtils TextWidgets Crash GlobalAccel DBusAddons Wayland + CoreAddons People ActivitiesStats Activities KIO Prison PlasmaQuick Package + GuiAddons Archive ItemModels IconThemes UnitConversion ItemModels Init TextEditor + OPTIONAL_COMPONENTS DocTools) + +find_package(KDED CONFIG REQUIRED) + +find_package(KF5NetworkManagerQt ${KF5_MIN_VERSION}) +set_package_properties(KF5NetworkManagerQt PROPERTIES DESCRIPTION "Qt wrapper for NetworkManager API" + TYPE OPTIONAL + PURPOSE "Needed by geolocation data engine." + ) + +find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5Kirigami2 PROPERTIES + DESCRIPTION "A QtQuick based components set" + TYPE RUNTIME +) + +find_package(KF5QuickCharts ${KF5_MIN_VERSION} CONFIG) +set_package_properties(KF5QuickCharts PROPERTIES + DESCRIPTION "Used for rendering charts" + TYPE RUNTIME +) + +find_package(KUserFeedback) +find_package(KSysGuard CONFIG REQUIRED) + +find_package(KF5Baloo) +set_package_properties(KF5Baloo PROPERTIES DESCRIPTION "File Searching" + TYPE RECOMMENDED + PURPOSE "Needed for the File Search runner." + ) +find_package(Qalculate REQUIRED) +set_package_properties(Qalculate PROPERTIES DESCRIPTION "Qalculate Library" + URL "https://qalculate.github.io/" + PURPOSE "Needed for precise computation in the calculator runner." + ) + +find_package(KWinDBusInterface CONFIG REQUIRED) + +find_package(KF5Screen CONFIG REQUIRED) +find_package(KScreenLocker 5.13.80 REQUIRED) +find_package(ScreenSaverDBusInterface CONFIG REQUIRED) +find_package(LayerShellQt CONFIG REQUIRED) +find_package(KF5Holidays) +set_package_properties(KF5Holidays PROPERTIES DESCRIPTION "Holidays provider for Plasma calendar" + TYPE OPTIONAL + PURPOSE "Needed to for holidays plugin for Plasma Calendar." + ) + +find_package(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) +set_package_properties(Phonon4Qt5 PROPERTIES + DESCRIPTION "Qt-based audio library" + TYPE REQUIRED) + +find_package(Breeze ${PROJECT_VERSION} CONFIG) +set_package_properties(Breeze PROPERTIES + TYPE OPTIONAL + PURPOSE "For setting the default window decoration plugin") + +find_package(ZLIB) +set_package_properties(ZLIB PROPERTIES DESCRIPTION "Support for gzip compressed files and data streams" + URL "https://www.zlib.net" + TYPE REQUIRED + ) + +find_package(Fontconfig) +set_package_properties(Fontconfig PROPERTIES DESCRIPTION "Font access configuration library" + URL "https://www.freedesktop.org/wiki/Software/fontconfig" + TYPE OPTIONAL + PURPOSE "Needed to build font configuration and installation tools" + ) + + +find_package(X11) +set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" + URL "https://www.x.org" + TYPE OPTIONAL + PURPOSE "Required for building the X11 based workspace") + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3) +add_feature_info(PipeWire PipeWire_FOUND "Required for Wayland screencasting") + +if(PipeWire_FOUND) + find_package(Libdrm REQUIRED) +endif() + +find_package(QtWaylandScanner REQUIRED) +find_package(Qt5WaylandClient) +find_package(Qt5XkbCommonSupport) +find_package(PlasmaWaylandProtocols 1.6 REQUIRED) +find_package(Wayland REQUIRED COMPONENTS Client Server) # Server is used in autotests + +if(FONTCONFIG_FOUND) + # kfontinst + find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS PrintSupport) +endif() + +if(X11_FOUND) + find_package(XCB MODULE REQUIRED COMPONENTS XCB RANDR IMAGE) + set_package_properties(XCB PROPERTIES TYPE REQUIRED) + if(NOT X11_SM_FOUND) + message(FATAL_ERROR "\nThe X11 Session Management (SM) development package could not be found.\nPlease install libSM.\n") + endif(NOT X11_SM_FOUND) + + find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS X11Extras) +endif() + +if(X11_FOUND AND XCB_XCB_FOUND) + set(HAVE_X11 1) +endif() + +find_package(AppStreamQt 0.10.6) +set_package_properties(AppStreamQt PROPERTIES DESCRIPTION "Access metadata for listing available software" + URL "https://www.freedesktop.org/wiki/Distributions/AppStream/" + TYPE OPTIONAL) + +if(${AppStreamQt_FOUND}) + set(HAVE_APPSTREAMQT true) +endif() + +find_package(PackageKitQt5) +set_package_properties(PackageKitQt5 + PROPERTIES DESCRIPTION "Software Manager integration" + TYPE OPTIONAL + PURPOSE "Used to install additional language packages on demand" + ) +if(PackageKitQt5_FOUND) + set(HAVE_PACKAGEKIT TRUE) +endif() + + +find_package(KIOExtras) +set_package_properties(KIOExtras PROPERTIES DESCRIPTION "Common KIO slaves for operations." + PURPOSE "Show thumbnails in wallpaper selection." + TYPE RUNTIME + ) + +find_package(KIOFuse) +set_package_properties(KIOFuse PROPERTIES DESCRIPTION "Provide KIO support to legacy applications. " + TYPE RUNTIME + ) + +# Clipboard applet +ecm_find_qmlmodule(org.kde.prison 1.0) + +include(ConfigureChecks.cmake) + +include_directories("${CMAKE_CURRENT_BINARY_DIR}") + +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00) +add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055800) +add_definitions(-DKITEMMODELS_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054F00) + +configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) +configure_file(config-unix.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-unix.h ) +configure_file(config-X11.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-X11.h) +configure_file(config-appstream.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-appstream.h ) +add_subdirectory(login-sessions) + +plasma_install_package(lookandfeel org.kde.breeze.desktop look-and-feel lookandfeel) +plasma_install_package(lookandfeel.dark org.kde.breezedark.desktop look-and-feel lookandfeel) +plasma_install_package(lookandfeel.twilight org.kde.breezetwilight.desktop look-and-feel lookandfeel) + + +if (INSTALL_SDDM_THEME) + configure_file(sddm-theme/theme.conf.cmake ${CMAKE_CURRENT_BINARY_DIR}/sddm-theme/theme.conf) + +# Install the login theme into the SDDM directory +# Longer term we need to look at making SDDM load from look and feel somehow.. and allow copying at runtime + #NOTE this trailing slash is important to rename the directory + install(DIRECTORY sddm-theme/ DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze PATTERN "README.txt" EXCLUDE PATTERN "components" EXCLUDE PATTERN "dummydata" EXCLUDE + PATTERN "theme.conf.cmake" EXCLUDE) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sddm-theme/theme.conf DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze) + install(DIRECTORY lookandfeel/contents/components DESTINATION ${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze PATTERN "README.txt" EXCLUDE) +endif() + +if (INSTALL_SDDM_WAYLAND_SESSION) + install(FILES sddm-wayland-session/plasma-wayland.conf DESTINATION /etc/sddm.conf.d) +else() + message(STATUS "INSTALL_SDDM_WAYLAND_SESSION is disabled. As soon as it's installed, SDDM will default to use Wayland and KWin for its greeter session (BETA, do not deploy to final users yet).") +endif() + +add_definitions(-DQT_NO_URL_CAST_FROM_STRING) + +# locate qdbus in the Qt path because not every distro makes a symlink at /usr/bin/qdbus +query_qmake(QtBinariesDir QT_INSTALL_BINS) + +option(PLASMA_WAYLAND_DEFAULT_SESSION "Use Wayland session by default for Plasma" FALSE) + +if(KF5DocTools_FOUND) + add_subdirectory(doc) +endif() +add_subdirectory(libkworkspace) +add_subdirectory(libdbusmenuqt) +add_subdirectory(appmenu) + +add_subdirectory(libtaskmanager) +add_subdirectory(libnotificationmanager) +add_subdirectory(libcolorcorrect) +add_subdirectory(components) + +add_subdirectory(plasma-windowed) +add_subdirectory(shell) +add_subdirectory(freespacenotifier) +add_subdirectory(klipper) +add_subdirectory(krunner) +add_subdirectory(ksmserver) +add_subdirectory(logout-greeter) +add_subdirectory(ksplash) +add_subdirectory(systemmonitor) +add_subdirectory(statusnotifierwatcher) +add_subdirectory(startkde) +add_subdirectory(themes) + +add_subdirectory(kcms) + +add_subdirectory(containmentactions) +add_subdirectory(runners) +add_subdirectory(applets) +add_subdirectory(dataengines) +add_subdirectory(wallpapers) + +add_subdirectory(kioslave) +add_subdirectory(ktimezoned) +add_subdirectory(menu) +add_subdirectory(phonon) + +add_subdirectory(interactiveconsole) + +# This ensures pressing the eject button on a CD drive ejects the disc +# It listens to the Solid::OpticalDrive::ejectPressed signal that is only +# supported by Solid in the HAL backend and does nothing with UDev +if(CMAKE_SYSTEM_NAME MATCHES FreeBSD) +add_subdirectory(solidautoeject) +endif() + +ecm_optional_add_subdirectory(xembed-sni-proxy) + +ecm_optional_add_subdirectory(gmenu-dbusmenu-proxy) + +add_subdirectory(soliduiserver) + +if(KF5Holidays_FOUND) + add_subdirectory(plasmacalendarintegration) +endif() + +ki18n_install(po) + +install(FILES plasma-workspace.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +# add clang-format target for all our real source files +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/plasma/workspace/ConfigureChecks.cmake b/plasma/workspace/ConfigureChecks.cmake new file mode 100644 index 0000000000..7ab5e2995c --- /dev/null +++ b/plasma/workspace/ConfigureChecks.cmake @@ -0,0 +1,9 @@ +set(KWIN_BIN "kwin_x11" CACHE STRING "Name of the KWin binary") + +check_include_files(limits.h HAVE_LIMITS_H) +check_include_files(sys/time.h HAVE_SYS_TIME_H) # ksmserver, ksplashml, sftp +check_include_files(unistd.h HAVE_UNISTD_H) + +set(HAVE_FONTCONFIG ${FONTCONFIG_FOUND}) # kcms/{fonts,kfontinst} +set(HAVE_XCURSOR ${X11_Xcursor_FOUND}) # many uses +set(HAVE_XFIXES ${X11_Xfixes_FOUND}) # klipper, kicker diff --git a/plasma/workspace/ExtraDesktop.sh b/plasma/workspace/ExtraDesktop.sh new file mode 100644 index 0000000000..0c46ec147b --- /dev/null +++ b/plasma/workspace/ExtraDesktop.sh @@ -0,0 +1,4 @@ +#! /bin/sh +#This file outputs in a separate line each file with a .desktop syntax +#that needs to be translated but has a non .desktop extension +find -name \*.kdevtemplate -print diff --git a/plasma/workspace/LICENSES/BSD-2-Clause.txt b/plasma/workspace/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..baa80b56a2 --- /dev/null +++ b/plasma/workspace/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,22 @@ +Copyright (c) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plasma/workspace/LICENSES/BSD-3-Clause.txt b/plasma/workspace/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..0741db789e --- /dev/null +++ b/plasma/workspace/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plasma/workspace/LICENSES/CC0-1.0.txt b/plasma/workspace/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/plasma/workspace/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/plasma/workspace/LICENSES/GPL-2.0-only.txt b/plasma/workspace/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000000..3b6070fcd0 --- /dev/null +++ b/plasma/workspace/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +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. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/plasma/workspace/LICENSES/GPL-2.0-or-later.txt b/plasma/workspace/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000000..3b6070fcd0 --- /dev/null +++ b/plasma/workspace/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,311 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + +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. Also add information on how +to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/plasma/workspace/LICENSES/GPL-3.0-only.txt b/plasma/workspace/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000000..599077110a --- /dev/null +++ b/plasma/workspace/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,604 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs, and that you know you +can do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. +Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals +or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a “modified version” of the earlier work +or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the +Program. + +To “propagate” a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To “convey” a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making +modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The “System Libraries” of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A “Major Component”, in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive +it, in any medium, provided that you conspicuously and appropriately publish +on each copy an appropriate copyright notice; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to “keep intact all notices”. + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an “aggregate” if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, “normally used” refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, “control” includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To “grant” such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. “Knowingly relying” +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from +the conditions of this License. If you cannot convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the +GNU General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License “or any +later version” applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM +PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM +AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO +USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the “copyright” line and a +pointer to where the full notice is found. + + + Copyright (C) + +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 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it under certain +conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/plasma/workspace/LICENSES/LGPL-2.0-only.txt b/plasma/workspace/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000000..ec9eedc542 --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-2.0-only.txt @@ -0,0 +1,444 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1991 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 +because it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 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 Library General Public License for +more details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice + +That's all there is to it! diff --git a/plasma/workspace/LICENSES/LGPL-2.0-or-later.txt b/plasma/workspace/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000000..ec9eedc542 --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,444 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1991 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 +because it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 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 Library General Public License for +more details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice + +That's all there is to it! diff --git a/plasma/workspace/LICENSES/LGPL-2.1-only.txt b/plasma/workspace/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000000..aaaba1688e --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,462 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +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 Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/plasma/workspace/LICENSES/LGPL-2.1-or-later.txt b/plasma/workspace/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000000..aaaba1688e --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,462 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as +the successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + +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 Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/plasma/workspace/LICENSES/LGPL-3.0-only.txt b/plasma/workspace/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000000..9e2e9f7831 --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,144 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms +and conditions of version 3 of the GNU General Public License, supplemented +by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General +Public License, and the "GNU GPL" refers to version 3 of the GNU General Public +License. + +"The Library" refers to a covered work governed by this License, other than +an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the +Library, but which is not otherwise based on the Library. Defining a subclass +of a class defined by the Library is deemed a mode of using an interface provided +by the Library. + +A "Combined Work" is a work produced by combining or linking an Application +with the Library. The particular version of the Library with which the Combined +Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding +Source for the Combined Work, excluding any source code for portions of the +Combined Work that, considered in isolation, are based on the Application, +and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object +code and/or source code for the Application, including any data and utility +programs needed for reproducing the Combined Work from the Application, but +excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without +being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility +refers to a function or data to be supplied by an Application that uses the +facility (other than as an argument passed when the facility is invoked), +then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure +that, in the event an Application does not supply the function or data, the +facility still operates, and performs whatever part of its purpose remains +meaningful, or + +b) under the GNU GPL, with none of the additional permissions of this License +applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header +file that is part of the Library. You may convey such object code under terms +of your choice, provided that, if the incorporated material is not limited +to numerical parameters, data structure layouts and accessors, or small macros, +inline functions and templates (ten or fewer lines in length), you do both +of the following: + +a) Give prominent notice with each copy of the object code that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, +effectively do not restrict modification of the portions of the Library contained +in the Combined Work and reverse engineering for debugging such modifications, +if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library +is used in it and that the Library and its use are covered by this License. + +b) Accompany the Combined Work with a copy of the GNU GPL and this license +document. + +c) For a Combined Work that displays copyright notices during execution, include +the copyright notice for the Library among these notices, as well as a reference +directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + +0) Convey the Minimal Corresponding Source under the terms of this License, +and the Corresponding Application Code in a form suitable for, and under terms +that permit, the user to recombine or relink the Application with a modified +version of the Linked Version to produce a modified Combined Work, in the +manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + +1) Use a suitable shared library mechanism for linking with the Library. +A suitable mechanism is one that (a) uses at run time a copy of the Library +already present on the user's computer system, and (b) will operate properly +with a modified version of the Library that is interface-compatible with the +Linked Version. + +e) Provide Installation Information, but only if you would otherwise be required +to provide such information under section 6 of the GNU GPL, and only to the +extent that such information is necessary to install and execute a modified +version of the Combined Work produced by recombining or relinking the Application +with a modified version of the Linked Version. (If you use option 4d0, the +Installation Information must accompany the Minimal Corresponding Source and +Corresponding Application Code. If you use option 4d1, you must provide the +Installation Information in the manner specified by section 6 of the GNU GPL +for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side +by side in a single library together with other library facilities that are +not Applications and are not covered by this License, and convey such a combined +library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities, conveyed under the +terms of this License. + +b) Give prominent notice with the combined library that part of it is a work +based on the Library, and explaining where to find the accompanying uncombined +form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the +GNU Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you +received it specifies that a certain numbered version of the GNU Lesser General +Public License "or any later version" applies to it, you have the option of +following the terms and conditions either of that published version or of +any later version published by the Free Software Foundation. If the Library +as you received it does not specify a version number of the GNU Lesser General +Public License, you may choose any version of the GNU Lesser General Public +License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether +future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent +authorization for you to choose that version for the Library. diff --git a/plasma/workspace/LICENSES/LGPL-3.0-or-later.txt b/plasma/workspace/LICENSES/LGPL-3.0-or-later.txt new file mode 100644 index 0000000000..c9287dd363 --- /dev/null +++ b/plasma/workspace/LICENSES/LGPL-3.0-or-later.txt @@ -0,0 +1,71 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. diff --git a/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000000..60a2dffc9c --- /dev/null +++ b/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library 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) at any later version that is +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 as 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. diff --git a/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000000..232b3c5da1 --- /dev/null +++ b/plasma/workspace/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +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 3 of the license or (at your option) any later version +that is 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 as defined in Section 6 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. diff --git a/plasma/workspace/LICENSES/MIT.txt b/plasma/workspace/LICENSES/MIT.txt new file mode 100644 index 0000000000..f0fd20ab68 --- /dev/null +++ b/plasma/workspace/LICENSES/MIT.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plasma/workspace/applets/CMakeLists.txt b/plasma/workspace/applets/CMakeLists.txt new file mode 100644 index 0000000000..96af27138f --- /dev/null +++ b/plasma/workspace/applets/CMakeLists.txt @@ -0,0 +1,25 @@ +plasma_install_package(activitybar org.kde.plasma.activitybar) +add_subdirectory(icon) + +plasma_install_package(analog-clock org.kde.plasma.analogclock) +plasma_install_package(mediacontroller org.kde.plasma.mediacontroller) +plasma_install_package(lock_logout org.kde.plasma.lock_logout) +plasma_install_package(manage-inputmethod org.kde.plasma.manage-inputmethod) + +add_subdirectory(appmenu) +add_subdirectory(systemmonitor) +add_subdirectory(batterymonitor) +add_subdirectory(calendar) +add_subdirectory(devicenotifier) +add_subdirectory(digital-clock) +add_subdirectory(kicker) +add_subdirectory(panelspacer) + +plasma_install_package(clipboard org.kde.plasma.clipboard) + +if(NOT WIN32) +# #notifications +# #should compile also on windows? (even if doesn't really make sense) + add_subdirectory(notifications) + add_subdirectory(systemtray) +endif() diff --git a/plasma/workspace/applets/Mainpage.dox b/plasma/workspace/applets/Mainpage.dox new file mode 100644 index 0000000000..bb0fcbb8fd --- /dev/null +++ b/plasma/workspace/applets/Mainpage.dox @@ -0,0 +1,10 @@ +/** +* @mainpage Applets +* +* Plasma Applets +* +*/ + +// DOXYGEN_SET_PROJECT_NAME = Applets +// DOXYGEN_SET_RECURSIVE = YES +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/plasma/workspace/applets/activitybar/Messages.sh b/plasma/workspace/applets/activitybar/Messages.sh new file mode 100644 index 0000000000..74531aefe0 --- /dev/null +++ b/plasma/workspace/applets/activitybar/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.activitybar.pot diff --git a/plasma/workspace/applets/activitybar/contents/ui/main.qml b/plasma/workspace/applets/activitybar/contents/ui/main.qml new file mode 100644 index 0000000000..acf14fb42f --- /dev/null +++ b/plasma/workspace/applets/activitybar/contents/ui/main.qml @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2013 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // PC3 Tabbar only has top and bottom tab positions, not left and right +import org.kde.activities 0.1 as Activities + +Item { + + Layout.minimumWidth: tabBar.implicitWidth + Layout.minimumHeight: tabBar.implicitHeight + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + + PlasmaComponents.TabBar { + id: tabBar + anchors.fill: parent + tabPosition: { + switch (plasmoid.location) { + case PlasmaCore.Types.LeftEdge: + return Qt.LeftEdge; + case PlasmaCore.Types.RightEdge: + return Qt.RightEdge; + case PlasmaCore.Types.TopEdge: + return Qt.TopEdge; + default: + return Qt.BottomEdge; + } + } + + Repeater { + model: Activities.ActivityModel { + id: activityModel + shownStates: "Running" + } + delegate: PlasmaComponents.TabButton { + id: tab + checked: model.current + text: model.name + activeFocusOnTab: true + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + activityModel.setCurrentActivity(model.id, function() {}); + break; + } + } + Accessible.checked: model.current + Accessible.name: model.name + Accessible.description: i18n("Switch to activity %1", model.name) + Accessible.role: Accessible.Button + onClicked: { + activityModel.setCurrentActivity(model.id, function() {}); + } + Component.onCompleted: { + if(model.current) { + tabBar.currentTab = tab; + } + } + onCheckedChanged: { + if(model.current) { + tabBar.currentTab = tab; + } + } + } + } + } + + Component.onCompleted: { + plasmoid.removeAction("configure"); + } +} diff --git a/plasma/workspace/applets/activitybar/metadata.json b/plasma/workspace/applets/activitybar/metadata.json new file mode 100644 index 0000000000..1f615ba30c --- /dev/null +++ b/plasma/workspace/applets/activitybar/metadata.json @@ -0,0 +1,166 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "bhush94@gmail.com", + "Name": "Bhushan Shah", + "Name[ar]": "Bhushan Shah", + "Name[az]": "Bhushan Shah", + "Name[ca]": "Bhushan Shah", + "Name[cs]": "Bhushan Shah", + "Name[de]": "Bhushan Shah", + "Name[en_GB]": "Bhushan Shah", + "Name[es]": "Bhushan Shah", + "Name[eu]": "Bhushan Shah", + "Name[fi]": "Bhushan Shah", + "Name[fr]": "Bhushan Shah", + "Name[hu]": "Bhushan Shah", + "Name[ia]": "Bhushan Shah", + "Name[it]": "Bhushan Shah", + "Name[ko]": "Bhushan Shah", + "Name[lt]": "Bhushan Shah", + "Name[nl]": "Bhushan Shah", + "Name[nn]": "Bhushan Shah", + "Name[pa]": "ਭੂਸ਼ਨ ਸ਼ਾਹ", + "Name[pl]": "Bhushan Shah", + "Name[pt_BR]": "Bhushan Shah", + "Name[ro]": "Bhushan Shah", + "Name[ru]": "Bhushan Shah", + "Name[sk]": "Bhushan Shah", + "Name[sl]": "Bhushan Shah", + "Name[sv]": "Bhushan Shah", + "Name[ta]": "புஷன் ஷா", + "Name[tr]": "Buşan Şah", + "Name[uk]": "Bhushan Shah", + "Name[vi]": "Bhushan Shah", + "Name[x-test]": "xxBhushan Shahxx", + "Name[zh_CN]": "Bhushan Shah" + } + ], + "Category": "Windows and Tasks", + "Description": "Tab bar to switch activities", + "Description[ar]": "شريط ألسنة لتبديل الأنشطة", + "Description[az]": "İş Otaqlarına keçid üçün vərəq zolağı", + "Description[ca]": "Barra de pestanyes per a canviar d'activitat", + "Description[cs]": "Panel pro přepínání aktivit", + "Description[de]": "Leiste zum Wechseln von Aktivitäten", + "Description[en_GB]": "Tab bar to switch activities", + "Description[es]": "Barra de pestañas para cambiar actividades", + "Description[eu]": "Jardueraz aldatzeko fitxa-barra", + "Description[fi]": "Välilehtipalkki aktiviteettivaihtoon", + "Description[fr]": "Barre d'onglets permettant le basculement entre activités", + "Description[hu]": "Aktivitások közötti váltásra szolgáló lapozóelem", + "Description[ia]": "Barra de scheda pro commutar activitates", + "Description[it]": "Barra a schede per cambiare attività", + "Description[ko]": "활동 사이를 전환할 수 있는 탭 표시줄", + "Description[lt]": "Kortelių juosta, skirta perjungti veiklas", + "Description[nl]": "Tabbladbalk om van activiteit te wisselen", + "Description[nn]": "Fanelinje for å byta mellom aktivitetar", + "Description[pa]": "ਸਰਗਰਮੀਆਂ ਨੂੰ ਬਦਲਣ ਲਈ ਟੈਬ ਪੱਟੀ", + "Description[pl]": "Pasek kart do przełączania aktywności", + "Description[pt_BR]": "Barra de abas para alternar atividades", + "Description[ro]": "Bară cu file pentru comutarea activităților", + "Description[ru]": "Переключение комнат", + "Description[sk]": "Panel na prepínanie aktivít", + "Description[sl]": "Vrstica zavihkov za preklapljanje med dejavnostmi", + "Description[sv]": "Flikrad för att byta aktiviteter", + "Description[ta]": "செயல்பாடுகளுக்கிடையே தாவ உதவும் கீற்றுப் பட்டை", + "Description[tr]": "Etkinlikleri değiştirmek için sekme çubuğu", + "Description[uk]": "Панель з вкладками для перемикання дій", + "Description[vi]": "Thanh thẻ để chuyển Hoạt động", + "Description[x-test]": "xxTab bar to switch activitiesxx", + "Description[zh_CN]": "切换活动的标签栏", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "tab-new", + "Id": "org.kde.plasma.activitybar", + "License": "GPL-2.0+", + "Name": "Activity Bar", + "Name[ar]": "شريط أنشطة", + "Name[ast]": "Barra d'actividaes", + "Name[az]": "İş Otağı paneli", + "Name[be@latin]": "Panel zaniatkaŭ", + "Name[bg]": "Лента за активност", + "Name[bs]": "Traka aktivnosti", + "Name[ca@valencia]": "Barra d'activitat", + "Name[ca]": "Barra d'activitat", + "Name[cs]": "Pruh aktivit", + "Name[da]": "Aktivitetslinje", + "Name[de]": "Aktivitätsleiste", + "Name[el]": "Γραμμή δραστηριότητας", + "Name[en_GB]": "Activity Bar", + "Name[eo]": "Aktiveca Nivelo", + "Name[es]": "Barra de actividad", + "Name[et]": "Tegevusriba", + "Name[eu]": "Jarduera-barra", + "Name[fi]": "Aktiviteettipalkki", + "Name[fr]": "Barre d'activités", + "Name[fy]": "Aktiviteitsbalke", + "Name[ga]": "Barra Gníomhaíochta", + "Name[gl]": "Barra de actividades", + "Name[gu]": "ક્રિયા પટ્ટી", + "Name[he]": "סרגל פעילויות", + "Name[hi]": "गतिविधि पट्टी", + "Name[hne]": "सक्रियता पट्टी", + "Name[hr]": "Traka aktivnosti", + "Name[hsb]": "Pas aktiwitow", + "Name[hu]": "Aktivitásjelző", + "Name[ia]": "Barra de activitate", + "Name[id]": "Bilah Aktivitas", + "Name[is]": "Virknislá", + "Name[it]": "Barra delle attività", + "Name[ja]": "アクティビティバー", + "Name[kk]": "Белсенділік панелі", + "Name[km]": "របារ​សកម្មភាព", + "Name[kn]": "ಚಟುವಟಿಕೆ ಪಟ್ಟಿ", + "Name[ko]": "활동 표시줄", + "Name[ku]": "Darika Çalakiyan", + "Name[lt]": "Veiklų juosta", + "Name[lv]": "Aktivitāšu josla", + "Name[mk]": "Лента за активности", + "Name[ml]": "ആക്ടിവിറ്റി ബാര്‍", + "Name[mr]": "कार्यपध्दती पट्टी", + "Name[nb]": "Aktivitetsstolpe", + "Name[nds]": "Aktivitetenbalken", + "Name[nl]": "Activiteitenbalk", + "Name[nn]": "Aktivitetslinje", + "Name[pa]": "ਸਰਗਰਮੀ ਪੱਟੀ", + "Name[pl]": "Pasek aktywności", + "Name[pt]": "Barra de Actividades", + "Name[pt_BR]": "Barra de atividades", + "Name[ro]": "Bară de activitate", + "Name[ru]": "Панель комнат", + "Name[si]": "ක්‍රියා තීරුව", + "Name[sk]": "Panel aktivít", + "Name[sl]": "Pas z dejavnostmi", + "Name[sr@ijekavian]": "трака активности", + "Name[sr@ijekavianlatin]": "traka aktivnosti", + "Name[sr@latin]": "traka aktivnosti", + "Name[sr]": "трака активности", + "Name[sv]": "Aktivitetsrad", + "Name[ta]": "செயல்பாடு பட்டை", + "Name[te]": "క్రియాశీలత పట్టీ", + "Name[tg]": "Навори фаъолият", + "Name[th]": "แถบกิจกรรม", + "Name[tr]": "Etkinlik Çubuğu", + "Name[ug]": "پائالىيەت بالدىقى", + "Name[uk]": "Панель дій", + "Name[vi]": "Thanh Hoạt động", + "Name[wa]": "Bår d' activité", + "Name[x-test]": "xxActivity Barxx", + "Name[zh_CN]": "活动栏", + "Name[zh_TW]": "活動列", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.activities" + ] +} diff --git a/plasma/workspace/applets/analog-clock/Messages.sh b/plasma/workspace/applets/analog-clock/Messages.sh new file mode 100644 index 0000000000..59d3103593 --- /dev/null +++ b/plasma/workspace/applets/analog-clock/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.analogclock.pot diff --git a/plasma/workspace/applets/analog-clock/contents/config/config.qml b/plasma/workspace/applets/analog-clock/contents/config/config.qml new file mode 100644 index 0000000000..5811d937ac --- /dev/null +++ b/plasma/workspace/applets/analog-clock/contents/config/config.qml @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("Appearance") + icon: "preferences-desktop-color" + source: "configGeneral.qml" + } +} diff --git a/plasma/workspace/applets/analog-clock/contents/config/main.xml b/plasma/workspace/applets/analog-clock/contents/config/main.xml new file mode 100644 index 0000000000..07be98ee39 --- /dev/null +++ b/plasma/workspace/applets/analog-clock/contents/config/main.xml @@ -0,0 +1,17 @@ + + + + + + + false + + + false + + + + diff --git a/plasma/workspace/applets/analog-clock/contents/ui/Hand.qml b/plasma/workspace/applets/analog-clock/contents/ui/Hand.qml new file mode 100644 index 0000000000..1351c60cb1 --- /dev/null +++ b/plasma/workspace/applets/analog-clock/contents/ui/Hand.qml @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2012 Viranch Mehta + SPDX-FileCopyrightText: 2012 Marco Martin + SPDX-FileCopyrightText: 2013 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.core 2.0 as PlasmaCore + +PlasmaCore.SvgItem { + id: handRoot + + property alias rotation: rotation.angle + property double svgScale + property double horizontalRotationOffset: 0 + property double verticalRotationOffset: 0 + property string rotationCenterHintId + readonly property double horizontalRotationCenter: { + if (svg.hasElement(rotationCenterHintId)) { + var hintedCenterRect = svg.elementRect(rotationCenterHintId), + handRect = svg.elementRect(elementId), + hintedX = hintedCenterRect.x - handRect.x + hintedCenterRect.width/2; + return Math.round(hintedX * svgScale) + Math.round(hintedX * svgScale) % 2; + } + return width/2; + } + readonly property double verticalRotationCenter: { + if (svg.hasElement(rotationCenterHintId)) { + var hintedCenterRect = svg.elementRect(rotationCenterHintId), + handRect = svg.elementRect(elementId), + hintedY = hintedCenterRect.y - handRect.y + hintedCenterRect.height/2; + return Math.round(hintedY * svgScale) + width % 2; + } + return width/2; + } + + width: Math.round(naturalSize.width * svgScale) + Math.round(naturalSize.width * svgScale) % 2 + height: Math.round(naturalSize.height * svgScale) + width % 2 + anchors { + top: clock.verticalCenter + topMargin: -verticalRotationCenter + verticalRotationOffset + left: clock.horizontalCenter + leftMargin: -horizontalRotationCenter + horizontalRotationOffset + } + + svg: clockSvg + transform: Rotation { + id: rotation + angle: 0 + origin { + x: handRoot.horizontalRotationCenter + y: handRoot.verticalRotationCenter + } + Behavior on angle { + RotationAnimation { + id: anim + duration: PlasmaCore.Units.longDuration + direction: RotationAnimation.Clockwise + easing.type: Easing.OutElastic + easing.overshoot: 0.5 + } + } + } +} diff --git a/plasma/workspace/applets/analog-clock/contents/ui/analogclock.qml b/plasma/workspace/applets/analog-clock/contents/ui/analogclock.qml new file mode 100644 index 0000000000..36b43ce12f --- /dev/null +++ b/plasma/workspace/applets/analog-clock/contents/ui/analogclock.qml @@ -0,0 +1,229 @@ +/* + SPDX-FileCopyrightText: 2012 Viranch Mehta + SPDX-FileCopyrightText: 2012 Marco Martin + SPDX-FileCopyrightText: 2013 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.calendar 2.0 as PlasmaCalendar +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +Item { + id: analogclock + + width: PlasmaCore.Units.gridUnit * 15 + height: PlasmaCore.Units.gridUnit * 15 + property int hours + property int minutes + property int seconds + property bool showSecondsHand: plasmoid.configuration.showSecondHand + property bool showTimezone: plasmoid.configuration.showTimezoneString + property int tzOffset + + Plasmoid.backgroundHints: "NoBackground"; + Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation + + Plasmoid.toolTipMainText: Qt.formatDate(dataSource.data["Local"]["DateTime"],"dddd") + Plasmoid.toolTipSubText: Qt.formatTime(dataSource.data["Local"]["DateTime"], Qt.locale().timeFormat(Locale.LongFormat)) + "\n" + + Qt.formatDate(dataSource.data["Local"]["DateTime"], Qt.locale().dateFormat(Locale.LongFormat).replace(/(^dddd.?\s)|(,?\sdddd$)/, "")) + + PlasmaCore.DataSource { + id: dataSource + engine: "time" + connectedSources: "Local" + interval: showSecondsHand ? 1000 : 30000 + onDataChanged: { + var date = new Date(data["Local"]["DateTime"]); + hours = date.getHours(); + minutes = date.getMinutes(); + seconds = date.getSeconds(); + } + Component.onCompleted: { + onDataChanged(); + } + } + + function dateTimeChanged() + { + //console.log("Date/time changed!"); + + var currentTZOffset = dataSource.data["Local"]["Offset"] / 60; + if (currentTZOffset !== tzOffset) { + tzOffset = currentTZOffset; + //console.log("TZ offset changed: " + tzOffset); + Date.timeZoneUpdated(); // inform the QML JS engine about TZ change + } + } + + Component.onCompleted: { + tzOffset = new Date().getTimezoneOffset(); + //console.log("Initial TZ offset: " + tzOffset); + dataSource.onDataChanged.connect(dateTimeChanged); + } + + Plasmoid.compactRepresentation: Item { + id: representation + Layout.minimumWidth: plasmoid.formFactor !== PlasmaCore.Types.Vertical ? representation.height : PlasmaCore.Units.gridUnit + Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? representation.width : PlasmaCore.Units.gridUnit + + MouseArea { + anchors.fill: parent + onClicked: plasmoid.expanded = !plasmoid.expanded + } + + + PlasmaCore.Svg { + id: clockSvg + imagePath: "widgets/clock" + function estimateHorizontalHandShadowOffset() { + var id = "hint-hands-shadow-offset-to-west"; + if (hasElement(id)) { + return -elementSize(id).width; + } + id = "hint-hands-shadows-offset-to-east"; + if (hasElement(id)) { + return elementSize(id).width; + } + return 0; + } + function estimateVerticalHandShadowOffset() { + var id = "hint-hands-shadow-offset-to-north"; + if (hasElement(id)) { + return -elementSize(id).height; + } + id = "hint-hands-shadow-offset-to-south"; + if (hasElement(id)) { + return elementSize(id).height; + } + return 0; + } + property double naturalHorizontalHandShadowOffset: estimateHorizontalHandShadowOffset() + property double naturalVerticalHandShadowOffset: estimateVerticalHandShadowOffset() + onRepaintNeeded: { + naturalHorizontalHandShadowOffset = estimateHorizontalHandShadowOffset(); + naturalVerticalHandShadowOffset = estimateVerticalHandShadowOffset(); + } + } + + Item { + id: clock + width: parent.width + anchors { + top: parent.top + bottom: showTimezone ? timezoneBg.top : parent.bottom + } + readonly property double svgScale: face.width / face.naturalSize.width + readonly property double horizontalShadowOffset: + Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) % 2 + readonly property double verticalShadowOffset: + Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) % 2 + + PlasmaCore.SvgItem { + id: face + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: Math.min(parent.width, parent.height) + svg: clockSvg + elementId: "ClockFace" + } + + Hand { + elementId: "HourHandShadow" + rotationCenterHintId: "hint-hourhandshadow-rotation-center-offset" + horizontalRotationOffset: clock.horizontalShadowOffset + verticalRotationOffset: clock.verticalShadowOffset + rotation: 180 + hours * 30 + (minutes/2) + svgScale: clock.svgScale + + } + Hand { + elementId: "HourHand" + rotationCenterHintId: "hint-hourhand-rotation-center-offset" + rotation: 180 + hours * 30 + (minutes/2) + svgScale: clock.svgScale + } + + Hand { + elementId: "MinuteHandShadow" + rotationCenterHintId: "hint-minutehandshadow-rotation-center-offset" + horizontalRotationOffset: clock.horizontalShadowOffset + verticalRotationOffset: clock.verticalShadowOffset + rotation: 180 + minutes * 6 + svgScale: clock.svgScale + } + Hand { + elementId: "MinuteHand" + rotationCenterHintId: "hint-minutehand-rotation-center-offset" + rotation: 180 + minutes * 6 + svgScale: clock.svgScale + } + + Hand { + elementId: "SecondHandShadow" + rotationCenterHintId: "hint-secondhandshadow-rotation-center-offset" + horizontalRotationOffset: clock.horizontalShadowOffset + verticalRotationOffset: clock.verticalShadowOffset + rotation: 180 + seconds * 6 + visible: showSecondsHand + svgScale: clock.svgScale + } + Hand { + elementId: "SecondHand" + rotationCenterHintId: "hint-secondhand-rotation-center-offset" + rotation: 180 + seconds * 6 + visible: showSecondsHand + svgScale: clock.svgScale + } + + PlasmaCore.SvgItem { + id: center + width: naturalSize.width * clock.svgScale + height: naturalSize.height * clock.svgScale + anchors.centerIn: clock + svg: clockSvg + elementId: "HandCenterScrew" + z: 1000 + } + + PlasmaCore.SvgItem { + anchors.fill: face + svg: clockSvg + elementId: "Glass" + width: naturalSize.width * clock.svgScale + height: naturalSize.height * clock.svgScale + } + } + + PlasmaCore.FrameSvgItem { + id: timezoneBg + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 10 + } + imagePath: "widgets/background" + width: childrenRect.width + margins.right + margins.left + height: childrenRect.height + margins.top + margins.bottom + visible: showTimezone + PlasmaComponents.Label { + id: timezoneText + x: timezoneBg.margins.left + y: timezoneBg.margins.top + text: dataSource.data["Local"]["Timezone"] + } + } + } + Plasmoid.fullRepresentation: PlasmaCalendar.MonthView { + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 20 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 20 + + today: dataSource.data["Local"]["DateTime"] + } + +} diff --git a/plasma/workspace/applets/analog-clock/contents/ui/configGeneral.qml b/plasma/workspace/applets/analog-clock/contents/ui/configGeneral.qml new file mode 100644 index 0000000000..57591edb6a --- /dev/null +++ b/plasma/workspace/applets/analog-clock/contents/ui/configGeneral.qml @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2013 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 +import org.kde.kirigami 2.5 as Kirigami + + +Kirigami.FormLayout { + property alias cfg_showSecondHand: showSecondHandCheckBox.checked + property alias cfg_showTimezoneString: showTimezoneCheckBox.checked + + anchors { + left: parent.left + right: parent.right + } + + CheckBox { + id: showSecondHandCheckBox + text: i18n("Show seconds hand") + Kirigami.FormData.label: i18n("General:") + } + CheckBox { + id: showTimezoneCheckBox + text: i18n("Show time zone") + } +} diff --git a/plasma/workspace/applets/analog-clock/metadata.json b/plasma/workspace/applets/analog-clock/metadata.json new file mode 100644 index 0000000000..8e64e73d0e --- /dev/null +++ b/plasma/workspace/applets/analog-clock/metadata.json @@ -0,0 +1,181 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "Date and Time", + "Description": "A clock with hands", + "Description[ar]": "ساعة بِعقارب", + "Description[az]": "Əqrəbli saat", + "Description[ca]": "Un rellotge amb manetes", + "Description[cs]": "Ručičkové hodiny", + "Description[de]": "Eine Uhr mit Zeigern", + "Description[en_GB]": "A clock with hands", + "Description[es]": "Un reloj con manecillas", + "Description[eu]": "Erloju orraztuna", + "Description[fi]": "Viisarikello", + "Description[fr]": "Une horloge avec des mains", + "Description[hu]": "Óra mutatókkal", + "Description[ia]": "Un horologio con manos", + "Description[it]": "Un orologio con lancette", + "Description[ko]": "시침과 분침이 있는 시계", + "Description[lt]": "Laikrodis su rodyklėmis", + "Description[nl]": "Een klok met wijzers", + "Description[nn]": "Ei klokke med visarar", + "Description[pa]": "ਸੂਈਆਂ ਵਾਲੀ ਘੜੀ", + "Description[pl]": "Zegar ze wskazówkami", + "Description[pt_BR]": "Um relógio com ponteiros", + "Description[ro]": "Ceas cu săgeți", + "Description[ru]": "Часы со стрелками", + "Description[sk]": "Ručičkové hodiny", + "Description[sl]": "Ura s kazalci", + "Description[sv]": "En klocka med visare", + "Description[ta]": "முட்களை கொண்ட கடிகாரம்", + "Description[tr]": "İbreli bir saat", + "Description[uk]": "Годинник зі стрілками", + "Description[vi]": "Một đồng hồ với các kim", + "Description[x-test]": "xxA clock with handsxx", + "Description[zh_CN]": "带指针的时钟", + "EnabledByDefault": true, + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-system-time", + "Id": "org.kde.plasma.analogclock", + "License": "GPL-2.0+", + "Name": "Analog Clock", + "Name[af]": "Analooghorlosie", + "Name[ar]": "ساعة تناظرية", + "Name[ast]": "Reló analóxicu", + "Name[az]": "Əqrəbli saat", + "Name[be@latin]": "Analahavy hadzińnik", + "Name[be]": "Аналагавы гадзіннік", + "Name[bg]": "Аналогов часовник", + "Name[bn]": "অ্যানালগ ঘড়ি", + "Name[bs]": "Analogni sat", + "Name[ca@valencia]": "Rellotge analògic", + "Name[ca]": "Rellotge analògic", + "Name[cs]": "Analogové hodiny", + "Name[csb]": "Analogòwi zédżer", + "Name[da]": "Analogt ur", + "Name[de]": "Analoge Uhr", + "Name[el]": "Αναλογικό ρολόι", + "Name[en_GB]": "Analogue Clock", + "Name[eo]": "Analoga horloĝo", + "Name[es]": "Reloj analógico", + "Name[et]": "Analoogkell", + "Name[eu]": "Erloju analogikoa", + "Name[fa]": "ساعت قیاسی", + "Name[fi]": "Analoginen kello", + "Name[fr]": "Horloge analogique", + "Name[fy]": "Analoge klok", + "Name[ga]": "Clog Analógach", + "Name[gl]": "Reloxo analóxico", + "Name[gu]": "સાદી ઘડિયાળ", + "Name[he]": "שעון מחוגים", + "Name[hi]": "एनॉलॉग घड़ी", + "Name[hne]": "एनालाग घड़ी", + "Name[hr]": "Analogni sat", + "Name[hsb]": "Analogny časnik", + "Name[hu]": "Mutatós óra", + "Name[ia]": "Horologio analogic", + "Name[id]": "Jam Analog", + "Name[is]": "Skífuklukka", + "Name[it]": "Orologio analogico", + "Name[ja]": "アナログ時計", + "Name[kk]": "Аналогты сағат", + "Name[km]": "នាឡិកា​អាណាឡូក", + "Name[kn]": "ಅವಿಚ್ಛಿನ್ನಾತ್ಮಕ (ಅನಲಾಗ್, ಸಾಂಪ್ರದಾಯಿಕ) ಗಡಿಯಾರ", + "Name[ko]": "아날로그 시계", + "Name[ku]": "Demjimêra Analog", + "Name[lt]": "Analoginis laikrodis", + "Name[lv]": "Rādītāju pulkstenis", + "Name[mai]": "एनालाग घडी", + "Name[mk]": "Аналоген часовник", + "Name[ml]": "അനലോഗ് ഘടികാരം", + "Name[mr]": "एनॉलॉग घड्याळ", + "Name[nb]": "Analog klokke", + "Name[nds]": "Analoog Klock", + "Name[ne]": "एनालग घडी", + "Name[nl]": "Analoge klok", + "Name[nn]": "Analog klokke", + "Name[oc]": "Relòtge analogic", + "Name[pa]": "ਐਨਾਲਾਗ ਘੜੀ", + "Name[pl]": "Zegar analogowy", + "Name[pt]": "Relógio Analógico", + "Name[pt_BR]": "Relógio Analógico", + "Name[ro]": "Ceas analog", + "Name[ru]": "Часы с циферблатом", + "Name[se]": "Analogalaš diibmu", + "Name[si]": "ප්‍රතිසම ඔරලෝසුව", + "Name[sk]": "Analógové hodiny", + "Name[sl]": "Analogna ura", + "Name[sr@ijekavian]": "аналогни сат", + "Name[sr@ijekavianlatin]": "analogni sat", + "Name[sr@latin]": "analogni sat", + "Name[sr]": "аналогни сат", + "Name[sv]": "Analog klocka", + "Name[ta]": "சுழல் கடிகாரம்", + "Name[te]": "ఎనలాగ్ గడియారం", + "Name[tg]": "Соати аналогӣ", + "Name[th]": "นาฬิกาแอนะล็อก", + "Name[tr]": "Analog Saat", + "Name[ug]": "ئانالوگ سائەت", + "Name[uk]": "Аналоговий годинник", + "Name[uz@cyrillic]": "Аналог соат", + "Name[uz]": "Analog soat", + "Name[vi]": "Đồng hồ kim", + "Name[wa]": "Ôrlodje analodjike", + "Name[x-test]": "xxAnalog Clockxx", + "Name[zh_CN]": "模拟时钟", + "Name[zh_TW]": "類比時鐘", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "3.0", + "Website": "https://userbase.kde.org/Plasma/Clocks" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/analogclock.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.time", + "org.kde.plasma.date" + ], + "X-Plasma-StandAloneApp": true +} diff --git a/plasma/workspace/applets/appmenu/CMakeLists.txt b/plasma/workspace/applets/appmenu/CMakeLists.txt new file mode 100644 index 0000000000..56768977e1 --- /dev/null +++ b/plasma/workspace/applets/appmenu/CMakeLists.txt @@ -0,0 +1,6 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.appmenu\") + +add_subdirectory(lib) +add_subdirectory(plugin) + +plasma_install_package(package org.kde.plasma.appmenu) diff --git a/plasma/workspace/applets/appmenu/Messages.sh b/plasma/workspace/applets/appmenu/Messages.sh new file mode 100644 index 0000000000..54e96ee928 --- /dev/null +++ b/plasma/workspace/applets/appmenu/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.appmenu.pot diff --git a/plasma/workspace/applets/appmenu/lib/CMakeLists.txt b/plasma/workspace/applets/appmenu/lib/CMakeLists.txt new file mode 100644 index 0000000000..c70afcc0c0 --- /dev/null +++ b/plasma/workspace/applets/appmenu/lib/CMakeLists.txt @@ -0,0 +1,9 @@ +kcoreaddons_add_plugin(plasma_applet_appmenu SOURCES appmenuapplet.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(plasma_applet_appmenu + Qt::Widgets + Qt::Quick + Qt::DBus + KF5::Plasma + KF5::WindowSystem + PW::LibTaskManager) diff --git a/plasma/workspace/applets/appmenu/lib/appmenuapplet.cpp b/plasma/workspace/applets/appmenu/lib/appmenuapplet.cpp new file mode 100644 index 0000000000..0552fbb658 --- /dev/null +++ b/plasma/workspace/applets/appmenu/lib/appmenuapplet.cpp @@ -0,0 +1,292 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#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 KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ + ++s_refs; + // 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, 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; + Q_EMIT modelChanged(); + } +} + +int AppMenuApplet::view() const +{ + return m_viewType; +} + +void AppMenuApplet::setView(int type) +{ + if (m_viewType != type) { + m_viewType = type; + Q_EMIT viewChanged(); + } +} + +int AppMenuApplet::currentIndex() const +{ + return m_currentIndex; +} + +void AppMenuApplet::setCurrentIndex(int currentIndex) +{ + if (m_currentIndex != currentIndex) { + m_currentIndex = currentIndex; + Q_EMIT currentIndexChanged(); + } +} + +QQuickItem *AppMenuApplet::buttonGrid() const +{ + return m_buttonGrid; +} + +void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) +{ + if (m_buttonGrid != buttonGrid) { + m_buttonGrid = buttonGrid; + Q_EMIT buttonGridChanged(); + } +} + +QMenu *AppMenuApplet::createMenu(int idx) const +{ + QMenu *menu = nullptr; + QAction *action = nullptr; + + if (view() == CompactView) { + menu = new QMenu(); + for (int i = 0; i < m_model->rowCount(); 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) { + actionMenu->setProperty("_breeze_menu_is_top", true); + 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()); + + // hide the old menu only after showing the new one to avoid brief focus flickering on X11. + // on wayland, you can't have more than one grabbing popup at a time so we show it after + // the menu has hidden. thankfully, wayland doesn't have this flickering. + if (!KWindowSystem::isPlatformWayland()) { + actionMenu->popup(pos); + } + + if (view() == FullView) { + 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(); + } + } + + if (KWindowSystem::isPlatformWayland()) { + actionMenu->popup(pos); + } + + 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; + Q_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; + Q_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; + } + + Q_EMIT requestActivateIndex(buttonIndex); + } + + return false; +} + +K_PLUGIN_CLASS_WITH_JSON(AppMenuApplet, "../package/metadata.json") + +#include "appmenuapplet.moc" diff --git a/plasma/workspace/applets/appmenu/lib/appmenuapplet.h b/plasma/workspace/applets/appmenu/lib/appmenuapplet.h new file mode 100644 index 0000000000..e39be529f9 --- /dev/null +++ b/plasma/workspace/applets/appmenu/lib/appmenuapplet.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include + +class QQuickItem; +class QMenu; +class AppMenuModel; + +class AppMenuApplet : public Plasma::Applet +{ + Q_OBJECT + + Q_PROPERTY(AppMenuModel *model READ model WRITE setModel NOTIFY modelChanged) + + Q_PROPERTY(int view READ view WRITE setView NOTIFY viewChanged) + + Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) + + Q_PROPERTY(QQuickItem *buttonGrid READ buttonGrid WRITE setButtonGrid NOTIFY buttonGridChanged) + +public: + enum ViewType { + FullView, + CompactView, + }; + + explicit AppMenuApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~AppMenuApplet() override; + + void init() override; + + int currentIndex() const; + + QQuickItem *buttonGrid() const; + void setButtonGrid(QQuickItem *buttonGrid); + + AppMenuModel *model() const; + void setModel(AppMenuModel *model); + + int view() const; + void setView(int type); + +Q_SIGNALS: + void modelChanged(); + void viewChanged(); + void currentIndexChanged(); + void buttonGridChanged(); + void requestActivateIndex(int index); + +public Q_SLOTS: + void trigger(QQuickItem *ctx, int idx); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + QMenu *createMenu(int idx) const; + void setCurrentIndex(int currentIndex); + void onMenuAboutToHide(); + + int m_currentIndex = -1; + int m_viewType = FullView; + QPointer m_currentMenu; + QPointer m_buttonGrid; + QPointer m_model; + static int s_refs; +}; diff --git a/plasma/workspace/applets/appmenu/package/contents/config/config.qml b/plasma/workspace/applets/appmenu/package/contents/config/config.qml new file mode 100644 index 0000000000..17ec6c9311 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/contents/config/config.qml @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("Appearance") + icon: "preferences-desktop-color" + source: "configGeneral.qml" + } +} diff --git a/plasma/workspace/applets/appmenu/package/contents/config/main.xml b/plasma/workspace/applets/appmenu/package/contents/config/main.xml new file mode 100644 index 0000000000..4891308ea7 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/contents/config/main.xml @@ -0,0 +1,15 @@ + + + + + + + + false + + + + diff --git a/plasma/workspace/applets/appmenu/package/contents/ui/MenuDelegate.qml b/plasma/workspace/applets/appmenu/package/contents/ui/MenuDelegate.qml new file mode 100644 index 0000000000..ebbf8d53a1 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/contents/ui/MenuDelegate.qml @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2020 Carson Black + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.10 +import QtQuick.Controls 2.10 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PC3 +import org.kde.kirigami 2.12 as Kirigami + +AbstractButton { + id: controlRoot + + hoverEnabled: true + + enum State { + Rest, + Hover, + Down + } + + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.SecondaryControl + Kirigami.MnemonicData.label: controlRoot.text + + leftPadding: rest.margins.left + topPadding: rest.margins.top + rightPadding: rest.margins.right + bottomPadding: rest.margins.bottom + + background: Item { + id: background + + property int state: { + if (controlRoot.down) { + return MenuDelegate.State.Down + } else if (controlRoot.hovered) { + return MenuDelegate.State.Hover + } + return MenuDelegate.State.Rest + } + + PlasmaCore.FrameSvgItem { + id: rest + anchors.fill: parent + visible: background.state == MenuDelegate.State.Rest + imagePath: "widgets/menubaritem" + prefix: "normal" + } + PlasmaCore.FrameSvgItem { + id: hover + anchors.fill: parent + visible: background.state == MenuDelegate.State.Hover + imagePath: "widgets/menubaritem" + prefix: "hover" + } + PlasmaCore.FrameSvgItem { + id: down + anchors.fill: parent + visible: background.state == MenuDelegate.State.Down + imagePath: "widgets/menubaritem" + prefix: "pressed" + } + } + + contentItem: PC3.Label { + text: controlRoot.Kirigami.MnemonicData.richTextLabel + } +} diff --git a/plasma/workspace/applets/appmenu/package/contents/ui/configGeneral.qml b/plasma/workspace/applets/appmenu/package/contents/ui/configGeneral.qml new file mode 100644 index 0000000000..90b1d436e1 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/contents/ui/configGeneral.qml @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.5 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +Kirigami.FormLayout { + anchors.left: parent.left + anchors.right: parent.right + + property alias cfg_compactView: compactViewRadioButton.checked + + RadioButton { + id: compactViewRadioButton + text: i18n("Use single button for application menu") + } + + RadioButton { + id: fullViewRadioButton + checked: !compactViewRadioButton.checked + text: i18n("Show full application menu") + } +} diff --git a/plasma/workspace/applets/appmenu/package/contents/ui/main.qml b/plasma/workspace/applets/appmenu/package/contents/ui/main.qml new file mode 100644 index 0000000000..8a3d858020 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/contents/ui/main.qml @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2013 Heena Mahour + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.8 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.kquickcontrolsaddons 2.0 // For KCMShell +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.private.appmenu 1.0 as AppMenuPrivate +import org.kde.kirigami 2.5 as Kirigami + +Item { + id: root + + readonly property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical + readonly property bool view: plasmoid.configuration.compactView + readonly property bool menuAvailable: appMenuModel.menuAvailable + + readonly property bool kcmAuthorized: KCMShell.authorize(["style.desktop"]).length > 0 + + onViewChanged: { + plasmoid.nativeInterface.view = view + } + + Plasmoid.constraintHints: PlasmaCore.Types.CanFillArea + Plasmoid.preferredRepresentation: (plasmoid.configuration.compactView) ? Plasmoid.compactRepresentation : Plasmoid.fullRepresentation + + Plasmoid.compactRepresentation: PlasmaComponents3.ToolButton { + readonly property int fakeIndex: 0 + Layout.fillWidth: false + Layout.fillHeight: false + Layout.minimumWidth: implicitWidth + Layout.maximumWidth: implicitWidth + enabled: menuAvailable + checkable: menuAvailable && plasmoid.nativeInterface.currentIndex === fakeIndex + checked: checkable + icon.name: "application-menu" + onClicked: plasmoid.nativeInterface.trigger(this, 0); + } + + Plasmoid.fullRepresentation: GridLayout { + id: buttonGrid + + Plasmoid.status: { + if (menuAvailable && plasmoid.nativeInterface.currentIndex > -1 && buttonRepeater.count > 0) { + return PlasmaCore.Types.NeedsAttentionStatus; + } else { + //when we're not enabled set to active to show the configure button + return buttonRepeater.count > 0 ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatus; + } + } + + Layout.minimumWidth: implicitWidth + Layout.minimumHeight: implicitHeight + + flow: root.vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight + rowSpacing: 0 + columnSpacing: 0 + + Component.onCompleted: { + plasmoid.nativeInterface.buttonGrid = buttonGrid + + // using a Connections {} doesn't work for some reason in Qt >= 5.8 + plasmoid.nativeInterface.requestActivateIndex.connect(function (index) { + var idx = Math.max(0, Math.min(buttonRepeater.count - 1, index)) + var button = buttonRepeater.itemAt(index) + if (button) { + button.clicked() + } + }); + + plasmoid.activated.connect(function () { + var button = buttonRepeater.itemAt(0); + if (button) { + button.clicked(); + } + }); + } + + // So we can show mnemonic underlines only while Alt is pressed + PlasmaCore.DataSource { + id: keystateSource + engine: "keystate" + connectedSources: ["Alt"] + } + + PlasmaComponents3.ToolButton { + id: noMenuPlaceholder + visible: buttonRepeater.count == 0 + text: plasmoid.title + Layout.fillWidth: root.vertical + Layout.fillHeight: !root.vertical + } + + Repeater { + id: buttonRepeater + model: appMenuModel.visible ? appMenuModel : null + + MenuDelegate { + readonly property int buttonIndex: index + + Layout.fillWidth: root.vertical + Layout.fillHeight: !root.vertical + text: activeMenu + // TODO: Alt and other modifiers might be unavailable on Wayland + Kirigami.MnemonicData.active: keystateSource.data.Alt !== undefined && keystateSource.data.Alt.Pressed + + down: pressed || plasmoid.nativeInterface.currentIndex === index + + visible: text !== "" + onClicked: { + plasmoid.nativeInterface.trigger(this, index) + + checked = Qt.binding(function() { + return plasmoid.nativeInterface.currentIndex === index; + }); + } + + // QMenu opens on press, so we'll replicate that here + MouseArea { + anchors.fill: parent + hoverEnabled: plasmoid.nativeInterface.currentIndex !== -1 + onPressed: parent.clicked() + onEntered: parent.clicked() + } + } + } + } + + AppMenuPrivate.AppMenuModel { + id: appMenuModel + screenGeometry: plasmoid.screenGeometry + onRequestActivateIndex: plasmoid.nativeInterface.requestActivateIndex(index) + Component.onCompleted: { + plasmoid.nativeInterface.model = appMenuModel + } + } +} diff --git a/plasma/workspace/applets/appmenu/package/metadata.json b/plasma/workspace/applets/appmenu/package/metadata.json new file mode 100644 index 0000000000..f40a3eced7 --- /dev/null +++ b/plasma/workspace/applets/appmenu/package/metadata.json @@ -0,0 +1,136 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Category": "Windows and Tasks", + "Description": "Global menubar in your Plasma Desktop", + "Description[ar]": "شريط القائمة الشاملة في سطح مكتب بلازما", + "Description[az]": "Plasma iş masanızın qlobal menyu zolağı", + "Description[ca]": "Barra de menús global a l'escriptori Plasma", + "Description[de]": "Globale Menüleiste auf der Plasma-Arbeitsfläche", + "Description[en_GB]": "Global menubar in your Plasma Desktop", + "Description[es]": "Barra de menú global para el escritorio Plasma", + "Description[eu]": "Zure Plasma mahaigaineko menu-barra orokorra", + "Description[fi]": "Työpöydänlaajuinen valikkorivi Plasma-työpöydälle", + "Description[fr]": "Barre globale de menus de votre bureau Plasma", + "Description[hu]": "Globális menüsáv a Plasma asztalon", + "Description[ia]": "Barra de menu global in tu Scriptorio de Plasma", + "Description[it]": "Barra dei menu globale nel tuo desktop Plasma", + "Description[ko]": "Plasma 데스크톱에 전역 메뉴 표시줄 표시", + "Description[lt]": "Visuotinė meniu juosta jūsų Plasma darbalaukyje", + "Description[nl]": "Globale menubalk in uw Plasma-bureaublad", + "Description[nn]": "Global menylinje på Plasma-skrivebordet", + "Description[pa]": "ਤੁਹਾਡੇ ਪਲਾਜ਼ਮਾ ਡੈਸਕਟਾਪ ਵਿੱਚ ਗਲੋਬਲ ਮੇਨੂ-ਪੱਟੀ", + "Description[pl]": "Globalny pasek menu na pulpicie Plazmy", + "Description[pt_BR]": "Barra de menu global na sua área de trabalho Plasma", + "Description[ro]": "Bară de meniu globală în biroul Plasma", + "Description[ru]": "Строка меню приложения на рабочем столе Plasma", + "Description[sk]": "Globálna ponuka na vašej Ploche Plasma", + "Description[sl]": "Splošna menijska vrstica na vašem namizju Plasma", + "Description[sv]": "Global menyrad på Plasmaskrivbordet", + "Description[ta]": "பணிமேடையிலேயே செயலிகளின் பட்டிகளை காட்டும்", + "Description[tr]": "Plazma Masaüstünüzde global menü çubuğu", + "Description[uk]": "Загальна панель меню у вашій стільниці Плазми", + "Description[vi]": "Thanh trình đơn toàn cục ở bàn làm việc Plasma của bạn", + "Description[x-test]": "xxGlobal menubar in your Plasma Desktopxx", + "Description[zh_CN]": "Plasma 桌面上的全局菜单", + "FormFactors": [ + "desktop" + ], + "Icon": "show-menu", + "Id": "org.kde.plasma.appmenu", + "License": "GPL-2.0+", + "Name": "Global Menu", + "Name[ar]": "القائمة العموميّة", + "Name[ast]": "Menú global", + "Name[az]": "Qlobal menyu", + "Name[ca@valencia]": "Menú global", + "Name[ca]": "Menú global", + "Name[cs]": "Globální nabídka", + "Name[da]": "Global menu", + "Name[de]": "Globales Menü", + "Name[el]": "Καθολικό μενού", + "Name[en_GB]": "Global Menu", + "Name[es]": "Menú global", + "Name[et]": "Globaalne menüü", + "Name[eu]": "Menu orokorra", + "Name[fi]": "Yleisvalikko", + "Name[fr]": "Menu global", + "Name[gl]": "Menú global", + "Name[he]": "תפריט גלובלי", + "Name[hi]": "वैश्विक मेन्यू", + "Name[hsb]": "Globalny meni", + "Name[hu]": "Globális menü", + "Name[ia]": "Menu Global", + "Name[id]": "Menu Global", + "Name[it]": "Menu globale", + "Name[ko]": "전역 메뉴", + "Name[lt]": "Visuotinis meniu", + "Name[lv]": "Globālā izvēlne", + "Name[ml]": "പൊതു മെനു", + "Name[nl]": "Globaal menu", + "Name[nn]": "Global meny", + "Name[pa]": "ਗਲੋਬਲ ਮੇਨੂ", + "Name[pl]": "Menu globalne", + "Name[pt]": "Menu Global", + "Name[pt_BR]": "Menu global", + "Name[ro]": "Meniu global", + "Name[ru]": "Меню приложения", + "Name[sk]": "Globálna ponuka", + "Name[sl]": "Splošni meni", + "Name[sr@ijekavian]": "Глобални мени", + "Name[sr@ijekavianlatin]": "Globalni meni", + "Name[sr@latin]": "Globalni meni", + "Name[sr]": "Глобални мени", + "Name[sv]": "Global meny", + "Name[ta]": "பொதுவான பட்டி", + "Name[tg]": "Феҳристи умумӣ", + "Name[tr]": "Genel Menü", + "Name[uk]": "Загальне меню", + "Name[vi]": "Trình đơn toàn cục", + "Name[x-test]": "xxGlobal Menuxx", + "Name[zh_CN]": "全局菜单", + "Name[zh_TW]": "全域選單", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "2.0", + "Website": "https://kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml" +} diff --git a/plasma/workspace/applets/appmenu/plugin/CMakeLists.txt b/plasma/workspace/applets/appmenu/plugin/CMakeLists.txt new file mode 100644 index 0000000000..46dc9e36f4 --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/CMakeLists.txt @@ -0,0 +1,19 @@ +set(appmenuapplet_SRCS + appmenumodel.cpp + appmenuplugin.cpp +) + +add_library(appmenuplugin SHARED ${appmenuapplet_SRCS}) +target_link_libraries(appmenuplugin + Qt::Core + Qt::Widgets + Qt::Quick + KF5::Plasma + KF5::WindowSystem + KF5::I18n + PW::LibTaskManager + dbusmenuqt) + +install(TARGETS appmenuplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/appmenu) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/appmenu) diff --git a/plasma/workspace/applets/appmenu/plugin/appmenumodel.cpp b/plasma/workspace/applets/appmenu/plugin/appmenumodel.cpp new file mode 100644 index 0000000000..5ac6a4d4ec --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/appmenumodel.cpp @@ -0,0 +1,327 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "appmenumodel.h" + +#include +#include +#include +#include +#include +#include + +// Includes for the menu search. +#include +#include +#include +#include + +#include +#include + +class KDBusMenuImporter : public DBusMenuImporter +{ +public: + KDBusMenuImporter(const QString &service, const QString &path, QObject *parent) + : DBusMenuImporter(service, path, parent) + { + } + +protected: + QIcon iconForName(const QString &name) override + { + return QIcon::fromTheme(name); + } +}; + +AppMenuModel::AppMenuModel(QObject *parent) + : QAbstractListModel(parent) + , m_serviceWatcher(new QDBusServiceWatcher(this)) + , m_tasksModel(new TaskManager::TasksModel(this)) +{ + m_tasksModel->setFilterByScreen(true); + connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, this, &AppMenuModel::onActiveWindowChanged); + connect(m_tasksModel, + &TaskManager::TasksModel::dataChanged, + [=](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) { + Q_UNUSED(topLeft) + Q_UNUSED(bottomRight) + if (roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuObjectPath) + || roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuServiceName) || roles.isEmpty()) { + onActiveWindowChanged(); + } + }); + connect(m_tasksModel, &TaskManager::TasksModel::activityChanged, this, &AppMenuModel::onActiveWindowChanged); + connect(m_tasksModel, &TaskManager::TasksModel::virtualDesktopChanged, this, &AppMenuModel::onActiveWindowChanged); + connect(m_tasksModel, &TaskManager::TasksModel::countChanged, this, &AppMenuModel::onActiveWindowChanged); + connect(m_tasksModel, &TaskManager::TasksModel::screenGeometryChanged, this, &AppMenuModel::screenGeometryChanged); + + connect(this, &AppMenuModel::modelNeedsUpdate, this, [this] { + if (!m_updatePending) { + m_updatePending = true; + QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); + } + }); + + onActiveWindowChanged(); + + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + // if our current DBus connection gets lost, close the menu + // we'll select the new menu when the focus changes + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) { + if (serviceName == m_serviceName) { + setMenuAvailable(false); + Q_EMIT modelNeedsUpdate(); + } + }); + + // X11 has funky menu behaviour that prevents this from working properly. + if (KWindowSystem::isPlatformWayland()) { + m_searchAction = new QAction(this); + m_searchAction->setText(i18n("Search")); + m_searchAction->setObjectName(QStringLiteral("appmenu")); + + m_searchMenu.reset(new QMenu); + auto searchAction = new QWidgetAction(this); + auto searchBar = new QLineEdit; + searchBar->setClearButtonEnabled(true); + searchBar->setPlaceholderText(i18n("Search…")); + searchBar->setMinimumWidth(200); + searchBar->setContentsMargins(4, 4, 4, 4); + connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, [=]() { + searchBar->setText(QString()); + }); + connect(searchBar, &QLineEdit::textChanged, [=]() mutable { + insertSearchActionsIntoMenu(searchBar->text()); + }); + connect(searchBar, &QLineEdit::returnPressed, [=]() mutable { + if (m_currentSearchActions.first()) { + m_currentSearchActions.first()->trigger(); + } + }); + connect(this, &AppMenuModel::modelNeedsUpdate, this, [this, searchBar]() mutable { + insertSearchActionsIntoMenu(searchBar->text()); + }); + searchAction->setDefaultWidget(searchBar); + m_searchMenu->addAction(searchAction); + m_searchMenu->addSeparator(); + m_searchAction->setMenu(m_searchMenu.get()); + } +} + +AppMenuModel::~AppMenuModel() = default; + +bool AppMenuModel::menuAvailable() const +{ + return m_menuAvailable; +} + +void AppMenuModel::setMenuAvailable(bool set) +{ + if (m_menuAvailable != set) { + m_menuAvailable = set; + setVisible(true); + Q_EMIT menuAvailableChanged(); + } +} + +bool AppMenuModel::visible() const +{ + return m_visible; +} + +void AppMenuModel::setVisible(bool visible) +{ + if (m_visible != visible) { + m_visible = visible; + Q_EMIT visibleChanged(); + } +} + +QRect AppMenuModel::screenGeometry() const +{ + return m_tasksModel->screenGeometry(); +} + +void AppMenuModel::setScreenGeometry(QRect geometry) +{ + m_tasksModel->setScreenGeometry(geometry); +} + +int AppMenuModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + if (!m_menuAvailable || !m_menu) { + return 0; + } + + return m_menu->actions().count() + (KWindowSystem::isPlatformWayland() ? 1 : 0); +} + +void AppMenuModel::removeSearchActionsFromMenu() +{ + for (const auto &action : m_currentSearchActions) { + m_searchAction->menu()->removeAction(action); + } + m_currentSearchActions = QList(); +} + +void AppMenuModel::insertSearchActionsIntoMenu(const QString &filter) +{ + removeSearchActionsFromMenu(); + if (filter.isEmpty()) { + return; + } + const auto actions = flatActionList(); + for (const auto &action : actions) { + if (action->text().contains(filter, Qt::CaseInsensitive)) { + m_searchAction->menu()->addAction(action); + m_currentSearchActions << action; + } + } +} + +void AppMenuModel::update() +{ + beginResetModel(); + endResetModel(); + m_updatePending = false; +} + +void AppMenuModel::onActiveWindowChanged() +{ + const QModelIndex activeTaskIndex = m_tasksModel->activeTask(); + const QString objectPath = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuObjectPath).toString(); + const QString serviceName = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuServiceName).toString(); + + if (!objectPath.isEmpty() && !serviceName.isEmpty()) { + setMenuAvailable(true); + updateApplicationMenu(serviceName, objectPath); + setVisible(true); + Q_EMIT modelNeedsUpdate(); + } else { + setMenuAvailable(false); + setVisible(false); + } +} + +QHash AppMenuModel::roleNames() const +{ + QHash roleNames; + roleNames[MenuRole] = QByteArrayLiteral("activeMenu"); + roleNames[ActionRole] = QByteArrayLiteral("activeActions"); + return roleNames; +} + +QList AppMenuModel::flatActionList() +{ + QList ret; + if (!m_menuAvailable || !m_menu) { + return ret; + } + const auto actions = m_menu->findChildren(); + for (auto &action : actions) { + if (action->menu() == nullptr) { + ret << action; + } + } + return ret; +} + +QVariant AppMenuModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (row < 0 || !m_menuAvailable || !m_menu) { + return QVariant(); + } + + const auto actions = m_menu->actions(); + if (row == actions.count() && KWindowSystem::isPlatformWayland()) { + if (role == MenuRole) { + return m_searchAction->text(); + } else if (role == ActionRole) { + return QVariant::fromValue((void *)m_searchAction); + } + } + if (row >= actions.count()) { + return QVariant(); + } + + if (role == MenuRole) { // TODO this should be Qt::DisplayRole + return actions.at(row)->text(); + } else if (role == ActionRole) { + return QVariant::fromValue((void *)actions.at(row)); + } + + return QVariant(); +} + +void AppMenuModel::updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath) +{ + if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) { + if (m_importer) { + QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); + } + return; + } + + m_serviceName = serviceName; + m_serviceWatcher->setWatchedServices(QStringList({m_serviceName})); + + m_menuObjectPath = menuObjectPath; + + if (m_importer) { + m_importer->deleteLater(); + } + + m_importer = new KDBusMenuImporter(serviceName, menuObjectPath, this); + QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); + + connect(m_importer.data(), &DBusMenuImporter::menuUpdated, this, [=](QMenu *menu) { + m_menu = m_importer->menu(); + if (m_menu.isNull() || menu != m_menu) { + return; + } + + // cache first layer of sub menus, which we'll be popping up + const auto actions = m_menu->actions(); + for (QAction *a : actions) { + // signal dataChanged when the action changes + connect(a, &QAction::changed, this, [this, a] { + if (m_menuAvailable && m_menu) { + const int actionIdx = m_menu->actions().indexOf(a); + if (actionIdx > -1) { + const QModelIndex modelIdx = index(actionIdx, 0); + Q_EMIT dataChanged(modelIdx, modelIdx); + } + } + }); + + connect(a, &QAction::destroyed, this, &AppMenuModel::modelNeedsUpdate); + + if (a->menu()) { + m_importer->updateMenu(a->menu()); + } + } + + setMenuAvailable(true); + Q_EMIT modelNeedsUpdate(); + }); + + connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) { + // TODO submenus + if (!m_menuAvailable || !m_menu) { + return; + } + + const auto actions = m_menu->actions(); + auto it = std::find(actions.begin(), actions.end(), action); + if (it != actions.end()) { + Q_EMIT requestActivateIndex(it - actions.begin()); + } + }); +} diff --git a/plasma/workspace/applets/appmenu/plugin/appmenumodel.h b/plasma/workspace/applets/appmenu/plugin/appmenumodel.h new file mode 100644 index 0000000000..c5d530dcd0 --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/appmenumodel.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QMenu; +class QModelIndex; +class QDBusServiceWatcher; +class KDBusMenuImporter; + +class AppMenuModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool menuAvailable READ menuAvailable WRITE setMenuAvailable NOTIFY menuAvailableChanged) + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged) + + Q_PROPERTY(QRect screenGeometry READ screenGeometry WRITE setScreenGeometry NOTIFY screenGeometryChanged) + +public: + explicit AppMenuModel(QObject *parent = nullptr); + ~AppMenuModel() override; + + enum AppMenuRole { + MenuRole = Qt::UserRole + 1, // TODO this should be Qt::DisplayRole + ActionRole, + }; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + void updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath); + + bool menuAvailable() const; + void setMenuAvailable(bool set); + + bool visible() const; + + QRect screenGeometry() const; + void setScreenGeometry(QRect geometry); + QList flatActionList(); + +Q_SIGNALS: + void requestActivateIndex(int index); + void bringToFocus(int index); + +private Q_SLOTS: + void onActiveWindowChanged(); + void setVisible(bool visible); + void update(); + +Q_SIGNALS: + void menuAvailableChanged(); + void modelNeedsUpdate(); + void screenGeometryChanged(); + void visibleChanged(); + +private: + bool m_menuAvailable; + bool m_updatePending = false; + bool m_visible = true; + + TaskManager::TasksModel *m_tasksModel; + + //! current active window used + WId m_currentWindowId = 0; + //! window that its menu initialization may be delayed + WId m_delayedMenuWindowId = 0; + + QScopedPointer m_searchMenu; + QPointer m_menu; + QPointer m_searchAction; + QList m_currentSearchActions; + + void removeSearchActionsFromMenu(); + void insertSearchActionsIntoMenu(const QString &filter = QString()); + + QDBusServiceWatcher *m_serviceWatcher; + QString m_serviceName; + QString m_menuObjectPath; + + QPointer m_importer; +}; diff --git a/plasma/workspace/applets/appmenu/plugin/appmenuplugin.cpp b/plasma/workspace/applets/appmenu/plugin/appmenuplugin.cpp new file mode 100644 index 0000000000..4404e3ab94 --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/appmenuplugin.cpp @@ -0,0 +1,15 @@ +/* + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "appmenuplugin.h" +#include "appmenumodel.h" +#include + +void AppmenuPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.appmenu")); + qmlRegisterType(uri, 1, 0, "AppMenuModel"); +} diff --git a/plasma/workspace/applets/appmenu/plugin/appmenuplugin.h b/plasma/workspace/applets/appmenu/plugin/appmenuplugin.h new file mode 100644 index 0000000000..b7a4f5e763 --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/appmenuplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class AppmenuPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/applets/appmenu/plugin/qmldir b/plasma/workspace/applets/appmenu/plugin/qmldir new file mode 100644 index 0000000000..b5094ba13f --- /dev/null +++ b/plasma/workspace/applets/appmenu/plugin/qmldir @@ -0,0 +1,3 @@ +module org.kde.plasma.private.appmenu + +plugin appmenuplugin diff --git a/plasma/workspace/applets/batterymonitor/CMakeLists.txt b/plasma/workspace/applets/batterymonitor/CMakeLists.txt new file mode 100644 index 0000000000..fb7aea96f7 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/CMakeLists.txt @@ -0,0 +1 @@ +plasma_install_package(package org.kde.plasma.battery) diff --git a/plasma/workspace/applets/batterymonitor/Messages.sh b/plasma/workspace/applets/batterymonitor/Messages.sh new file mode 100644 index 0000000000..7a9d1582e0 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.battery.pot diff --git a/plasma/workspace/applets/batterymonitor/README.txt b/plasma/workspace/applets/batterymonitor/README.txt new file mode 100644 index 0000000000..5b352e833e --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/README.txt @@ -0,0 +1,32 @@ +Obviously, this is a battery monitor applet for Plasma. + +The catch of those four .svg files is the following: + +battery-inkscape.svg is an so-called "Inkscape SVG", it's the one + you want to edit end export to +battery.svg as plain .svg file. + +Likewise, for the Oxygen theme. + +This file contains various layers: + +* Battery The "background", an empty battery +* AcAdapter A flash, saying "Ac is plugged in" +* Fill10 10% filled, red +* Fill20 20% filled, orange +* Fill20 30% filled, green (and so on...) +* Fill30 +* Fill40 +* Fill50 +* Fill60 +* Fill70 +* Fill80 +* Fill90 +* Fill010 +* Shadow An Oxygen-style shadow under the battery + +The Battery layer is always rendered as the first step. On top +of that, we render only one of the FillN layers. If the AC +Adapter is plugged in, we paint that layer on top of the cake. +It makes no difference when we paint the Shadow, there's no +overlap between shadow and other layers. diff --git a/plasma/workspace/applets/batterymonitor/package/contents/config/main.xml b/plasma/workspace/applets/batterymonitor/package/contents/config/main.xml new file mode 100644 index 0000000000..31dff08787 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/config/main.xml @@ -0,0 +1,16 @@ + + + + + + + + false + + + + + diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/BadgeOverlay.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/BadgeOverlay.qml new file mode 100644 index 0000000000..1b7aa823cf --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/BadgeOverlay.qml @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtGraphicalEffects 1.15 + +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore + +Rectangle { + property alias text: label.text + property Item icon + + color: PlasmaCore.ColorScope.backgroundColor + width: Math.max(PlasmaCore.Units.gridUnit, label.width + PlasmaCore.Units.devicePixelRatio * 2) + height: label.height + radius: PlasmaCore.Units.devicePixelRatio * 3 + opacity: 0.9 + + PlasmaComponents3.Label { + id: label + anchors.centerIn: parent + font.pixelSize: Math.max(icon.height / 4, PlasmaCore.Theme.smallestFont.pixelSize * 0.8) + } + + layer.enabled: true + layer.effect: DropShadow { + horizontalOffset: 0 + verticalOffset: 0 + radius: PlasmaCore.Units.devicePixelRatio * 2 + samples: radius * 2 + color: Qt.rgba(0, 0, 0, 0.5) + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/BatteryItem.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/BatteryItem.qml new file mode 100644 index 0000000000..b0f0fd4627 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/BatteryItem.qml @@ -0,0 +1,199 @@ +/* + SPDX-FileCopyrightText: 2012-2013 Daniel Nicoletti + SPDX-FileCopyrightText: 2013-2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kcoreaddons 1.0 as KCoreAddons +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.workspace.components 2.0 + +import "logic.js" as Logic + +RowLayout { + id: root + + // We'd love to use `required` properties, especially since the model provides role names for them; + // but unfortunately some of those roles have whitespaces in their name, which QML doesn't have any + // workaround for (raw identifiers like r#try in Rust would've helped here). + // + // type: { + // Capacity: int, + // Energy: real, + // "Is Power Supply": bool, + // Percent: int, + // "Plugged In": bool, + // "Pretty Name": string, + // Product: string, + // State: "Discharging"|"Charging"|"FullyCharged"|etc., + // Type: string, + // Vendor: string, + // }? + property var battery + + // NOTE: According to the UPower spec this property is only valid for primary batteries, however + // UPower seems to set the Present property false when a device is added but not probed yet + readonly property bool isPresent: root.battery["Plugged in"] + + readonly property bool isBroken: root.battery.Capacity > 0 && root.battery.Capacity < 50 + + property int remainingTime: 0 + + // Existing instance of a slider to use as a reference to calculate extra + // margins for a progress bar, so that the row of labels on top of it + // could visually look as if it were on the same distance from the bar as + // they are from the slider. + property PlasmaComponents3.Slider matchHeightOfSlider: PlasmaComponents3.Slider {} + readonly property real extraMargin: Math.max(0, Math.floor((matchHeightOfSlider.height - chargeBar.height) / 2)) + + spacing: PlasmaCore.Units.gridUnit + + BatteryIcon { + id: batteryIcon + + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + + batteryType: root.battery.Type + percent: root.battery.Percent + hasBattery: root.isPresent + pluggedIn: root.battery.State === "Charging" && root.battery["Is Power Supply"] + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: root.isPresent ? Qt.AlignTop : Qt.AlignVCenter + spacing: 0 + + RowLayout { + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents3.Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: root.battery["Pretty Name"] + } + + PlasmaComponents3.Label { + text: Logic.stringForBatteryState(root.battery) + visible: root.battery["Is Power Supply"] + enabled: false + } + + PlasmaComponents3.Label { + horizontalAlignment: Text.AlignRight + visible: root.isPresent + text: i18nc("Placeholder is battery percentage", "%1%", root.battery.Percent) + } + } + + PlasmaComponents3.ProgressBar { + id: chargeBar + + Layout.fillWidth: true + Layout.topMargin: root.extraMargin + Layout.bottomMargin: root.extraMargin + + from: 0 + to: 100 + visible: root.isPresent + value: Number(root.battery.Percent) + } + + // This gridLayout basically emulates an at-most-two-rows table with a + // single wide fillWidth/columnSpan header. Not really worth it trying + // to refactor it into some more clever fancy model-delegate stuff. + GridLayout { + id: details + + Layout.fillWidth: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + + columns: 2 + columnSpacing: PlasmaCore.Units.smallSpacing + rowSpacing: 0 + + component LeftLabel : PlasmaComponents3.Label { + // fillWidth is true, so using internal alignment + horizontalAlignment: Text.AlignLeft + Layout.fillWidth: true + font: PlasmaCore.Theme.smallestFont + wrapMode: Text.WordWrap + enabled: false + } + component RightLabel : PlasmaComponents3.Label { + // fillWidth is false, so using external (grid-cell-internal) alignment + Layout.alignment: Qt.AlignRight + Layout.fillWidth: false + font: PlasmaCore.Theme.smallestFont + enabled: false + } + + PlasmaComponents3.Label { + Layout.fillWidth: true + Layout.columnSpan: 2 + + text: root.isBroken && typeof root.battery.Capacity !== "undefined" + ? i18n("This battery's health is at only %1% and it should be replaced. Contact the manufacturer.", root.battery.Capacity) + : "" + font: PlasmaCore.Theme.smallestFont + color: PlasmaCore.Theme.neutralTextColor + visible: root.isBroken + wrapMode: Text.WordWrap + } + + readonly property bool remainingTimeRowVisible: root.battery !== null + && root.remainingTime > 0 + && root.battery["Is Power Supply"] + && ["Discharging", "Charging"].includes(root.battery.State) + + LeftLabel { + text: root.battery.State === "Charging" + ? i18n("Time To Full:") + : i18n("Remaining Time:") + visible: details.remainingTimeRowVisible + } + + RightLabel { + text: KCoreAddons.Format.formatDuration(root.remainingTime, KCoreAddons.FormatTypes.HideSeconds) + visible: details.remainingTimeRowVisible + } + + readonly property bool healthRowVisible: root.battery !== null + && root.battery["Is Power Supply"] + && root.battery.Capacity !== "" + && typeof root.battery.Capacity === "number" + && !root.isBroken + + LeftLabel { + text: i18n("Battery Health:") + visible: details.healthRowVisible + } + + RightLabel { + text: details.healthRowVisible + ? i18nc("Placeholder is battery health percentage", "%1%", root.battery.Capacity) + : "" + visible: details.healthRowVisible + } + } + + InhibitionHint { + Layout.fillWidth: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + + readonly property var chargeStopThreshold: pmSource.data["Battery"] ? pmSource.data["Battery"]["Charge Stop Threshold"] : undefined + readonly property bool pluggedIn: pmSource.data["AC Adapter"] !== undefined && pmSource.data["AC Adapter"]["Plugged in"] + visible: pluggedIn && typeof chargeStopThreshold === "number" && chargeStopThreshold > 0 && chargeStopThreshold < 100 + iconSource: "kt-speed-limits" // FIXME good icon + text: i18n("Battery is configured to charge up to approximately %1%.", chargeStopThreshold || 0) + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/BrightnessItem.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/BrightnessItem.qml new file mode 100644 index 0000000000..4a916c61b7 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/BrightnessItem.qml @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2012-2013 Daniel Nicoletti + SPDX-FileCopyrightText: 2013, 2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore + +RowLayout { + id: root + + property alias icon: image.source + property alias label: title.text + property alias slider: control + property alias value: control.value + property alias maximumValue: control.to + property alias stepSize: control.stepSize + property alias showPercentage: percent.visible + + readonly property real percentage: Math.round(100 * value / maximumValue) + + signal moved() + + spacing: PlasmaCore.Units.gridUnit + + PlasmaCore.IconItem { + id: image + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 0 + + RowLayout { + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents3.Label { + id: title + Layout.fillWidth: true + } + + PlasmaComponents3.Label { + id: percent + Layout.alignment: Qt.AlignRight + text: i18nc("Placeholder is brightness percentage", "%1%", root.percentage) + } + } + + PlasmaComponents3.Slider { + id: control + Layout.fillWidth: true + // Don't allow the slider to turn off the screen + // Please see https://git.reviewboard.kde.org/r/122505/ for more information + from: to > 100 ? 1 : 0 + stepSize: 1 + onMoved: root.moved() + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/CompactRepresentation.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/CompactRepresentation.qml new file mode 100644 index 0000000000..ce6753fb56 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/CompactRepresentation.qml @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2013 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.workspace.components 2.0 as WorkspaceComponents + +MouseArea { + id: root + + property real itemSize: Math.min(root.height, root.width/view.count) + readonly property bool isConstrained: plasmoid.formFactor === PlasmaCore.Types.Vertical || plasmoid.formFactor === PlasmaCore.Types.Horizontal + property real brightnessError: 0 + property QtObject batteries + property bool hasBatteries: false + + hoverEnabled: true + + onClicked: plasmoid.expanded = !plasmoid.expanded + + // "No Batteries" case + PlasmaCore.IconItem { + anchors.fill: parent + visible: !root.hasBatteries + source: plasmoid.icon + active: parent.containsMouse + } + + // We have any batteries; show their status + //Should we consider turning this into a Flow item? + Row { + visible: root.hasBatteries + anchors.centerIn: parent + Repeater { + id: view + + model: root.isConstrained ? 1 : root.batteries + + Item { + id: batteryContainer + + property int percent: root.isConstrained ? pmSource.data["Battery"]["Percent"] : model["Percent"] + property bool pluggedIn: pmSource.data["AC Adapter"] && pmSource.data["AC Adapter"]["Plugged in"] && (root.isConstrained || model["Is Power Supply"]) + + height: root.itemSize + width: root.width/view.count + + property real iconSize: Math.min(width, height) + + WorkspaceComponents.BatteryIcon { + id: batteryIcon + + anchors.centerIn: parent + height: batteryContainer.iconSize + width: height + + hasBattery: root.hasBatteries + percent: batteryContainer.percent + pluggedIn: batteryContainer.pluggedIn + } + + BadgeOverlay { + anchors.bottom: parent.bottom + anchors.right: parent.right + + visible: plasmoid.configuration.showPercentage + + text: i18nc("battery percentage below battery icon", "%1%", percent) + icon: batteryIcon + } + } + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/InhibitionHint.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/InhibitionHint.qml new file mode 100644 index 0000000000..6b6e573a29 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/InhibitionHint.qml @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore + +RowLayout { + property alias iconSource: iconItem.source + property alias text: label.text + + spacing: PlasmaCore.Units.smallSpacing + + PlasmaCore.IconItem { + id: iconItem + Layout.preferredWidth: PlasmaCore.Units.iconSizes.small + Layout.preferredHeight: PlasmaCore.Units.iconSizes.small + visible: valid + } + + PlasmaComponents3.Label { + id: label + Layout.fillWidth: true + font: PlasmaCore.Theme.smallestFont + wrapMode: Text.WordWrap + elide: Text.ElideRight + maximumLineCount: 4 + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/PopupDialog.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/PopupDialog.qml new file mode 100644 index 0000000000..e8820fd921 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/PopupDialog.qml @@ -0,0 +1,170 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2013-2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kquickcontrolsaddons 2.1 +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +PlasmaComponents3.Page { + id: dialog + + property alias model: batteryList.model + property bool pluggedIn + + property int remainingTime + + property bool isBrightnessAvailable + property bool isKeyboardBrightnessAvailable + + property string activeProfile + property var profiles + + // List of active power management inhibitions (applications that are + // blocking sleep and screen locking). + // + // type: [{ + // Icon: string, + // Name: string, + // Reason: string, + // }] + property var inhibitions: [] + property bool inhibitsLidAction + + property string inhibitionReason + property string degradationReason + // type: [{ Name: string, Icon: string, Profile: string, Reason: string }] + required property var profileHolds + + signal powerManagementChanged(bool disabled) + signal activateProfileRequested(string profile) + + header: PlasmaExtras.PlasmoidHeading { + leftPadding: PlasmaCore.Units.smallSpacing + contentItem: PowerManagementItem { + id: pmSwitch + + inhibitions: dialog.inhibitions + inhibitsLidAction: dialog.inhibitsLidAction + pluggedIn: dialog.pluggedIn + onDisabledChanged: powerManagementChanged(disabled) + + KeyNavigation.tab: batteryList + KeyNavigation.backtab: keyboardBrightnessSlider + } + } + + FocusScope { + anchors.fill: parent + + focus: true + + ColumnLayout { + anchors { + fill: parent + topMargin: PlasmaCore.Units.smallSpacing * 2 + leftMargin: PlasmaCore.Units.smallSpacing + rightMargin: PlasmaCore.Units.smallSpacing + } + spacing: PlasmaCore.Units.smallSpacing * 2 + + BrightnessItem { + id: brightnessSlider + + Layout.fillWidth: true + + icon: "video-display-brightness" + label: i18n("Display Brightness") + visible: isBrightnessAvailable + value: batterymonitor.screenBrightness + maximumValue: batterymonitor.maximumScreenBrightness + KeyNavigation.tab: keyboardBrightnessSlider + KeyNavigation.backtab: batteryList + stepSize: batterymonitor.maximumScreenBrightness/100 + + onMoved: batterymonitor.screenBrightness = value + + // Manually dragging the slider around breaks the binding + Connections { + target: batterymonitor + function onScreenBrightnessChanged() { + brightnessSlider.value = batterymonitor.screenBrightness; + } + } + } + + BrightnessItem { + id: keyboardBrightnessSlider + + Layout.fillWidth: true + + icon: "input-keyboard-brightness" + label: i18n("Keyboard Brightness") + showPercentage: false + value: batterymonitor.keyboardBrightness + maximumValue: batterymonitor.maximumKeyboardBrightness + visible: isKeyboardBrightnessAvailable + KeyNavigation.tab: pmSwitch + KeyNavigation.backtab: brightnessSlider + + onMoved: batterymonitor.keyboardBrightness = value + + // Manually dragging the slider around breaks the binding + Connections { + target: batterymonitor + function onKeyboardBrightnessChanged() { + keyboardBrightnessSlider.value = batterymonitor.keyboardBrightness; + } + } + } + + PowerProfileItem { + Layout.fillWidth: true + + activeProfile: dialog.activeProfile + inhibitionReason: dialog.inhibitionReason + visible: dialog.profiles.length > 0 + degradationReason: dialog.degradationReason + profileHolds: dialog.profileHolds + onActivateProfileRequested: dialog.activateProfileRequested(profile) + } + + PlasmaComponents3.ScrollView { + id: batteryScrollView + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + // additional margin, because the bottom of PowerProfileItem + // and the top of BatteryItem are more dense. + Layout.topMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: batteryList + + boundsBehavior: Flickable.StopAtBounds + spacing: PlasmaCore.Units.smallSpacing * 2 + + KeyNavigation.tab: brightnessSlider + KeyNavigation.backtab: pmSwitch + + delegate: BatteryItem { + width: ListView.view.width + battery: model + remainingTime: dialog.remainingTime + matchHeightOfSlider: brightnessSlider.slider + } + } + } + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerManagementItem.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerManagementItem.qml new file mode 100644 index 0000000000..18ae7cb602 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerManagementItem.qml @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2012-2013 Daniel Nicoletti + SPDX-FileCopyrightText: 2013, 2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kquickcontrolsaddons 2.1 +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore + +ColumnLayout { + id: root + + property alias disabled: pmCheckBox.checked + property bool pluggedIn + + // List of active power management inhibitions (applications that are + // blocking sleep and screen locking). + // + // type: [{ + // Icon: string, + // Name: string, + // Reason: string, + // }] + property var inhibitions: [] + property bool inhibitsLidAction + + // UI to manually inhibit sleep and screen locking + PlasmaComponents3.CheckBox { + id: pmCheckBox + Layout.fillWidth: true + text: i18nc("Minimize the length of this string as much as possible", "Manually block sleep and screen locking") + checked: false + } + + // Separator line + PlasmaCore.SvgItem { + Layout.fillWidth: true + + visible: inhibitionReasonsLayout.visible + + elementId: "horizontal-line" + svg: PlasmaCore.Svg { + imagePath: "widgets/line" + } + } + + // list of automatic inhibitions + ColumnLayout { + id: inhibitionReasonsLayout + + Layout.fillWidth: true + visible: root.inhibitsLidAction || (root.inhibitions.length > 0 && !root.disabled) + + InhibitionHint { + Layout.fillWidth: true + visible: root.inhibitsLidAction + iconSource: "computer-laptop" + text: i18nc("Minimize the length of this string as much as possible", "Your laptop is configured not to sleep when closing the lid while an external monitor is connected.") + } + + PlasmaComponents3.Label { + id: inhibitionExplanation + Layout.fillWidth: true + // Don't need to show the inhibitions when power management + // isn't enabled anyway + visible: root.inhibitions.length > 1 && !root.disabled + font: PlasmaCore.Theme.smallestFont + wrapMode: Text.WordWrap + elide: Text.ElideRight + maximumLineCount: 3 + text: i18np("%1 application is currently blocking sleep and screen locking:", + "%1 applications are currently blocking sleep and screen locking:", + root.inhibitions.length) + } + + Repeater { + // Don't need to show the inhibitions when power management + // is manually disabled anyway + visible: !root.disabled + model: !root.disabled ? root.inhibitions : null + + InhibitionHint { + property string icon: modelData.Icon + property string name: modelData.Name + property string reason: modelData.Reason + + Layout.fillWidth: true + iconSource: icon + text: (root.inhibitions.length === 1) + ? (reason + ? i18n("%1 is currently blocking sleep and screen locking (%2)", name, reason) + : i18n("%1 is currently blocking sleep and screen locking (unknown reason)", name)) + : (reason + ? i18nc("Application name: reason for preventing sleep and screen locking", "%1: %2", name, reason) + : i18nc("Application name: reason for preventing sleep and screen locking", "%1: unknown reason", name)) + } + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerProfileItem.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerProfileItem.qml new file mode 100644 index 0000000000..65bedb5c24 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/PowerProfileItem.qml @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2021 Kai Uwe Broulik + * SPDX-FileCopyrightText: 2021 David Redondo + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.core 2.1 as PlasmaCore + +RowLayout { + id: root + + property string activeProfile + + property string inhibitionReason + readonly property bool inhibited: inhibitionReason !== "" + + property string degradationReason + + // type: [{ Name: string, Icon: string, Profile: string, Reason: string }] + required property var profileHolds + + // The canBeInhibited property mean that this profile's availability + // depends on root.inhibited value (and thus on the + // inhibitionReason string). + readonly property var profileData: [ + { + label: i18n("Power Save"), + profile: "power-saver", + canBeInhibited: false, + }, { + label: i18n("Balanced"), + profile: "balanced", + canBeInhibited: false, + }, { + label: i18n("Performance"), + profile: "performance", + canBeInhibited: true, + } + ] + + readonly property int activeProfileIndex: profileData.findIndex(data => data.profile === activeProfile) + // type: typeof(profileData[])? + readonly property var activeProfileData: activeProfileIndex !== -1 ? profileData[activeProfileIndex] : undefined + // type: typeof(profileHolds) + readonly property var activeHolds: profileHolds.filter(hold => hold.Profile === activeProfile) + + signal activateProfileRequested(string profile) + + spacing: PlasmaCore.Units.gridUnit + + PlasmaCore.IconItem { + source: "speedometer" + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 0 + + PlasmaComponents3.Label { + text: i18n("Power Profile") + } + + PlasmaComponents3.Slider { + Layout.fillWidth: true + + from: 0 + to: 2 + stepSize: 1 + value: root.activeProfileIndex + snapMode: PlasmaComponents3.Slider.SnapAlways + onMoved: { + const { canBeInhibited, profile } = root.profileData[value]; + if (!(canBeInhibited && root.inhibited)) { + activateProfileRequested(profile); + } else { + value = Qt.binding(() => root.activeProfileIndex); + } + } + + // fake having a disabled second half + Rectangle { + z: -1 + visible: root.inhibited + color: PlasmaCore.Theme.backgroundColor + anchors { + top: parent.background.top + left: parent.horizontalCenter + leftMargin: 1 + right: parent.right + bottom: parent.background.bottom + } + opacity: 0.4 + } + } + + RowLayout { + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + Repeater { + id: repeater + model: root.profileData + PlasmaComponents3.Label { + // Same preferredWidth combined with fillWidth results in equal sizes for all + Layout.fillWidth: true + Layout.preferredWidth: 1 + horizontalAlignment: switch (index) { + case 0: + return Text.AlignLeft; // first + case repeater.count - 1: + return Text.AlignRight; // last + default: + return Text.AlignHCenter; // middle + } + elide: Text.ElideMiddle + + // Disable label for inhibited items to reinforce unavailability + enabled: !(root.profileData[index].canBeInhibited && root.inhibited) + + text: modelData.label + } + } + } + + // NOTE Only one of these will be visible at a time since the daemon will only set one depending + // on its version + InhibitionHint { + Layout.fillWidth: true + + visible: root.inhibited + iconSource: "dialog-information" + text: switch(root.inhibitionReason) { + case "lap-detected": + return i18n("Performance mode has been disabled to reduce heat generation because the computer has detected that it may be sitting on your lap.") + case "high-operating-temperature": + return i18n("Performance mode is unavailable because the computer is running too hot.") + default: + return i18n("Performance mode is unavailable.") + } + } + + InhibitionHint { + Layout.fillWidth: true + + visible: root.activeProfile === "performance" && root.degradationReason !== "" + iconSource: "dialog-information" + text: switch(root.degradationReason) { + case "lap-detected": + return i18n("Performance may be lowered to reduce heat generation because the computer has detected that it may be sitting on your lap.") + case "high-operating-temperature": + return i18n("Performance may be reduced because the computer is running too hot.") + default: + return i18n("Performance may be reduced.") + } + } + + InhibitionHint { + Layout.fillWidth: true + + visible: root.activeHolds.length > 0 && root.activeProfileData !== undefined + text: root.activeProfileData !== undefined + ? i18np("One application has requested activating %2:", + "%1 applications have requested activating %2:", + root.activeHolds.length, + i18n(root.activeProfileData.label)) + : "" + } + + Repeater { + model: root.activeHolds + InhibitionHint { + Layout.fillWidth: true + + x: PlasmaCore.Units.smallSpacing + iconSource: modelData.Icon + text: i18nc("%1 is the name of the application, %2 is the reason provided by it for activating performance mode", + "%1: %2", modelData.Name, modelData.Reason) + } + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/logic.js b/plasma/workspace/applets/batterymonitor/package/contents/ui/logic.js new file mode 100644 index 0000000000..b2bab1694d --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/logic.js @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + SPDX-FileCopyrightText: 2012 Viranch Mehta + SPDX-FileCopyrightText: 2014-2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +function stringForBatteryState(batteryData) { + if (batteryData["Plugged in"]) { + switch(batteryData["State"]) { + case "Discharging": return i18n("Discharging"); + case "FullyCharged": return i18n("Fully Charged"); + case "Charging": return i18n("Charging"); + // when in doubt we're not charging + default: return i18n("Not Charging"); + } + } else { + return i18nc("Battery is currently not present in the bay", "Not present"); + } +} + +function updateBrightness(rootItem, source) { + if (rootItem.updateScreenBrightnessJob || rootItem.updateKeyboardBrightnessJob) + return; + + if (!source.data["PowerDevil"]) { + return; + } + + // we don't want passive brightness change send setBrightness call + rootItem.disableBrightnessUpdate = true; + + if (typeof source.data["PowerDevil"]["Screen Brightness"] === 'number') { + rootItem.screenBrightness = source.data["PowerDevil"]["Screen Brightness"]; + } + if (typeof source.data["PowerDevil"]["Keyboard Brightness"] === 'number') { + rootItem.keyboardBrightness = source.data["PowerDevil"]["Keyboard Brightness"]; + } + rootItem.disableBrightnessUpdate = false; +} + +function updateInhibitions(rootItem, source) { + const inhibitions = []; + + if (source.data["Inhibitions"]) { + for (let key in pmSource.data["Inhibitions"]) { + if (key === "plasmashell" || key === "plasmoidviewer") { // ignore our own inhibition + continue; + } + + inhibitions.push(pmSource.data["Inhibitions"][key]); + } + } + + rootItem.inhibitions = inhibitions; +} diff --git a/plasma/workspace/applets/batterymonitor/package/contents/ui/main.qml b/plasma/workspace/applets/batterymonitor/package/contents/ui/main.qml new file mode 100644 index 0000000000..ad223dcd96 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/contents/ui/main.qml @@ -0,0 +1,348 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2013-2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.kcoreaddons 1.0 as KCoreAddons +import org.kde.kquickcontrolsaddons 2.1 // For KCMShell +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + +import "logic.js" as Logic + +Item { + id: batterymonitor + + property QtObject pmSource: PlasmaCore.DataSource { + id: pmSource + engine: "powermanagement" + connectedSources: sources + onSourceAdded: { + disconnectSource(source); + connectSource(source); + } + onSourceRemoved: { + disconnectSource(source); + } + onDataChanged: { + Logic.updateBrightness(batterymonitor, pmSource); + Logic.updateInhibitions(batterymonitor, pmSource); + } + } + property QtObject batteries: PlasmaCore.SortFilterModel { + id: batteries + filterRole: "Is Power Supply" + sortOrder: Qt.DescendingOrder + sourceModel: PlasmaCore.SortFilterModel { + sortRole: "Pretty Name" + sortOrder: Qt.AscendingOrder + sortCaseSensitivity: Qt.CaseInsensitive + sourceModel: PlasmaCore.DataModel { + dataSource: pmSource + sourceFilter: "Battery[0-9]+" + } + } + } + property QtObject updateScreenBrightnessJob + property QtObject updateKeyboardBrightnessJob + + readonly property bool isBrightnessAvailable: pmSource.data["PowerDevil"] && pmSource.data["PowerDevil"]["Screen Brightness Available"] ? true : false + readonly property bool isKeyboardBrightnessAvailable: pmSource.data["PowerDevil"] && pmSource.data["PowerDevil"]["Keyboard Brightness Available"] ? true : false + readonly property bool hasBatteries: batteries.count > 0 && pmSource.data["Battery"]["Has Cumulative"] + readonly property bool hasBrightness: isBrightnessAvailable || isKeyboardBrightnessAvailable + readonly property bool kcmAuthorized: KCMShell.authorize("powerdevilprofilesconfig.desktop").length > 0 + readonly property bool kcmEnergyInformationAuthorized: KCMShell.authorize("kcm_energyinfo.desktop").length > 0 + readonly property int maximumScreenBrightness: pmSource.data["PowerDevil"] ? pmSource.data["PowerDevil"]["Maximum Screen Brightness"] || 0 : 0 + readonly property int maximumKeyboardBrightness: pmSource.data["PowerDevil"] ? pmSource.data["PowerDevil"]["Maximum Keyboard Brightness"] || 0 : 0 + readonly property int remainingTime: Number(pmSource.data["Battery"]["Remaining msec"]) + + property bool powermanagementDisabled: false + property bool disableBrightnessUpdate: true + property int screenBrightness + property int keyboardBrightness + + // List of active power management inhibitions (applications that are + // blocking sleep and screen locking). + // + // type: [{ + // Icon: string, + // Name: string, + // Reason: string, + // }] + property var inhibitions: [] + + function action_configure() { + KCMShell.openSystemSettings("kcm_powerdevilprofilesconfig"); + } + + function action_energyinformationkcm() { + KCMShell.openInfoCenter("kcm_energyinfo"); + } + + function action_showPercentage() { + if (!plasmoid.configuration.showPercentage) { + plasmoid.configuration.showPercentage = true; + } else { + plasmoid.configuration.showPercentage = false; + } + } + + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 10 + Plasmoid.switchHeight: PlasmaCore.Units.gridUnit * 10 + Plasmoid.title: (hasBatteries && hasBrightness ? i18n("Battery and Brightness") : + hasBrightness ? i18n("Brightness") : + hasBatteries ? i18n("Battery") : i18n("Power Management")) + + LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + Plasmoid.status: { + if (powermanagementDisabled) { + return PlasmaCore.Types.ActiveStatus; + } + + if (pmSource.data.Battery["Has Cumulative"] + && !((pmSource.data["AC Adapter"]["Plugged in"] && pmSource.data["Battery"]["State"] === "FullyCharged") || + // When we are using a charge threshold, the kernel + // may stop charging within a percentage point of the actual threshold + // and this is considered correct behavior, so we have to handle + // that. See https://bugzilla.kernel.org/show_bug.cgi?id=215531. + (pmSource.data["AC Adapter"]["Plugged in"] + && typeof pmSource.data["Battery"]["Charge Stop Threshold"] === "number" + && (pmSource.data.Battery.Percent >= pmSource.data["Battery"]["Charge Stop Threshold"] - 1 + && pmSource.data.Battery.Percent <= pmSource.data["Battery"]["Charge Stop Threshold"] + 1) + // Also, Upower may give us a status of "Not charging" rather than + // "Fully charged", so we need to account for that as well. See + // https://gitlab.freedesktop.org/upower/upower/-/issues/142. + && (pmSource.data["Battery"]["State"] === "NoCharge" || pmSource.data["Battery"]["State"] === "FullyCharged")) + )){ + return PlasmaCore.Types.ActiveStatus; + } + + return PlasmaCore.Types.PassiveStatus; + } + + Plasmoid.toolTipMainText: { + if (!hasBatteries) { + return plasmoid.title + } else if (pmSource.data["Battery"]["State"] === "FullyCharged") { + return i18n("Fully Charged"); + } + + const percent = pmSource.data.Battery.Percent; + if (pmSource.data["AC Adapter"] && pmSource.data["AC Adapter"]["Plugged in"]) { + const state = pmSource.data.Battery.State; + if (state === "NoCharge") { + return i18n("Battery at %1%, not Charging", percent); + } else if (state === "Discharging") { + return i18n("Battery at %1%, plugged in but still discharging", percent); + } else if (state === "Charging") { + return i18n("Battery at %1%, Charging", percent); + } + } + return i18n("Battery at %1%", percent); + } + + Plasmoid.toolTipSubText: { + const parts = []; + + // Add special text for the "plugged in but still discharging" case + if (pmSource.data["AC Adapter"] && pmSource.data["AC Adapter"]["Plugged in"] && pmSource.data.Battery.State === "Discharging") { + parts.push(i18n("The power supply is not powerful enough to charge the battery")); + } + + if (batteries.count === 0) { + parts.push("No Batteries Available"); + } else if (remainingTime > 0) { + const remainingTimeString = KCoreAddons.Format.formatDuration(remainingTime, KCoreAddons.FormatTypes.HideSeconds); + if (pmSource.data["Battery"]["State"] === "FullyCharged") { + // Don't add anything + } else if (pmSource.data["AC Adapter"] && pmSource.data["AC Adapter"]["Plugged in"]) { + parts.push(i18nc("time until fully charged - HH:MM","%1 until fully charged", remainingTimeString)); + } else { + parts.push(i18nc("remaining time left of battery usage - HH:MM","%1 remaining", remainingTimeString)); + } + } else if (pmSource.data.Battery.State === "NoCharge") { + parts.push(i18n("Not charging")); + } // otherwise, don't add anything + + if (powermanagementDisabled) { + parts.push(i18n("Automatic sleep and screen locking are disabled")); + } + return parts.join("\n"); + } + + Plasmoid.icon: !hasBatteries ? "video-display-brightness" : "battery" + + onScreenBrightnessChanged: { + if (disableBrightnessUpdate) { + return; + } + const service = pmSource.serviceForSource("PowerDevil"); + const operation = service.operationDescription("setBrightness"); + operation.brightness = screenBrightness; + // show OSD only when the plasmoid isn't expanded since the moving slider is feedback enough + operation.silent = plasmoid.expanded; + updateScreenBrightnessJob = service.startOperationCall(operation); + updateScreenBrightnessJob.finished.connect(job => { + Logic.updateBrightness(batterymonitor, pmSource); + }); + } + + onKeyboardBrightnessChanged: { + if (disableBrightnessUpdate) { + return; + } + var service = pmSource.serviceForSource("PowerDevil"); + var operation = service.operationDescription("setKeyboardBrightness"); + operation.brightness = keyboardBrightness; + // show OSD only when the plasmoid isn't expanded since the moving slider is feedback enough + operation.silent = plasmoid.expanded; + updateKeyboardBrightnessJob = service.startOperationCall(operation); + updateKeyboardBrightnessJob.finished.connect(job => { + Logic.updateBrightness(batterymonitor, pmSource); + }); + } + + Plasmoid.compactRepresentation: CompactRepresentation { + hasBatteries: batterymonitor.hasBatteries + batteries: batterymonitor.batteries + + onWheel: { + const delta = wheel.angleDelta.y || wheel.angleDelta.x + + const maximumBrightness = batterymonitor.maximumScreenBrightness + // Don't allow the UI to turn off the screen + // Please see https://git.reviewboard.kde.org/r/122505/ for more information + const minimumBrightness = (maximumBrightness > 100 ? 1 : 0) + const stepSize = Math.max(1, maximumBrightness / 20) + + let newBrightness; + if (Math.abs(delta) < 120) { + // Touchpad scrolling + brightnessError += delta * stepSize / 120; + const change = Math.round(brightnessError); + brightnessError -= change; + newBrightness = batterymonitor.screenBrightness + change; + } else { + // Discrete/wheel scrolling + newBrightness = Math.round(batterymonitor.screenBrightness/stepSize + delta/120) * stepSize; + } + batterymonitor.screenBrightness = Math.max(minimumBrightness, Math.min(maximumBrightness, newBrightness)); + } + } + + Plasmoid.fullRepresentation: PopupDialog { + id: dialogItem + Layout.minimumWidth: PlasmaCore.Units.iconSizes.medium * 9 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 15 + // TODO Probably needs a sensible preferredHeight too + + model: plasmoid.expanded ? batteries : null + focus: true + + isBrightnessAvailable: batterymonitor.isBrightnessAvailable + isKeyboardBrightnessAvailable: batterymonitor.isKeyboardBrightnessAvailable + + pluggedIn: pmSource.data["AC Adapter"] !== undefined && pmSource.data["AC Adapter"]["Plugged in"] + remainingTime: batterymonitor.remainingTime + + readonly property string actuallyActiveProfile: pmSource.data["Power Profiles"] ? (pmSource.data["Power Profiles"]["Current Profile"] || "") : "" + activeProfile: actuallyActiveProfile + inhibitions: batterymonitor.inhibitions + inhibitsLidAction: pmSource.data["PowerDevil"] && pmSource.data["PowerDevil"]["Is Lid Present"] && !pmSource.data["PowerDevil"]["Triggers Lid Action"] ? true : false + profiles: pmSource.data["Power Profiles"] ? (pmSource.data["Power Profiles"]["Profiles"] || []) : [] + inhibitionReason: pmSource.data["Power Profiles"] ? (pmSource.data["Power Profiles"]["Performance Inhibited Reason"] || "") : "" + degradationReason: pmSource.data["Power Profiles"] ? (pmSource.data["Power Profiles"]["Performance Degraded Reason"] || "") : "" + profileHolds: pmSource.data["Power Profiles"] ? (pmSource.data["Power Profiles"]["Profile Holds"] || []) : [] + + property int cookie1: -1 + property int cookie2: -1 + onPowerManagementChanged: disabled => { + const service = pmSource.serviceForSource("PowerDevil"); + if (disabled) { + const reason = i18n("The battery applet has enabled system-wide inhibition"); + const op1 = service.operationDescription("beginSuppressingSleep"); + op1.reason = reason; + const op2 = service.operationDescription("beginSuppressingScreenPowerManagement"); + op2.reason = reason; + + const job1 = service.startOperationCall(op1); + job1.finished.connect(job => { + cookie1 = job.result; + }); + + const job2 = service.startOperationCall(op2); + job2.finished.connect(job => { + cookie2 = job.result; + }); + } else { + const op1 = service.operationDescription("stopSuppressingSleep"); + op1.cookie = cookie1; + const op2 = service.operationDescription("stopSuppressingScreenPowerManagement"); + op2.cookie = cookie2; + + const job1 = service.startOperationCall(op1); + job1.finished.connect(job => { + cookie1 = -1; + }); + + const job2 = service.startOperationCall(op2); + job2.finished.connect(job => { + cookie2 = -1; + }); + } + batterymonitor.powermanagementDisabled = disabled + } + + PlasmaCore.DataSource { + id: notificationSource + engine: "notifications" + } + + onActivateProfileRequested: { + dialogItem.activeProfile = profile; + const service = pmSource.serviceForSource("PowerDevil"); + const op = service.operationDescription("setPowerProfile"); + op.profile = profile; + + const job = service.startOperationCall(op); + job.finished.connect(job => { + dialogItem.activeProfile = Qt.binding(() => actuallyActiveProfile); + if (!job.result) { + const notifications = notificationSource.serviceForSource("notification") + const operation = notifications.operationDescription("createNotification"); + operation.appName = i18n("Battery and Brightness"); + operation.appIcon = "dialog-error"; + operation.icon = "dialog-error"; + operation.body = i18n("Failed to activate %1 mode", profile); + notifications.startOperationCall(operation); + } + }); + } + } + + Component.onCompleted: { + Logic.updateBrightness(batterymonitor, pmSource); + Logic.updateInhibitions(batterymonitor, pmSource) + + if (batterymonitor.kcmEnergyInformationAuthorized) { + plasmoid.setAction("energyinformationkcm", i18n("&Show Energy Information…"), "documentinfo"); + } + plasmoid.setAction("showPercentage", i18n("Show Battery Percentage on Icon"), "format-number-percent"); + plasmoid.action("showPercentage").checkable = true; + plasmoid.action("showPercentage").checked = Qt.binding(() => + plasmoid !== null && plasmoid.configuration.showPercentage); + + if (batterymonitor.kcmAuthorized) { + plasmoid.removeAction("configure"); + plasmoid.setAction("configure", i18n("&Configure Energy Saving…"), "configure", "alt+d, s"); + } + } +} diff --git a/plasma/workspace/applets/batterymonitor/package/metadata.json b/plasma/workspace/applets/batterymonitor/package/metadata.json new file mode 100644 index 0000000000..93c9e31579 --- /dev/null +++ b/plasma/workspace/applets/batterymonitor/package/metadata.json @@ -0,0 +1,202 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "sebas@kde.org, kde@privat.broulik.de", + "Name": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ar]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[az]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ca]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[cs]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[de]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[en_GB]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[es]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[eu]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[fi]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[fr]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[hu]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ia]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[it]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ko]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[lt]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[nl]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[nn]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[pl]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[pt_BR]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ro]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[ru]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[sk]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[sl]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[sv]": "Sebastian Kügler", + "Name[ta]": "ஸெபாஸ்டியன் கூக்லர், காய் ஊவே புரோலிக்", + "Name[tr]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[uk]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[vi]": "Sebastian Kügler, Kai Uwe Broulik", + "Name[x-test]": "xxSebastian Kügler, Kai Uwe Broulikxx", + "Name[zh_CN]": "Sebastian Kügler, Kai Uwe Broulik" + } + ], + "Category": "System Information", + "Description": "See the power status of your battery", + "Description[ar]": "طالع حالة طاقة البطارية", + "Description[az]": "Batareyanızın enerji səviyyəsinə baxın", + "Description[ca]": "Vegeu l'estat d'energia de la bateria", + "Description[cs]": "Zobrazí stav nabití vaší baterie", + "Description[de]": "Den Ladestatus des Akkus anzeigen", + "Description[en_GB]": "See the power status of your battery", + "Description[es]": "Ver el estado de energía de su batería", + "Description[eu]": "Ikusi zure bateriaren energia-egoera", + "Description[fi]": "Näyttää akun virtatilanteen", + "Description[fr]": "Consulter le niveau d'énergie de votre batterie", + "Description[hu]": "Az akkumulátorok állapotának megjelenítése", + "Description[ia]": "Vide le stato de energia de tu batteria", + "Description[it]": "Indica lo stato di carica della batteria", + "Description[ko]": "배터리 상태를 표시합니다", + "Description[lt]": "Rodyti akumuliatoriaus įkrovą", + "Description[nl]": "Zie de capaciteitsstatus van uw accu", + "Description[nn]": "Sjå straum­nivået på batteriet", + "Description[pa]": "ਆਪਣੀ ਬੈਟਰੀ ਦੀ ਊਰਜਾ ਹਾਲਤ ਵੇਖੋ", + "Description[pl]": "Monitoruje stan naładowania baterii", + "Description[pt_BR]": "Mostra o estado da carga da sua bateria", + "Description[ro]": "Vedeți starea acumulatorului dumneavoastră", + "Description[ru]": "Просмотр уровня заряда батареи", + "Description[sk]": "Zobrazenie stavu nabitia vašej batérie", + "Description[sl]": "Oglejte si stanje napolnjenosti baterije", + "Description[sv]": "Se batteriets laddningsstatus", + "Description[ta]": "உங்கள் மின்கலத்தின் ஆற்றல் நிலையைப் பாருங்கள்", + "Description[tr]": "Pilinizin güç durumunu görün", + "Description[uk]": "Перегляньте стан заряду акумулятора", + "Description[vi]": "Xem tình trạng nguồn điện của pin", + "Description[x-test]": "xxSee the power status of your batteryxx", + "Description[zh_CN]": "查看电池的电量状态", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "battery-full", + "Id": "org.kde.plasma.battery", + "License": "GPL-2.0+", + "Name": "Battery and Brightness", + "Name[ar]": "البطّاريّة والسّطوع", + "Name[ast]": "Batería y brillu", + "Name[az]": "Batareya və Parlaqlıq", + "Name[ca@valencia]": "Bateria i brillantor", + "Name[ca]": "Bateria i lluminositat", + "Name[cs]": "Baterie a Jas", + "Name[da]": "Batteri og lysstyrke", + "Name[de]": "Akku und Bildschirmhelligkeit", + "Name[el]": "Μπαταρία και λαμπρότητα", + "Name[en_GB]": "Battery and Brightness", + "Name[es]": "Batería y brillo", + "Name[et]": "Aku ja heledus", + "Name[eu]": "Bateria eta distira", + "Name[fi]": "Akku ja kirkkaus", + "Name[fr]": "Batterie et luminosité", + "Name[gl]": "Batería e brillo", + "Name[he]": "סוללה ובהירות", + "Name[hi]": "बैटरी और चमक ", + "Name[hsb]": "Akuw a swětłosć", + "Name[hu]": "Akkumulátor és fényerő", + "Name[ia]": "Intensitate de illumination e batteria", + "Name[id]": "Baterai dan Kecerahan", + "Name[is]": "Rafhlaða og skjábirta", + "Name[it]": "Batteria e luminosità", + "Name[ja]": "バッテリーと明るさ", + "Name[ko]": "배터리와 밝기", + "Name[lt]": "Akumuliatorius ir ryškumas", + "Name[lv]": "Baterija un gaišums", + "Name[ml]": "ബാറ്ററിയും തെളിച്ചവും", + "Name[nl]": "Batterij en helderheid", + "Name[nn]": "Batteri og lysstyrke", + "Name[pa]": "ਬੈਟਰੀ ਤੇ ਚਮਕ", + "Name[pl]": "Bateria i jasność", + "Name[pt]": "Bateria e Brilho", + "Name[pt_BR]": "Bateria e brilho da tela", + "Name[ro]": "Acumulator și luminozitate", + "Name[ru]": "Батарея и яркость", + "Name[sk]": "Batéria a jas", + "Name[sl]": "Baterija in svetlost", + "Name[sr@ijekavian]": "Батерија и освјетљај", + "Name[sr@ijekavianlatin]": "Baterija i osvjetljaj", + "Name[sr@latin]": "Baterija i osvetljaj", + "Name[sr]": "Батерија и осветљај", + "Name[sv]": "Batteri och ljusstyrka", + "Name[ta]": "மின்கலமும் பிரகாசமும்", + "Name[tg]": "Батарея ва дурахшонӣ", + "Name[tr]": "Pil ve Ekran Parlaklığı", + "Name[uk]": "Акумулятор і яскравість дисплея", + "Name[vi]": "Pin và độ sáng", + "Name[x-test]": "xxBattery and Brightnessxx", + "Name[zh_CN]": "电池和亮度", + "Name[zh_TW]": "電池與亮度", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "3.0", + "Website": "https://vizzzion.org" + }, + "Keywords": "Power Management;Battery;System;Energy;", + "Keywords[ar]": "إدارة الطاقة;بطارية;نظام;طاقة;", + "Keywords[ast]": "Xestión enerxética;Xestión d'enerxía;Batería;Sistema;Enerxía;", + "Keywords[az]": "El. Enerjisi İdarəsi;Batareya;Enerji", + "Keywords[bs]": "Upravljanje napajenjem;Baterija;Sistem;Energija;", + "Keywords[ca@valencia]": "Sistema de gestió d'energia;Bateria;Sistema;Energia;", + "Keywords[ca]": "Sistema de gestió d'energia;Bateria;Sistema;Energia;", + "Keywords[cs]": "Správa napájení;Baterie;Systém;Energie;", + "Keywords[da]": "Strømstyring;battery;system;energi;", + "Keywords[de]": "Energieverwaltungssystem;Akku;System;Energie;", + "Keywords[el]": "Διαχείριση ισχύος;Μπαταρία;Σύστημα;Ενέργεια;", + "Keywords[en_GB]": "Power Management;Battery;System;Energy;", + "Keywords[es]": "Gestión de energía;Batería;Sistema;Energía;", + "Keywords[et]": "Toitehaldus;aku;süsteem;energia;", + "Keywords[eu]": "energia kudeaketa;bateria;sistema;energia;", + "Keywords[fi]": "Virranhallinta;Akku;Järjestelmä;Virransäästö;", + "Keywords[fr]": "Gestion de l'énergie;Batterie;Système;Énergie;", + "Keywords[gl]": "Xestión da enerxía;batería;sistema;enerxía;", + "Keywords[he]": "Power Management; Battery; System; Energy;ניהול חשמל;חשמל; סוללה; מערכת;אנרגיה; בטריה;", + "Keywords[hi]": "विद्युत प्रबंधन;बैटरी;तंत्र;ऊर्जा;", + "Keywords[hsb]": "Power Management;Battery;System;Energy;akuw;energija;milina;", + "Keywords[hu]": "Energiakezelés;Akkumulátor;Rendszer;Energia;", + "Keywords[ia]": "Gestion de energia;Batteria;Systema; Energia;", + "Keywords[id]": "Pengelolaan Daya;Baterai;Sistem;Energi;", + "Keywords[is]": "Orkustjórnun;Rafhlaða;Kerfi;Afl;", + "Keywords[it]": "Gestione energetica;Batteria;Sistema;Energia;", + "Keywords[ja]": "電源管理;バッテリー;システム;エネルギー;", + "Keywords[kk]": "Power Management;Battery;System;Energy;", + "Keywords[ko]": "Power Management;Battery;System;Energy;전원 관리;전력 관리;배터리;시스템;에너지;", + "Keywords[lt]": "Energijos valdymas;Baterija;Sistema;Energija;Akumuliatorius;Maitinimo valdymas;", + "Keywords[ml]": "ഊർജ്ജനിയന്ത്രണം;ബാറ്ററി;സിസ്റ്റം;ഊ‍ർജ്ജം", + "Keywords[mr]": "वीज व्यवस्थापन; बॅटरी; प्रणाली; ऊर्जा;", + "Keywords[nb]": "Strømstyring; Batteri; System; Energi;", + "Keywords[nds]": "Stroompleeg,Batterie,Systeem,Energie", + "Keywords[nl]": "Energiebeheer;Batterij;Accu;Systeem;Energie;", + "Keywords[nn]": "straumstyring;batteri;system;energi;", + "Keywords[pa]": "ਪਾਵਰ ਪਰਬੰਧ;ਬੈਟਰੀ;ਸਿਸਟਮ;ਊਰਜਾ;", + "Keywords[pl]": "Zarządzanie Energią;Bateria;System;Energia;", + "Keywords[pt]": "Gestão de Energia;Bateria;Sistema;Energia;", + "Keywords[pt_BR]": "Gerenciamento de energia;Bateria;Sistema;Energia;", + "Keywords[ro]": "gestiunea alimentării;acumulator;sistem;energie;", + "Keywords[ru]": "Power Management;Battery;System;Energy;управление питанием;батарея;система;энергия;энергопотребление;", + "Keywords[sk]": "Správa napájania;Batéria;Systém;Energia;", + "Keywords[sl]": "Upravljanje z energijo;Baterija;Sistem;Energija;", + "Keywords[sr@ijekavian]": "Power Management;Battery;System;Energy;управљање напајањем;батерија; систем;енергија;", + "Keywords[sr@ijekavianlatin]": "Power Management;Battery;System;Energy;upravljanje napajanjem;baterija; sistem;energija;", + "Keywords[sr@latin]": "Power Management;Battery;System;Energy;upravljanje napajanjem;baterija; sistem;energija;", + "Keywords[sr]": "Power Management;Battery;System;Energy;управљање напајањем;батерија; систем;енергија;", + "Keywords[sv]": "Strömhantering;Batteri;System;Energi;", + "Keywords[ta]": "Power Management;Battery;System;Energy;ஆற்றல்;மின்கலம்;கணினி;ஆற்றல் மேலாண்மை;", + "Keywords[tr]": "Güç Yönetimi;Pil;Sistem;Enerji;", + "Keywords[uk]": "Power Management;Battery;System;Energy;керування живленням;акумулятор;система;енергія;батарея;батарейка;живлення;", + "Keywords[vi]": "Power Management;Battery;System;Energy;Quản lí nguồn điện;Pin;Hệ thống;Năng lượng;", + "Keywords[x-test]": "xxPower Managementxx;xxBatteryxx;xxSystemxx;xxEnergyxx;", + "Keywords[zh_CN]": "电源管理;电池;系统;能源;", + "Keywords[zh_TW]": "Power Management;Battery;System;Energy;", + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-DBusActivationService": "org.kde.Solid.PowerManagement", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "Hardware", + "X-Plasma-Provides": [ + "org.kde.plasma.powermanagement" + ] +} diff --git a/plasma/workspace/applets/calendar/CMakeLists.txt b/plasma/workspace/applets/calendar/CMakeLists.txt new file mode 100644 index 0000000000..a691b85061 --- /dev/null +++ b/plasma/workspace/applets/calendar/CMakeLists.txt @@ -0,0 +1,6 @@ +kcoreaddons_add_plugin(plasma_applet_calendar SOURCES calendarapplet.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(plasma_applet_calendar + KF5::Plasma) + +plasma_install_package(package org.kde.plasma.calendar) diff --git a/plasma/workspace/applets/calendar/Messages.sh b/plasma/workspace/applets/calendar/Messages.sh new file mode 100644 index 0000000000..b484a1c48b --- /dev/null +++ b/plasma/workspace/applets/calendar/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.calendar.pot diff --git a/plasma/workspace/applets/calendar/calendarapplet.cpp b/plasma/workspace/applets/calendar/calendarapplet.cpp new file mode 100644 index 0000000000..a1f5dacdae --- /dev/null +++ b/plasma/workspace/applets/calendar/calendarapplet.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "calendarapplet.h" + +#include + +CalendarApplet::CalendarApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ +} + +CalendarApplet::~CalendarApplet() +{ +} + +int CalendarApplet::weekNumber(const QDateTime &dateTime) const +{ + return dateTime.date().weekNumber(); +} + +K_PLUGIN_CLASS_WITH_JSON(CalendarApplet, "package/metadata.json") + +#include "calendarapplet.moc" diff --git a/plasma/workspace/applets/calendar/calendarapplet.h b/plasma/workspace/applets/calendar/calendarapplet.h new file mode 100644 index 0000000000..2bda934f33 --- /dev/null +++ b/plasma/workspace/applets/calendar/calendarapplet.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class QDateTime; + +class CalendarApplet : public Plasma::Applet +{ + Q_OBJECT + +public: + explicit CalendarApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~CalendarApplet() override; + + Q_INVOKABLE int weekNumber(const QDateTime &dateTime) const; +}; diff --git a/plasma/workspace/applets/calendar/package/contents/config/config.qml b/plasma/workspace/applets/calendar/package/contents/config/config.qml new file mode 100644 index 0000000000..58918e4264 --- /dev/null +++ b/plasma/workspace/applets/calendar/package/contents/config/config.qml @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2013 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("General") + icon: "preferences-desktop-plasma" + source: "configGeneral.qml" + } +} diff --git a/plasma/workspace/applets/calendar/package/contents/config/main.xml b/plasma/workspace/applets/calendar/package/contents/config/main.xml new file mode 100644 index 0000000000..728ac4e6c3 --- /dev/null +++ b/plasma/workspace/applets/calendar/package/contents/config/main.xml @@ -0,0 +1,22 @@ + + + + + + + 9 + + + 17 + + + false + + + d + + + diff --git a/plasma/workspace/applets/calendar/package/contents/images/mini-calendar.svgz b/plasma/workspace/applets/calendar/package/contents/images/mini-calendar.svgz new file mode 100644 index 0000000000000000000000000000000000000000..20de0a285c78ed6385330cb78c70a57335338cec GIT binary patch literal 3859 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 literal 0 HcmV?d00001 diff --git a/plasma/workspace/applets/calendar/package/contents/ui/configGeneral.qml b/plasma/workspace/applets/calendar/package/contents/ui/configGeneral.qml new file mode 100644 index 0000000000..62d65217bd --- /dev/null +++ b/plasma/workspace/applets/calendar/package/contents/ui/configGeneral.qml @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2015 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QtControls +import org.kde.kirigami 2.5 as Kirigami + +Kirigami.FormLayout { + id: generalPage + + anchors.left: parent.left + anchors.right: parent.right + + property alias cfg_showWeekNumbers: showWeekNumbers.checked + property string cfg_compactDisplay + + + QtControls.CheckBox { + id: showWeekNumbers + + Kirigami.FormData.label: i18n("Calendar version:") + + text: i18n("Show week numbers") + } + + + Item { + Kirigami.FormData.isSection: true + } + + + QtControls.RadioButton { + Kirigami.FormData.label: i18nc("What information is shown in the calendar icon", "Compact version:") + + text: i18nc("Show the number of the day (eg. 31) in the icon", "Show day of the month") + + checked: cfg_compactDisplay == "d" + onCheckedChanged: if (checked) cfg_compactDisplay = "d" + } + QtControls.RadioButton { + text: i18nc("Show the week number (eg. 50) in the icon", "Show week number") + + checked: cfg_compactDisplay == "w" + onCheckedChanged: if (checked) cfg_compactDisplay = "w" + } +} diff --git a/plasma/workspace/applets/calendar/package/contents/ui/main.qml b/plasma/workspace/applets/calendar/package/contents/ui/main.qml new file mode 100644 index 0000000000..97d1c2d158 --- /dev/null +++ b/plasma/workspace/applets/calendar/package/contents/ui/main.qml @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2013 Heena Mahour + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.12 +import QtQuick.Layouts 1.12 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import org.kde.plasma.calendar 2.0 + +Item { + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 12 + Plasmoid.switchHeight: PlasmaCore.Units.gridUnit * 12 + + Plasmoid.toolTipMainText: Qt.formatDate(dataSource.data.Local.DateTime, "dddd") + Plasmoid.toolTipSubText: { + // this logic is taken from digital-clock: + // remove "dddd" from the locale format string + // /all/ locales in LongFormat have "dddd" either + // at the beginning or at the end. so we just + // remove it + the delimiter and space + var format = Qt.locale().dateFormat(Locale.LongFormat); + format = format.replace(/(^dddd.?\s)|(,?\sdddd$)/, ""); + return Qt.formatDate(dataSource.data.Local.DateTime, format) + } + + Layout.minimumWidth: PlasmaCore.Units.iconSizes.large + Layout.minimumHeight: PlasmaCore.Units.iconSizes.large + + PlasmaCore.DataSource { + id: dataSource + engine: "time" + connectedSources: ["Local"] + interval: 60000 + intervalAlignment: PlasmaCore.Types.AlignToMinute + } + + Plasmoid.compactRepresentation: MouseArea { + onClicked: plasmoid.expanded = !plasmoid.expanded + + PlasmaCore.IconItem { + anchors.fill: parent + + source: Qt.resolvedUrl("../images/mini-calendar.svgz") + + PlasmaComponents3.Label { + id: monthLabel + y: parent.y + parent.height * 0.05; + x: 0 + width: parent.width + height: parent.height * 0.2 + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + fontSizeMode: Text.Fit + minimumPointSize: 1 + + /* color must be black because it's set on top of a white icon */ + color: "black" + + text: { + var d = new Date(dataSource.data.Local.DateTime); + return Qt.formatDate(d, "MMM"); + } + visible: parent.width > PlasmaCore.Units.gridUnit * 3 + } + + PlasmaComponents3.Label { + anchors.top: monthLabel.bottom + x: 0 + width: parent.width + height: parent.height * 0.6 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + minimumPointSize: 1 + font.pixelSize: 1000 + + fontSizeMode: Text.Fit + + /* color must be black because it's set on top of a white icon */ + color: "black" + text: { + var d = new Date(dataSource.data.Local.DateTime) + var format = plasmoid.configuration.compactDisplay + + if (format === "w") { + return plasmoid.nativeInterface.weekNumber(d) + } + + return Qt.formatDate(d, format) + } + } + } + } + + Plasmoid.fullRepresentation: Item { + + // sizing taken from digital clock + readonly property int _minimumWidth: calendar.showWeekNumbers ? Math.round(_minimumHeight * 1.75) : Math.round(_minimumHeight * 1.5) + readonly property int _minimumHeight: PlasmaCore.Units.gridUnit * 14 + + Layout.preferredWidth: _minimumWidth + Layout.preferredHeight: Math.round(_minimumHeight * 1.5) + + MonthView { + id: calendar + today: dataSource.data["Local"]["DateTime"] + showWeekNumbers: plasmoid.configuration.showWeekNumbers + + anchors.fill: parent + } + } +} diff --git a/plasma/workspace/applets/calendar/package/metadata.json b/plasma/workspace/applets/calendar/package/metadata.json new file mode 100644 index 0000000000..8880ac93e0 --- /dev/null +++ b/plasma/workspace/applets/calendar/package/metadata.json @@ -0,0 +1,171 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "sebas@kde.org", + "Name": "Sebastian Kügler", + "Name[ar]": "Sebastian Kügler", + "Name[az]": "Sebastian Kügler", + "Name[ca]": "Sebastian Kügler", + "Name[cs]": "Sebastian Kügler", + "Name[de]": "Sebastian Kügler", + "Name[en_GB]": "Sebastian Kügler", + "Name[es]": "Sebastian Kügler", + "Name[eu]": "Sebastian Kügler", + "Name[fi]": "Sebastian Kügler", + "Name[fr]": "Sebastian Kügler", + "Name[hu]": "Sebastian Kügler", + "Name[ia]": "Sebastian Kügler", + "Name[it]": "Sebastian Kügler", + "Name[ko]": "Sebastian Kügler", + "Name[lt]": "Sebastian Kügler", + "Name[nl]": "Sebastian Kügler", + "Name[nn]": "Sebastian Kügler", + "Name[pa]": "Sebastian Kügler", + "Name[pl]": "Sebastian Kügler", + "Name[pt_BR]": "Sebastian Kügler", + "Name[ro]": "Sebastian Kügler", + "Name[ru]": "Sebastian Kügler", + "Name[sk]": "Sebastian Kügler", + "Name[sl]": "Sebastian Kügler", + "Name[sv]": "Sebastian Kügler", + "Name[ta]": "ஸெபாஸ்டியன் கூக்லர்", + "Name[tr]": "Sebastian Kügler", + "Name[uk]": "Sebastian Kügler", + "Name[vi]": "Sebastian Kügler", + "Name[x-test]": "xxSebastian Küglerxx", + "Name[zh_CN]": "Sebastian Kügler" + } + ], + "Category": "Date and Time", + "Description": "Month display with your appointments and events", + "Description[ar]": "عرض للشّهر فيه المواعيد والأحداث", + "Description[az]": "Hər aya planlaşdırılmış işlərin və tədbirlərinizin təqvimi", + "Description[ca]": "Mostra el mes amb les cites i els esdeveniments", + "Description[de]": "Monatsanzeige mit Ihren Verabredungen und Terminen", + "Description[en_GB]": "Month display with your appointments and events", + "Description[es]": "Calendario mensual con sus citas y eventos", + "Description[eu]": "Hileko ikuspegia zure hitzordu eta gertaerekin", + "Description[fi]": "Kalenterimerkintöjen kuukausinäkymä", + "Description[fr]": "Affichage par mois de vos rendez-vous et évènements", + "Description[hu]": "Megjeleníti a hónapban esedékes időpontokat és eseményeket", + "Description[ia]": "Monstrator de mense con tu appunctamentos e eventos", + "Description[it]": "Visualizzazione del mese con i tuoi appuntamenti ed eventi", + "Description[ko]": "내 약속과 이벤트가 있는 달력 표시", + "Description[lt]": "Jūsų užduotys ir įvykiai mėnesiui", + "Description[nl]": "Maandweergave met uw afspraken en gebeurtenissen", + "Description[nn]": "Månadsvising med avtaler og hendingar", + "Description[pa]": "ਤੁਹਾਡੀਆਂ ਮੁਲਾਕਾਤਾਂ ਤੇ ਸਮਾਗਮਾਂ ਨਾਲ ਮਹੀਨਾ ਵੇਖੋ", + "Description[pl]": "Wyświetlanie miesiąca z twoimi spotkaniami i wydarzeniami", + "Description[pt_BR]": "Exibição mensal com seus compromissos e eventos", + "Description[ro]": "Afișarea lunii cu programări și evenimente", + "Description[ru]": "Календарь на месяц с запланированными делами и событиями", + "Description[sk]": "Zobrazenie mesiaca s vašimi schôdzkami a udalosťami", + "Description[sl]": "Mesečni prikaz z vašimi sestanki in dogodki", + "Description[sv]": "Månatlig visning av möten och händelser", + "Description[ta]": "நிகழ்வுகள் மற்றும் சந்திப்புகளைக் காட்டும் மாத நாள்காட்டி", + "Description[tr]": "Randevularınız ve etkinliklerinizle birlikte ay görünümü", + "Description[uk]": "Показ розкладу місяця із вашими записами зустрічей і подій", + "Description[vi]": "Hiển thị tháng với các buổi hẹn và sự kiện của bạn", + "Description[x-test]": "xxMonth display with your appointments and eventsxx", + "Description[zh_CN]": "按月显示您的预约和事件", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "office-calendar", + "Id": "org.kde.plasma.calendar", + "License": "GPL-2.0+", + "Name": "Calendar", + "Name[ar]": "التقويم", + "Name[ast]": "Calendariu", + "Name[az]": "Təqvim", + "Name[be@latin]": "Kalandar", + "Name[bg]": "Календар", + "Name[bn]": "ক্যালেণ্ডার", + "Name[bn_IN]": "বর্ষপঞ্জি", + "Name[bs]": "Kalendar", + "Name[ca@valencia]": "Calendari", + "Name[ca]": "Calendari", + "Name[cs]": "Kalendář", + "Name[csb]": "Kalãdôrz", + "Name[da]": "Kalender", + "Name[de]": "Kalender", + "Name[el]": "Ημερολόγιο", + "Name[en_GB]": "Calendar", + "Name[eo]": "Kalendaro", + "Name[es]": "Calendario", + "Name[et]": "Kalender", + "Name[eu]": "Egutegia", + "Name[fa]": "تقویم", + "Name[fi]": "Kalenteri", + "Name[fr]": "Calendrier", + "Name[fy]": "Aginda", + "Name[ga]": "Féilire", + "Name[gl]": "Calendario", + "Name[gu]": "કેલેન્ડર", + "Name[he]": "לוח שנה", + "Name[hi]": "कैलेन्डर", + "Name[hne]": "कलेन्डर", + "Name[hr]": "Kalendar", + "Name[hsb]": "Protyka", + "Name[hu]": "Naptár", + "Name[ia]": "Calendario", + "Name[id]": "Kalender", + "Name[is]": "Dagatal", + "Name[it]": "Calendario", + "Name[ja]": "カレンダー", + "Name[kk]": "Күнтізбе", + "Name[km]": "ប្រតិទិន", + "Name[kn]": "ದಿನಸೂಚಿ", + "Name[ko]": "달력", + "Name[ku]": "Salname", + "Name[lt]": "Kalendorius", + "Name[lv]": "Kalendārs", + "Name[mai]": "कैलेंडर", + "Name[mk]": "Календар", + "Name[ml]": "കലണ്ടര്‍", + "Name[mr]": "दिनदर्शिका", + "Name[nb]": "Kalender", + "Name[nds]": "Kalenner", + "Name[nl]": "Kalender", + "Name[nn]": "Kalender", + "Name[pa]": "ਕੈਲੰਡਰ", + "Name[pl]": "Kalendarz", + "Name[pt]": "Calendário", + "Name[pt_BR]": "Calendário", + "Name[ro]": "Calendar", + "Name[ru]": "Календарь", + "Name[si]": "දිනදර්ශනය", + "Name[sk]": "Kalendár", + "Name[sl]": "Koledar", + "Name[sr@ijekavian]": "календар", + "Name[sr@ijekavianlatin]": "kalendar", + "Name[sr@latin]": "kalendar", + "Name[sr]": "календар", + "Name[sv]": "Kalender", + "Name[ta]": "நாட்காட்டி", + "Name[tg]": "Тақвим", + "Name[th]": "ปฏิทิน", + "Name[tr]": "Takvim", + "Name[ug]": "يىلنامە", + "Name[uk]": "Календар", + "Name[vi]": "Lịch", + "Name[wa]": "Calindrî", + "Name[x-test]": "xxCalendarxx", + "Name[zh_CN]": "日历", + "Name[zh_TW]": "行事曆", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "2.0", + "Website": "https://kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.date" + ], + "X-Plasma-StandAloneApp": true +} diff --git a/plasma/workspace/applets/clipboard/Messages.sh b/plasma/workspace/applets/clipboard/Messages.sh new file mode 100644 index 0000000000..f283f6e9d9 --- /dev/null +++ b/plasma/workspace/applets/clipboard/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.clipboard.pot diff --git a/plasma/workspace/applets/clipboard/contents/ui/BarcodePage.qml b/plasma/workspace/applets/clipboard/contents/ui/BarcodePage.qml new file mode 100644 index 0000000000..e5bb16c85d --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/BarcodePage.qml @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For ContextMenu +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.prison 1.0 as Prison + +ColumnLayout { + id: barcodeView + + property alias text: barcodeItem.content + + Keys.onPressed: { + if (event.key == Qt.Key_Escape) { + stack.pop() + event.accepted = true; + } + } + + property var header: PlasmaExtras.PlasmoidHeading { + RowLayout { + anchors.fill: parent + PlasmaComponents3.Button { + Layout.fillWidth: true + icon.name: "go-previous-view" + text: i18n("Return to Clipboard") + onClicked: stack.pop() + } + + Component { + id: menuItemComponent + PlasmaComponents.MenuItem { } + } + + PlasmaComponents.ContextMenu { + id: menu + visualParent: configureButton + placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup + onStatusChanged: { + if (status == PlasmaComponents.DialogStatus.Closed) { + configureButton.checked = false; + } + } + + Component.onCompleted: { + [ + {text: i18n("QR Code"), type: Prison.Barcode.QRCode}, + {text: i18n("Data Matrix"), type: Prison.Barcode.DataMatrix}, + {text: i18nc("Aztec barcode", "Aztec"), type: Prison.Barcode.Aztec}, + {text: i18n("Code 39"), type: Prison.Barcode.Code39}, + {text: i18n("Code 93"), type: Prison.Barcode.Code93}, + {text: i18n("Code 128"), type: Prison.Barcode.Code128} + ].forEach((item) => { + let menuItem = menuItemComponent.createObject(menu, { + text: item.text, + checkable: true, + checked: Qt.binding(() => { + return barcodeItem.barcodeType === item.type; + }) + }); + menuItem.clicked.connect(() => { + barcodeItem.barcodeType = item.type; + }); + menu.addMenuItem(menuItem); + }); + } + } + PlasmaComponents3.ToolButton { + id: configureButton + checkable: true + icon.name: "configure" + onClicked: menu.openRelative() + + PlasmaComponents3.ToolTip { + text: i18n("Change the QR code type") + } + } + } + } + + Item { + Layout.fillWidth: parent + Layout.fillHeight: parent + Layout.topMargin: PlasmaCore.Units.smallSpacing + + Prison.Barcode { + id: barcodeItem + readonly property bool valid: implicitWidth > 0 && implicitHeight > 0 && implicitWidth <= width && implicitHeight <= height + anchors.fill: parent + barcodeType: Prison.Barcode.QRCode + // Cannot set visible to false as we need it to re-render when changing its size + opacity: valid ? 1 : 0 + } + + PlasmaComponents3.Label { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: i18n("Creating QR code failed") + wrapMode: Text.WordWrap + visible: barcodeItem.implicitWidth === 0 && barcodeItem.implicitHeight === 0 + } + + PlasmaComponents3.Label { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: i18n("The QR code is too large to be displayed") + wrapMode: Text.WordWrap + visible: barcodeItem.implicitWidth > barcodeItem.width || barcodeItem.implicitHeight > barcodeItem.height + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/ClipboardItemDelegate.qml b/plasma/workspace/applets/clipboard/contents/ui/ClipboardItemDelegate.qml new file mode 100644 index 0000000000..06df21438e --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/ClipboardItemDelegate.qml @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents2 +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons + +PlasmaComponents2.ListItem { + id: menuItem + + property bool supportsBarcodes + property int maximumNumberOfPreviews: Math.floor(width / (PlasmaCore.Units.gridUnit * 4 + PlasmaCore.Units.smallSpacing)) + readonly property real gradientThreshold: (label.width - toolButtonsLoader.width) / label.width + // Consider tall to be > about 1.5x the default height for purposes of top-aligning + // the buttons to preserve Fitts' Law when deleting multiple items in a row, + // or else the top-alignment doesn't look deliberate enough and people will think + // it's a bug + readonly property bool isTall: height > Math.round(PlasmaCore.Units.gridUnit * 2.5) + + signal itemSelected(string uuid) + signal remove(string uuid) + signal edit(string uuid) + signal barcode(string text) + signal action(string uuid) + + // the 1.6 comes from ToolButton's default height + height: Math.max(label.height, Math.round(PlasmaCore.Units.gridUnit * 1.6)) + 2 * PlasmaCore.Units.smallSpacing + + enabled: true + + onClicked: { + menuItem.itemSelected(UuidRole); + if (plasmoid.hideOnWindowDeactivate) { + plasmoid.expanded = false; + } else { + forceActiveFocus(); // Or activeFocus will always be false after clicking buttons in the heading + } + } + + Keys.onDeletePressed: { + remove(UuidRole); + } + + ListView.onIsCurrentItemChanged: { + if (ListView.isCurrentItem) { + labelMask.source = label // calculate on demand + } + } + + // this stuff here is used so we can fade out the text behind the tool buttons + Item { + id: labelMaskSource + anchors.fill: label + visible: false + + Rectangle { + anchors.centerIn: parent + rotation: LayoutMirroring.enabled ? 90 : -90 // you cannot even rotate gradients without QtGraphicalEffects + width: parent.height + height: parent.width + + gradient: Gradient { + GradientStop { position: 0.0; color: "white" } + GradientStop { position: gradientThreshold - 0.25; color: "white"} + GradientStop { position: gradientThreshold; color: "transparent"} + GradientStop { position: 1; color: "transparent"} + } + } + } + + OpacityMask { + id: labelMask + anchors.fill: label + cached: true + maskSource: labelMaskSource + visible: !!source && menuItem.ListView.isCurrentItem + } + + Item { + id: label + height: childrenRect.height + visible: !menuItem.ListView.isCurrentItem + anchors { + left: parent.left + leftMargin: PlasmaCore.Units.gridUnit / 2 - listMargins.left + right: parent.right + verticalCenter: parent.verticalCenter + } + + Loader { + width: parent.width + source: ["Text", "Image", "Url"][TypeRole] + "ItemDelegate.qml" + } + } + + Loader { + id: toolButtonsLoader + + anchors { + right: label.right + verticalCenter: parent.verticalCenter + } + source: "DelegateToolButtons.qml" + active: menuItem.ListView.isCurrentItem + + // It's not recommended to change anchors via conditional bindings, use AnchorChanges instead. + // See https://doc.qt.io/qt-5/qtquick-positioning-anchors.html#changing-anchors + states: [ + State { + when: menuItem.isTall + + AnchorChanges { + target: toolButtonsLoader + anchors.top: parent.top + anchors.verticalCenter: undefined + } + } + ] + + onActiveChanged: { + if (active) { + menuItem.KeyNavigation.tab = toolButtonsLoader.item.children[0] + menuItem.KeyNavigation.right = toolButtonsLoader.item.children[0] + // break binding, once it was loaded, never unload + active = true; + } + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/ClipboardPage.qml b/plasma/workspace/applets/clipboard/contents/ui/ClipboardPage.qml new file mode 100644 index 0000000000..42734e7069 --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/ClipboardPage.qml @@ -0,0 +1,202 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.4 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kirigami 2.19 as Kirigami // for InputMethod.willShowOnActive + +Menu { + id: clipboardMenu + Keys.onPressed: { + function forwardToFilter() { + if (event.text !== "" && !filter.activeFocus) { + clipboardMenu.view.currentIndex = -1 + if (event.matches(StandardKey.Paste)) { + filter.paste(); + } else { + filter.text = ""; + filter.text += event.text; + } + filter.forceActiveFocus(); + event.accepted = true; + } + } + if (stack.currentItem !== clipboardMenu) { + event.accepted = false; + return; + } + switch(event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: { + if (clipboardMenu.view.currentIndex >= 0) { + var uuid = clipboardMenu.model.get(clipboardMenu.view.currentIndex).UuidRole + if (uuid) { + clipboardSource.service(uuid, "select") + if (plasmoid.hideOnWindowDeactivate) { + plasmoid.expanded = false; + } + } + } + break; + } + case Qt.Key_Escape: { + if (filter.text != "") { + filter.text = ""; + event.accepted = true; + } + break; + } + case Qt.Key_F: { + if (event.modifiers & Qt.ControlModifier) { + filter.forceActiveFocus(); + filter.selectAll(); + event.accepted = true; + } else { + forwardToFilter(); + } + break; + } + case Qt.Key_Tab: + case Qt.Key_Backtab: { + // prevent search filter from getting Tab key events + break; + } + case Qt.Key_Backspace: { + // filter.text += event.text wil break if the key is backspace + filter.forceActiveFocus(); + filter.text = filter.text.slice(0, -1); + event.accepted = true; + break; + } + default: { + forwardToFilter(); + } + } + } + + Keys.forwardTo: [stack.currentItem] + + property var header: PlasmaExtras.PlasmoidHeading { + RowLayout { + anchors.fill: parent + enabled: clipboardMenu.model.count > 0 || filter.text.length > 0 + + PlasmaComponents3.TextField { + id: filter + placeholderText: i18n("Search…") + clearButtonShown: true + Layout.fillWidth: true + + inputMethodHints: Qt.ImhNoPredictiveText + + Keys.onUpPressed: clipboardMenu.arrowKeyPressed(event) + Keys.onDownPressed: clipboardMenu.arrowKeyPressed(event) + + Connections { + target: main + function onClearSearchField() { + filter.clear() + } + } + } + PlasmaComponents3.ToolButton { + visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) && plasmoid.action("clearHistory").visible + + icon.name: "edit-clear-history" + onClicked: { + clipboardSource.service("", "clearHistory") + filter.clear() + } + + PlasmaComponents3.ToolTip { + text: i18n("Clear history") + } + } + } + } + + model: PlasmaCore.SortFilterModel { + sourceModel: clipboardSource.models.clipboard + filterRole: "DisplayRole" + filterRegExp: filter.text + } + supportsBarcodes: { + try { + let prisonTest = Qt.createQmlObject("import QtQml 2.0; import org.kde.prison 1.0; QtObject {}", this); + prisonTest.destroy(); + } catch (e) { + console.log("Barcodes not supported:", e); + return false; + } + return true; + } + onItemSelected: clipboardSource.service(uuid, "select") + onRemove: clipboardSource.service(uuid, "remove") + onEdit: { + stack.push(Qt.resolvedUrl("EditPage.qml"), { + text: clipboardMenu.model.get(clipboardMenu.view.currentIndex).DisplayRole, + uuid: uuid + }); + } + onBarcode: { + stack.push(Qt.resolvedUrl("BarcodePage.qml"), { + text: text + }); + } + onAction: clipboardSource.service(uuid, "action") + + Component.onCompleted: { + // Intercept up/down key to prevent ListView from accepting the key event. + clipboardMenu.view.Keys.upPressed.connect(clipboardMenu.arrowKeyPressed); + clipboardMenu.view.Keys.downPressed.connect(clipboardMenu.arrowKeyPressed); + + // Focus on the search field when the applet is opened for the first time + // but only when doing so wouldn't make the virtual keyboar appear, since + // that's annoying! + if (!Kirigami.InputMethod.willShowOnActive) { + filter.forceActiveFocus(); + } + } + + function goToCurrent() { + clipboardMenu.view.positionViewAtIndex(clipboardMenu.view.currentIndex, ListView.Contain); + if (clipboardMenu.view.currentIndex !== -1) { + clipboardMenu.view.currentItem.forceActiveFocus(); + } + } + + function arrowKeyPressed(event) { + switch (event.key) { + case Qt.Key_Up: { + if (clipboardMenu.view.currentIndex === 0) { + clipboardMenu.view.currentIndex = -1; + filter.forceActiveFocus(); + filter.selectAll(); + } else { + clipboardMenu.view.decrementCurrentIndex(); + goToCurrent(); + } + event.accepted = true; + break; + } + case Qt.Key_Down: { + clipboardMenu.view.incrementCurrentIndex(); + goToCurrent(); + event.accepted = true; + break; + } + default: + break; + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/DelegateToolButtons.qml b/plasma/workspace/applets/clipboard/contents/ui/DelegateToolButtons.qml new file mode 100644 index 0000000000..6a92fb3b05 --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/DelegateToolButtons.qml @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +RowLayout { + id: toolButtonsLayout + visible: menuItem.ListView.isCurrentItem + + PlasmaComponents3.ToolButton { + id: actionToolButton + // TODO: only show for items supporting actions? + icon.name: "system-run" + onClicked: menuItem.action(UuidRole) + + PlasmaComponents3.ToolTip { + text: i18n("Invoke action") + } + KeyNavigation.right: barcodeToolButton + } + PlasmaComponents3.ToolButton { + id: barcodeToolButton + icon.name: "view-barcode-qr" + visible: supportsBarcodes + onClicked: menuItem.barcode(DisplayRole) + + PlasmaComponents3.ToolTip { + text: i18n("Show QR code") + } + KeyNavigation.right: editToolButton + } + PlasmaComponents3.ToolButton { + id: editToolButton + icon.name: "document-edit" + enabled: !clipboardSource.editing + visible: TypeRole === 0 + onClicked: menuItem.edit(UuidRole) + + PlasmaComponents3.ToolTip { + text: i18n("Edit contents") + } + KeyNavigation.right: deleteToolButton + } + PlasmaComponents3.ToolButton { + id: deleteToolButton + icon.name: "edit-delete" + onClicked: menuItem.remove(UuidRole) + + PlasmaComponents3.ToolTip { + text: i18n("Remove from history") + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/EditPage.qml b/plasma/workspace/applets/clipboard/contents/ui/EditPage.qml new file mode 100644 index 0000000000..6456585f9b --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/EditPage.qml @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2021 Bharadwaj Raju + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.15 as QQC2 // For StackView +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +ColumnLayout { + spacing: 0 + property alias text: textArea.text + property string uuid + + property var header: Item {} + + Keys.onPressed: { + if (event.key === Qt.Key_Escape) { + stack.pop() + event.accepted = true; + } + } + + function saveAndExit() { + clipboardSource.edit(uuid, text); + stack.pop(); + done(); + } + + function done() { + // The modified item will be pushed to the top, and we would like to highlight the real first item + Qt.callLater(() => {stack.initialItem.view.currentIndex = 0;}); + } + + QQC2.StackView.onStatusChanged: { + if (QQC2.StackView.status === QQC2.StackView.Active) { + textArea.forceActiveFocus(); + textArea.cursorPosition = textArea.text.length; + } + } + + PlasmaComponents3.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2 + Layout.rightMargin: PlasmaComponents3.ScrollBar.vertical.visible ? 0 : PlasmaCore.Units.smallSpacing * 2 + Layout.topMargin: PlasmaCore.Units.smallSpacing * 2 + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + PlasmaComponents3.TextArea { + id: textArea + wrapMode: Text.Wrap + textFormat: TextEdit.PlainText + + Keys.onPressed: { + if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) { + saveAndExit(); + event.accepted = true; + } else { + event.accepted = false; + } + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignRight + Layout.margins: PlasmaCore.Units.smallSpacing * 2 + PlasmaComponents3.Button { + text: i18nc("@action:button", "Save") + icon.name: "document-save" + onClicked: saveAndExit() + } + PlasmaComponents3.Button { + text: i18nc("@action:button", "Cancel") + icon.name: "dialog-cancel" + onClicked: stack.pop() + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/ImageItemDelegate.qml b/plasma/workspace/applets/clipboard/contents/ui/ImageItemDelegate.qml new file mode 100644 index 0000000000..dc7e6a9ae2 --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/ImageItemDelegate.qml @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons +import org.kde.plasma.core 2.0 as PlasmaCore + +Item { + height: childrenRect.height + + KQuickControlsAddons.QPixmapItem { + id: previewPixmap + + width: Math.min(Math.round(height * nativeWidth/nativeHeight), parent.width) + height: Math.min(nativeHeight, PlasmaCore.Units.gridUnit * 4 + PlasmaCore.Units.smallSpacing * 2) + + // align left + // right in RTL + anchors.left: parent.left + + pixmap: DecorationRole + smooth: true + fillMode: KQuickControlsAddons.QPixmapItem.PreserveAspectFit + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/Menu.qml b/plasma/workspace/applets/clipboard/contents/ui/Menu.qml new file mode 100644 index 0000000000..3e7056dbbe --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/Menu.qml @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQml 2.15 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For Highlight +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import org.kde.kirigami 2.12 as Kirigami + +PlasmaComponents3.ScrollView { + id: menu + + property alias view: menuListView + property alias model: menuListView.model + property bool supportsBarcodes + + background: null + + signal itemSelected(string uuid) + signal remove(string uuid) + signal edit(string uuid) + signal barcode(string text) + signal action(string uuid) + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + contentWidth: availableWidth - contentItem.leftMargin - contentItem.rightMargin + + contentItem: ListView { + id: menuListView + focus: true + + highlight: PlasmaComponents.Highlight { } + highlightMoveDuration: 0 + highlightResizeDuration: 0 + currentIndex: -1 + + topMargin: PlasmaCore.Units.smallSpacing * 2 + bottomMargin: PlasmaCore.Units.smallSpacing * 2 + leftMargin: PlasmaCore.Units.smallSpacing * 2 + rightMargin: PlasmaCore.Units.smallSpacing * 2 + spacing: PlasmaCore.Units.smallSpacing + + reuseItems: true + + delegate: ClipboardItemDelegate { + // FIXME: removing this causes a binding loop + width: menuListView.width - menuListView.leftMargin - menuListView.rightMargin + + supportsBarcodes: menu.supportsBarcodes + + onItemSelected: menu.itemSelected(uuid) + onRemove: menu.remove(uuid) + onEdit: menu.edit(uuid) + onBarcode: menu.barcode(text) + onAction: menu.action(uuid) + + Binding { + target: menuListView; when: containsMouse + property: "currentIndex"; value: index + restoreMode: Binding.RestoreBinding + } + } + + PlasmaExtras.PlaceholderMessage { + id: emptyHint + + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + + visible: menuListView.count === 0 + text: model.filterRegExp.length > 0 ? i18n("No matches") : i18n("Clipboard is empty") + } + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/TextItemDelegate.qml b/plasma/workspace/applets/clipboard/contents/ui/TextItemDelegate.qml new file mode 100644 index 0000000000..76ece91ca4 --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/TextItemDelegate.qml @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +PlasmaComponents3.Label { + maximumLineCount: 3 + verticalAlignment: Text.AlignVCenter + + text: { + var highlightFontTag = "%1" + + var text = DisplayRole.slice(0, 100) + + // first escape any HTML characters to prevent privacy issues + text = text.replace(/&/g, "&").replace(//g, ">") + + // color code leading or trailing whitespace + // the first regex is basically "trim" + text = text.replace(/^\s+|\s+$/gm, function(match) { + // then inside the trimmed characters ("match") we replace each one individually + match = match.replace(/ /g, "␣") // space + .replace(/\t/g, "↹") // tab + .replace(/\n/g, "↵") // return + return highlightFontTag.arg(match) + }) + + // finally turn line breaks into HTML br tags + text = text.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "
") + + return text + } + elide: Text.ElideRight + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + textFormat: Text.StyledText +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/UrlItemDelegate.qml b/plasma/workspace/applets/clipboard/contents/ui/UrlItemDelegate.qml new file mode 100644 index 0000000000..0bfe1adcfc --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/UrlItemDelegate.qml @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons + +Item { + id: previewItem + height: PlasmaCore.Units.gridUnit * 4 + PlasmaCore.Units.smallSpacing * 2 + + ListView { + id: previewList + model: DisplayRole.split(" ", maximumNumberOfPreviews) + property int itemWidth: PlasmaCore.Units.gridUnit * 4 + property int itemHeight: PlasmaCore.Units.gridUnit * 4 + interactive: false + + spacing: PlasmaCore.Units.smallSpacing + orientation: Qt.Horizontal + width: (itemWidth + spacing) * model.length + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + } + + delegate: Item { + width: previewList.itemWidth + height: previewList.itemHeight + y: Math.round((parent.height - previewList.itemHeight) / 2) + clip: true + + KQuickControlsAddons.QPixmapItem { + id: previewPixmap + + anchors.centerIn: parent + + Component.onCompleted: { + function result(job) { + if (!job.error) { + pixmap = job.result.preview; + previewPixmap.width = job.result.previewWidth + previewPixmap.height = job.result.previewHeight + } + } + var service = clipboardSource.serviceForSource(UuidRole) + var operation = service.operationDescription("preview"); + operation.url = modelData; + // We request a bigger size and then clip out a square in the middle + // so we get uniform delegate sizes without distortion + operation.previewWidth = previewList.itemWidth * 2; + operation.previewHeight = previewList.itemHeight * 2; + var serviceJob = service.startOperationCall(operation); + serviceJob.finished.connect(result); + } + } + Rectangle { + id: overlay + color: PlasmaCore.Theme.textColor + opacity: 0.6 + height: PlasmaCore.Units.gridUnit + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + PlasmaComponents3.Label { + font: PlasmaCore.Theme.smallestFont + color: PlasmaCore.Theme.backgroundColor + maximumLineCount: 1 + anchors { + verticalCenter: overlay.verticalCenter + left: overlay.left + right: overlay.right + leftMargin: PlasmaCore.Units.smallSpacing + rightMargin: PlasmaCore.Units.smallSpacing + } + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + text: { + var u = modelData.split("/"); + return decodeURIComponent(u[u.length - 1]); + } + } + } + } + PlasmaComponents3.Label { + property int additionalItems: DisplayRole.split(" ").length - maximumNumberOfPreviews + visible: additionalItems > 0 + opacity: 0.6 + text: i18nc("Indicator that there are more urls in the clipboard than previews shown", "+%1", additionalItems) + anchors { + left: previewList.right + right: parent.right + bottom: parent.bottom + margins: PlasmaCore.Units.smallSpacing + + } + verticalAlignment: Text.AlignBottom + horizontalAlignment: Text.AlignCenter + font: PlasmaCore.Theme.smallestFont + } +} diff --git a/plasma/workspace/applets/clipboard/contents/ui/clipboard.qml b/plasma/workspace/applets/clipboard/contents/ui/clipboard.qml new file mode 100644 index 0000000000..1ea0428a1b --- /dev/null +++ b/plasma/workspace/applets/clipboard/contents/ui/clipboard.qml @@ -0,0 +1,112 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.15 as QQC2 // For StackView +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + id: main + + property bool isClipboardEmpty: clipboardSource.data["clipboard"]["empty"] + + signal clearSearchField + + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 5 + Plasmoid.switchHeight: PlasmaCore.Units.gridUnit * 5 + Plasmoid.status: isClipboardEmpty ? PlasmaCore.Types.PassiveStatus : PlasmaCore.Types.ActiveStatus + Plasmoid.toolTipMainText: i18n("Clipboard Contents") + Plasmoid.toolTipSubText: isClipboardEmpty ? i18n("Clipboard is empty") : clipboardSource.data["clipboard"]["current"] + Plasmoid.toolTipTextFormat: Text.PlainText + Plasmoid.icon: "klipper" + + function action_configure() { + clipboardSource.service("", "configureKlipper"); + } + + function action_clearHistory() { + clipboardSource.service("", "clearHistory") + clearSearchField() + } + + onIsClipboardEmptyChanged: { + if (isClipboardEmpty) { + // We need to hide the applet before changing its status to passive + // because only the active applet can hide itself + if (plasmoid.hideOnWindowDeactivate) + plasmoid.expanded = false; + Plasmoid.status = PlasmaCore.Types.PassiveStatus; + } else { + Plasmoid.status = PlasmaCore.Types.ActiveStatus + } + } + + + Component.onCompleted: { + plasmoid.removeAction("configure"); + plasmoid.setAction("configure", i18n("Configure Clipboard…"), "configure", "alt+d, s"); + + plasmoid.setAction("clearHistory", i18n("Clear History"), "edit-clear-history"); + plasmoid.action("clearHistory").visible = Qt.binding(() => { + return !main.isClipboardEmpty; + }); + } + + PlasmaCore.DataSource { + id: clipboardSource + property bool editing: false; + engine: "org.kde.plasma.clipboard" + connectedSources: "clipboard" + function service(uuid, op) { + var service = clipboardSource.serviceForSource(uuid); + var operation = service.operationDescription(op); + return service.startOperationCall(operation); + } + function edit(uuid, text) { + clipboardSource.editing = true; + const service = clipboardSource.serviceForSource(uuid); + const operation = service.operationDescription("edit"); + operation.text = text; + const job = service.startOperationCall(operation); + job.finished.connect(function() { + clipboardSource.editing = false; + }); + } + } + + Plasmoid.fullRepresentation: PlasmaExtras.Representation { + id: dialogItem + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 5 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 5 + collapseMarginsHint: true + + focus: true + + header: stack.currentItem.header + + property alias listMargins: listItemSvg.margins + + PlasmaCore.FrameSvgItem { + id : listItemSvg + imagePath: "widgets/listitem" + prefix: "normal" + visible: false + } + + Keys.forwardTo: [stack.currentItem] + + QQC2.StackView { + id: stack + anchors.fill: parent + initialItem: ClipboardPage {} + } + } +} diff --git a/plasma/workspace/applets/clipboard/metadata.json b/plasma/workspace/applets/clipboard/metadata.json new file mode 100644 index 0000000000..86e797d3ee --- /dev/null +++ b/plasma/workspace/applets/clipboard/metadata.json @@ -0,0 +1,144 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org", + "Name": "Martin Gräßlin", + "Name[ar]": "Martin Gräßlin", + "Name[az]": "Martin Gräßlin", + "Name[ca]": "Martin Gräßlin", + "Name[cs]": "Martin Gräßlin", + "Name[de]": "Martin Gräßlin", + "Name[en_GB]": "Martin Gräßlin", + "Name[es]": "Martin Gräßlin", + "Name[eu]": "Martin Gräßlin", + "Name[fi]": "Martin Gräßlin", + "Name[fr]": "Martin Gräßlin", + "Name[hu]": "Martin Gräßlin", + "Name[ia]": "Martin Gräßlin", + "Name[it]": "Martin Gräßlin", + "Name[ko]": "Martin Gräßlin", + "Name[lt]": "Martin Gräßlin", + "Name[nl]": "Martin Gräßlin", + "Name[nn]": "Martin Gräßlin", + "Name[pl]": "Martin Gräßlin", + "Name[pt_BR]": "Martin Gräßlin", + "Name[ro]": "Martin Gräßlin", + "Name[ru]": "Martin Gräßlin", + "Name[sk]": "Martin Gräßlin", + "Name[sl]": "Martin Gräßlin", + "Name[sv]": "Martin Gräßlin", + "Name[ta]": "மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Martin Gräßlin", + "Name[uk]": "Martin Gräßlin", + "Name[vi]": "Martin Gräßlin", + "Name[x-test]": "xxMartin Gräßlinxx", + "Name[zh_CN]": "Martin Gräßlin" + } + ], + "Category": "Clipboard", + "Description": "Provides access to the clipboard history", + "Description[ar]": "اطّلع على تاريخ الحافظة", + "Description[az]": "Mübadilə yaddaşı tarixçəsinə girirşi təmin edir", + "Description[ca]": "Proporciona accés a l'historial del porta-retalls", + "Description[cs]": "Poskytuje přístup k historii schránky", + "Description[de]": "Ermöglicht den Zugriff auf den Verlauf der Zwischenablage", + "Description[en_GB]": "Provides access to the clipboard history", + "Description[es]": "Proporciona acceso al historial del portapapeles", + "Description[eu]": "Arbeleko historialera sarbidea ematen du", + "Description[fi]": "Tarjoaa pääsyn leikepöytähistoriaan", + "Description[fr]": "Donne accès à l'historique du presse-papier", + "Description[hu]": "Hozzáférést nyújt a vágólap előzményeihez", + "Description[ia]": "Forni accesso al chronologia del area de transferentia", + "Description[it]": "Fornisce l'accesso alla cronologia degli appunti", + "Description[ko]": "클립보드 기록 표시", + "Description[lt]": "Suteikia prieigą prie iškarpinės istorijos", + "Description[nl]": "Biedt toegang tot de geschiedenis van klembord", + "Description[nn]": "Få tilgang til utklippstavle­loggen", + "Description[pa]": "ਕਲਿੱਪਬੋਰਡ ਅਤੀਤ ਲਈ ਪਹੁੰਚ ਦਿੰਦਾ ਹੈ", + "Description[pl]": "Zapewnia dostęp do historii schowka", + "Description[pt_BR]": "Fornece acesso ao histórico da área de transferência", + "Description[ro]": "Oferă acces la istoricul clipboard-ului", + "Description[ru]": "Предоставляет доступ к журналу буфера обмена", + "Description[sk]": "Poskytuje prístup k histórii schránky", + "Description[sl]": "Ponuja dostop do zgodovine odložišča", + "Description[sv]": "Ger tillgång till klippbordshistoriken", + "Description[ta]": "பிடிப்புப்பலகையின் வரலாற்றை அணுக உதவும்", + "Description[tr]": "Pano geçmişine erişim sağlar", + "Description[uk]": "Надає доступ до журналу буфера обміну даними", + "Description[vi]": "Cho phép truy cập lịch sử bảng nháp", + "Description[x-test]": "xxProvides access to the clipboard historyxx", + "Description[zh_CN]": "访问剪贴板历史", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "klipper", + "Id": "org.kde.plasma.clipboard", + "License": "GPL-2.0+", + "Name": "Clipboard", + "Name[ar]": "الحافظة", + "Name[ast]": "Cartafueyu", + "Name[az]": "Mübadilə buferi", + "Name[bs]": "Klipbord", + "Name[ca@valencia]": "Porta-retalls", + "Name[ca]": "Porta-retalls", + "Name[cs]": "Schránka", + "Name[da]": "Udklipsholder", + "Name[de]": "Zwischenablage", + "Name[el]": "Πρόχειρο", + "Name[en_GB]": "Clipboard", + "Name[es]": "Portapapeles", + "Name[et]": "Lõikepuhver", + "Name[eu]": "Arbela", + "Name[fi]": "Leikepöytä", + "Name[fr]": "Presse-papiers", + "Name[gl]": "Portapapeis", + "Name[he]": "לוח העתקה", + "Name[hi]": "क्लिपबोर्ड", + "Name[hsb]": "Zapisnik (Clipboard)", + "Name[hu]": "Vágólap", + "Name[ia]": "Area de transferentia", + "Name[id]": "PapanKlip", + "Name[is]": "Klippispjald", + "Name[it]": "Appunti", + "Name[ja]": "クリップボード", + "Name[ko]": "클립보드", + "Name[lt]": "Iškarpinė", + "Name[lv]": "Starpliktuve", + "Name[ml]": "ക്ലിപ്ബോര്‍ഡ്", + "Name[nb]": "Utklippstavle", + "Name[nds]": "Twischenaflaag", + "Name[nl]": "Klembord", + "Name[nn]": "Utklippstavle", + "Name[pa]": "ਕਲਿੱਪਬੋਰਡ", + "Name[pl]": "Schowek", + "Name[pt]": "Área de Transferência", + "Name[pt_BR]": "Área de transferência", + "Name[ro]": "Clipboard", + "Name[ru]": "Буфер обмена", + "Name[sk]": "Schránka", + "Name[sl]": "Odložišče", + "Name[sr@ijekavian]": "клипборд", + "Name[sr@ijekavianlatin]": "klipbord", + "Name[sr@latin]": "klipbord", + "Name[sr]": "клипборд", + "Name[sv]": "Klippbord", + "Name[ta]": "பிடிப்புப்பலகை", + "Name[tg]": "Ҳофизаи муваққатӣ", + "Name[tr]": "Pano", + "Name[uk]": "Буфер обміну", + "Name[vi]": "Bảng nháp", + "Name[x-test]": "xxClipboardxx", + "Name[zh_CN]": "剪贴板", + "Name[zh_TW]": "剪貼簿", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/clipboard.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "SystemServices" +} diff --git a/plasma/workspace/applets/devicenotifier/CMakeLists.txt b/plasma/workspace/applets/devicenotifier/CMakeLists.txt new file mode 100644 index 0000000000..23d70e2ef6 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/CMakeLists.txt @@ -0,0 +1,3 @@ +plasma_install_package(package org.kde.plasma.devicenotifier) + +install(FILES test-predicate-openinwindow.desktop DESTINATION ${KDE_INSTALL_DATADIR}/solid/actions ) diff --git a/plasma/workspace/applets/devicenotifier/Messages.sh b/plasma/workspace/applets/devicenotifier/Messages.sh new file mode 100644 index 0000000000..5d044820dc --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.devicenotifier.pot diff --git a/plasma/workspace/applets/devicenotifier/package/contents/config/main.xml b/plasma/workspace/applets/devicenotifier/package/contents/config/main.xml new file mode 100644 index 0000000000..26c9e5e6f3 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/package/contents/config/main.xml @@ -0,0 +1,27 @@ + + + + + + + + true + + + + false + + + + false + + + + true + + + + diff --git a/plasma/workspace/applets/devicenotifier/package/contents/ui/DeviceItem.qml b/plasma/workspace/applets/devicenotifier/package/contents/ui/DeviceItem.qml new file mode 100644 index 0000000000..bc0eab1649 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/package/contents/ui/DeviceItem.qml @@ -0,0 +1,283 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2012 Jacopo De Simoi + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + SPDX-FileCopyrightText: 2020 Nate Graham + SPDX-FileCopyrightText: 2022 Harald Sitter + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.12 as QQC2 +import QtQml.Models 2.14 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kquickcontrolsaddons 2.0 + +PlasmaExtras.ExpandableListItem { + id: deviceItem + + property string udi + readonly property int state: sdSource.data[udi] ? sdSource.data[udi].State : 0 + readonly property int operationResult: (model["Operation result"]) + + readonly property bool isMounted: devicenotifier.isMounted(udi) + readonly property bool hasMessage: statusSource.lastUdi == udi && statusSource.data[statusSource.last] ? true : false + readonly property var message: hasMessage ? statusSource.data[statusSource.last] || ({}) : ({}) + readonly property var types: model["Device Types"] + readonly property bool hasStorageAccess: types && types.indexOf("Storage Access") !== -1 + readonly property bool hasPortableMediaPlayer: types && types.indexOf("Portable Media Player") !== -1 + readonly property var supportedProtocols: model["Supported Protocols"] + + readonly property double freeSpace: sdSource.data[udi] && sdSource.data[udi]["Free Space"] ? sdSource.data[udi]["Free Space"] : -1.0 + readonly property double totalSpace: sdSource.data[udi] && sdSource.data[udi]["Size"] ? sdSource.data[udi]["Size"] : -1.0 + property bool freeSpaceKnown: freeSpace > 0 && totalSpace > 0 + + readonly property bool isRootVolume: sdSource.data[udi] && sdSource.data[udi]["File Path"] ? sdSource.data[udi]["File Path"] == "/" : false + readonly property bool isRemovable: sdSource.data[udi] && sdSource.data[udi]["Removable"] ? sdSource.data[udi]["Removable"] : false + + onOperationResultChanged: { + if (!popupIconTimer.running) { + if (operationResult == 1) { + devicenotifier.popupIcon = "dialog-ok" + popupIconTimer.restart() + } else if (operationResult == 2) { + devicenotifier.popupIcon = "dialog-error" + popupIconTimer.restart() + } + } + } + + onHasMessageChanged: { + if (deviceItem.hasMessage) { + messageHighlight.highlight(this) + } + } + + Connections { + target: unmountAll + function onClicked() { + removableActionTriggered(); + } + } + + Connections { + target: plasmoid.action("unmountAllDevices") + function onTriggered() { + removableActionTriggered(); + } + } + + // this keeps the delegate around for 5 seconds after the device has been + // removed in case there was a message, such as "you can now safely remove this" + ListView.onRemove: SequentialAnimation { + PropertyAction { target: deviceItem; property: "ListView.delayRemove"; value: deviceItem.hasMessage } + PropertyAction { target: deviceItem; property: "isEnabled"; value: false } + // Reset action model to hide the arrow + PropertyAction { target: deviceItem; property: "contextualActionsModel"; value: [] } + PropertyAction { target: deviceItem; property: "icon"; value: statusSource.lastIcon } + PropertyAction { target: deviceItem; property: "title"; value: statusSource.lastDescription } + PropertyAction { target: deviceItem; property: "subtitle"; value: statusSource.lastMessage } + PauseAnimation { duration: messageHighlightAnimator.duration } + // Otherwise the last message will show again when this device reappears + ScriptAction { script: statusSource.clearMessage(); } + // Otherwise there are briefly multiple highlight effects + PropertyAction { target: devicenotifier; property: "currentIndex"; value: -1 } + PropertyAction { target: deviceItem; property: "ListView.delayRemove"; value: false } + } + + Timer { + id: updateStorageSpaceTimer + interval: 5000 + repeat: true + running: isMounted && plasmoid.expanded + triggeredOnStart: true // Update the storage space as soon as we open the plasmoid + onTriggered: { + const service = sdSource.serviceForSource(udi); + const operation = service.operationDescription("updateFreespace"); + service.startOperationCall(operation); + } + } + + Timer { + id: unmountTimer + interval: 1000 + repeat: false + } + + Component { + id: deviceActionComponent + QQC2.Action { } + } + + function removableActionTriggered() { + if (model["Removable"] && isMounted) { + actionTriggered(); + } + } + + function actionTriggered() { + let service + let operationName + let operation + const wasMounted = isMounted; + if (!hasStorageAccess || !isRemovable || !isMounted) { + service = hpSource.serviceForSource(udi); + operation = service.operationDescription('invokeAction'); + const supportsMTP = supportedProtocols && supportedProtocols.indexOf("mtp") !== -1 + if (!hasStorageAccess && hasPortableMediaPlayer && supportsMTP) { + operation.predicate = "solid_mtp.desktop" // this lives in kio-extras! + } else { + operation.predicate = "test-predicate-openinwindow.desktop"; + } + } else { + service = sdSource.serviceForSource(udi); + operation = service.operationDescription("unmount"); + unmountTimer.restart(); + } + service.startOperationCall(operation); + if (wasMounted) { + deviceItem.collapse(); + } + } + + + // When there's no better icon available, show a placeholder icon instead + // of nothing + icon: sdSource.data[udi] ? sdSource.data[udi].Icon : "device-notifier" + + iconEmblem: { + if (deviceItem.hasMessage) { + if (deviceItem.message.solidError === 0) { + return "emblem-information" + } else { + return "emblem-error" + } + } else if (deviceItem.state == 0 && Emblems && Emblems[0]) { + return Emblems[0] + } else { + return "" + } + } + + title: sdSource.data[udi] ? sdSource.data[udi].Description : "" + + subtitle: { + if (deviceItem.hasMessage) { + return deviceItem.message.error + } + if (deviceItem.state == 0) { + if (!hpSource.data[udi]) { + return "" + } + if (freeSpaceKnown) { + const freeSpaceText = sdSource.data[udi]["Free Space Text"] + const totalSpaceText = sdSource.data[udi]["Size Text"] + return i18nc("@info:status Free disk space", "%1 free of %2", freeSpaceText, totalSpaceText) + } + return "" + } else if (deviceItem.state == 1) { + return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Accessing…") + } else if (unmountTimer.running) { + // Unmounting, shown if unmount takes less than 1 second + return i18nc("Removing is a less technical word for Unmounting; translation should be short and mean \'Currently unmounting this device\'", "Removing…") + } else { + // Unmounting; shown if unmount takes longer than 1 second + return i18n("Don't unplug yet! Files are still being transferred...") + } + } + + subtitleCanWrap: true + + // Color the subtitle red for disks with less than 5% free space + subtitleColor: { + if (freeSpaceKnown) { + if (freeSpace / totalSpace <= 0.05) { + return PlasmaCore.Theme.negativeTextColor + } + } + return PlasmaCore.Theme.textColor + } + + defaultActionButtonAction: QQC2.Action { + icon.name: { + if (isRemovable) { + return isMounted ? "media-eject" : "document-open-folder" + } else { + return "document-open-folder" + } + } + text: { + // TODO: this entire logic and the semi-replication thereof in actionTriggered is really silly. + // We have a fairly exhaustive predicate system, we should use it to assertain if a given udi is actionable + // and then we simply pick the sensible default action of a suitable predicate. + // - It's possible for there to be no StorageAccess (e.g. MTP devices don't have one) + // - It's possible for the root volume to be on a removable disk + if (!hasStorageAccess || !isRemovable || isRootVolume) { + return i18n("Open in File Manager") + } else { + if (!isMounted) { + return i18n("Mount and Open") + } else if (types && types.indexOf("OpticalDisc") !== -1) { + return i18n("Eject") + } else { + return i18n("Safely remove") + } + } + } + onTriggered: actionTriggered() + } + + isBusy: deviceItem.state != 0 + + // We need a JS array full of QQC2 actions; this Instantiator creates them + // from the actions list of the data source + Instantiator { + model: hpSource.data[udi] ? hpSource.data[udi].actions : [] + delegate: QQC2.Action { + text: modelData.text + icon.name: modelData.icon + // We only want to show the "Show in file manager" action for + // mounted removable disks (for everything else, this action is + // already the primary one shown on the list item) + enabled: { + if (modelData.predicate != "test-predicate-openinwindow.desktop") { + return true; + } + return deviceItem.isRemovable && deviceItem.isMounted; + } + onTriggered: { + const service = hpSource.serviceForSource(udi); + const operation = service.operationDescription('invokeAction'); + operation.predicate = modelData.predicate; + service.startOperationCall(operation); + devicenotifier.currentIndex = -1; + } + } + onObjectAdded: contextualActionsModel.push(object) + } + + // "Mount" action that does not open it in the file manager + QQC2.Action { + id: mountWithoutOpeningAction + + text: i18n("Mount") + icon.name: "media-mount" + + // Only show for unmounted removable devices + enabled: deviceItem.isRemovable && !deviceItem.isMounted + + onTriggered: { + const service = sdSource.serviceForSource(udi); + const operation = service.operationDescription("mount"); + service.startOperationCall(operation); + } + } + + Component.onCompleted: { + contextualActionsModel.push(mountWithoutOpeningAction); + } +} diff --git a/plasma/workspace/applets/devicenotifier/package/contents/ui/FullRepresentation.qml b/plasma/workspace/applets/devicenotifier/package/contents/ui/FullRepresentation.qml new file mode 100644 index 0000000000..1c13a025a4 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/package/contents/ui/FullRepresentation.qml @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2012 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 David Edmundson + SPDX-FileCopyrightText: 2014 Marco Martin + SPDX-FileCopyrightText: 2020 Nate Graham + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.2 +import QtQuick.Window 2.2 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For Highlight +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +PlasmaExtras.Representation { + id: fullRep + property bool spontaneousOpen: false + + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 12 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 12 + + collapseMarginsHint: true + + header: PlasmaExtras.PlasmoidHeading { + visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) && devicenotifier.mountedRemovables > 1 + PlasmaComponents3.ToolButton { + id: unmountAll + anchors.right: parent.right + visible: devicenotifier.mountedRemovables > 1; + + icon.name: "media-eject" + text: i18n("Remove All") + + PlasmaComponents3.ToolTip { + text: i18n("Click to safely remove all devices") + } + } + } + + MouseArea { + id: fullRepMouseArea + hoverEnabled: true + } + + PlasmaCore.DataSource { + id: userActivitySource + engine: "powermanagement" + connectedSources: "UserActivity" + property int polls: 0 + //poll only on plasmoid expanded + interval: !fullRepMouseArea.containsMouse && !fullRep.Window.active && spontaneousOpen && plasmoid.expanded ? 3000 : 0 + onIntervalChanged: polls = 0; + onDataChanged: { + //only do when polling + if (interval == 0 || polls++ < 1) { + return; + } + + if (userActivitySource.data["UserActivity"]["IdleTime"] < interval) { + plasmoid.expanded = false; + spontaneousOpen = false; + } + } + } + + + // this item is reparented to a delegate that is showing a message to draw focus to it + PlasmaComponents.Highlight { + id: messageHighlight + visible: false + + OpacityAnimator { + id: messageHighlightAnimator + target: messageHighlight + from: 1 + to: 0 + duration: PlasmaCore.Units.veryLongDuration * 8 + easing.type: Easing.InOutQuad + Component.onCompleted: devicenotifier.isMessageHighlightAnimatorRunning = Qt.binding(() => running); + } + + Connections { + target: statusSource + function onLastChanged() { + if (!statusSource.last) { + messageHighlightAnimator.stop() + messageHighlight.visible = false + } + } + } + + function highlight(item) { + parent = item + width = Qt.binding(function() { return item.width }) + height = Qt.binding(function() { return item.height }) + opacity = 1 // Animator is threaded so the old opacity might be visible for a frame or two + visible = true + messageHighlightAnimator.start() + } + } + + Connections { + target: plasmoid + function onExpandedChanged() { + if (!plasmoid.expanded) { + statusSource.clearMessage(); + } + } + } + + PlasmaComponents3.ScrollView { + id: scrollView + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + anchors.fill: parent + contentWidth: availableWidth - contentItem.leftMargin - contentItem.rightMargin + + contentItem: ListView { + id: notifierDialog + focus: true + + model: filterModel + + delegate: DeviceItem { + udi: DataEngineSource + } + highlight: PlasmaComponents.Highlight { } + highlightMoveDuration: 0 + highlightResizeDuration: 0 + + topMargin: PlasmaCore.Units.smallSpacing * 2 + bottomMargin: PlasmaCore.Units.smallSpacing * 2 + leftMargin: PlasmaCore.Units.smallSpacing * 2 + rightMargin: PlasmaCore.Units.smallSpacing * 2 + spacing: PlasmaCore.Units.smallSpacing + + currentIndex: devicenotifier.currentIndex + + //this is needed to make SectionScroller actually work + //acceptable since one doesn't have a billion of devices + cacheBuffer: 1000 + + // FIXME: the model is sorted by timestamp, not type, this results in sections possibly getting listed + // multiple times + section { + property: "Type Description" + delegate: Item { + height: Math.floor(childrenRect.height) + width: notifierDialog.width - (scrollView.PlasmaComponents3.ScrollBar.vertical.visible ? PlasmaCore.Units.smallSpacing * 4 : 0) + PlasmaExtras.Heading { + level: 3 + opacity: 0.6 + text: section + } + } + } + + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + text: plasmoid.configuration.removableDevices ? i18n("No removable devices attached") : i18n("No disks available") + visible: notifierDialog.count === 0 && !messageHighlightAnimator.running + } + } + } +} diff --git a/plasma/workspace/applets/devicenotifier/package/contents/ui/devicenotifier.qml b/plasma/workspace/applets/devicenotifier/package/contents/ui/devicenotifier.qml new file mode 100644 index 0000000000..c6bd0e73f2 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/package/contents/ui/devicenotifier.qml @@ -0,0 +1,347 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2012 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 David Edmundson + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kquickcontrolsaddons 2.0 // For KCMShell + +Item { + id: devicenotifier + + readonly property bool openAutomounterKcmAuthorized: KCMShell.authorize("device_automounter_kcm.desktop").length > 0 + + property string devicesType: { + if (plasmoid.configuration.allDevices) { + return "all" + } else if (plasmoid.configuration.removableDevices) { + return "removable" + } else { + return "nonRemovable" + } + } + property string popupIcon: "device-notifier" + + property bool itemClicked: false + property int currentIndex: -1 + property var connectedRemovables: [] + property int mountedRemovables: 0 + + // QTBUG-50380: As soon as the item gets removed from the model, all of ListView's + // properties (count, contentHeight) pretend the delegate doesn't exist anymore + // causing our "No devices" heading to overlap with the remaining device + property bool isMessageHighlightAnimatorRunning: false + + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 10 + Plasmoid.switchHeight: PlasmaCore.Units.gridUnit * 10 + + Plasmoid.toolTipMainText: filterModel.count > 0 && filterModel.get(0) ? i18n("Most Recent Device") : i18n("No Devices Available") + Plasmoid.toolTipSubText: { + if (filterModel.count > 0) { + var data = filterModel.get(0) + if (data && data.Description) { + return data.Description + } + } + return "" + } + Plasmoid.icon: { + if (filterModel.count > 0) { + var data = filterModel.get(0) + if (data && data.Icon) { + return data.Icon + } + } + return "device-notifier" + } + + Plasmoid.status: (filterModel.count > 0 || isMessageHighlightAnimatorRunning) ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus + + PlasmaCore.DataSource { + id: hpSource + engine: "hotplug" + connectedSources: sources + interval: 0 + + onSourceAdded: { + disconnectSource(source); + connectSource(source); + sdSource.connectedSources = sources + } + onSourceRemoved: { + disconnectSource(source); + } + } + + Plasmoid.compactRepresentation: PlasmaCore.IconItem { + source: devicenotifier.popupIcon + width: PlasmaCore.Units.iconSizes.medium; + height: PlasmaCore.Units.iconSizes.medium; + active: compactMouse.containsMouse + MouseArea { + id: compactMouse + anchors.fill: parent + hoverEnabled: true + onClicked: plasmoid.expanded = !plasmoid.expanded + } + } + Plasmoid.fullRepresentation: FullRepresentation {} + + PlasmaCore.DataSource { + id: sdSource + engine: "soliddevice" + interval: 0 + property string last + onSourceAdded: { + disconnectSource(source); + connectSource(source); + last = source; + processLastDevice(true); + if (data[source].Removable) { + devicenotifier.connectedRemovables.push(source); + devicenotifier.connectedRemovables = devicenotifier.connectedRemovables; + } + } + + onSourceRemoved: { + disconnectSource(source); + var index = devicenotifier.connectedRemovables.indexOf(source); + if (index >= 0) { + devicenotifier.connectedRemovables.splice(index, 1); + devicenotifier.connectedRemovables = devicenotifier.connectedRemovables; + } + } + + onDataChanged: { + processLastDevice(true); + var counter = 0; + for (var i = 0; i < devicenotifier.connectedRemovables.length; i++) { + if (isMounted(devicenotifier.connectedRemovables[i])) { + counter++; + } + } + if (counter !== devicenotifier.mountedRemovables) { + devicenotifier.mountedRemovables = counter; + } + } + + onNewData: { + last = sourceName; + processLastDevice(false); + } + + function isViableDevice(udi) { + if (devicesType === "all") { + return true; + } + + var device = data[udi]; + if (!device) { + return false; + } + + return (devicesType === "removable" && device.Removable) + || (devicesType === "nonRemovable" && !device.Removable); + } + + function processLastDevice(expand) { + if (last && isViableDevice(last)) { + if (expand && hpSource.data[last] && hpSource.data[last].added) { + devicenotifier.popupIcon = "preferences-desktop-notification"; + expandTimer.restart(); + popupIconTimer.restart(); + } + last = ""; + } + } + } + + PlasmaCore.SortFilterModel { + id: filterModel + sourceModel: PlasmaCore.DataModel { + dataSource: sdSource + } + filterRole: "Removable" + filterRegExp: { + if (devicesType === "removable") { + return "true" + } else if (devicesType === "nonRemovable") { + return "false" + } else { + return "" + } + } + sortRole: "Timestamp" + sortOrder: Qt.DescendingOrder + } + + PlasmaCore.DataSource { + id: statusSource + engine: "devicenotifications" + property string last + property string lastUdi + property string lastDescription + property string lastMessage + property string lastIcon + onSourceAdded: { + last = source; + disconnectSource(source); + connectSource(source); + } + onSourceRemoved: disconnectSource(source) + onDataChanged: { + if (last) { + lastUdi = data[last].udi + lastDescription = sdSource.data[lastUdi] ? sdSource.data[lastUdi].Description : "" + lastMessage = data[last].error + lastIcon = sdSource.data[lastUdi] ? sdSource.data[lastUdi].Icon : "device-notifier" + + if (sdSource.isViableDevice(lastUdi)) { + plasmoid.expanded = true + plasmoid.fullRepresentationItem.spontaneousOpen = true; + } + } + } + + function clearMessage() { + last = "" + lastUdi = "" + lastDescription = "" + lastMessage = "" + lastIcon = "" + } + } + + property var showRemovableDevicesAction + property var showNonRemovableDevicesAction + property var showAllDevicesAction + property var openAutomaticallyAction + + Component.onCompleted: { + if (sdSource.connectedSources.count === 0) { + Plasmoid.status = PlasmaCore.Types.PassiveStatus; + } + + plasmoid.setAction("unmountAllDevices", i18n("Remove All"), "media-eject"); + plasmoid.action("unmountAllDevices").visible = Qt.binding(() => { + return devicenotifier.mountedRemovables > 1; + }); + + plasmoid.setActionSeparator("sep0"); + + plasmoid.setAction("showRemovableDevices", i18n("Removable Devices"), "drive-removable-media"); + devicenotifier.showRemovableDevicesAction = plasmoid.action("showRemovableDevices"); + devicenotifier.showRemovableDevicesAction.checkable = true; + devicenotifier.showRemovableDevicesAction.checked = Qt.binding(() => {return plasmoid.configuration.removableDevices;}); + plasmoid.setActionGroup("showRemovableDevices", "devicesShown"); + + plasmoid.setAction("showNonRemovableDevices", i18n("Non Removable Devices"), "drive-harddisk"); + devicenotifier.showNonRemovableDevicesAction = plasmoid.action("showNonRemovableDevices"); + devicenotifier.showNonRemovableDevicesAction.checkable = true; + devicenotifier.showNonRemovableDevicesAction.checked = Qt.binding(() => {return plasmoid.configuration.nonRemovableDevices;}); + plasmoid.setActionGroup("showNonRemovableDevices", "devicesShown"); + + plasmoid.setAction("showAllDevices", i18n("All Devices")); + devicenotifier.showAllDevicesAction = plasmoid.action("showAllDevices"); + devicenotifier.showAllDevicesAction.checkable = true; + devicenotifier.showAllDevicesAction.checked = Qt.binding(() => {return plasmoid.configuration.allDevices;}); + plasmoid.setActionGroup("showAllDevices", "devicesShown"); + + plasmoid.setActionSeparator("sep"); + + plasmoid.setAction("openAutomatically", i18n("Show popup when new device is plugged in")); + devicenotifier.openAutomaticallyAction = plasmoid.action("openAutomatically"); + devicenotifier.openAutomaticallyAction.checkable = true; + devicenotifier.openAutomaticallyAction.checked = Qt.binding(() => {return plasmoid.configuration.popupOnNewDevice;}); + + plasmoid.setActionSeparator("sep2"); + + if (devicenotifier.openAutomounterKcmAuthorized) { + plasmoid.removeAction("configure"); + plasmoid.setAction("configure", i18nc("Open auto mounter kcm", "Configure Removable Devices…"), "configure") + } + } + + function action_configure() { + KCMShell.openSystemSettings("kcm_device_automounter") + } + + function action_showRemovableDevices() { + plasmoid.configuration.removableDevices = true; + plasmoid.configuration.nonRemovableDevices = false; + plasmoid.configuration.allDevices = false; + } + + function action_showNonRemovableDevices() { + plasmoid.configuration.removableDevices = false; + plasmoid.configuration.nonRemovableDevices = true; + plasmoid.configuration.allDevices = false; + } + + function action_showAllDevices() { + plasmoid.configuration.removableDevices = false; + plasmoid.configuration.nonRemovableDevices = false; + plasmoid.configuration.allDevices = true; + } + + function action_openAutomatically() { + plasmoid.configuration.popupOnNewDevice = !plasmoid.configuration.popupOnNewDevice; + } + + Plasmoid.onExpandedChanged: { + popupEventSlot(plasmoid.expanded); + } + + function popupEventSlot(popped) { + if (!popped) { + // reset the property that lets us remember if an item was clicked + // (versus only hovered) for autohide purposes + devicenotifier.itemClicked = true; + devicenotifier.currentIndex = -1; + } + } + + function isMounted(udi) { + if (!sdSource.data[udi]) { + return false; + } + + var types = sdSource.data[udi]["Device Types"]; + if (types.indexOf("Storage Access") >= 0) { + return sdSource.data[udi]["Accessible"]; + } + + return (types.indexOf("Storage Volume") >= 0 && types.indexOf("OpticalDisc") >= 0) + } + + Timer { + id: popupIconTimer + interval: 3000 + onTriggered: devicenotifier.popupIcon = "device-notifier"; + } + + Timer { + id: expandTimer + interval: 250 + onTriggered: { + // We don't show a UI for it, but there is a hidden option to not + // show the popup on new device attachment if the user has added + // the text "popupOnNewDevice=false" to their + // plasma-org.kde.plasma.desktop-appletsrc file. + if (plasmoid.configuration.popupOnNewDevice) { // Bug 351592 + plasmoid.expanded = true; + plasmoid.fullRepresentationItem.spontaneousOpen = true; + } + } + } + +} diff --git a/plasma/workspace/applets/devicenotifier/package/metadata.json b/plasma/workspace/applets/devicenotifier/package/metadata.json new file mode 100644 index 0000000000..8dcd738b17 --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/package/metadata.json @@ -0,0 +1,133 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "wilderkde@gmail.com", + "Name": "Viranch Mehta, Jacopo De Simoi", + "Name[ar]": "Viranch Mehta, Jacopo De Simoi", + "Name[az]": "Viranch Mehta, Jacopo De Simoi", + "Name[ca]": "Viranch Mehta, Jacopo De Simoi", + "Name[cs]": "Viranch Mehta, Jacopo De Simoi", + "Name[de]": "Viranch Mehta, Jacopo De Simoi", + "Name[en_GB]": "Viranch Mehta, Jacopo De Simoi", + "Name[es]": "Viranch Mehta, Jacopo De Simoi", + "Name[eu]": "Viranch Mehta, Jacopo De Simoi", + "Name[fi]": "Viranch Mehta, Jacopo De Simoi", + "Name[fr]": "Viranch Mehta, Jacopo De Simoi", + "Name[hu]": "Viranch Mehta, Jacopo De Simoi", + "Name[ia]": "Viranch Mehta, Jacopo De Simoi", + "Name[it]": "Viranch Mehta, Jacopo De Simoi", + "Name[ko]": "Viranch Mehta, Jacopo De Simoi", + "Name[lt]": "Viranch Mehta, Jacopo De Simoi", + "Name[nl]": "Viranch Mehta, Jacopo De Simoi", + "Name[nn]": "Viranch Mehta, Jacopo De Simoi", + "Name[pl]": "Viranch Mehta, Jacopo De Simoi", + "Name[pt_BR]": "Viranch Mehta, Jacopo De Simoi", + "Name[ro]": "Viranch Mehta, Jacopo De Simoi", + "Name[ru]": "Viranch Mehta, Jacopo De Simoi", + "Name[sk]": "Viranch Mehta, Jacopo De Simoi", + "Name[sl]": "Viranch Mehta, Jacopo De Simoi", + "Name[sv]": "Viranch Mehta", + "Name[tr]": "Viranch Mehta, Jacopo De Simoi", + "Name[uk]": "Viranch Mehta, Jacopo De Simoi", + "Name[vi]": "Viranch Mehta, Jacopo De Simoi", + "Name[x-test]": "xxViranch Mehta, Jacopo De Simoixx", + "Name[zh_CN]": "Viranch Mehta, Jacopo De Simoi" + } + ], + "Category": "System Information", + "Description": "Notifications and access for devices", + "Description[ar]": "إخطارات ووصول الأجهزة", + "Description[az]": "Cihazlar üçün bildirişlər və onlara giriş", + "Description[ca]": "Notificacions i accés als dispositius", + "Description[cs]": "Upozorňování a přístup k zařízením", + "Description[de]": "Benachrichtigungen und Zugriff für Geräte", + "Description[en_GB]": "Notifications and access for devices", + "Description[es]": "Notificaciones y accesos de dispositivos", + "Description[eu]": "Gailuetarako jakinarazpenak eta sarbidea", + "Description[fi]": "Ilmoitukset ja pääsy laitteisiin", + "Description[fr]": "Notifications et accès aux périphériques", + "Description[hu]": "Eszközök elérése, értesítő üzenetek kezelése", + "Description[ia]": "Notificationes e accesso pro dispositivos", + "Description[it]": "Notifiche e accesso per dispositivi", + "Description[ko]": "장치가 연결된 것을 알려 주고 접근할 수 있도록 합니다", + "Description[lt]": "Pranešimai apie įrenginius ir prieiga prie jų", + "Description[nl]": "Meldingen en toegang voor apparaten", + "Description[nn]": "Varsling og tilgang til einingar", + "Description[pa]": "ਡਿਵਾਈਸਾਂ ਲਈ ਨੋਟੀਫਿਕੇਸ਼ਨ ਅਤੇ ਪਹੁੰਚ", + "Description[pl]": "Powiadomienia i dostęp do urządzeń", + "Description[pt_BR]": "Notificações e acesso aos dispositivos", + "Description[ro]": "Notificări și acces pentru dispozitive", + "Description[ru]": "Уведомление о подключаемых устройствах и доступ к ним", + "Description[sk]": "Upozornenia a prístup k zariadeniam", + "Description[sl]": "Obvestila in dostop do naprav", + "Description[sv]": "Underrättelser och åtkomst av enheter", + "Description[ta]": "புதிய சாதனங்களுக்கான அறிவிப்புகள் மற்றும் அணுகல்", + "Description[tr]": "Aygıt erişimi ve bildirimler", + "Description[uk]": "Сповіщення і доступ до пристроїв", + "Description[vi]": "Thông báo và cách truy cập cho các thiết bị", + "Description[x-test]": "xxNotifications and access for devicesxx", + "Description[zh_CN]": "设备的通知和访问", + "EnabledByDefault": true, + "FormFactors": [ + "tablet", + "desktop" + ], + "Icon": "device-notifier", + "Id": "org.kde.plasma.devicenotifier", + "License": "GPL-2.0+", + "Name": "Disks & Devices", + "Name[ar]": "الأجهزة والأقراص", + "Name[ast]": "Discos y preseos", + "Name[az]": "Disklər və Qurğular", + "Name[ca]": "Discs i dispositius", + "Name[cs]": "Disky & Zařízení", + "Name[da]": "Diske og enheder", + "Name[de]": "Datenträger & Geräte", + "Name[en_GB]": "Disks & Devices", + "Name[es]": "Discos y dispositivos", + "Name[et]": "Kettad ja seadmed", + "Name[eu]": "Diskoak eta gailuak", + "Name[fi]": "Levyt ja laitteet", + "Name[fr]": "Disques & périphériques", + "Name[hi]": "डिस्क और उपकरण", + "Name[hsb]": "Tačele a graty", + "Name[hu]": "Lemezek és eszközök", + "Name[ia]": "Discos & Dispositivos", + "Name[id]": "Perangkat & Disk", + "Name[it]": "Dischi e dispositivi", + "Name[ko]": "디스크와 장치", + "Name[lt]": "Diskai ir įrenginiai", + "Name[ml]": "ഡിസ്കുകളും ഉപകരണങ്ങളും", + "Name[nl]": "Schijven & apparaten", + "Name[nn]": "Diskar og einingar", + "Name[pa]": "ਡਿਸਕ ਤੇ ਡਿਵਾਈਸ", + "Name[pl]": "Dyski i urządzenia", + "Name[pt]": "Discos & Dispositivos", + "Name[pt_BR]": "Discos e dispositivos", + "Name[ro]": "Disc și dispozitive", + "Name[ru]": "Диски и устройства", + "Name[sk]": "Disky a zariadenia", + "Name[sl]": "Diski in naprave", + "Name[sv]": "Diskar och enheter", + "Name[ta]": "வட்டுகளும் சாதனங்களும்", + "Name[tg]": "Дискҳо ва дастгоҳҳо", + "Name[tr]": "Diskler ve Cihazlar", + "Name[uk]": "Диски і пристрої", + "Name[vi]": "Đĩa & Thiết bị", + "Name[x-test]": "xxDisks & Devicesxx", + "Name[zh_CN]": "磁盘和设备", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://userbase.kde.org/Plasma/DeviceNotifier" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/devicenotifier.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "Hardware", + "X-Plasma-Provides": [ + "org.kde.plasma.removabledevices" + ] +} diff --git a/plasma/workspace/applets/devicenotifier/test-predicate-openinwindow.desktop b/plasma/workspace/applets/devicenotifier/test-predicate-openinwindow.desktop new file mode 100644 index 0000000000..56d5fddc6f --- /dev/null +++ b/plasma/workspace/applets/devicenotifier/test-predicate-openinwindow.desktop @@ -0,0 +1,78 @@ +[Desktop Entry] +X-KDE-Solid-Predicate=[ [ [ StorageVolume.ignored == false AND StorageVolume.usage == 'FileSystem' ] OR [ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ] ] OR StorageAccess.ignored == false ] +Type=Service +Actions=open; + +[Desktop Action open] +Name=Open with File Manager +Name[ar]=افتح بمدير الملفات +Name[az]=Fayl menecerində açmaq +Name[bg]=Отваряне с файлов мениджър +Name[bs]=Otvori menadžerom datoteka +Name[ca]=Obre amb el gestor de fitxers +Name[ca@valencia]=Obri amb el gestor de fitxers +Name[cs]=Otevřít ve správci souborů +Name[csb]=Òtemkni w &menadzerze lopków +Name[da]=Åbn med filhåndtering +Name[de]=Mit Dateiverwaltung öffnen +Name[el]=Άνοιγμα με τον διαχειριστή αρχείων +Name[en_GB]=Open with File Manager +Name[es]=Abrir con el navegador de archivos +Name[et]=Ava failihalduriga +Name[eu]=Ireki fitxategi-kudeatzailearekin +Name[fi]=Avaa tiedostonhallinnassa +Name[fr]=Ouvrir dans le gestionnaire de fichiers +Name[fy]=Iepenje mei triembehearder +Name[ga]=Oscail le Bainisteoir na gComhad +Name[gl]=Abrir co xestor de ficheiros +Name[gu]=ફાઇલ વ્યવસ્થાપક સાથે ખોલો +Name[he]=פתיחה באמצעות מנהל הקבצים +Name[hi]=फ़ाइल प्रबंधक से खोलें +Name[hr]=Otvori pomoću upravitelja datoteka +Name[hsb]=Z datajowym rjadowakom wočinić +Name[hu]=Megnyitás a fájlkezelővel +Name[ia]=Aperi con le gerente de files +Name[id]=Buka dengan Pengelola File +Name[is]=Opna með skráastjóra +Name[it]=Apri con il gestore dei file +Name[ja]=ファイルマネージャで開く +Name[kk]=Файл менеджерінде ашу +Name[km]=បើក​ជា​មួយ​កម្មវិធី​គ្រប់គ្រង​ឯកសារ +Name[kn]=ಕಡತ ವ್ಯವಸ್ಥಾಪಕದೊಂದಿಗೆ ತೆರೆ +Name[ko]=파일 관리자로 열기 +Name[lt]=Atverti naudojant failų tvarkytuvę +Name[lv]=Atvērt datņu pārvaldniekā +Name[mk]=Отвори со менаџер на датотеки +Name[ml]=ഫയലുകളുടെ നടത്തിപ്പുകാരനില്‍ തുറക്കുക +Name[mr]=फाईल व्यवस्थापकात उघडा +Name[nb]=Åpne med filbehandler +Name[nds]=Mit Dateipleger opmaken +Name[nl]=Openen met bestandsbeheerder +Name[nn]=Opna i filhandsamar +Name[pa]=ਫਾਇਲ ਮੈਨੇਜਰ ਨਾਲ ਖੋਲ੍ਹੋ +Name[pl]=Otwórz w przeglądarce plików +Name[pt]=Abrir com o Gestor de Ficheiros +Name[pt_BR]=Abrir com o gerenciador de arquivos +Name[ro]=Deschide cu gestionarul de fișiere +Name[ru]=Открыть в диспетчере файлов +Name[si]=ගොනු කළමණාකරු සමඟ ආරම්භ කරන්න +Name[sk]=Otvoriť v správcovi súborov +Name[sl]=Odpri v upravljalniku datotek +Name[sr]=Отвори менаџером фајлова +Name[sr@ijekavian]=Отвори менаџером фајлова +Name[sr@ijekavianlatin]=Otvori menadžerom fajlova +Name[sr@latin]=Otvori menadžerom fajlova +Name[sv]=Öppna med filhanterare +Name[ta]=கோப்பு மேலாளரைக் கொண்டு திற +Name[tg]=Кушодан ба воситаи мудири файлҳо +Name[th]=เปิดใช้งานผ่านเครื่องมือจัดการแฟ้ม +Name[tr]=Dosya Yöneticisi ile aç +Name[ug]=ھۆججەت باشقۇرغۇدا ئاچ +Name[uk]=Відкрити за допомогою менеджера файлів +Name[vi]=Mở bằng Trình quản lí tệp +Name[wa]=Drovi avou l' manaedjeu des fitchîs +Name[x-test]=xxOpen with File Managerxx +Name[zh_CN]=用文件管理器打开 +Name[zh_TW]=使用檔案管理員開啟 +Exec=kde-open5 "%f" +Icon=system-file-manager diff --git a/plasma/workspace/applets/digital-clock/CMakeLists.txt b/plasma/workspace/applets/digital-clock/CMakeLists.txt new file mode 100644 index 0000000000..7eed012ffe --- /dev/null +++ b/plasma/workspace/applets/digital-clock/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(plugin) + +plasma_install_package(package org.kde.plasma.digitalclock) diff --git a/plasma/workspace/applets/digital-clock/Messages.sh b/plasma/workspace/applets/digital-clock/Messages.sh new file mode 100644 index 0000000000..7fe4ba13fb --- /dev/null +++ b/plasma/workspace/applets/digital-clock/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.digitalclock.pot diff --git a/plasma/workspace/applets/digital-clock/package/contents/config/config.qml b/plasma/workspace/applets/digital-clock/package/contents/config/config.qml new file mode 100644 index 0000000000..53d525f1dd --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/config/config.qml @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2013 Bhushan Shah + SPDX-FileCopyrightText: 2015 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 +import QtQml 2.2 + +import org.kde.plasma.configuration 2.0 +import org.kde.plasma.calendar 2.0 as PlasmaCalendar + +ConfigModel { + id: configModel + + ConfigCategory { + name: i18n("Appearance") + icon: "preferences-desktop-color" + source: "configAppearance.qml" + } + ConfigCategory { + name: i18n("Calendar") + icon: "office-calendar" + source: "configCalendar.qml" + } + ConfigCategory { + name: i18n("Time Zones") + icon: "preferences-system-time" + source: "configTimeZones.qml" + } + + property Instantiator __eventPlugins: Instantiator { + model: PlasmaCalendar.EventPluginsManager.model + delegate: ConfigCategory { + name: model.display + icon: model.decoration + source: model.configUi + visible: plasmoid.configuration.enabledCalendarPlugins.indexOf(model.pluginPath) > -1 + } + + onObjectAdded: configModel.appendCategory(object) + onObjectRemoved: configModel.removeCategory(object) + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/config/main.xml b/plasma/workspace/applets/digital-clock/package/contents/config/main.xml new file mode 100644 index 0000000000..eb86dd4a59 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/config/main.xml @@ -0,0 +1,95 @@ + + + + + + + + false + + + + false + + + + true + + + + shortDate + + + + ddd d + + + + + + + + false + + + + false + + + default + + + + Local + + + + Local + + + + false + + + + + + + + + Code + + + + false + + + + 1 + + + + -1 + + + + + + + + false + + + + + + + + + Adaptive + + + diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/CalendarView.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/CalendarView.qml new file mode 100644 index 0000000000..e9ed0d559c --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/CalendarView.qml @@ -0,0 +1,633 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Martin Klapetek + SPDX-FileCopyrightText: 2021 Carl Schwan + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import QtQml 2.15 + +import org.kde.kquickcontrolsaddons 2.0 // For kcmshell +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.calendar 2.0 as PlasmaCalendar +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.private.digitalclock 1.0 + +// Top-level layout containing: +// - Left column with world clock and agenda view +// - Right column with current date header and calendar +PlasmaExtras.Representation { + id: calendar + + // The "sensible" values + property int _minimumWidth: (calendar.showAgenda || calendar.showClocks) ? PlasmaCore.Units.gridUnit * 45 : PlasmaCore.Units.gridUnit * 22 + property int _minimumHeight: PlasmaCore.Units.gridUnit * 25 + + PlasmaCore.ColorScope.inherit: false + PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.NormalColorGroup + + Layout.minimumWidth: _minimumWidth + Layout.minimumHeight: _minimumHeight + Layout.preferredWidth: _minimumWidth + Layout.preferredHeight: _minimumHeight + Layout.maximumWidth: _minimumWidth + Layout.maximumHeight: _minimumHeight + + collapseMarginsHint: true + + readonly property int paddings: PlasmaCore.Units.smallSpacing + readonly property bool showAgenda: PlasmaCalendar.EventPluginsManager.enabledPlugins.length > 0 + readonly property bool showClocks: plasmoid.configuration.selectedTimeZones.length > 1 + + property alias borderWidth: monthView.borderWidth + property alias monthView: monthView + + property bool debug: false + + property bool isExpanded: plasmoid.expanded + + onIsExpandedChanged: { + // clear all the selections when the plasmoid is showing/hiding + monthView.resetToToday(); + } + + // Header containing date and pin button + header: PlasmaExtras.PlasmoidHeading { + id: headerArea + implicitHeight: calendarHeader.implicitHeight + + // Agenda view header + // ----------------- + ColumnLayout { + id: eventHeader + + anchors.left: parent.left + width: visible ? parent.width / 2 - 1 : 0 + + visible: calendar.showAgenda || calendar.showClocks + RowLayout { + PlasmaExtras.Heading { + Layout.fillWidth: true + Layout.leftMargin: calendar.paddings // Match calendar title + + text: monthView.currentDate.toLocaleDateString(Qt.locale(), Locale.LongFormat) + } + } + RowLayout { + // Heading text + PlasmaExtras.Heading { + visible: agenda.visible + + Layout.fillWidth: true + Layout.leftMargin: calendar.paddings + + level: 2 + + text: i18n("Events") + maximumLineCount: 1 + elide: Text.ElideRight + } + PlasmaComponents3.ToolButton { + visible: agenda.visible && ApplicationIntegration.calendarInstalled + text: i18nc("@action:button Add event", "Add…") + Layout.rightMargin: calendar.paddings + icon.name: "list-add" + onClicked: ApplicationIntegration.launchCalendar() + } + } + } + + // Vertical separator line between columns + // ======================================= + PlasmaCore.SvgItem { + id: headerSeparator + anchors.left: eventHeader.right + anchors.bottomMargin: PlasmaCore.Units.smallSpacing * 2 + width: visible ? 1 : 0 + height: calendarHeader.height - PlasmaCore.Units.smallSpacing * 2 + visible: eventHeader.visible + + elementId: "vertical-line" + svg: PlasmaCore.Svg { + imagePath: "widgets/line" + } + } + + GridLayout { + id: calendarHeader + width: calendar.showAgenda || calendar.showClocks ? parent.width / 2 : parent.width + anchors.left: headerSeparator.right + columns: 6 + rows: 2 + + PlasmaExtras.Heading { + Layout.row: 0 + Layout.column: 0 + Layout.columnSpan: 3 + Layout.fillWidth: true + Layout.leftMargin: calendar.paddings + PlasmaCore.Units.smallSpacing + text: monthView.selectedYear === (new Date()).getFullYear() ? monthView.selectedMonth : i18nc("Format: month year", "%1 %2", monthView.selectedMonth, monthView.selectedYear.toString()) + } + + PlasmaComponents3.ToolButton { + Layout.row: 0 + Layout.column: 4 + Layout.alignment: Qt.AlignRight + visible: plasmoid.action("configure").enabled + icon.name: "configure" + onClicked: plasmoid.action("configure").trigger() + PlasmaComponents3.ToolTip { + text: plasmoid.action("configure").text + } + } + + // Allows the user to keep the calendar open for reference + PlasmaComponents3.ToolButton { + Layout.row: 0 + Layout.column: 5 + checkable: true + checked: plasmoid.configuration.pin + onToggled: plasmoid.configuration.pin = checked + icon.name: "window-pin" + PlasmaComponents3.ToolTip { + text: i18n("Keep Open") + } + } + + PlasmaComponents3.TabBar { + id: tabbar + currentIndex: monthView.currentIndex + Layout.row: 1 + Layout.column: 0 + Layout.columnSpan: 3 + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing + + PlasmaComponents3.TabButton { + text: i18n("Days"); + onClicked: monthView.showMonthView(); + display: PlasmaComponents3.AbstractButton.TextOnly + } + PlasmaComponents3.TabButton { + text: i18n("Months"); + onClicked: monthView.showYearView(); + display: PlasmaComponents3.AbstractButton.TextOnly + } + PlasmaComponents3.TabButton { + text: i18n("Years"); + onClicked: monthView.showDecadeView(); + display: PlasmaComponents3.AbstractButton.TextOnly + } + } + + PlasmaComponents3.ToolButton { + id: previousButton + property string tooltip + Layout.row: 1 + Layout.column: 3 + + Layout.leftMargin: PlasmaCore.Units.smallSpacing + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-next" : "go-previous" + onClicked: monthView.previousView() + Accessible.name: tooltip + PlasmaComponents3.ToolTip { + text: { + switch(monthView.calendarViewDisplayed) { + case PlasmaCalendar.MonthView.CalendarView.DayView: + return i18n("Previous month") + case PlasmaCalendar.MonthView.CalendarView.MonthView: + return i18n("Previous year") + case PlasmaCalendar.MonthView.CalendarView.YearView: + return i18n("Previous decade") + default: + return ""; + } + } + } + } + + PlasmaComponents3.ToolButton { + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + Layout.row: 1 + Layout.column: 4 + onClicked: monthView.resetToToday() + text: i18ndc("libplasma5", "Reset calendar to today", "Today") + Accessible.description: i18nd("libplasma5", "Reset calendar to today") + } + + PlasmaComponents3.ToolButton { + id: nextButton + property string tooltip + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + Layout.row: 1 + Layout.column: 5 + + icon.name: Qt.application.layoutDirection === Qt.RightToLeft ? "go-previous" : "go-next" + onClicked: monthView.nextView() + Accessible.name: tooltip + PlasmaComponents3.ToolTip { + text: { + switch(monthView.calendarViewDisplayed) { + case PlasmaCalendar.MonthView.CalendarView.DayView: + return i18n("Next month") + case PlasmaCalendar.MonthView.CalendarView.MonthView: + return i18n("Next year") + case PlasmaCalendar.MonthView.CalendarView.YearView: + return i18n("Next decade") + default: + return ""; + } + } + } + } + } + } + + // Left column containing agenda view and time zones + // ================================================== + ColumnLayout { + id: leftColumn + + visible: calendar.showAgenda || calendar.showClocks + width: parent.width / 2 - 1 + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + + // Agenda view itself + Item { + id: agenda + visible: calendar.showAgenda + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 4 + + function formatDateWithoutYear(date) { + // Unfortunatelly Qt overrides ECMA's Date.toLocaleDateString(), + // which is able to return locale-specific date-and-month-only date + // formats, with its dumb version that only supports Qt::DateFormat + // enum subset. So to get a day-and-month-only date format string we + // must resort to this magic and hope there are no locales that use + // other separators... + var format = Qt.locale().dateFormat(Locale.ShortFormat).replace(/[./ ]*Y{2,4}[./ ]*/i, ''); + return Qt.formatDate(date, format); + } + + function dateEquals(date1, date2) { + const values1 = [ + date1.getFullYear(), + date1.getMonth(), + date1.getDate() + ]; + + const values2 = [ + date2.getFullYear(), + date2.getMonth(), + date2.getDate() + ]; + + return values1.every((value, index) => { + return (value === values2[index]); + }, false) + } + + Connections { + target: monthView + + function onCurrentDateChanged() { + // Apparently this is needed because this is a simple QList being + // returned and if the list for the current day has 1 event and the + // user clicks some other date which also has 1 event, QML sees the + // sizes match and does not update the labels with the content. + // Resetting the model to null first clears it and then correct data + // are displayed. + holidaysList.model = null; + holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate); + } + } + + Connections { + target: monthView.daysModel + + function onAgendaUpdated(updatedDate) { + if (agenda.dateEquals(updatedDate, monthView.currentDate)) { + holidaysList.model = null; + holidaysList.model = monthView.daysModel.eventsForDate(monthView.currentDate); + } + } + } + + Connections { + target: plasmoid.configuration + + onEnabledCalendarPluginsChanged: { + PlasmaCalendar.EventPluginsManager.enabledPlugins = plasmoid.configuration.enabledCalendarPlugins; + } + } + + Binding { + target: plasmoid + property: "hideOnWindowDeactivate" + value: !plasmoid.configuration.pin + restoreMode: Binding.RestoreBinding + } + + TextMetrics { + id: dateLabelMetrics + + // Date/time are arbitrary values with all parts being two-digit + readonly property string timeString: Qt.formatTime(new Date(2000, 12, 12, 12, 12, 12, 12)) + readonly property string dateString: agenda.formatDateWithoutYear(new Date(2000, 12, 12, 12, 12, 12)) + + font: PlasmaCore.Theme.defaultFont + text: timeString.length > dateString.length ? timeString : dateString + } + + PlasmaComponents3.ScrollView { + id: holidaysView + anchors.fill: parent + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + ListView { + id: holidaysList + highlight: Item {} + + delegate: PlasmaComponents3.ItemDelegate { + id: eventItem + width: holidaysList.width + padding: calendar.paddings + leftPadding: calendar.paddings + PlasmaCore.Units.smallSpacing * 2 + text: eventTitle.text + hoverEnabled: true + property bool hasTime: { + // Explicitly all-day event + if (modelData.isAllDay) { + return false; + } + // Multi-day event which does not start or end today (so + // is all-day from today's point of view) + if (modelData.startDateTime - monthView.currentDate < 0 && + modelData.endDateTime - monthView.currentDate > 86400000) { // 24hrs in ms + return false; + } + + // Non-explicit all-day event + const startIsMidnight = modelData.startDateTime.getHours() === 0 + && modelData.startDateTime.getMinutes() === 0; + + const endIsMidnight = modelData.endDateTime.getHours() === 0 + && modelData.endDateTime.getMinutes() === 0; + + const sameDay = modelData.startDateTime.getDate() === modelData.endDateTime.getDate() + && modelData.startDateTime.getDay() === modelData.endDateTime.getDay() + + return !(startIsMidnight && endIsMidnight && sameDay); + } + + PlasmaComponents3.ToolTip { + text: modelData.description + visible: text !== "" && eventItem.hovered + } + + contentItem: GridLayout { + id: eventGrid + columns: 3 + rows: 2 + rowSpacing: 0 + columnSpacing: 2 * PlasmaCore.Units.smallSpacing + + Rectangle { + id: eventColor + + Layout.row: 0 + Layout.column: 0 + Layout.rowSpan: 2 + Layout.fillHeight: true + + color: modelData.eventColor + width: 5 * PlasmaCore.Units.devicePixelRatio + visible: modelData.eventColor !== "" + } + + PlasmaComponents3.Label { + id: startTimeLabel + + readonly property bool startsToday: modelData.startDateTime - monthView.currentDate >= 0 + readonly property bool startedYesterdayLessThan12HoursAgo: modelData.startDateTime - monthView.currentDate >= -43200000 //12hrs in ms + + Layout.row: 0 + Layout.column: 1 + Layout.minimumWidth: dateLabelMetrics.width + + text: startsToday || startedYesterdayLessThan12HoursAgo + ? Qt.formatTime(modelData.startDateTime) + : agenda.formatDateWithoutYear(modelData.startDateTime) + horizontalAlignment: Qt.AlignRight + visible: eventItem.hasTime + } + + PlasmaComponents3.Label { + id: endTimeLabel + + readonly property bool endsToday: modelData.endDateTime - monthView.currentDate <= 86400000 // 24hrs in ms + readonly property bool endsTomorrowInLessThan12Hours: modelData.endDateTime - monthView.currentDate <= 86400000 + 43200000 // 36hrs in ms + + Layout.row: 1 + Layout.column: 1 + Layout.minimumWidth: dateLabelMetrics.width + + text: endsToday || endsTomorrowInLessThan12Hours + ? Qt.formatTime(modelData.endDateTime) + : agenda.formatDateWithoutYear(modelData.endDateTime) + horizontalAlignment: Qt.AlignRight + opacity: 0.7 + + visible: eventItem.hasTime + } + + PlasmaComponents3.Label { + id: eventTitle + + Layout.row: 0 + Layout.column: 2 + Layout.fillWidth: true + + elide: Text.ElideRight + text: modelData.title + verticalAlignment: Text.AlignVCenter + maximumLineCount: 2 + } + } + } + } + } + + PlasmaExtras.Heading { + anchors.fill: holidaysView + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.leftMargin: PlasmaCore.Units.largeSpacing + anchors.rightMargin: PlasmaCore.Units.largeSpacing + text: monthView.isToday(monthView.currentDate) ? i18n("No events for today") + : i18n("No events for this day"); + level: 3 + enabled: false + visible: holidaysList.count == 0 + } + } + + // Horizontal separator line between events and time zones + PlasmaCore.SvgItem { + visible: worldClocks.visible && agenda.visible + + Layout.fillWidth: true + Layout.preferredHeight: naturalSize.height + + elementId: "horizontal-line" + svg: PlasmaCore.Svg { + imagePath: "widgets/line" + } + } + + // Clocks stuff + // ------------ + // Header text + button to change time & timezone + PlasmaExtras.PlasmoidHeading { + visible: worldClocks.visible + leftInset: 0 + rightInset: 0 + rightPadding: PlasmaCore.Units.smallSpacing + contentItem: RowLayout { + PlasmaExtras.Heading { + Layout.leftMargin: calendar.paddings + PlasmaCore.Units.smallSpacing * 2 + Layout.fillWidth: true + + level: 2 + + text: i18n("Time Zones") + maximumLineCount: 1 + elide: Text.ElideRight + } + + PlasmaComponents3.ToolButton { + visible: KCMShell.authorize("kcm_clock.desktop").length > 0 + text: i18n("Switch…") + icon.name: "preferences-system-time" + onClicked: KCMShell.openSystemSettings("kcm_clock") + + PlasmaComponents3.ToolTip { + text: i18n("Switch to another timezone") + } + } + } + } + + // Clocks view itself + PlasmaComponents3.ScrollView { + id: worldClocks + visible: calendar.showClocks + + Layout.fillWidth: true + Layout.fillHeight: !agenda.visible + Layout.minimumHeight: visible ? PlasmaCore.Units.gridUnit * 7 : 0 + Layout.maximumHeight: agenda.visible ? PlasmaCore.Units.gridUnit * 10 : -1 + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + ListView { + id: clocksList + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: PlasmaCore.Units.smallSpacing * 2 + + highlight: Item {} + + model: { + let timezones = []; + for (let i = 0; i < plasmoid.configuration.selectedTimeZones.length; i++) { + timezones.push(plasmoid.configuration.selectedTimeZones[i]); + } + + return timezones; + } + + delegate: PlasmaComponents3.ItemDelegate { + id: listItem + readonly property bool isCurrentTimeZone: modelData === plasmoid.configuration.lastSelectedTimezone + width: clocksList.width + padding: calendar.paddings + leftPadding: calendar.paddings + PlasmaCore.Units.smallSpacing * 2 + + contentItem: RowLayout { + PlasmaComponents3.Label { + text: root.nameForZone(modelData) + font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal + maximumLineCount: 1 + elide: Text.ElideRight + } + + PlasmaComponents3.Label { + Layout.fillWidth: true + horizontalAlignment: Qt.AlignRight + text: root.timeForZone(modelData) + font.weight: listItem.isCurrentTimeZone ? Font.Bold : Font.Normal + elide: Text.ElideRight + maximumLineCount: 1 + } + } + } + } + } + } + + // Vertical separator line between columns + // ======================================= + PlasmaCore.SvgItem { + id: mainSeparator + visible: leftColumn.visible + anchors { + right: monthViewWrapper.left + top: parent.top + bottom: parent.bottom + } + width: 1 + + elementId: "vertical-line" + svg: PlasmaCore.Svg { + imagePath: "widgets/line" + } + } + + // Right column containing calendar + // =============================== + FocusScope { + id: monthViewWrapper + width: calendar.showAgenda || calendar.showClocks ? parent.width / 2 : parent.width + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + PlasmaCalendar.MonthView { + id: monthView + anchors.margins: PlasmaCore.Units.smallSpacing + borderOpacity: 0.25 + today: root.tzDate + firstDayOfWeek: plasmoid.configuration.firstDayOfWeek > -1 + ? plasmoid.configuration.firstDayOfWeek + : Qt.locale().firstDayOfWeek + showWeekNumbers: plasmoid.configuration.showWeekNumbers + showCustomHeader: true + } + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/DigitalClock.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/DigitalClock.qml new file mode 100644 index 0000000000..d7f2ae7865 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/DigitalClock.qml @@ -0,0 +1,720 @@ +/* + SPDX-FileCopyrightText: 2013 Heena Mahour + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2013 Martin Klapetek + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.6 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as Components // Date label height breaks on vertical panel with PC3 version +import org.kde.plasma.private.digitalclock 1.0 + +Item { + id: main + + property string timeFormat + property date currentTime + + property bool showSeconds: plasmoid.configuration.showSeconds + property bool showLocalTimezone: plasmoid.configuration.showLocalTimezone + property bool showDate: plasmoid.configuration.showDate + property var dateFormat: { + if (plasmoid.configuration.dateFormat === "custom") { + return plasmoid.configuration.customDateFormat; // str + } else if (plasmoid.configuration.dateFormat === "longDate") { + return Qt.SystemLocaleLongDate; // int + } else if (plasmoid.configuration.dateFormat === "isoDate") { + return Qt.ISODate; // int + } else { // "shortDate" + return Qt.SystemLocaleShortDate; // int + } + } + + property string lastSelectedTimezone: plasmoid.configuration.lastSelectedTimezone + property int displayTimezoneFormat: plasmoid.configuration.displayTimezoneFormat + property int use24hFormat: plasmoid.configuration.use24hFormat + + property string lastDate: "" + property int tzOffset + + // This is the index in the list of user selected timezones + property int tzIndex: 0 + + // if showing the date and the time in one line or + // if the date/timezone cannot be fit with the smallest font to its designated space + property bool oneLineMode: { + if (plasmoid.configuration.dateDisplayFormat === 1) { + // BesideTime + return true; + } else if (plasmoid.configuration.dateDisplayFormat === 2) { + // BelowTime + return false; + } else { + // Adaptive + return plasmoid.formFactor === PlasmaCore.Types.Horizontal && + main.height <= 2 * PlasmaCore.Theme.smallestFont.pixelSize && + (main.showDate || timezoneLabel.visible); + } + } + + onDateFormatChanged: { + setupLabels(); + } + + onDisplayTimezoneFormatChanged: { setupLabels(); } + onStateChanged: { setupLabels(); } + + onLastSelectedTimezoneChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + onShowSecondsChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + onShowLocalTimezoneChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + onShowDateChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + onUse24hFormatChanged: { timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)) } + + Connections { + target: plasmoid + function onContextualActionsAboutToShow() { + ClipboardMenu.secondsIncluded = main.showSeconds; + ClipboardMenu.currentDate = main.currentTime; + } + } + + Connections { + target: plasmoid.configuration + function onSelectedTimeZonesChanged() { + // If the currently selected timezone was removed, + // default to the first one in the list + var lastSelectedTimezone = plasmoid.configuration.lastSelectedTimezone; + if (plasmoid.configuration.selectedTimeZones.indexOf(lastSelectedTimezone) === -1) { + plasmoid.configuration.lastSelectedTimezone = plasmoid.configuration.selectedTimeZones[0]; + } + + setupLabels(); + setTimezoneIndex(); + } + } + + states: [ + State { + name: "horizontalPanel" + when: plasmoid.formFactor === PlasmaCore.Types.Horizontal && !main.oneLineMode + + PropertyChanges { + target: main + Layout.fillHeight: true + Layout.fillWidth: false + Layout.minimumWidth: contentItem.width + Layout.maximumWidth: Layout.minimumWidth + } + + PropertyChanges { + target: contentItem + + height: timeLabel.height + (main.showDate || timezoneLabel.visible ? 0.8 * timeLabel.height : 0) + width: Math.max(timeLabel.paintedWidth + (main.showDate ? timezoneLabel.paintedWidth : 0), + timezoneLabel.paintedWidth, dateLabel.paintedWidth) + PlasmaCore.Units.smallSpacing * 2 + } + + PropertyChanges { + target: labelsGrid + + rows: main.showDate ? 1 : 2 + } + + AnchorChanges { + target: labelsGrid + + anchors.horizontalCenter: contentItem.horizontalCenter + } + + PropertyChanges { + target: timeLabel + + height: sizehelper.height + width: sizehelper.contentWidth + + font.pixelSize: timeLabel.height + } + + PropertyChanges { + target: timezoneLabel + + height: main.showDate ? 0.7 * timeLabel.height : 0.8 * timeLabel.height + width: main.showDate ? timezoneLabel.paintedWidth : timeLabel.width + + font.pixelSize: timezoneLabel.height + } + + PropertyChanges { + target: dateLabel + + height: 0.8 * timeLabel.height + width: dateLabel.paintedWidth + verticalAlignment: Text.AlignVCenter + + font.pixelSize: dateLabel.height + } + + AnchorChanges { + target: dateLabel + + anchors.top: labelsGrid.bottom + anchors.horizontalCenter: labelsGrid.horizontalCenter + } + + PropertyChanges { + target: sizehelper + + /* + * The value 0.71 was picked by testing to give the clock the right + * size (aligned with tray icons). + * Value 0.56 seems to be chosen rather arbitrary as well such that + * the time label is slightly larger than the date or timezone label + * and still fits well into the panel with all the applied margins. + */ + height: Math.min(main.showDate || timezoneLabel.visible ? main.height * 0.56 : main.height * 0.71, + 3 * PlasmaCore.Theme.defaultFont.pixelSize) + + font.pixelSize: sizehelper.height + } + }, + + State { + name: "oneLineDate" + // the one-line mode has no effect on a vertical panel because it would never fit + when: plasmoid.formFactor !== PlasmaCore.Types.Vertical && main.oneLineMode + + PropertyChanges { + target: main + Layout.fillHeight: true + Layout.fillWidth: false + Layout.minimumWidth: contentItem.width + Layout.maximumWidth: Layout.minimumWidth + + } + + PropertyChanges { + target: contentItem + + height: sizehelper.height + width: dateLabel.width + dateLabel.anchors.rightMargin + labelsGrid.width + } + + AnchorChanges { + target: labelsGrid + + anchors.right: contentItem.right + } + + PropertyChanges { + target: dateLabel + + height: timeLabel.height + width: dateLabel.paintedWidth + PlasmaCore.Units.smallSpacing + + font.pixelSize: 1024 + verticalAlignment: Text.AlignVCenter + anchors.rightMargin: labelsGrid.columnSpacing + + fontSizeMode: Text.VerticalFit + } + + AnchorChanges { + target: dateLabel + + anchors.right: labelsGrid.left + anchors.verticalCenter: labelsGrid.verticalCenter + } + + PropertyChanges { + target: timeLabel + + height: sizehelper.height + width: sizehelper.contentWidth + + fontSizeMode: Text.VerticalFit + } + + PropertyChanges { + target: timezoneLabel + + height: 0.7 * timeLabel.height + width: timezoneLabel.paintedWidth + + fontSizeMode: Text.VerticalFit + horizontalAlignment: Text.AlignHCenter + } + + PropertyChanges { + target: sizehelper + + height: Math.min(main.height, 3 * PlasmaCore.Theme.defaultFont.pixelSize) + + fontSizeMode: Text.VerticalFit + font.pixelSize: 3 * PlasmaCore.Theme.defaultFont.pixelSize + } + }, + + State { + name: "verticalPanel" + when: plasmoid.formFactor === PlasmaCore.Types.Vertical + + PropertyChanges { + target: main + Layout.fillHeight: false + Layout.fillWidth: true + Layout.maximumHeight: contentItem.height + Layout.minimumHeight: Layout.maximumHeight + } + + PropertyChanges { + target: contentItem + + height: main.showDate ? labelsGrid.height + dateLabel.contentHeight : labelsGrid.height + width: main.width + } + + PropertyChanges { + target: labelsGrid + + rows: 2 + } + + PropertyChanges { + target: timeLabel + + height: sizehelper.contentHeight + width: main.width + + font.pixelSize: Math.min(timeLabel.height, 3 * PlasmaCore.Theme.defaultFont.pixelSize) + fontSizeMode: Text.HorizontalFit + } + + PropertyChanges { + target: timezoneLabel + + height: Math.max(0.7 * timeLabel.height, minimumPixelSize) + width: main.width + + fontSizeMode: Text.Fit + minimumPixelSize: dateLabel.minimumPixelSize + elide: Text.ElideRight + } + + PropertyChanges { + target: dateLabel + + width: main.width + //NOTE: in order for Text.Fit to work as intended, the actual height needs to be quite big, in order for the font to enlarge as much it needs for the available width, and then request a sensible height, for which contentHeight will need to be considered as opposed to height + height: PlasmaCore.Units.gridUnit * 10 + + fontSizeMode: Text.Fit + verticalAlignment: Text.AlignTop + // Those magic numbers are purely what looks nice as maximum size, here we have it the smallest + // between slightly bigger than the default font (1.4 times) and a bit smaller than the time font + font.pixelSize: Math.min(0.7 * timeLabel.height, PlasmaCore.Theme.defaultFont.pixelSize * 1.4) + elide: Text.ElideRight + wrapMode: Text.WordWrap + } + + AnchorChanges { + target: dateLabel + + anchors.top: labelsGrid.bottom + anchors.horizontalCenter: labelsGrid.horizontalCenter + } + + PropertyChanges { + target: sizehelper + + width: main.width + + fontSizeMode: Text.HorizontalFit + font.pixelSize: 3 * PlasmaCore.Theme.defaultFont.pixelSize + } + }, + + State { + name: "other" + when: plasmoid.formFactor !== PlasmaCore.Types.Vertical && plasmoid.formFactor !== PlasmaCore.Types.Horizontal + + PropertyChanges { + target: main + Layout.fillHeight: false + Layout.fillWidth: false + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 3 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 3 + } + + PropertyChanges { + target: contentItem + + height: main.height + width: main.width + } + + PropertyChanges { + target: labelsGrid + + rows: 2 + } + + PropertyChanges { + target: timeLabel + + height: sizehelper.height + width: main.width + + fontSizeMode: Text.Fit + } + + PropertyChanges { + target: timezoneLabel + + height: 0.7 * timeLabel.height + width: main.width + + fontSizeMode: Text.Fit + minimumPixelSize: 1 + } + + PropertyChanges { + target: dateLabel + + height: 0.7 * timeLabel.height + font.pixelSize: 1024 + width: Math.max(timeLabel.contentWidth, PlasmaCore.Units.gridUnit * 3) + verticalAlignment: Text.AlignVCenter + + fontSizeMode: Text.Fit + minimumPixelSize: 1 + wrapMode: Text.WordWrap + } + + AnchorChanges { + target: dateLabel + + anchors.top: labelsGrid.bottom + anchors.horizontalCenter: labelsGrid.horizontalCenter + } + + PropertyChanges { + target: sizehelper + + height: { + if (main.showDate) { + if (timezoneLabel.visible) { + return 0.4 * main.height + } + return 0.56 * main.height + } else if (timezoneLabel.visible) { + return 0.59 * main.height + } + return main.height + } + width: main.width + + fontSizeMode: Text.Fit + font.pixelSize: 1024 + } + } + ] + + MouseArea { + anchors.fill: parent + + property int wheelDelta: 0 + + onClicked: plasmoid.expanded = !plasmoid.expanded + + onWheel: { + if (!plasmoid.configuration.wheelChangesTimezone) { + return; + } + + var delta = wheel.angleDelta.y || wheel.angleDelta.x + var newIndex = main.tzIndex; + wheelDelta += delta; + // magic number 120 for common "one click" + // See: https://doc.qt.io/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop + while (wheelDelta >= 120) { + wheelDelta -= 120; + newIndex--; + } + while (wheelDelta <= -120) { + wheelDelta += 120; + newIndex++; + } + + if (newIndex >= plasmoid.configuration.selectedTimeZones.length) { + newIndex = 0; + } else if (newIndex < 0) { + newIndex = plasmoid.configuration.selectedTimeZones.length - 1; + } + + if (newIndex !== main.tzIndex) { + plasmoid.configuration.lastSelectedTimezone = plasmoid.configuration.selectedTimeZones[newIndex]; + main.tzIndex = newIndex; + + dataSource.dataChanged(); + setupLabels(); + } + } + } + + /* + * Visible elements + * + */ + Item { + id: contentItem + anchors.verticalCenter: main.verticalCenter + + Grid { + id: labelsGrid + + rows: 1 + horizontalItemAlignment: Grid.AlignHCenter + verticalItemAlignment: Grid.AlignVCenter + + flow: Grid.TopToBottom + columnSpacing: PlasmaCore.Units.smallSpacing + + Components.Label { + id: timeLabel + + font { + family: plasmoid.configuration.fontFamily || PlasmaCore.Theme.defaultFont.family + weight: plasmoid.configuration.boldText ? Font.Bold : PlasmaCore.Theme.defaultFont.weight + italic: plasmoid.configuration.italicText + pixelSize: 1024 + pointSize: -1 // Because we're setting the pixel size instead + // TODO: remove once this label is ported to PC3 + } + minimumPixelSize: 1 + + text: { + // get the time for the given timezone from the dataengine + var now = dataSource.data[plasmoid.configuration.lastSelectedTimezone]["DateTime"]; + // get current UTC time + var msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); + // add the dataengine TZ offset to it + var currentTime = new Date(msUTC + (dataSource.data[plasmoid.configuration.lastSelectedTimezone]["Offset"] * 1000)); + + main.currentTime = currentTime; + return Qt.formatTime(currentTime, main.timeFormat); + } + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Components.Label { + id: timezoneLabel + + font.weight: timeLabel.font.weight + font.italic: timeLabel.font.italic + font.pixelSize: 1024 + font.pointSize: -1 // Because we're setting the pixel size instead + // TODO: remove once this label is ported to PC3 + minimumPixelSize: 1 + + visible: text.length > 0 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + Components.Label { + id: dateLabel + + visible: main.showDate + + font.family: timeLabel.font.family + font.weight: timeLabel.font.weight + font.italic: timeLabel.font.italic + font.pixelSize: 1024 + font.pointSize: -1 // Because we're setting the pixel size instead + // TODO: remove once this label is ported to PC3 + minimumPixelSize: 1 + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + /* + * end: Visible Elements + * + */ + + Components.Label { + id: sizehelper + + font.family: timeLabel.font.family + font.weight: timeLabel.font.weight + font.italic: timeLabel.font.italic + minimumPixelSize: 1 + + visible: false + } + + FontMetrics { + id: timeMetrics + + font.family: timeLabel.font.family + font.weight: timeLabel.font.weight + font.italic: timeLabel.font.italic + } + + // Qt's QLocale does not offer any modular time creating like Klocale did + // eg. no "gimme time with seconds" or "gimme time without seconds and with timezone". + // QLocale supports only two formats - Long and Short. Long is unusable in many situations + // and Short does not provide seconds. So if seconds are enabled, we need to add it here. + // + // What happens here is that it looks for the delimiter between "h" and "m", takes it + // and appends it after "mm" and then appends "ss" for the seconds. + function timeFormatCorrection(timeFormatString) { + var regexp = /(hh*)(.+)(mm)/i + var match = regexp.exec(timeFormatString); + + var hours = match[1]; + var delimiter = match[2]; + var minutes = match[3] + var seconds = "ss"; + var amPm = "AP"; + var uses24hFormatByDefault = timeFormatString.toLowerCase().indexOf("ap") === -1; + + // because QLocale is incredibly stupid and does not convert 12h/24h clock format + // when uppercase H is used for hours, needs to be h or hh, so toLowerCase() + var result = hours.toLowerCase() + delimiter + minutes; + + if (main.showSeconds) { + result += delimiter + seconds; + } + + // add "AM/PM" either if the setting is the default and locale uses it OR if the user unchecked "use 24h format" + if ((main.use24hFormat == Qt.PartiallyChecked && !uses24hFormatByDefault) || main.use24hFormat == Qt.Unchecked) { + result += " " + amPm; + } + + main.timeFormat = result; + setupLabels(); + } + + function setupLabels() { + var showTimezone = main.showLocalTimezone || (plasmoid.configuration.lastSelectedTimezone !== "Local" + && dataSource.data["Local"]["Timezone City"] !== dataSource.data[plasmoid.configuration.lastSelectedTimezone]["Timezone City"]); + + var timezoneString = ""; + + if (showTimezone) { + // format timezone as tz code, city or UTC offset + if (displayTimezoneFormat === 0) { + timezoneString = dataSource.data[lastSelectedTimezone]["Timezone Abbreviation"] + } else if (displayTimezoneFormat === 1) { + timezoneString = TimezonesI18n.i18nCity(dataSource.data[lastSelectedTimezone]["Timezone City"]); + } else if (displayTimezoneFormat === 2) { + var lastOffset = dataSource.data[lastSelectedTimezone]["Offset"]; + var symbol = lastOffset > 0 ? '+' : ''; + var hours = Math.floor(lastOffset / 3600); + var minutes = Math.floor(lastOffset % 3600 / 60); + + timezoneString = "UTC" + symbol + hours.toString().padStart(2, '0') + ":" + minutes.toString().padStart(2, '0'); + } + + timezoneLabel.text = (main.showDate || main.oneLineMode) && plasmoid.formFactor === PlasmaCore.Types.Horizontal ? "(" + timezoneString + ")" : timezoneString; + } else { + // this clears the label and that makes it hidden + timezoneLabel.text = timezoneString; + } + + + if (main.showDate) { + dateLabel.text = Qt.formatDate(main.currentTime, main.dateFormat); + } else { + // clear it so it doesn't take space in the layout + dateLabel.text = ""; + } + + // find widest character between 0 and 9 + var maximumWidthNumber = 0; + var maximumAdvanceWidth = 0; + for (var i = 0; i <= 9; i++) { + var advanceWidth = timeMetrics.advanceWidth(i); + if (advanceWidth > maximumAdvanceWidth) { + maximumAdvanceWidth = advanceWidth; + maximumWidthNumber = i; + } + } + // replace all placeholders with the widest number (two digits) + var format = main.timeFormat.replace(/(h+|m+|s+)/g, "" + maximumWidthNumber + maximumWidthNumber); // make sure maximumWidthNumber is formatted as string + // build the time string twice, once with an AM time and once with a PM time + var date = new Date(2000, 0, 1, 1, 0, 0); + var timeAm = Qt.formatTime(date, format); + var advanceWidthAm = timeMetrics.advanceWidth(timeAm); + date.setHours(13); + var timePm = Qt.formatTime(date, format); + var advanceWidthPm = timeMetrics.advanceWidth(timePm); + // set the sizehelper's text to the widest time string + if (advanceWidthAm > advanceWidthPm) { + sizehelper.text = timeAm; + } else { + sizehelper.text = timePm; + } + } + + function dateTimeChanged() + { + var doCorrections = false; + + if (main.showDate) { + // If the date has changed, force size recalculation, because the day name + // or the month name can now be longer/shorter, so we need to adjust applet size + const currentDate = Qt.formatDateTime(dataSource.data["Local"]["DateTime"], "yyyy-MM-dd"); + if (main.lastDate !== currentDate) { + doCorrections = true; + main.lastDate = currentDate + } + } + + var currentTZOffset = dataSource.data["Local"]["Offset"] / 60; + if (currentTZOffset !== tzOffset) { + doCorrections = true; + tzOffset = currentTZOffset; + Date.timeZoneUpdated(); // inform the QML JS engine about TZ change + } + + if (doCorrections) { + timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)); + } + } + + function setTimezoneIndex() { + for (var i = 0; i < plasmoid.configuration.selectedTimeZones.length; i++) { + if (plasmoid.configuration.selectedTimeZones[i] === plasmoid.configuration.lastSelectedTimezone) { + main.tzIndex = i; + break; + } + } + } + + Component.onCompleted: { + // Sort the timezones according to their offset + // Calling sort() directly on plasmoid.configuration.selectedTimeZones + // has no effect, so sort a copy and then assign the copy to it + var sortArray = plasmoid.configuration.selectedTimeZones; + sortArray.sort(function(a, b) { + return dataSource.data[a]["Offset"] - dataSource.data[b]["Offset"]; + }); + plasmoid.configuration.selectedTimeZones = sortArray; + + setTimezoneIndex(); + tzOffset = -(new Date().getTimezoneOffset()); + dateTimeChanged(); + timeFormatCorrection(Qt.locale().timeFormat(Locale.ShortFormat)); + dataSource.onDataChanged.connect(dateTimeChanged); + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/MonthMenu.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/MonthMenu.qml new file mode 100644 index 0000000000..39720a8f5e --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/MonthMenu.qml @@ -0,0 +1,9 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import org.kde.plasma.calendar 2.0 as PlasmaCalendar + +PlasmaCalendar.MonthMenu { } diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/Tooltip.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/Tooltip.qml new file mode 100644 index 0000000000..b18f790a73 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/Tooltip.qml @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2015 Martin Klapetek + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + id: tooltipContentItem + + property int preferredTextWidth: PlasmaCore.Units.gridUnit * 20 + + implicitWidth: mainLayout.implicitWidth + PlasmaCore.Units.gridUnit + implicitHeight: mainLayout.implicitHeight + PlasmaCore.Units.gridUnit + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.NormalColorGroup + PlasmaCore.ColorScope.inherit: false + + ColumnLayout { + id: mainLayout + anchors { + left: parent.left + top: parent.top + margins: PlasmaCore.Units.gridUnit / 2 + } + + spacing: 0 + + PlasmaExtras.Heading { + id: tooltipMaintext + level: 3 + Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) + Layout.maximumWidth: preferredTextWidth + elide: Text.ElideRight + text: clocks.visible ? Qt.formatDate(tzDate, Locale.LongFormat) : Qt.formatDate(tzDate,"dddd") + } + + PlasmaComponents3.Label { + id: tooltipSubtext + Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) + Layout.maximumWidth: preferredTextWidth + text: Qt.formatDate(tzDate, dateFormatString) + opacity: 0.6 + visible: !clocks.visible + } + + GridLayout { + id: clocks + Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) + Layout.maximumWidth: preferredTextWidth + Layout.maximumHeight: childrenRect.height + columns: 2 + visible: plasmoid.configuration.selectedTimeZones.length > 1 + rowSpacing: 0 + + Repeater { + model: { + // The timezones need to be duplicated in the array + // because we need their data twice - once for the name + // and once for the time and the Repeater delegate cannot + // be one Item with two Labels because that wouldn't work + // in a grid then + var timezones = []; + for (var i = 0; i < plasmoid.configuration.selectedTimeZones.length; i++) { + timezones.push(plasmoid.configuration.selectedTimeZones[i]); + timezones.push(plasmoid.configuration.selectedTimeZones[i]); + } + + return timezones; + } + + PlasmaComponents3.Label { + id: timezone + // Layout.fillWidth is buggy here + Layout.alignment: index % 2 === 0 ? Qt.AlignRight : Qt.AlignLeft + + wrapMode: Text.NoWrap + text: index % 2 == 0 ? nameForZone(modelData) : timeForZone(modelData) + font.weight: modelData === plasmoid.configuration.lastSelectedTimezone ? Font.Bold : Font.Normal + elide: Text.ElideNone + opacity: 0.6 + } + } + } + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/configAppearance.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/configAppearance.qml new file mode 100644 index 0000000000..a3ef914210 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/configAppearance.qml @@ -0,0 +1,288 @@ +/* + SPDX-FileCopyrightText: 2013 Bhushan Shah + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.3 as QtControls +import QtQuick.Layouts 1.0 as QtLayouts +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.calendar 2.0 as PlasmaCalendar +import org.kde.kquickcontrolsaddons 2.0 // For KCMShell +import org.kde.kirigami 2.5 as Kirigami + +QtLayouts.ColumnLayout { + id: appearancePage + + signal configurationChanged + + property string cfg_fontFamily + property alias cfg_boldText: boldCheckBox.checked + property string cfg_timeFormat: "" + property alias cfg_italicText: italicCheckBox.checked + + property alias cfg_showLocalTimezone: showLocalTimezone.checked + property alias cfg_displayTimezoneFormat: displayTimezoneFormat.currentIndex + property alias cfg_showSeconds: showSeconds.checked + + property alias cfg_showDate: showDate.checked + property string cfg_dateFormat: "shortDate" + property alias cfg_customDateFormat: customDateFormat.text + property alias cfg_use24hFormat: use24hFormat.currentIndex + property alias cfg_dateDisplayFormat: dateDisplayFormat.currentIndex + + onCfg_fontFamilyChanged: { + // HACK by the time we populate our model and/or the ComboBox is finished the value is still undefined + if (cfg_fontFamily) { + for (var i = 0, j = fontsModel.count; i < j; ++i) { + if (fontsModel.get(i).value === cfg_fontFamily) { + fontFamilyComboBox.currentIndex = i + break + } + } + } + } + + ListModel { + id: fontsModel + Component.onCompleted: { + var arr = [] // use temp array to avoid constant binding stuff + arr.push({text: i18nc("Use default font", "Default"), value: ""}) + + var fonts = Qt.fontFamilies() + var foundIndex = 0 + for (var i = 0, j = fonts.length; i < j; ++i) { + arr.push({text: fonts[i], value: fonts[i]}) + } + append(arr) + } + } + + Kirigami.FormLayout { + QtLayouts.Layout.fillWidth: true + + QtLayouts.RowLayout { + Kirigami.FormData.label: i18n("Information:") + + QtControls.CheckBox { + id: showDate + text: i18n("Show date") + } + + QtControls.ComboBox { + id: dateDisplayFormat + enabled: showDate.checked + visible: plasmoid.formFactor !== PlasmaCore.Types.Vertical + model: [ + i18n("Adaptive location"), + i18n("Always beside time"), + i18n("Always below time"), + ] + onActivated: cfg_dateDisplayFormat = currentIndex + } + } + + QtControls.CheckBox { + id: showSeconds + text: i18n("Show seconds") + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.ColumnLayout { + Kirigami.FormData.label: i18n("Show time zone:") + Kirigami.FormData.buddyFor: showLocalTimeZoneWhenDifferent + + QtControls.RadioButton { + id: showLocalTimeZoneWhenDifferent + text: i18n("Only when different from local time zone") + } + + QtControls.RadioButton { + id: showLocalTimezone + text: i18n("Always") + } + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.RowLayout { + Kirigami.FormData.label: i18n("Display time zone as:") + + QtControls.ComboBox { + id: displayTimezoneFormat + model: [ + i18n("Code"), + i18n("City"), + i18n("Offset from UTC time"), + ] + onActivated: cfg_displayTimezoneFormat = currentIndex + } + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.RowLayout { + QtLayouts.Layout.fillWidth: true + Kirigami.FormData.label: i18n("Time display:") + + QtControls.ComboBox { + id: use24hFormat + model: [ + i18n("12-Hour"), + i18n("Use Region Defaults"), + i18n("24-Hour") + ] + onCurrentIndexChanged: cfg_use24hFormat = currentIndex + } + + QtControls.Button { + visible: KCMShell.authorize("kcm_formats.desktop").length > 0 + text: i18n("Change Regional Settings…") + icon.name: "preferences-desktop-locale" + onClicked: KCMShell.openSystemSettings("kcm_formats") + } + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.RowLayout { + Kirigami.FormData.label: i18n("Date format:") + enabled: showDate.checked + + QtControls.ComboBox { + id: dateFormat + textRole: "label" + model: [ + { + 'label': i18n("Long Date"), + 'name': "longDate", + format: Qt.SystemLocaleLongDate + }, + { + 'label': i18n("Short Date"), + 'name': "shortDate", + format: Qt.SystemLocaleShortDate + }, + { + 'label': i18n("ISO Date"), + 'name': "isoDate", + format: Qt.ISODate + }, + { + 'label': i18nc("custom date format", "Custom"), + 'name': "custom" + } + ] + onCurrentIndexChanged: cfg_dateFormat = model[currentIndex]["name"] + + Component.onCompleted: { + for (var i = 0; i < model.length; i++) { + if (model[i]["name"] === plasmoid.configuration.dateFormat) { + dateFormat.currentIndex = i; + } + } + } + } + + QtControls.Label { + QtLayouts.Layout.fillWidth: true + textFormat: Text.PlainText + text: Qt.formatDate(new Date(), cfg_dateFormat === "custom" ? customDateFormat.text + : dateFormat.model[dateFormat.currentIndex].format) + } + } + + QtControls.TextField { + id: customDateFormat + QtLayouts.Layout.fillWidth: true + enabled: showDate.checked + visible: cfg_dateFormat == "custom" + } + + QtControls.Label { + text: i18n("Time Format Documentation") + enabled: showDate.checked + visible: cfg_dateFormat == "custom" + wrapMode: Text.Wrap + QtLayouts.Layout.preferredWidth: QtLayouts.Layout.maximumWidth + QtLayouts.Layout.maximumWidth: Kirigami.Units.gridUnit * 16 + + onLinkActivated: Qt.openUrlExternally(link) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // We don't want to eat clicks on the Label + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.RowLayout { + QtLayouts.Layout.fillWidth: true + + Kirigami.FormData.label: i18n("Font style:") + + QtControls.ComboBox { + id: fontFamilyComboBox + QtLayouts.Layout.fillWidth: true + currentIndex: 0 + // ComboBox's sizing is just utterly broken + QtLayouts.Layout.minimumWidth: Kirigami.Units.gridUnit * 10 + model: fontsModel + // doesn't autodeduce from model because we manually populate it + textRole: "text" + + onCurrentIndexChanged: { + var current = model.get(currentIndex) + if (current) { + cfg_fontFamily = current.value + appearancePage.configurationChanged() + } + } + } + + QtControls.Button { + id: boldCheckBox + QtControls.ToolTip { + text: i18n("Bold text") + } + icon.name: "format-text-bold" + checkable: true + Accessible.name: QtControls.ToolTip.text + } + + QtControls.Button { + id: italicCheckBox + QtControls.ToolTip { + text: i18n("Italic text") + } + icon.name: "format-text-italic" + checkable: true + Accessible.name: QtControls.ToolTip.text + } + } + } + Item { + QtLayouts.Layout.fillHeight: true + } + + Component.onCompleted: { + if (!plasmoid.configuration.showLocalTimezone) { + showLocalTimeZoneWhenDifferent.checked = true; + } + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/configCalendar.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/configCalendar.qml new file mode 100644 index 0000000000..4826aea479 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/configCalendar.qml @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2015 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.4 as QtControls +import QtQuick.Layouts 1.0 as QtLayouts +import org.kde.plasma.calendar 2.0 as PlasmaCalendar +import org.kde.kirigami 2.5 as Kirigami + +Item { + id: calendarPage + width: childrenRect.width + height: childrenRect.height + + signal configurationChanged + + property alias cfg_showWeekNumbers: showWeekNumbers.checked + property int cfg_firstDayOfWeek + + function saveConfig() + { + plasmoid.configuration.enabledCalendarPlugins = PlasmaCalendar.EventPluginsManager.enabledPlugins; + } + + Kirigami.FormLayout { + anchors { + left: parent.left + right: parent.right + } + + QtControls.CheckBox { + id: showWeekNumbers + Kirigami.FormData.label: i18n("General:") + text: i18n("Show week numbers") + } + + QtLayouts.RowLayout { + QtLayouts.Layout.fillWidth: true + Kirigami.FormData.label: i18n("First day of week:") + + QtControls.ComboBox { + id: firstDayOfWeekCombo + textRole: "text" + model: [-1, 0, 1, 5, 6].map((day) => { + return { + day, + text: day === -1 ? i18n("Use Region Defaults") : Qt.locale().dayName(day) + }; + }) + onActivated: cfg_firstDayOfWeek = model[index].day + currentIndex: model.findIndex((item) => { + return item.day === cfg_firstDayOfWeek; + }) + } + } + + Item { + Kirigami.FormData.isSection: true + } + + QtLayouts.ColumnLayout { + Kirigami.FormData.label: i18n("Available Plugins:") + Kirigami.FormData.buddyFor: children[1] // 0 is the Repeater + + Repeater { + id: calendarPluginsRepeater + model: PlasmaCalendar.EventPluginsManager.model + delegate: QtLayouts.RowLayout { + QtControls.CheckBox { + text: model.display + checked: model.checked + onClicked: { + //needed for model's setData to be called + model.checked = checked; + calendarPage.configurationChanged(); + } + } + } + } + } + } + + Component.onCompleted: { + PlasmaCalendar.EventPluginsManager.populateEnabledPluginsList(plasmoid.configuration.enabledCalendarPlugins); + } +} + diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/configTimeZones.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/configTimeZones.qml new file mode 100644 index 0000000000..c5f74747f1 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/configTimeZones.qml @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2013 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.12 +import QtQuick.Controls 2.8 as QQC2 +import QtQuick.Layouts 1.0 +import QtQuick.Dialogs 1.1 + +import org.kde.kquickcontrolsaddons 2.0 // For kcmshell +import org.kde.plasma.private.digitalclock 1.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.14 as Kirigami + +ColumnLayout { + id: timeZonesPage + + property alias cfg_selectedTimeZones: timeZones.selectedTimeZones + property alias cfg_wheelChangesTimezone: enableWheelCheckBox.checked + + + TimeZoneModel { + + id: timeZones + onSelectedTimeZonesChanged: { + if (selectedTimeZones.length === 0) { + // Don't let the user remove all time zones + messageWidget.visible = true; + timeZones.selectLocalTimeZone(); + } + } + } + + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + // Or else the page becomes scrollable when the list has a lot of items, + // rather than the list becoming scrollable, which is what we want + Layout.maximumHeight: timeZonesPage.parent.height - Kirigami.Units.gridUnit * 7 + + Component.onCompleted: background.visible = true // enable border + + // HACK: Hide unnecesary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + ListView { + id: configuredTimezoneList + clip: true // Avoid visual glitches + focus: true // keyboard navigation + activeFocusOnTab: true // keyboard navigation + + model: TimeZoneFilterProxy { + sourceModel: timeZones + onlyShowChecked: true + } + // We have no concept of selection in this list, so don't pre-select + // the first item + currentIndex: -1 + + delegate: Kirigami.BasicListItem { + id: timeZoneListItem + property bool isCurrent: plasmoid.configuration.lastSelectedTimezone === model.timeZoneId + + bold: isCurrent + + // Don't want a highlight effect here because it doesn't look good + hoverEnabled: false + activeBackgroundColor: "transparent" + activeTextColor: Kirigami.Theme.textColor + + reserveSpaceForSubtitle: true + // FIXME: this should have already evaluated to false because + // the list item doesn't have an icon + reserveSpaceForIcon: false + + // TODO: create Kirigami.MutuallyExclusiveListItem to be the + // RadioButton equivalent of Kirigami.CheckableListItem, + // and then port to use that in Plasma 5.22 + leading: QQC2.RadioButton { + id: radioButton + visible: configuredTimezoneList.count > 1 + checked: timeZoneListItem.isCurrent + onToggled: clickAction.trigger() + } + + label: model.city + subtitle: isCurrent && configuredTimezoneList.count > 1 ? i18n("Clock is currently using this time zone") : "" + + action: Kirigami.Action { + id: clickAction + onTriggered: plasmoid.configuration.lastSelectedTimezone = model.timeZoneId + } + + trailing: RowLayout { + QQC2.Button { + visible: model.isLocalTimeZone && KCMShell.authorize("kcm_clock.desktop").length > 0 + text: i18n("Switch Local Time Zone…") + icon.name: "preferences-system-time" + onClicked: KCMShell.openSystemSettings("kcm_clock") + } + QQC2.Button { + visible: !model.isLocalTimeZone && configuredTimezoneList.count > 1 + icon.name: "edit-delete" + onClicked: model.checked = false; + QQC2.ToolTip { + text: i18n("Remove this time zone") + } + } + } + } + + section { + property: "isLocalTimeZone" + delegate: Kirigami.ListSectionHeader { + label: section == "true" ? i18n("System's Local Time Zone") : i18n("Additional Time Zones") + } + } + + Kirigami.PlaceholderMessage { + visible: configuredTimezoneList.count === 1 + anchors { + top: parent.verticalCenter // Visual offset for system timezone and header + left: parent.left + right: parent.right + leftMargin: Kirigami.Units.largeSpacing * 6 + rightMargin: Kirigami.Units.largeSpacing * 6 + } + text: i18n("Add more time zones to display all of them in the applet's pop-up, or use one of them for the clock itself") + } + } + } + + QQC2.Button { + Layout.alignment: Qt.AlignLeft // Explicitly set so it gets reversed for LTR mode + text: i18n("Add Time Zones…") + icon.name: "list-add" + onClicked: timezoneSheet.open() + } + + QQC2.CheckBox { + id: enableWheelCheckBox + enabled: configuredTimezoneList.count > 1 + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing + text: i18n("Switch displayed time zone by scrolling over clock applet") + } + + Kirigami.Separator { + Layout.fillWidth: true + } + + QQC2.Label { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing * 2 + Layout.rightMargin: Kirigami.Units.largeSpacing * 2 + text: i18n("Note that using a different time zone for the clock does not change the systemwide local time zone. When you travel, switch the local time zone instead.") + font: Kirigami.Theme.smallFont + wrapMode: Text.Wrap + } + + Item { + // Tighten up the layout + Layout.fillHeight: true + } + + Kirigami.OverlaySheet { + id: timezoneSheet + + onSheetOpenChanged: { + filter.text = ""; + messageWidget.visible = false; + if (sheetOpen) { + filter.forceActiveFocus() + } + } + + // Need to manually set the parent when using this in a Plasma config dialog + parent: timeZonesPage.parent + + header: ColumnLayout { + Layout.preferredWidth: Kirigami.Units.gridUnit * 25 + + Kirigami.Heading { + Layout.fillWidth: true + text: i18n("Add More Timezones") + wrapMode: Text.Wrap + } + Kirigami.SearchField { + id: filter + Layout.fillWidth: true + } + Kirigami.InlineMessage { + id: messageWidget + Layout.fillWidth: true + type: Kirigami.MessageType.Warning + text: i18n("At least one time zone needs to be enabled. Your local timezone was enabled automatically.") + showCloseButton: true + } + } + + footer: QQC2.DialogButtonBox { + standardButtons: QQC2.DialogButtonBox.Ok + onAccepted: timezoneSheet.close() + } + + ListView { + id: listView + focus: true // keyboard navigation + activeFocusOnTab: true // keyboard navigation + implicitWidth: Kirigami.Units.gridUnit * 25 + + model: TimeZoneFilterProxy { + sourceModel: timeZones + filterString: filter.text + } + + delegate: QQC2.CheckDelegate { + id: checkbox + width: listView.width + focus: true // keyboard navigation + text: !city || city.indexOf("UTC") === 0 ? comment : comment ? i18n("%1, %2 (%3)", city, region, comment) : i18n("%1, %2", city, region) + checked: model.checked + onToggled: { + model.checked = checkbox.checked + listView.currentIndex = index // highlight + listView.forceActiveFocus() // keyboard navigation + } + highlighted: ListView.isCurrentItem + } + } + } +} diff --git a/plasma/workspace/applets/digital-clock/package/contents/ui/main.qml b/plasma/workspace/applets/digital-clock/package/contents/ui/main.qml new file mode 100644 index 0000000000..6ee3e206cc --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/contents/ui/main.qml @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2013 Heena Mahour + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.plasma.private.digitalclock 1.0 +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.plasma.calendar 2.0 as PlasmaCalendar + +Item { + id: root + + width: PlasmaCore.Units.gridUnit * 10 + height: PlasmaCore.Units.gridUnit * 4 + property string dateFormatString: setDateFormatString() + Plasmoid.backgroundHints: PlasmaCore.Types.ShadowBackground | PlasmaCore.Types.ConfigurableBackground + property date tzDate: { + // get the time for the given timezone from the dataengine + var now = dataSource.data[plasmoid.configuration.lastSelectedTimezone]["DateTime"]; + // get current UTC time + var msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); + // add the dataengine TZ offset to it + return new Date(msUTC + (dataSource.data[plasmoid.configuration.lastSelectedTimezone]["Offset"] * 1000)); + } + + function initTimezones() { + var tz = Array() + if (plasmoid.configuration.selectedTimeZones.indexOf("Local") === -1) { + tz.push("Local"); + } + root.allTimezones = tz.concat(plasmoid.configuration.selectedTimeZones); + } + + function timeForZone(zone) { + var compactRepresentationItem = plasmoid.compactRepresentationItem; + if (!compactRepresentationItem) { + return ""; + } + + // get the time for the given timezone from the dataengine + var now = dataSource.data[zone]["DateTime"]; + // get current UTC time + var msUTC = now.getTime() + (now.getTimezoneOffset() * 60000); + // add the dataengine TZ offset to it + var dateTime = new Date(msUTC + (dataSource.data[zone]["Offset"] * 1000)); + + var formattedTime = Qt.formatTime(dateTime, compactRepresentationItem.timeFormat); + + if (dateTime.getDay() !== dataSource.data["Local"]["DateTime"].getDay()) { + formattedTime += " (" + Qt.formatDate(dateTime, compactRepresentationItem.dateFormat) + ")"; + } + + return formattedTime; + } + + function nameForZone(zone) { + // add the timezone string to the clock + var timezoneString = plasmoid.configuration.displayTimezoneAsCode ? dataSource.data[zone]["Timezone Abbreviation"] + : TimezonesI18n.i18nCity(dataSource.data[zone]["Timezone City"]); + + return timezoneString; + } + + Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation + Plasmoid.compactRepresentation: DigitalClock { } + Plasmoid.fullRepresentation: CalendarView { } + + Plasmoid.toolTipItem: Loader { + id: tooltipLoader + + Layout.minimumWidth: item ? item.implicitWidth : 0 + Layout.maximumWidth: item ? item.implicitWidth : 0 + Layout.minimumHeight: item ? item.implicitHeight : 0 + Layout.maximumHeight: item ? item.implicitHeight : 0 + + source: "Tooltip.qml" + } + + //We need Local to be *always* present, even if not disaplayed as + //it's used for formatting in ToolTip.dateTimeChanged() + property var allTimezones + Connections { + target: plasmoid.configuration + function onSelectedTimeZonesChanged() { root.initTimezones(); } + } + + PlasmaCore.DataSource { + id: dataSource + engine: "time" + connectedSources: allTimezones + interval: plasmoid.configuration.showSeconds ? 1000 : 60000 + intervalAlignment: plasmoid.configuration.showSeconds ? PlasmaCore.Types.NoAlignment : PlasmaCore.Types.AlignToMinute + } + + function setDateFormatString() { + // remove "dddd" from the locale format string + // /all/ locales in LongFormat have "dddd" either + // at the beginning or at the end. so we just + // remove it + the delimiter and space + var format = Qt.locale().dateFormat(Locale.LongFormat); + format = format.replace(/(^dddd.?\s)|(,?\sdddd$)/, ""); + return format; + } + + function action_clockkcm() { + KCMShell.openSystemSettings("kcm_clock"); + } + + function action_formatskcm() { + KCMShell.openSystemSettings("kcm_formats"); + } + + Component.onCompleted: { + plasmoid.setAction("clipboard", i18n("Copy to Clipboard"), "edit-copy"); + ClipboardMenu.setupMenu(plasmoid.action("clipboard")); + + root.initTimezones(); + if (KCMShell.authorize("kcm_clock.desktop").length > 0) { + plasmoid.setAction("clockkcm", i18n("Adjust Date and Time…"), "clock"); + } + if (KCMShell.authorize("kcm_formats.desktop").length > 0) { + plasmoid.setAction("formatskcm", i18n("Set Time Format…"), "gnumeric-format-thousand-separator"); + } + + // Set the list of enabled plugins from config + // to the manager + PlasmaCalendar.EventPluginsManager.enabledPlugins = plasmoid.configuration.enabledCalendarPlugins; + } +} diff --git a/plasma/workspace/applets/digital-clock/package/metadata.json b/plasma/workspace/applets/digital-clock/package/metadata.json new file mode 100644 index 0000000000..73ed56df18 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/package/metadata.json @@ -0,0 +1,179 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mklapetek@kde.org", + "Name": "Martin Klapetek", + "Name[ar]": "Martin Klapetek", + "Name[az]": "Martin Klapetek", + "Name[ca]": "Martin Klapetek", + "Name[cs]": "Martin Klapetek", + "Name[de]": "Martin Klapetek", + "Name[en_GB]": "Martin Klapetek", + "Name[es]": "Martin Klapetek", + "Name[eu]": "Martin Klapetek", + "Name[fi]": "Martin Klapetek", + "Name[fr]": "Martin Klapetek", + "Name[hu]": "Martin Klapetek", + "Name[ia]": "Martin Klapetek", + "Name[it]": "Martin Klapetek", + "Name[ko]": "Martin Klapetek", + "Name[lt]": "Martin Klapetek", + "Name[nl]": "Martin Klapetek", + "Name[nn]": "Martin Klapetek", + "Name[pl]": "Martin Klapetek", + "Name[pt_BR]": "Martin Klapetek", + "Name[ro]": "Martin Klapetek", + "Name[ru]": "Martin Klapetek", + "Name[sk]": "Martin Klapetek", + "Name[sl]": "Martin Klapetek", + "Name[sv]": "Martin Klapetek", + "Name[tr]": "Martin Klapetek", + "Name[uk]": "Martin Klapetek", + "Name[vi]": "Martin Klapetek", + "Name[x-test]": "xxMartin Klapetekxx", + "Name[zh_CN]": "Martin Klapetek" + } + ], + "Category": "Date and Time", + "Description": "Time displayed in a digital format", + "Description[ar]": "الوقت بتنسيق رقميّ", + "Description[az]": "Saat, rəqəmsal formatda göstərilir", + "Description[ca]": "Mostra l'hora en un format digital", + "Description[cs]": "Čas zobrazený v digitální podobě", + "Description[de]": "Die Uhrzeit in digitaler Form anzeigen", + "Description[en_GB]": "Time displayed in a digital format", + "Description[es]": "La hora mostrada en formato digital", + "Description[eu]": "Ordua formatu digitalean azaldua", + "Description[fi]": "Digitaalimuodossa esitetty aika", + "Description[fr]": "Affichage de l'heure au format numérique", + "Description[hu]": "Digitális formában kijelzett idő", + "Description[ia]": "Tempore monstrate in formato digital", + "Description[it]": "Ora visualizzata in un formato digitale", + "Description[ko]": "디지털 형식으로 시간 표시", + "Description[lt]": "Laikas rodomas skaitmeniniu formatu", + "Description[nl]": "Tijd weergegeven in een digitale opmaak", + "Description[nn]": "Klokka vist i digitalformat", + "Description[pa]": "ਡਿਜਿਟਲ ਫਾਰਮੈਟ ਵਿੱਚ ਸਮਾਂ ਵੇਖੋ", + "Description[pl]": "Wyświetla czas w formacie cyfrowym", + "Description[pt_BR]": "Hora exibida em formato digital", + "Description[ro]": "Ora afișată în format digital", + "Description[ru]": "Показ времени цифрами", + "Description[sk]": "Čas zobrazený v digitálnom formáte", + "Description[sl]": "Prikaz časa v digitalni obliki", + "Description[sv]": "Tid visad med digitalformat", + "Description[ta]": "எண் வடிவில் நேரத்தைக் காட்டும்", + "Description[tr]": "Dijital biçimde gösterilen saat", + "Description[uk]": "Час, показаний у цифровому форматі", + "Description[vi]": "Thời gian hiển thị ở dạng số", + "Description[x-test]": "xxTime displayed in a digital formatxx", + "Description[zh_CN]": "以数字格式显示时间", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-system-time", + "Id": "org.kde.plasma.digitalclock", + "License": "GPL-2.0+", + "Name": "Digital Clock", + "Name[af]": "Digitale horlosie", + "Name[ar]": "ساعة رقمية", + "Name[ast]": "Reló dixital", + "Name[az]": "Rəqəmsal saat", + "Name[be@latin]": "Ličbavy hadzińnik", + "Name[be]": "Лічбавы гадзіннік", + "Name[bg]": "Цифров часовник", + "Name[bn]": "ডিজিটাল ঘড়ি", + "Name[bn_IN]": "ডিজিট্যাল ঘড়ি", + "Name[bs]": "Digitalni sat", + "Name[ca@valencia]": "Rellotge digital", + "Name[ca]": "Rellotge digital", + "Name[cs]": "Digitální hodiny", + "Name[csb]": "Cyfrowi zédżer", + "Name[da]": "Digitalt ur", + "Name[de]": "Digitale Uhr", + "Name[el]": "Ψηφιακό ρολόι", + "Name[en_GB]": "Digital Clock", + "Name[eo]": "Cifereca horloĝo", + "Name[es]": "Reloj digital", + "Name[et]": "Digikell", + "Name[eu]": "Erloju digitala", + "Name[fi]": "Digitaalinen kello", + "Name[fr]": "Horloge numérique", + "Name[fy]": "Digitale klok", + "Name[ga]": "Clog Digiteach", + "Name[gl]": "Reloxo dixital", + "Name[gu]": "ડિજીટલ ઘડિયાળ", + "Name[he]": "שעון דיגיטלי", + "Name[hi]": "डिजिटल घड़ी", + "Name[hne]": "डिजिटल घड़ी", + "Name[hr]": "Digitalni sat", + "Name[hsb]": "Digitalny časnik", + "Name[hu]": "Digitális óra", + "Name[ia]": "Horologio digital", + "Name[id]": "Jam Digital", + "Name[is]": "Stafræn klukka", + "Name[it]": "Orologio digitale", + "Name[ja]": "デジタル時計", + "Name[kk]": "Цифрлық сағат", + "Name[km]": "នាឡិកា​ឌីជីថល", + "Name[kn]": "ಅಂಕೀಯ (ಡಿಜಿಟಲ್) ಗಡಿಯಾರ", + "Name[ko]": "디지털 시계", + "Name[ku]": "Demjimêra Dîjîtal", + "Name[lt]": "Skaitmeninis laikrodis", + "Name[lv]": "Ciparu pulkstenis", + "Name[mai]": "डिजिटल घडी", + "Name[mk]": "Дигитален часовник", + "Name[ml]": "ഡിജിറ്റല്‍ ഘടികാരം", + "Name[mr]": "डिजिटल घड्याळ", + "Name[nb]": "Digital klokke", + "Name[nds]": "Digitaal Klock", + "Name[ne]": "डिजिटल घडी", + "Name[nl]": "Digitale klok", + "Name[nn]": "Digital klokke", + "Name[oc]": "Relòtge numeric", + "Name[or]": "ସାଂଖିକ ଘଡ଼ି", + "Name[pa]": "ਡਿਜਿਟਲ ਘੜੀ", + "Name[pl]": "Zegar cyfrowy", + "Name[pt]": "Relógio Digital", + "Name[pt_BR]": "Relógio digital", + "Name[ro]": "Ceas digital", + "Name[ru]": "Цифровые часы", + "Name[se]": "Digitálalaš diibmu", + "Name[si]": "අංකිත ඔරලෝසුව", + "Name[sk]": "Digitálne hodiny", + "Name[sl]": "Digitalna ura", + "Name[sr@ijekavian]": "дигитални сат", + "Name[sr@ijekavianlatin]": "digitalni sat", + "Name[sr@latin]": "digitalni sat", + "Name[sr]": "дигитални сат", + "Name[sv]": "Digitalklocka", + "Name[ta]": "எண்ணுரு கடிகாரம்", + "Name[te]": "డిజిటల్ గడియారం", + "Name[tg]": "Соати рақамӣ", + "Name[th]": "นาฬิกาดิจิทัล", + "Name[tr]": "Dijital Saat", + "Name[ug]": "رەقەملىك سائەت", + "Name[uk]": "Цифровий годинник", + "Name[uz@cyrillic]": "Рақамли соат", + "Name[uz]": "Raqamli soat", + "Name[vi]": "Đồng hồ số", + "Name[wa]": "Ôrlodje didjitåle", + "Name[x-test]": "xxDigital Clockxx", + "Name[zh_CN]": "数字时钟", + "Name[zh_TW]": "數位時鐘", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "3.0", + "Website": "https://kde.org/plasma-desktop" + }, + "X-KDE-ParentApp": "org.kde.plasmashell", + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.time", + "org.kde.plasma.date" + ] +} diff --git a/plasma/workspace/applets/digital-clock/plugin/CMakeLists.txt b/plasma/workspace/applets/digital-clock/plugin/CMakeLists.txt new file mode 100644 index 0000000000..bd5fe19e7c --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/CMakeLists.txt @@ -0,0 +1,31 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.digitalclock\") + +find_package(IsoCodes) +set_package_properties(IsoCodes PROPERTIES DESCRIPTION "ISO language, territory, currency, script codes and their translations" + URL "https://salsa.debian.org/iso-codes-team/iso-codes" + PURPOSE "Translation of country names in digital clock applet" + TYPE RUNTIME + ) + +set(digitalclockplugin_SRCS + timezonemodel.cpp + timezonesi18n.cpp + digitalclockplugin.cpp + clipboardmenu.cpp + applicationintegration.cpp + ) + +add_library(digitalclockplugin SHARED ${digitalclockplugin_SRCS}) +target_link_libraries(digitalclockplugin + PRIVATE + Qt::Core + Qt::Qml + Qt::Widgets # for QAction... + KF5::CoreAddons + KF5::KIOGui + KF5::Service + KF5::I18n) + +install(TARGETS digitalclockplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/digitalclock) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/digitalclock) diff --git a/plasma/workspace/applets/digital-clock/plugin/applicationintegration.cpp b/plasma/workspace/applets/digital-clock/plugin/applicationintegration.cpp new file mode 100644 index 0000000000..4bdd68391b --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/applicationintegration.cpp @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#include "applicationintegration.h" +#include +#include + +ApplicationIntegration::ApplicationIntegration(QObject *parent) + : QObject(parent) +{ + const auto services = KApplicationTrader::queryByMimeType(QStringLiteral("text/calendar")); + + if (!services.isEmpty()) { + const KService::Ptr app = services.first(); + + if (app->desktopEntryName() == QLatin1String("org.kde.korganizer") || app->desktopEntryName() == QLatin1String("org.kde.kalendar")) { + m_calendarService = app; + } + } +} + +bool ApplicationIntegration::calendarInstalled() const +{ + return m_calendarService != nullptr; +} + +void ApplicationIntegration::launchCalendar() const +{ + Q_ASSERT(m_calendarService); + + auto job = new KIO::ApplicationLauncherJob(m_calendarService); + job->start(); +} diff --git a/plasma/workspace/applets/digital-clock/plugin/applicationintegration.h b/plasma/workspace/applets/digital-clock/plugin/applicationintegration.h new file mode 100644 index 0000000000..5972ccaff3 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/applicationintegration.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan +// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL + +#pragma once +#include +#include + +class ApplicationIntegration : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool calendarInstalled READ calendarInstalled CONSTANT) + +public: + explicit ApplicationIntegration(QObject *parent = nullptr); + ~ApplicationIntegration() = default; + + bool calendarInstalled() const; + Q_INVOKABLE void launchCalendar() const; + +private: + KService::Ptr m_calendarService; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.cpp b/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.cpp new file mode 100644 index 0000000000..6afa1a0f6a --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "clipboardmenu.h" +#include "klocalizedstring.h" + +#include +#include +#include +#include + +ClipboardMenu::ClipboardMenu(QObject *parent) + : QObject(parent) +{ +} + +ClipboardMenu::~ClipboardMenu() = default; + +QDateTime ClipboardMenu::currentDate() const +{ + return m_currentDate; +} + +void ClipboardMenu::setCurrentDate(const QDateTime ¤tDate) +{ + if (m_currentDate != currentDate) { + m_currentDate = currentDate; + Q_EMIT currentDateChanged(); + } +} + +bool ClipboardMenu::secondsIncluded() const +{ + return m_secondsIncluded; +} + +void ClipboardMenu::setSecondsIncluded(const bool &secondsIncluded) +{ + if (m_secondsIncluded != secondsIncluded) { + m_secondsIncluded = secondsIncluded; + Q_EMIT secondsIncludedChanged(); + } +} + +void ClipboardMenu::setupMenu(QAction *action) +{ + QMenu *menu = new QMenu; + + /* + * The refresh rate of m_currentDate depends of what is shown in the plasmoid. + * If only minutes are shown, the value is updated at the full minute and + * seconds are always 0 or 59 and thus useless/confusing to offer for copy. + * Use a reference to the config's showSeconds to decide if seconds are sent + * to the clipboard. There was no workaround found ... + */ + connect(menu, &QMenu::aboutToShow, this, [this, menu] { + menu->clear(); + + const QDate date = m_currentDate.date(); + const QTime time = m_currentDate.time(); + const QRegularExpression rx("[^0-9:]"); + const QChar ws = QLatin1Char(' '); + QString s; + QAction *a; + + s = QLocale().toString(date, QLocale::ShortFormat); + a = menu->addAction(s); + a->setData(s); + s = date.toString(Qt::ISODate); + a = menu->addAction(s); + a->setData(s); + s = QLocale().toString(date, QLocale::LongFormat); + a = menu->addAction(s); + a->setData(s); + + menu->addSeparator(); + + s = QLocale().toString(time, QLocale::ShortFormat); + a = menu->addAction(s); + a->setData(s); + if (m_secondsIncluded) { + s = QLocale().toString(time, QLocale::LongFormat); + s.remove(rx); + a = menu->addAction(s); + a->setData(s); + s = QLocale().toString(time, QLocale::LongFormat); + a = menu->addAction(s); + a->setData(s); + } + + menu->addSeparator(); + + s = QLocale().toString(time, QLocale::ShortFormat) + ws + QLocale().toString(time, QLocale::ShortFormat); + a = menu->addAction(s); + a->setData(s); + if (m_secondsIncluded) { + s = QLocale().toString(time, QLocale::ShortFormat) + ws + QLocale().toString(time, QLocale::LongFormat).remove(rx); + a = menu->addAction(s); + a->setData(s); + s = QLocale().toString(time, QLocale::ShortFormat) + ws + QLocale().toString(time, QLocale::LongFormat); + a = menu->addAction(s); + a->setData(s); + } + s = date.toString(Qt::ISODate) + ws + QLocale().toString(time, QLocale::ShortFormat); + a = menu->addAction(s); + a->setData(s); + if (m_secondsIncluded) { + s = date.toString(Qt::ISODate) + ws + QLocale().toString(time, QLocale::LongFormat).remove(rx); + a = menu->addAction(s); + a->setData(s); + s = date.toString(Qt::ISODate) + ws + QLocale().toString(time, QLocale::LongFormat); + a = menu->addAction(s); + a->setData(s); + } + s = QLocale().toString(date, QLocale::LongFormat) + ws + QLocale().toString(time, QLocale::ShortFormat); + a = menu->addAction(s); + a->setData(s); + + menu->addSeparator(); + + QMenu *otherCalendarsMenu = menu->addMenu(i18n("Other Calendars")); + + /* Add ICU Calendars if QLocale is ready for + Chinese, Coptic, Ethiopic, (Gregorian), Hebrew, Indian, Islamic, Persian + + otherCalendarsMenu->addSeparator(); + */ + s = QString::number(m_currentDate.toMSecsSinceEpoch() / 1000); + a = otherCalendarsMenu->addAction(i18nc("unix timestamp (seconds since 1.1.1970)", "%1 (UNIX Time)", s)); + a->setData(s); + s = QString::number(qreal(2440587.5) + qreal(m_currentDate.toMSecsSinceEpoch()) / qreal(86400000), 'f', 5); + a = otherCalendarsMenu->addAction(i18nc("for astronomers (days and decimals since ~7000 years ago)", "%1 (Julian Date)", s)); + a->setData(s); + }); + + connect(menu, &QMenu::triggered, menu, [](QAction *action) { + qApp->clipboard()->setText(action->data().toString()); + qApp->clipboard()->setText(action->data().toString(), QClipboard::Selection); + }); + + // QMenu cannot have QAction as parent and setMenu doesn't take ownership + connect(action, &QObject::destroyed, menu, &QObject::deleteLater); + + action->setMenu(menu); +} diff --git a/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.h b/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.h new file mode 100644 index 0000000000..2cc8ba8f3b --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/clipboardmenu.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include + +class QAction; + +class ClipboardMenu : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QDateTime currentDate READ currentDate WRITE setCurrentDate NOTIFY currentDateChanged) + Q_PROPERTY(bool secondsIncluded READ secondsIncluded WRITE setSecondsIncluded NOTIFY secondsIncludedChanged) + +public: + explicit ClipboardMenu(QObject *parent = nullptr); + virtual ~ClipboardMenu(); + + QDateTime currentDate() const; + void setCurrentDate(const QDateTime &date); + + bool secondsIncluded() const; + void setSecondsIncluded(const bool &secondsIncluded); + + Q_INVOKABLE void setupMenu(QAction *action); + +Q_SIGNALS: + void currentDateChanged(); + void secondsIncludedChanged(); + +private: + QDateTime m_currentDate; + bool m_secondsIncluded = false; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.cpp b/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.cpp new file mode 100644 index 0000000000..f1a07c94aa --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "digitalclockplugin.h" +#include "applicationintegration.h" +#include "clipboardmenu.h" +#include "timezonemodel.h" +#include "timezonesi18n.h" + +#include + +static QObject *timezonesi18n_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + + return new TimezonesI18n(); +} + +static QObject *clipboardMenu_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine); + Q_UNUSED(scriptEngine); + + return new ClipboardMenu(); +} + +void DigitalClockPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.digitalclock")); + + qmlRegisterType(uri, 1, 0, "TimeZoneModel"); + qmlRegisterType(uri, 1, 0, "TimeZoneFilterProxy"); + qmlRegisterSingletonType(uri, 1, 0, "TimezonesI18n", timezonesi18n_singletontype_provider); + + qmlRegisterSingletonType(uri, 1, 0, "ClipboardMenu", clipboardMenu_singletontype_provider); + + qmlRegisterSingletonType(uri, 1, 0, "ApplicationIntegration", [](QQmlEngine *engine, QJSEngine *scriptEngine) { + Q_UNUSED(engine); + Q_UNUSED(scriptEngine); + return new ApplicationIntegration(); + }); +} diff --git a/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.h b/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.h new file mode 100644 index 0000000000..a91a9d5f2d --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/digitalclockplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +class DigitalClockPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/qmldir b/plasma/workspace/applets/digital-clock/plugin/qmldir new file mode 100644 index 0000000000..4d1d209529 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/qmldir @@ -0,0 +1,3 @@ +module org.kde.plasma.private.digitalclock + +plugin digitalclockplugin diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonedata.h b/plasma/workspace/applets/digital-clock/plugin/timezonedata.h new file mode 100644 index 0000000000..d1044e9a40 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonedata.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class TimeZoneData +{ +public: + QString id; + QString region; + QString city; + QString comment; + bool checked = false; + bool isLocalTimeZone = false; + int offsetFromUtc = 0; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonemodel.cpp b/plasma/workspace/applets/digital-clock/plugin/timezonemodel.cpp new file mode 100644 index 0000000000..c19b5006ea --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonemodel.cpp @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "timezonemodel.h" +#include "timezonesi18n.h" + +#include +#include +#include + +TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) + : QSortFilterProxyModel(parent) +{ + m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive); +} + +bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!sourceModel() || (m_filterString.isEmpty() && !m_onlyShowChecked)) { + return true; + } + + const bool checked = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CheckedRole).toBool(); + if (m_onlyShowChecked && !checked) { + return false; + } + + const QString city = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CityRole).toString(); + const QString region = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::RegionRole).toString(); + const QString comment = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CommentRole).toString(); + + if (m_stringMatcher.indexIn(city) != -1 || m_stringMatcher.indexIn(region) != -1 || m_stringMatcher.indexIn(comment) != -1) { + return true; + } + + return false; +} + +void TimeZoneFilterProxy::setFilterString(const QString &filterString) +{ + m_filterString = filterString; + m_stringMatcher.setPattern(filterString); + Q_EMIT filterStringChanged(); + invalidateFilter(); +} + +void TimeZoneFilterProxy::setOnlyShowChecked(const bool show) +{ + if (m_onlyShowChecked == show) { + return; + } + m_onlyShowChecked = show; + Q_EMIT onlyShowCheckedChanged(); +} + +//============================================================================= + +TimeZoneModel::TimeZoneModel(QObject *parent) + : QAbstractListModel(parent) + , m_timezonesI18n(new TimezonesI18n(this)) +{ + update(); +} + +TimeZoneModel::~TimeZoneModel() +{ +} + +int TimeZoneModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant TimeZoneModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid()) { + TimeZoneData currentData = m_data.at(index.row()); + + switch (role) { + case TimeZoneIdRole: + return currentData.id; + case RegionRole: + return currentData.region; + case CityRole: + return currentData.city; + case CommentRole: + return currentData.comment; + case CheckedRole: + return currentData.checked; + case IsLocalTimeZoneRole: + return currentData.isLocalTimeZone; + } + } + + return QVariant(); +} + +bool TimeZoneModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || value.isNull()) { + return false; + } + + if (role == CheckedRole) { + m_data[index.row()].checked = value.toBool(); + Q_EMIT dataChanged(index, index); + + if (m_data[index.row()].checked) { + m_selectedTimeZones.append(m_data[index.row()].id); + m_offsetData.insert(m_data[index.row()].id, m_data[index.row()].offsetFromUtc); + } else { + m_selectedTimeZones.removeAll(m_data[index.row()].id); + m_offsetData.remove(m_data[index.row()].id); + } + + sortTimeZones(); + + Q_EMIT selectedTimeZonesChanged(); + return true; + } + + return false; +} + +void TimeZoneModel::update() +{ + beginResetModel(); + m_data.clear(); + + QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); + const QStringList data = QString::fromUtf8(localZone.id()).split(QLatin1Char('/')); + + TimeZoneData local; + local.isLocalTimeZone = true; + local.id = QStringLiteral("Local"); + local.region = i18nc("This means \"Local Timezone\"", "Local"); + local.city = m_timezonesI18n->i18nCity(data.last()); + local.comment = i18n("System's local time zone"); + local.checked = false; + + m_data.append(local); + + QStringList cities; + QHash zonesByCity; + + const QList systemTimeZones = QTimeZone::availableTimeZoneIds(); + + for (auto it = systemTimeZones.constBegin(); it != systemTimeZones.constEnd(); ++it) { + const QTimeZone zone(*it); + const QStringList splitted = QString::fromUtf8(zone.id()).split(QStringLiteral("/")); + + // CITY | COUNTRY | CONTINENT + const QString key = QStringLiteral("%1|%2|%3").arg(splitted.last(), QLocale::countryToString(zone.country()), splitted.first()); + + cities.append(key); + zonesByCity.insert(key, zone); + } + cities.sort(Qt::CaseInsensitive); + + for (const QString &key : qAsConst(cities)) { + const QTimeZone timeZone = zonesByCity.value(key); + QString comment = timeZone.comment(); + + if (!comment.isEmpty()) { + comment = i18n(comment.toUtf8()); + } + + const QStringList cityCountryContinent = key.split(QLatin1Char('|')); + + TimeZoneData newData; + newData.isLocalTimeZone = false; + newData.id = timeZone.id(); + newData.region = timeZone.country() == QLocale::AnyCountry + ? QString() + : m_timezonesI18n->i18nContinents(cityCountryContinent.at(2)) + QLatin1Char('/') + m_timezonesI18n->i18nCountry(timeZone.country()); + newData.city = m_timezonesI18n->i18nCity(cityCountryContinent.at(0)); + newData.comment = comment; + newData.checked = false; + newData.offsetFromUtc = timeZone.offsetFromUtc(QDateTime::currentDateTimeUtc()); + m_data.append(newData); + } + + endResetModel(); +} + +void TimeZoneModel::setSelectedTimeZones(const QStringList &selectedTimeZones) +{ + m_selectedTimeZones = selectedTimeZones; + for (int i = 0; i < m_data.size(); i++) { + if (m_selectedTimeZones.contains(m_data.at(i).id)) { + m_data[i].checked = true; + m_offsetData.insert(m_data[i].id, m_data[i].offsetFromUtc); + + QModelIndex index = createIndex(i, 0); + Q_EMIT dataChanged(index, index); + } + } + + sortTimeZones(); +} + +void TimeZoneModel::selectLocalTimeZone() +{ + m_data[0].checked = true; + + QModelIndex index = createIndex(0, 0); + Q_EMIT dataChanged(index, index); + + m_selectedTimeZones << m_data[0].id; + Q_EMIT selectedTimeZonesChanged(); +} + +QHash TimeZoneModel::roleNames() const +{ + return QHash({ + {TimeZoneIdRole, "timeZoneId"}, + {RegionRole, "region"}, + {CityRole, "city"}, + {CommentRole, "comment"}, + {CheckedRole, "checked"}, + {IsLocalTimeZoneRole, "isLocalTimeZone"}, + }); +} + +void TimeZoneModel::sortTimeZones() +{ + std::sort(m_selectedTimeZones.begin(), m_selectedTimeZones.end(), [this](const QString &a, const QString &b) { + return m_offsetData.value(a) < m_offsetData.value(b); + }); +} diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonemodel.h b/plasma/workspace/applets/digital-clock/plugin/timezonemodel.h new file mode 100644 index 0000000000..0f6b23f917 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonemodel.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "timezonedata.h" + +class TimezonesI18n; + +class TimeZoneFilterProxy : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filterString WRITE setFilterString MEMBER m_filterString NOTIFY filterStringChanged) + Q_PROPERTY(bool onlyShowChecked WRITE setOnlyShowChecked MEMBER m_onlyShowChecked NOTIFY onlyShowCheckedChanged) + +public: + explicit TimeZoneFilterProxy(QObject *parent = nullptr); + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + void setFilterString(const QString &filterString); + void setOnlyShowChecked(const bool show); + +Q_SIGNALS: + void filterStringChanged(); + void onlyShowCheckedChanged(); + +private: + QString m_filterString; + bool m_onlyShowChecked = false; + QStringMatcher m_stringMatcher; +}; + +//============================================================================= + +class TimeZoneModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QStringList selectedTimeZones WRITE setSelectedTimeZones MEMBER m_selectedTimeZones NOTIFY selectedTimeZonesChanged) + +public: + explicit TimeZoneModel(QObject *parent = nullptr); + ~TimeZoneModel() override; + + enum Roles { + TimeZoneIdRole = Qt::UserRole + 1, + RegionRole, + CityRole, + CommentRole, + CheckedRole, + IsLocalTimeZoneRole, + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + void update(); + void setSelectedTimeZones(const QStringList &selectedTimeZones); + + Q_INVOKABLE void selectLocalTimeZone(); + +Q_SIGNALS: + void selectedTimeZonesChanged(); + +protected: + QHash roleNames() const override; + +private: + void sortTimeZones(); + + QList m_data; + QHash m_offsetData; // used for sorting + QStringList m_selectedTimeZones; + TimezonesI18n *m_timezonesI18n; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.cpp b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.cpp new file mode 100644 index 0000000000..1bc08996f0 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.cpp @@ -0,0 +1,307 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "timezonesi18n.h" + +#include + +#include "timezonesi18n_generated.h" + +TimezonesI18n::TimezonesI18n(QObject *parent) + : QObject(parent) + , m_isInitialized(false) +{ +} + +QString TimezonesI18n::i18nCity(const QString &city) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nCities.value(city); +} + +QString TimezonesI18n::i18nContinents(const QString &continent) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nContinents.value(continent); +} + +QString TimezonesI18n::i18nCountry(QLocale::Country country) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nCountries.value(country); +} + +void TimezonesI18n::init() +{ + m_i18nCities = TimezonesI18nData::timezoneCityToL10nMap(); + m_i18nContinents = TimezonesI18nData::timezoneContinentToL10nMap(); + +#define ENTRY_ISO_3166(qlocale_enum, string) {QLocale::qlocale_enum, i18nd("iso_3166", string)} + /* Make sure the country names match their versions in iso-codes, + * ISO 3166. */ + m_i18nCountries = QHash({ + ENTRY_ISO_3166(IvoryCoast, "Côte d'Ivoire"), + ENTRY_ISO_3166(Ghana, "Ghana"), + ENTRY_ISO_3166(Ethiopia, "Ethiopia"), + ENTRY_ISO_3166(Algeria, "Algeria"), + ENTRY_ISO_3166(Eritrea, "Eritrea"), + ENTRY_ISO_3166(Mali, "Mali"), + ENTRY_ISO_3166(CentralAfricanRepublic, "Central African Republic"), + ENTRY_ISO_3166(Gambia, "Gambia"), + ENTRY_ISO_3166(GuineaBissau, "Guinea-Bissau"), + ENTRY_ISO_3166(Malawi, "Malawi"), + ENTRY_ISO_3166(CongoBrazzaville, "Congo"), + ENTRY_ISO_3166(Burundi, "Burundi"), + ENTRY_ISO_3166(Egypt, "Egypt"), + ENTRY_ISO_3166(Morocco, "Morocco"), + ENTRY_ISO_3166(Spain, "Spain"), + ENTRY_ISO_3166(Guinea, "Guinea"), + ENTRY_ISO_3166(Senegal, "Senegal"), + ENTRY_ISO_3166(Tanzania, "Tanzania"), + ENTRY_ISO_3166(Djibouti, "Djibouti"), + ENTRY_ISO_3166(Cameroon, "Cameroon"), + ENTRY_ISO_3166(WesternSahara, "Western Sahara"), + ENTRY_ISO_3166(SierraLeone, "Sierra Leone"), + ENTRY_ISO_3166(Botswana, "Botswana"), + ENTRY_ISO_3166(BouvetIsland, "Bouvet Island"), + ENTRY_ISO_3166(Zimbabwe, "Zimbabwe"), + ENTRY_ISO_3166(SouthAfrica, "South Africa"), + ENTRY_ISO_3166(SouthSudan, "South Sudan"), + ENTRY_ISO_3166(Uganda, "Uganda"), + ENTRY_ISO_3166(Sudan, "Sudan"), + ENTRY_ISO_3166(Rwanda, "Rwanda"), + ENTRY_ISO_3166(CongoKinshasa, "Congo, The Democratic Republic of the"), + ENTRY_ISO_3166(Nigeria, "Nigeria"), + ENTRY_ISO_3166(Gabon, "Gabon"), + ENTRY_ISO_3166(Togo, "Togo"), + ENTRY_ISO_3166(Angola, "Angola"), + ENTRY_ISO_3166(Zambia, "Zambia"), + ENTRY_ISO_3166(EquatorialGuinea, "Equatorial Guinea"), + ENTRY_ISO_3166(Mozambique, "Mozambique"), + ENTRY_ISO_3166(Lesotho, "Lesotho"), + ENTRY_ISO_3166(Swaziland, "Swaziland"), + ENTRY_ISO_3166(Somalia, "Somalia"), + ENTRY_ISO_3166(Liberia, "Liberia"), + ENTRY_ISO_3166(Kenya, "Kenya"), + ENTRY_ISO_3166(Chad, "Chad"), + ENTRY_ISO_3166(Niger, "Niger"), + ENTRY_ISO_3166(Mauritania, "Mauritania"), + ENTRY_ISO_3166(BurkinaFaso, "Burkina Faso"), + ENTRY_ISO_3166(Benin, "Benin"), + ENTRY_ISO_3166(SaoTomeAndPrincipe, "Sao Tome and Principe"), + ENTRY_ISO_3166(Libya, "Libya"), + ENTRY_ISO_3166(Tunisia, "Tunisia"), + ENTRY_ISO_3166(Namibia, "Namibia"), + ENTRY_ISO_3166(UnitedStates, "United States"), + ENTRY_ISO_3166(Anguilla, "Anguilla"), + ENTRY_ISO_3166(AntiguaAndBarbuda, "Antigua and Barbuda"), + ENTRY_ISO_3166(Brazil, "Brazil"), + ENTRY_ISO_3166(Argentina, "Argentina"), + ENTRY_ISO_3166(Aruba, "Aruba"), + ENTRY_ISO_3166(Paraguay, "Paraguay"), + ENTRY_ISO_3166(Canada, "Canada"), + ENTRY_ISO_3166(Mexico, "Mexico"), + ENTRY_ISO_3166(Barbados, "Barbados"), + ENTRY_ISO_3166(Belize, "Belize"), + ENTRY_ISO_3166(Colombia, "Colombia"), + ENTRY_ISO_3166(Venezuela, "Venezuela"), + ENTRY_ISO_3166(FrenchGuiana, "French Guiana"), + ENTRY_ISO_3166(CaymanIslands, "Cayman Islands"), + ENTRY_ISO_3166(CostaRica, "Costa Rica"), + ENTRY_ISO_3166(CuraSao, "Curaçao"), + ENTRY_ISO_3166(Greenland, "Greenland"), + ENTRY_ISO_3166(Dominica, "Dominica"), + ENTRY_ISO_3166(ElSalvador, "El Salvador"), + ENTRY_ISO_3166(TurksAndCaicosIslands, "Turks and Caicos Islands"), + ENTRY_ISO_3166(Grenada, "Grenada"), + ENTRY_ISO_3166(Guadeloupe, "Guadeloupe"), + ENTRY_ISO_3166(Guatemala, "Guatemala"), + ENTRY_ISO_3166(Ecuador, "Ecuador"), + ENTRY_ISO_3166(Guyana, "Guyana"), + ENTRY_ISO_3166(Cuba, "Cuba"), + ENTRY_ISO_3166(Jamaica, "Jamaica"), + ENTRY_ISO_3166(Bonaire, "Bonaire, Sint Eustatius and Saba"), + ENTRY_ISO_3166(Bolivia, "Bolivia"), + ENTRY_ISO_3166(Peru, "Peru"), + ENTRY_ISO_3166(SintMaarten, "Sint Maarten (Dutch part)"), + ENTRY_ISO_3166(Nicaragua, "Nicaragua"), + ENTRY_ISO_3166(SaintMartin, "Saint Martin (French part)"), + ENTRY_ISO_3166(Martinique, "Martinique"), + ENTRY_ISO_3166(SaintPierreAndMiquelon, "Saint Pierre and Miquelon"), + ENTRY_ISO_3166(Uruguay, "Uruguay"), + ENTRY_ISO_3166(Montserrat, "Montserrat"), + ENTRY_ISO_3166(Bahamas, "Bahamas"), + ENTRY_ISO_3166(Panama, "Panama"), + ENTRY_ISO_3166(Suriname, "Suriname"), + ENTRY_ISO_3166(Haiti, "Haiti"), + ENTRY_ISO_3166(HeardAndMcDonaldIslands, "Heard Island and McDonald Islands"), + ENTRY_ISO_3166(TrinidadAndTobago, "Trinidad and Tobago"), + ENTRY_ISO_3166(PuertoRico, "Puerto Rico"), + ENTRY_ISO_3166(Chile, "Chile"), + ENTRY_ISO_3166(DominicanRepublic, "Dominican Republic"), + ENTRY_ISO_3166(SaintBarthelemy, "Saint Barthélemy"), + ENTRY_ISO_3166(SaintKittsAndNevis, "Saint Kitts and Nevis"), + ENTRY_ISO_3166(SaintLucia, "Saint Lucia"), + ENTRY_ISO_3166(UnitedStatesVirginIslands, "Virgin Islands, U.S."), + ENTRY_ISO_3166(SaintVincentAndTheGrenadines, "Saint Vincent and the Grenadines"), + ENTRY_ISO_3166(Honduras, "Honduras"), + ENTRY_ISO_3166(BritishVirginIslands, "Virgin Islands, British"), + ENTRY_ISO_3166(Antarctica, "Antarctica"), + ENTRY_ISO_3166(Australia, "Australia"), + ENTRY_ISO_3166(SvalbardAndJanMayenIslands, "Svalbard and Jan Mayen"), + ENTRY_ISO_3166(Yemen, "Yemen"), + ENTRY_ISO_3166(Kazakhstan, "Kazakhstan"), + ENTRY_ISO_3166(Jordan, "Jordan"), + ENTRY_ISO_3166(Russia, "Russian Federation"), + ENTRY_ISO_3166(Turkmenistan, "Turkmenistan"), + ENTRY_ISO_3166(Iraq, "Iraq"), + ENTRY_ISO_3166(Bahrain, "Bahrain"), + ENTRY_ISO_3166(Azerbaijan, "Azerbaijan"), + ENTRY_ISO_3166(Thailand, "Thailand"), + ENTRY_ISO_3166(Lebanon, "Lebanon"), + ENTRY_ISO_3166(Kyrgyzstan, "Kyrgyzstan"), + ENTRY_ISO_3166(Brunei, "Brunei Darussalam"), + ENTRY_ISO_3166(Mongolia, "Mongolia"), + ENTRY_ISO_3166(China, "China"), + ENTRY_ISO_3166(SriLanka, "Sri Lanka"), + ENTRY_ISO_3166(Syria, "Syrian Arab Republic"), + ENTRY_ISO_3166(Bangladesh, "Bangladesh"), + ENTRY_ISO_3166(EastTimor, "Timor-Leste"), + ENTRY_ISO_3166(UnitedArabEmirates, "United Arab Emirates"), + ENTRY_ISO_3166(Tajikistan, "Tajikistan"), + ENTRY_ISO_3166(PalestinianTerritories, "Palestine, State of"), + ENTRY_ISO_3166(Vietnam, "Vietnam"), + ENTRY_ISO_3166(HongKong, "Hong Kong"), + ENTRY_ISO_3166(Indonesia, "Indonesia"), + ENTRY_ISO_3166(Israel, "Israel"), + ENTRY_ISO_3166(Afghanistan, "Afghanistan"), + ENTRY_ISO_3166(Pakistan, "Pakistan"), + ENTRY_ISO_3166(Nepal, "Nepal"), + ENTRY_ISO_3166(India, "India"), + ENTRY_ISO_3166(Malaysia, "Malaysia"), + ENTRY_ISO_3166(Kuwait, "Kuwait"), + ENTRY_ISO_3166(Macau, "Macao"), + ENTRY_ISO_3166(Philippines, "Philippines"), + ENTRY_ISO_3166(Oman, "Oman"), + ENTRY_ISO_3166(Cyprus, "Cyprus"), + ENTRY_ISO_3166(Cambodia, "Cambodia"), + ENTRY_ISO_3166(NorthKorea, "Korea, Democratic People's Republic of"), + ENTRY_ISO_3166(Qatar, "Qatar"), + ENTRY_ISO_3166(Myanmar, "Myanmar"), + ENTRY_ISO_3166(SaudiArabia, "Saudi Arabia"), + ENTRY_ISO_3166(Uzbekistan, "Uzbekistan"), + ENTRY_ISO_3166(SouthKorea, "Korea, Republic of"), + ENTRY_ISO_3166(Singapore, "Singapore"), + ENTRY_ISO_3166(Taiwan, "Taiwan"), + ENTRY_ISO_3166(Georgia, "Georgia"), + ENTRY_ISO_3166(Iran, "Iran, Islamic Republic of"), + ENTRY_ISO_3166(Bhutan, "Bhutan"), + ENTRY_ISO_3166(Japan, "Japan"), + ENTRY_ISO_3166(Laos, "Lao People's Democratic Republic"), + ENTRY_ISO_3166(Armenia, "Armenia"), + ENTRY_ISO_3166(Portugal, "Portugal"), + ENTRY_ISO_3166(Bermuda, "Bermuda"), + ENTRY_ISO_3166(CapeVerde, "Cabo Verde"), + ENTRY_ISO_3166(FaroeIslands, "Faroe Islands"), + ENTRY_ISO_3166(Iceland, "Iceland"), + ENTRY_ISO_3166(SouthGeorgiaAndTheSouthSandwichIslands, "South Georgia and the South Sandwich Islands"), + ENTRY_ISO_3166(SaintHelena, "Saint Helena, Ascension and Tristan da Cunha"), + ENTRY_ISO_3166(FalklandIslands, "Falkland Islands (Malvinas)"), + ENTRY_ISO_3166(Netherlands, "Netherlands"), + ENTRY_ISO_3166(Andorra, "Andorra"), + ENTRY_ISO_3166(Greece, "Greece"), + ENTRY_ISO_3166(Serbia, "Serbia"), + ENTRY_ISO_3166(Germany, "Germany"), + ENTRY_ISO_3166(Slovakia, "Slovakia"), + ENTRY_ISO_3166(Belgium, "Belgium"), + ENTRY_ISO_3166(Romania, "Romania"), + ENTRY_ISO_3166(Hungary, "Hungary"), + ENTRY_ISO_3166(Moldova, "Moldova"), + ENTRY_ISO_3166(Denmark, "Denmark"), + ENTRY_ISO_3166(Ireland, "Ireland"), + ENTRY_ISO_3166(Gibraltar, "Gibraltar"), + ENTRY_ISO_3166(Guernsey, "Guernsey"), + ENTRY_ISO_3166(Finland, "Finland"), + ENTRY_ISO_3166(IsleOfMan, "Isle of Man"), + ENTRY_ISO_3166(Turkey, "Turkey"), + ENTRY_ISO_3166(Jersey, "Jersey"), + ENTRY_ISO_3166(Ukraine, "Ukraine"), + ENTRY_ISO_3166(Slovenia, "Slovenia"), + ENTRY_ISO_3166(UnitedKingdom, "United Kingdom"), + ENTRY_ISO_3166(Luxembourg, "Luxembourg"), + ENTRY_ISO_3166(Malta, "Malta"), + ENTRY_ISO_3166(AlandIslands, "Åland Islands"), + ENTRY_ISO_3166(Belarus, "Belarus"), + ENTRY_ISO_3166(Monaco, "Monaco"), + ENTRY_ISO_3166(Norway, "Norway"), + ENTRY_ISO_3166(France, "France"), + ENTRY_ISO_3166(Montenegro, "Montenegro"), + ENTRY_ISO_3166(CzechRepublic, "Czechia"), + ENTRY_ISO_3166(Latvia, "Latvia"), + ENTRY_ISO_3166(Italy, "Italy"), + ENTRY_ISO_3166(SanMarino, "San Marino"), + ENTRY_ISO_3166(BosniaAndHerzegowina, "Bosnia and Herzegovina"), + ENTRY_ISO_3166(Macedonia, "Macedonia, Republic of"), + ENTRY_ISO_3166(Bulgaria, "Bulgaria"), + ENTRY_ISO_3166(Sweden, "Sweden"), + ENTRY_ISO_3166(Estonia, "Estonia"), + ENTRY_ISO_3166(Albania, "Albania"), + ENTRY_ISO_3166(Liechtenstein, "Liechtenstein"), + ENTRY_ISO_3166(VaticanCityState, "Holy See (Vatican City State)"), + ENTRY_ISO_3166(Austria, "Austria"), + ENTRY_ISO_3166(Lithuania, "Lithuania"), + ENTRY_ISO_3166(Poland, "Poland"), + ENTRY_ISO_3166(Croatia, "Croatia"), + ENTRY_ISO_3166(Switzerland, "Switzerland"), + ENTRY_ISO_3166(Madagascar, "Madagascar"), + ENTRY_ISO_3166(BritishIndianOceanTerritory, "British Indian Ocean Territory"), + ENTRY_ISO_3166(ChristmasIsland, "Christmas Island"), + ENTRY_ISO_3166(CocosIslands, "Cocos (Keeling) Islands"), + ENTRY_ISO_3166(Comoros, "Comoros"), + ENTRY_ISO_3166(FrenchSouthernTerritories, "French Southern Territories"), + ENTRY_ISO_3166(Seychelles, "Seychelles"), + ENTRY_ISO_3166(Maldives, "Maldives"), + ENTRY_ISO_3166(Mauritius, "Mauritius"), + ENTRY_ISO_3166(Mayotte, "Mayotte"), + ENTRY_ISO_3166(Reunion, "Réunion"), + ENTRY_ISO_3166(Samoa, "Samoa"), + ENTRY_ISO_3166(NewZealand, "New Zealand"), + ENTRY_ISO_3166(Micronesia, "Micronesia, Federated States of"), + ENTRY_ISO_3166(Vanuatu, "Vanuatu"), + ENTRY_ISO_3166(Kiribati, "Kiribati"), + ENTRY_ISO_3166(TokelauCountry, "Tokelau"), + ENTRY_ISO_3166(Fiji, "Fiji"), + ENTRY_ISO_3166(TuvaluCountry, "Tuvalu"), + ENTRY_ISO_3166(FrenchPolynesia, "French Polynesia"), + ENTRY_ISO_3166(SolomonIslands, "Solomon Islands"), + ENTRY_ISO_3166(Guam, "Guam"), + ENTRY_ISO_3166(UnitedStatesMinorOutlyingIslands, "United States Minor Outlying Islands"), + ENTRY_ISO_3166(MarshallIslands, "Marshall Islands"), + ENTRY_ISO_3166(NauruCountry, "Nauru"), + ENTRY_ISO_3166(Niue, "Niue"), + ENTRY_ISO_3166(NorfolkIsland, "Norfolk Island"), + ENTRY_ISO_3166(NewCaledonia, "New Caledonia"), + ENTRY_ISO_3166(AmericanSamoa, "American Samoa"), + ENTRY_ISO_3166(Palau, "Palau"), + ENTRY_ISO_3166(Pitcairn, "Pitcairn"), + ENTRY_ISO_3166(PapuaNewGuinea, "Papua New Guinea"), + ENTRY_ISO_3166(CookIslands, "Cook Islands"), + ENTRY_ISO_3166(NorthernMarianaIslands, "Northern Mariana Islands"), + ENTRY_ISO_3166(Tonga, "Tonga"), + ENTRY_ISO_3166(WallisAndFutunaIslands, "Wallis and Futuna") + // {QLocale::Default, i18nc("This is a country name associated with a particular time zone in a zone selection dialog", + // "Default")} }, + }); +#undef ENTRY_ISO_3166 + + m_isInitialized = true; +} diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.h b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.h new file mode 100644 index 0000000000..399bb23d09 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include + +class TimezonesI18n : public QObject +{ + Q_OBJECT + +public: + explicit TimezonesI18n(QObject *parent = nullptr); + Q_INVOKABLE QString i18nCity(const QString &city); + Q_INVOKABLE QString i18nContinents(const QString &continent); + Q_INVOKABLE QString i18nCountry(QLocale::Country country); + +private: + void init(); + + QHash m_i18nCities; + QHash m_i18nContinents; + QHash m_i18nCountries; + bool m_isInitialized; +}; diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generate.rb b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generate.rb new file mode 100644 index 0000000000..6a4449db33 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generate.rb @@ -0,0 +1,113 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +# SPDX-FileCopyrightText: 2021 Harald Sitter + +# Generates timezone tables out of tzdata.zi backing data such that the pretty names may be translated. + +require 'erb' +require 'ostruct' + +# Wraps template rendering. Templating is implemented using the ERB template system. +class Template + def self.render(context) + erb = ERB.new(File.read("#{__dir__}/timezonesi18n_generated.h.erb")) + erb.result(context.instance_eval { binding }) + end +end + +# Context object for template rendering. Inside the template file anything defined in here may be used. +# For simplicity's sake continents and cities are not modeled as fully functional objects but rather use +# this context wrapper. Slightly less code. +class RenderContext < OpenStruct + def initialize(continents:, cities:) + super + end + + def city_description(city) + return 'This is a generic time zone name, localize as needed' if city.start_with?('UTC') + + 'This is a city associated with particular time zone' + end + + def continent_description(_continent) + 'This is a continent/area associated with a particular timezone' + end + + # prettify city name + def to_city_name(city) + city = city.tr('_', ' ') + { + 'DumontDUrville' => 'Dumont d’Urville', + 'ComodRivadavia' => 'Comodoro Rivadavia' + # TODO do we want these? seeing as different transliterations are in use, one is just as bad as the other, surely + # 'Uzhgorod' => 'Uzhhorod' # the tz has the legacy transliteration + # 'Zaporozhye' => 'Zaporizhzhia' + # 'Kiev' => 'Kyiv' + }.fetch(city, city) + end + + # prettify continent name + def to_contient_name(continent) + continent # leave unchanged, they are all lovely + end +end + +raise 'tzdata.zi missing, your tzdata was possibly built without' unless File.exist?('/usr/share/zoneinfo/tzdata.zi') + +data = File.read('/usr/share/zoneinfo/tzdata.zi') + +continents = [] +cities = [] +data.split("\n").each do |line| + line = line.strip + next if line[0] == '#' # comment + + # zi files have a tricky format. only look at lines that make sense for us (Zone entries and Link entries) + is_zone = line[0].downcase == 'z' + is_link = line[0].downcase == 'l' + next unless is_zone || is_link + + line_parts = line.split(' ') + + # Since we have 2 line formats, the location of the timezone is different in each + tz = is_zone ? line_parts[1] : line_parts[2] + parts = tz.split('/') + + # some links are fishy single names we can't use (e.g. `L Europe/Warsaw Poland`) + next if parts.size < 2 + + continent = parts[0] + + # some links also have fake continent values (e.g. `L America/Denver SystemV/MST7MDT`) followed by regions therein + next if %w[systemv us etc canada brazil mexico chile].any? { |x| x == continent.downcase } + + city = parts[-1] + + # some links are simply pointing at directions within a region/country + next if %w[north east south west].any? { |x| x == city.downcase } + # some links are abbreviations (e.g. `Australia/NSW` for new south wales) + # Australia has some more links for regions but we'd have to filter them individually and I can't be bothered. + next if city.size <= 3 && city.upcase == city + # some are Knox and also known as Knox_IN for reasons + next if city == 'Knox_IN' + + continents << continent + cities << city +end + +# make sure fake cities exist +(0..14).each do |i| + cities << format('UTC+%02d:00', i) + cities << format('UTC-%02d:00', i) +end +# The partial hours are from original code this script was built based on. They do not actually correspond to timezone +# links though, so I'm not sure how they work but keep them regardless since I hope there's magic going on somehwere. +cities += %w[UTC UTC+03:30 UTC+04:30 UTC+05:30 UTC+05:45 UTC+06:30 UTC+09:30 UTC-03:30 UTC-04:30] + +cities = cities.sort_by(&:downcase).uniq +continents = continents.sort_by(&:downcase).uniq + +data = Template.render(RenderContext.new(cities: cities, continents: continents)) +File.write("#{__dir__}/timezonesi18n_generated.h", data) diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h new file mode 100644 index 0000000000..4fde7dce83 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +// SPDX-FileCopyrightText: 2021 Harald Sitter + +// !!! This file was auto-generated. Do not edit it manually! !!! + +#pragma once + +#include +#include + +#include + +namespace TimezonesI18nData +{ +using TimezoneCityToL10nMap = QHash; +using TimezoneContinentToL10nMap = QHash; + +static TimezoneCityToL10nMap timezoneCityToL10nMap() +{ + return { + {QStringLiteral("Abidjan"), i18nc("This is a city associated with particular time zone", "Abidjan")}, + {QStringLiteral("Accra"), i18nc("This is a city associated with particular time zone", "Accra")}, + {QStringLiteral("Adak"), i18nc("This is a city associated with particular time zone", "Adak")}, + {QStringLiteral("Addis_Ababa"), i18nc("This is a city associated with particular time zone", "Addis Ababa")}, + {QStringLiteral("Adelaide"), i18nc("This is a city associated with particular time zone", "Adelaide")}, + {QStringLiteral("Aden"), i18nc("This is a city associated with particular time zone", "Aden")}, + {QStringLiteral("Algiers"), i18nc("This is a city associated with particular time zone", "Algiers")}, + {QStringLiteral("Almaty"), i18nc("This is a city associated with particular time zone", "Almaty")}, + {QStringLiteral("Amman"), i18nc("This is a city associated with particular time zone", "Amman")}, + {QStringLiteral("Amsterdam"), i18nc("This is a city associated with particular time zone", "Amsterdam")}, + {QStringLiteral("Anadyr"), i18nc("This is a city associated with particular time zone", "Anadyr")}, + {QStringLiteral("Anchorage"), i18nc("This is a city associated with particular time zone", "Anchorage")}, + {QStringLiteral("Andorra"), i18nc("This is a city associated with particular time zone", "Andorra")}, + {QStringLiteral("Anguilla"), i18nc("This is a city associated with particular time zone", "Anguilla")}, + {QStringLiteral("Antananarivo"), i18nc("This is a city associated with particular time zone", "Antananarivo")}, + {QStringLiteral("Antigua"), i18nc("This is a city associated with particular time zone", "Antigua")}, + {QStringLiteral("Apia"), i18nc("This is a city associated with particular time zone", "Apia")}, + {QStringLiteral("Aqtau"), i18nc("This is a city associated with particular time zone", "Aqtau")}, + {QStringLiteral("Aqtobe"), i18nc("This is a city associated with particular time zone", "Aqtobe")}, + {QStringLiteral("Araguaina"), i18nc("This is a city associated with particular time zone", "Araguaina")}, + {QStringLiteral("Aruba"), i18nc("This is a city associated with particular time zone", "Aruba")}, + {QStringLiteral("Ashgabat"), i18nc("This is a city associated with particular time zone", "Ashgabat")}, + {QStringLiteral("Ashkhabad"), i18nc("This is a city associated with particular time zone", "Ashkhabad")}, + {QStringLiteral("Asmara"), i18nc("This is a city associated with particular time zone", "Asmara")}, + {QStringLiteral("Asmera"), i18nc("This is a city associated with particular time zone", "Asmera")}, + {QStringLiteral("Astrakhan"), i18nc("This is a city associated with particular time zone", "Astrakhan")}, + {QStringLiteral("Asuncion"), i18nc("This is a city associated with particular time zone", "Asuncion")}, + {QStringLiteral("Athens"), i18nc("This is a city associated with particular time zone", "Athens")}, + {QStringLiteral("Atikokan"), i18nc("This is a city associated with particular time zone", "Atikokan")}, + {QStringLiteral("Atka"), i18nc("This is a city associated with particular time zone", "Atka")}, + {QStringLiteral("Atyrau"), i18nc("This is a city associated with particular time zone", "Atyrau")}, + {QStringLiteral("Auckland"), i18nc("This is a city associated with particular time zone", "Auckland")}, + {QStringLiteral("Azores"), i18nc("This is a city associated with particular time zone", "Azores")}, + {QStringLiteral("Baghdad"), i18nc("This is a city associated with particular time zone", "Baghdad")}, + {QStringLiteral("Bahia"), i18nc("This is a city associated with particular time zone", "Bahia")}, + {QStringLiteral("Bahia_Banderas"), i18nc("This is a city associated with particular time zone", "Bahia Banderas")}, + {QStringLiteral("Bahrain"), i18nc("This is a city associated with particular time zone", "Bahrain")}, + {QStringLiteral("Baku"), i18nc("This is a city associated with particular time zone", "Baku")}, + {QStringLiteral("Bamako"), i18nc("This is a city associated with particular time zone", "Bamako")}, + {QStringLiteral("Bangkok"), i18nc("This is a city associated with particular time zone", "Bangkok")}, + {QStringLiteral("Bangui"), i18nc("This is a city associated with particular time zone", "Bangui")}, + {QStringLiteral("Banjul"), i18nc("This is a city associated with particular time zone", "Banjul")}, + {QStringLiteral("Barbados"), i18nc("This is a city associated with particular time zone", "Barbados")}, + {QStringLiteral("Barnaul"), i18nc("This is a city associated with particular time zone", "Barnaul")}, + {QStringLiteral("Beirut"), i18nc("This is a city associated with particular time zone", "Beirut")}, + {QStringLiteral("Belem"), i18nc("This is a city associated with particular time zone", "Belem")}, + {QStringLiteral("Belfast"), i18nc("This is a city associated with particular time zone", "Belfast")}, + {QStringLiteral("Belgrade"), i18nc("This is a city associated with particular time zone", "Belgrade")}, + {QStringLiteral("Belize"), i18nc("This is a city associated with particular time zone", "Belize")}, + {QStringLiteral("Berlin"), i18nc("This is a city associated with particular time zone", "Berlin")}, + {QStringLiteral("Bermuda"), i18nc("This is a city associated with particular time zone", "Bermuda")}, + {QStringLiteral("Beulah"), i18nc("This is a city associated with particular time zone", "Beulah")}, + {QStringLiteral("Bishkek"), i18nc("This is a city associated with particular time zone", "Bishkek")}, + {QStringLiteral("Bissau"), i18nc("This is a city associated with particular time zone", "Bissau")}, + {QStringLiteral("Blanc-Sablon"), i18nc("This is a city associated with particular time zone", "Blanc-Sablon")}, + {QStringLiteral("Blantyre"), i18nc("This is a city associated with particular time zone", "Blantyre")}, + {QStringLiteral("Boa_Vista"), i18nc("This is a city associated with particular time zone", "Boa Vista")}, + {QStringLiteral("Bogota"), i18nc("This is a city associated with particular time zone", "Bogota")}, + {QStringLiteral("Boise"), i18nc("This is a city associated with particular time zone", "Boise")}, + {QStringLiteral("Bougainville"), i18nc("This is a city associated with particular time zone", "Bougainville")}, + {QStringLiteral("Bratislava"), i18nc("This is a city associated with particular time zone", "Bratislava")}, + {QStringLiteral("Brazzaville"), i18nc("This is a city associated with particular time zone", "Brazzaville")}, + {QStringLiteral("Brisbane"), i18nc("This is a city associated with particular time zone", "Brisbane")}, + {QStringLiteral("Broken_Hill"), i18nc("This is a city associated with particular time zone", "Broken Hill")}, + {QStringLiteral("Brunei"), i18nc("This is a city associated with particular time zone", "Brunei")}, + {QStringLiteral("Brussels"), i18nc("This is a city associated with particular time zone", "Brussels")}, + {QStringLiteral("Bucharest"), i18nc("This is a city associated with particular time zone", "Bucharest")}, + {QStringLiteral("Budapest"), i18nc("This is a city associated with particular time zone", "Budapest")}, + {QStringLiteral("Buenos_Aires"), i18nc("This is a city associated with particular time zone", "Buenos Aires")}, + {QStringLiteral("Bujumbura"), i18nc("This is a city associated with particular time zone", "Bujumbura")}, + {QStringLiteral("Busingen"), i18nc("This is a city associated with particular time zone", "Busingen")}, + {QStringLiteral("Cairo"), i18nc("This is a city associated with particular time zone", "Cairo")}, + {QStringLiteral("Calcutta"), i18nc("This is a city associated with particular time zone", "Calcutta")}, + {QStringLiteral("Cambridge_Bay"), i18nc("This is a city associated with particular time zone", "Cambridge Bay")}, + {QStringLiteral("Campo_Grande"), i18nc("This is a city associated with particular time zone", "Campo Grande")}, + {QStringLiteral("Canary"), i18nc("This is a city associated with particular time zone", "Canary")}, + {QStringLiteral("Canberra"), i18nc("This is a city associated with particular time zone", "Canberra")}, + {QStringLiteral("Cancun"), i18nc("This is a city associated with particular time zone", "Cancun")}, + {QStringLiteral("Cape_Verde"), i18nc("This is a city associated with particular time zone", "Cape Verde")}, + {QStringLiteral("Caracas"), i18nc("This is a city associated with particular time zone", "Caracas")}, + {QStringLiteral("Casablanca"), i18nc("This is a city associated with particular time zone", "Casablanca")}, + {QStringLiteral("Casey"), i18nc("This is a city associated with particular time zone", "Casey")}, + {QStringLiteral("Catamarca"), i18nc("This is a city associated with particular time zone", "Catamarca")}, + {QStringLiteral("Cayenne"), i18nc("This is a city associated with particular time zone", "Cayenne")}, + {QStringLiteral("Cayman"), i18nc("This is a city associated with particular time zone", "Cayman")}, + {QStringLiteral("Center"), i18nc("This is a city associated with particular time zone", "Center")}, + {QStringLiteral("Ceuta"), i18nc("This is a city associated with particular time zone", "Ceuta")}, + {QStringLiteral("Chagos"), i18nc("This is a city associated with particular time zone", "Chagos")}, + {QStringLiteral("Chatham"), i18nc("This is a city associated with particular time zone", "Chatham")}, + {QStringLiteral("Chicago"), i18nc("This is a city associated with particular time zone", "Chicago")}, + {QStringLiteral("Chihuahua"), i18nc("This is a city associated with particular time zone", "Chihuahua")}, + {QStringLiteral("Chisinau"), i18nc("This is a city associated with particular time zone", "Chisinau")}, + {QStringLiteral("Chita"), i18nc("This is a city associated with particular time zone", "Chita")}, + {QStringLiteral("Choibalsan"), i18nc("This is a city associated with particular time zone", "Choibalsan")}, + {QStringLiteral("Chongqing"), i18nc("This is a city associated with particular time zone", "Chongqing")}, + {QStringLiteral("Christmas"), i18nc("This is a city associated with particular time zone", "Christmas")}, + {QStringLiteral("Chungking"), i18nc("This is a city associated with particular time zone", "Chungking")}, + {QStringLiteral("Chuuk"), i18nc("This is a city associated with particular time zone", "Chuuk")}, + {QStringLiteral("Cocos"), i18nc("This is a city associated with particular time zone", "Cocos")}, + {QStringLiteral("Colombo"), i18nc("This is a city associated with particular time zone", "Colombo")}, + {QStringLiteral("ComodRivadavia"), i18nc("This is a city associated with particular time zone", "Comodoro Rivadavia")}, + {QStringLiteral("Comoro"), i18nc("This is a city associated with particular time zone", "Comoro")}, + {QStringLiteral("Conakry"), i18nc("This is a city associated with particular time zone", "Conakry")}, + {QStringLiteral("Copenhagen"), i18nc("This is a city associated with particular time zone", "Copenhagen")}, + {QStringLiteral("Coral_Harbour"), i18nc("This is a city associated with particular time zone", "Coral Harbour")}, + {QStringLiteral("Cordoba"), i18nc("This is a city associated with particular time zone", "Cordoba")}, + {QStringLiteral("Costa_Rica"), i18nc("This is a city associated with particular time zone", "Costa Rica")}, + {QStringLiteral("Creston"), i18nc("This is a city associated with particular time zone", "Creston")}, + {QStringLiteral("Cuiaba"), i18nc("This is a city associated with particular time zone", "Cuiaba")}, + {QStringLiteral("Curacao"), i18nc("This is a city associated with particular time zone", "Curacao")}, + {QStringLiteral("Currie"), i18nc("This is a city associated with particular time zone", "Currie")}, + {QStringLiteral("Dacca"), i18nc("This is a city associated with particular time zone", "Dacca")}, + {QStringLiteral("Dakar"), i18nc("This is a city associated with particular time zone", "Dakar")}, + {QStringLiteral("Damascus"), i18nc("This is a city associated with particular time zone", "Damascus")}, + {QStringLiteral("Danmarkshavn"), i18nc("This is a city associated with particular time zone", "Danmarkshavn")}, + {QStringLiteral("Dar_es_Salaam"), i18nc("This is a city associated with particular time zone", "Dar es Salaam")}, + {QStringLiteral("Darwin"), i18nc("This is a city associated with particular time zone", "Darwin")}, + {QStringLiteral("Davis"), i18nc("This is a city associated with particular time zone", "Davis")}, + {QStringLiteral("Dawson"), i18nc("This is a city associated with particular time zone", "Dawson")}, + {QStringLiteral("Dawson_Creek"), i18nc("This is a city associated with particular time zone", "Dawson Creek")}, + {QStringLiteral("Denver"), i18nc("This is a city associated with particular time zone", "Denver")}, + {QStringLiteral("Detroit"), i18nc("This is a city associated with particular time zone", "Detroit")}, + {QStringLiteral("Dhaka"), i18nc("This is a city associated with particular time zone", "Dhaka")}, + {QStringLiteral("Dili"), i18nc("This is a city associated with particular time zone", "Dili")}, + {QStringLiteral("Djibouti"), i18nc("This is a city associated with particular time zone", "Djibouti")}, + {QStringLiteral("Dominica"), i18nc("This is a city associated with particular time zone", "Dominica")}, + {QStringLiteral("Douala"), i18nc("This is a city associated with particular time zone", "Douala")}, + {QStringLiteral("Dubai"), i18nc("This is a city associated with particular time zone", "Dubai")}, + {QStringLiteral("Dublin"), i18nc("This is a city associated with particular time zone", "Dublin")}, + {QStringLiteral("DumontDUrville"), i18nc("This is a city associated with particular time zone", "Dumont d’Urville")}, + {QStringLiteral("Dushanbe"), i18nc("This is a city associated with particular time zone", "Dushanbe")}, + {QStringLiteral("Easter"), i18nc("This is a city associated with particular time zone", "Easter")}, + {QStringLiteral("Edmonton"), i18nc("This is a city associated with particular time zone", "Edmonton")}, + {QStringLiteral("Efate"), i18nc("This is a city associated with particular time zone", "Efate")}, + {QStringLiteral("Eirunepe"), i18nc("This is a city associated with particular time zone", "Eirunepe")}, + {QStringLiteral("El_Aaiun"), i18nc("This is a city associated with particular time zone", "El Aaiun")}, + {QStringLiteral("El_Salvador"), i18nc("This is a city associated with particular time zone", "El Salvador")}, + {QStringLiteral("Enderbury"), i18nc("This is a city associated with particular time zone", "Enderbury")}, + {QStringLiteral("Ensenada"), i18nc("This is a city associated with particular time zone", "Ensenada")}, + {QStringLiteral("Eucla"), i18nc("This is a city associated with particular time zone", "Eucla")}, + {QStringLiteral("Faeroe"), i18nc("This is a city associated with particular time zone", "Faeroe")}, + {QStringLiteral("Fakaofo"), i18nc("This is a city associated with particular time zone", "Fakaofo")}, + {QStringLiteral("Famagusta"), i18nc("This is a city associated with particular time zone", "Famagusta")}, + {QStringLiteral("Faroe"), i18nc("This is a city associated with particular time zone", "Faroe")}, + {QStringLiteral("Fiji"), i18nc("This is a city associated with particular time zone", "Fiji")}, + {QStringLiteral("Fort_Nelson"), i18nc("This is a city associated with particular time zone", "Fort Nelson")}, + {QStringLiteral("Fort_Wayne"), i18nc("This is a city associated with particular time zone", "Fort Wayne")}, + {QStringLiteral("Fortaleza"), i18nc("This is a city associated with particular time zone", "Fortaleza")}, + {QStringLiteral("Freetown"), i18nc("This is a city associated with particular time zone", "Freetown")}, + {QStringLiteral("Funafuti"), i18nc("This is a city associated with particular time zone", "Funafuti")}, + {QStringLiteral("Gaborone"), i18nc("This is a city associated with particular time zone", "Gaborone")}, + {QStringLiteral("Galapagos"), i18nc("This is a city associated with particular time zone", "Galapagos")}, + {QStringLiteral("Gambier"), i18nc("This is a city associated with particular time zone", "Gambier")}, + {QStringLiteral("Gaza"), i18nc("This is a city associated with particular time zone", "Gaza")}, + {QStringLiteral("Gibraltar"), i18nc("This is a city associated with particular time zone", "Gibraltar")}, + {QStringLiteral("Glace_Bay"), i18nc("This is a city associated with particular time zone", "Glace Bay")}, + {QStringLiteral("Godthab"), i18nc("This is a city associated with particular time zone", "Godthab")}, + {QStringLiteral("Goose_Bay"), i18nc("This is a city associated with particular time zone", "Goose Bay")}, + {QStringLiteral("Grand_Turk"), i18nc("This is a city associated with particular time zone", "Grand Turk")}, + {QStringLiteral("Grenada"), i18nc("This is a city associated with particular time zone", "Grenada")}, + {QStringLiteral("Guadalcanal"), i18nc("This is a city associated with particular time zone", "Guadalcanal")}, + {QStringLiteral("Guadeloupe"), i18nc("This is a city associated with particular time zone", "Guadeloupe")}, + {QStringLiteral("Guam"), i18nc("This is a city associated with particular time zone", "Guam")}, + {QStringLiteral("Guatemala"), i18nc("This is a city associated with particular time zone", "Guatemala")}, + {QStringLiteral("Guayaquil"), i18nc("This is a city associated with particular time zone", "Guayaquil")}, + {QStringLiteral("Guernsey"), i18nc("This is a city associated with particular time zone", "Guernsey")}, + {QStringLiteral("Guyana"), i18nc("This is a city associated with particular time zone", "Guyana")}, + {QStringLiteral("Halifax"), i18nc("This is a city associated with particular time zone", "Halifax")}, + {QStringLiteral("Harare"), i18nc("This is a city associated with particular time zone", "Harare")}, + {QStringLiteral("Harbin"), i18nc("This is a city associated with particular time zone", "Harbin")}, + {QStringLiteral("Havana"), i18nc("This is a city associated with particular time zone", "Havana")}, + {QStringLiteral("Hebron"), i18nc("This is a city associated with particular time zone", "Hebron")}, + {QStringLiteral("Helsinki"), i18nc("This is a city associated with particular time zone", "Helsinki")}, + {QStringLiteral("Hermosillo"), i18nc("This is a city associated with particular time zone", "Hermosillo")}, + {QStringLiteral("Ho_Chi_Minh"), i18nc("This is a city associated with particular time zone", "Ho Chi Minh")}, + {QStringLiteral("Hobart"), i18nc("This is a city associated with particular time zone", "Hobart")}, + {QStringLiteral("Hong_Kong"), i18nc("This is a city associated with particular time zone", "Hong Kong")}, + {QStringLiteral("Honolulu"), i18nc("This is a city associated with particular time zone", "Honolulu")}, + {QStringLiteral("Hovd"), i18nc("This is a city associated with particular time zone", "Hovd")}, + {QStringLiteral("Indianapolis"), i18nc("This is a city associated with particular time zone", "Indianapolis")}, + {QStringLiteral("Inuvik"), i18nc("This is a city associated with particular time zone", "Inuvik")}, + {QStringLiteral("Iqaluit"), i18nc("This is a city associated with particular time zone", "Iqaluit")}, + {QStringLiteral("Irkutsk"), i18nc("This is a city associated with particular time zone", "Irkutsk")}, + {QStringLiteral("Isle_of_Man"), i18nc("This is a city associated with particular time zone", "Isle of Man")}, + {QStringLiteral("Istanbul"), i18nc("This is a city associated with particular time zone", "Istanbul")}, + {QStringLiteral("Jakarta"), i18nc("This is a city associated with particular time zone", "Jakarta")}, + {QStringLiteral("Jamaica"), i18nc("This is a city associated with particular time zone", "Jamaica")}, + {QStringLiteral("Jan_Mayen"), i18nc("This is a city associated with particular time zone", "Jan Mayen")}, + {QStringLiteral("Jayapura"), i18nc("This is a city associated with particular time zone", "Jayapura")}, + {QStringLiteral("Jersey"), i18nc("This is a city associated with particular time zone", "Jersey")}, + {QStringLiteral("Jerusalem"), i18nc("This is a city associated with particular time zone", "Jerusalem")}, + {QStringLiteral("Johannesburg"), i18nc("This is a city associated with particular time zone", "Johannesburg")}, + {QStringLiteral("Johnston"), i18nc("This is a city associated with particular time zone", "Johnston")}, + {QStringLiteral("Juba"), i18nc("This is a city associated with particular time zone", "Juba")}, + {QStringLiteral("Jujuy"), i18nc("This is a city associated with particular time zone", "Jujuy")}, + {QStringLiteral("Juneau"), i18nc("This is a city associated with particular time zone", "Juneau")}, + {QStringLiteral("Kabul"), i18nc("This is a city associated with particular time zone", "Kabul")}, + {QStringLiteral("Kaliningrad"), i18nc("This is a city associated with particular time zone", "Kaliningrad")}, + {QStringLiteral("Kamchatka"), i18nc("This is a city associated with particular time zone", "Kamchatka")}, + {QStringLiteral("Kampala"), i18nc("This is a city associated with particular time zone", "Kampala")}, + {QStringLiteral("Karachi"), i18nc("This is a city associated with particular time zone", "Karachi")}, + {QStringLiteral("Kashgar"), i18nc("This is a city associated with particular time zone", "Kashgar")}, + {QStringLiteral("Kathmandu"), i18nc("This is a city associated with particular time zone", "Kathmandu")}, + {QStringLiteral("Katmandu"), i18nc("This is a city associated with particular time zone", "Katmandu")}, + {QStringLiteral("Kerguelen"), i18nc("This is a city associated with particular time zone", "Kerguelen")}, + {QStringLiteral("Khandyga"), i18nc("This is a city associated with particular time zone", "Khandyga")}, + {QStringLiteral("Khartoum"), i18nc("This is a city associated with particular time zone", "Khartoum")}, + {QStringLiteral("Kiev"), i18nc("This is a city associated with particular time zone", "Kiev")}, + {QStringLiteral("Kigali"), i18nc("This is a city associated with particular time zone", "Kigali")}, + {QStringLiteral("Kinshasa"), i18nc("This is a city associated with particular time zone", "Kinshasa")}, + {QStringLiteral("Kiritimati"), i18nc("This is a city associated with particular time zone", "Kiritimati")}, + {QStringLiteral("Kirov"), i18nc("This is a city associated with particular time zone", "Kirov")}, + {QStringLiteral("Knox"), i18nc("This is a city associated with particular time zone", "Knox")}, + {QStringLiteral("Kolkata"), i18nc("This is a city associated with particular time zone", "Kolkata")}, + {QStringLiteral("Kosrae"), i18nc("This is a city associated with particular time zone", "Kosrae")}, + {QStringLiteral("Kralendijk"), i18nc("This is a city associated with particular time zone", "Kralendijk")}, + {QStringLiteral("Krasnoyarsk"), i18nc("This is a city associated with particular time zone", "Krasnoyarsk")}, + {QStringLiteral("Kuala_Lumpur"), i18nc("This is a city associated with particular time zone", "Kuala Lumpur")}, + {QStringLiteral("Kuching"), i18nc("This is a city associated with particular time zone", "Kuching")}, + {QStringLiteral("Kuwait"), i18nc("This is a city associated with particular time zone", "Kuwait")}, + {QStringLiteral("Kwajalein"), i18nc("This is a city associated with particular time zone", "Kwajalein")}, + {QStringLiteral("La_Paz"), i18nc("This is a city associated with particular time zone", "La Paz")}, + {QStringLiteral("La_Rioja"), i18nc("This is a city associated with particular time zone", "La Rioja")}, + {QStringLiteral("Lagos"), i18nc("This is a city associated with particular time zone", "Lagos")}, + {QStringLiteral("Libreville"), i18nc("This is a city associated with particular time zone", "Libreville")}, + {QStringLiteral("Lima"), i18nc("This is a city associated with particular time zone", "Lima")}, + {QStringLiteral("Lindeman"), i18nc("This is a city associated with particular time zone", "Lindeman")}, + {QStringLiteral("Lisbon"), i18nc("This is a city associated with particular time zone", "Lisbon")}, + {QStringLiteral("Ljubljana"), i18nc("This is a city associated with particular time zone", "Ljubljana")}, + {QStringLiteral("Lome"), i18nc("This is a city associated with particular time zone", "Lome")}, + {QStringLiteral("London"), i18nc("This is a city associated with particular time zone", "London")}, + {QStringLiteral("Longyearbyen"), i18nc("This is a city associated with particular time zone", "Longyearbyen")}, + {QStringLiteral("Lord_Howe"), i18nc("This is a city associated with particular time zone", "Lord Howe")}, + {QStringLiteral("Los_Angeles"), i18nc("This is a city associated with particular time zone", "Los Angeles")}, + {QStringLiteral("Louisville"), i18nc("This is a city associated with particular time zone", "Louisville")}, + {QStringLiteral("Lower_Princes"), i18nc("This is a city associated with particular time zone", "Lower Princes")}, + {QStringLiteral("Luanda"), i18nc("This is a city associated with particular time zone", "Luanda")}, + {QStringLiteral("Lubumbashi"), i18nc("This is a city associated with particular time zone", "Lubumbashi")}, + {QStringLiteral("Lusaka"), i18nc("This is a city associated with particular time zone", "Lusaka")}, + {QStringLiteral("Luxembourg"), i18nc("This is a city associated with particular time zone", "Luxembourg")}, + {QStringLiteral("Macao"), i18nc("This is a city associated with particular time zone", "Macao")}, + {QStringLiteral("Macau"), i18nc("This is a city associated with particular time zone", "Macau")}, + {QStringLiteral("Maceio"), i18nc("This is a city associated with particular time zone", "Maceio")}, + {QStringLiteral("Macquarie"), i18nc("This is a city associated with particular time zone", "Macquarie")}, + {QStringLiteral("Madeira"), i18nc("This is a city associated with particular time zone", "Madeira")}, + {QStringLiteral("Madrid"), i18nc("This is a city associated with particular time zone", "Madrid")}, + {QStringLiteral("Magadan"), i18nc("This is a city associated with particular time zone", "Magadan")}, + {QStringLiteral("Mahe"), i18nc("This is a city associated with particular time zone", "Mahe")}, + {QStringLiteral("Majuro"), i18nc("This is a city associated with particular time zone", "Majuro")}, + {QStringLiteral("Makassar"), i18nc("This is a city associated with particular time zone", "Makassar")}, + {QStringLiteral("Malabo"), i18nc("This is a city associated with particular time zone", "Malabo")}, + {QStringLiteral("Maldives"), i18nc("This is a city associated with particular time zone", "Maldives")}, + {QStringLiteral("Malta"), i18nc("This is a city associated with particular time zone", "Malta")}, + {QStringLiteral("Managua"), i18nc("This is a city associated with particular time zone", "Managua")}, + {QStringLiteral("Manaus"), i18nc("This is a city associated with particular time zone", "Manaus")}, + {QStringLiteral("Manila"), i18nc("This is a city associated with particular time zone", "Manila")}, + {QStringLiteral("Maputo"), i18nc("This is a city associated with particular time zone", "Maputo")}, + {QStringLiteral("Marengo"), i18nc("This is a city associated with particular time zone", "Marengo")}, + {QStringLiteral("Mariehamn"), i18nc("This is a city associated with particular time zone", "Mariehamn")}, + {QStringLiteral("Marigot"), i18nc("This is a city associated with particular time zone", "Marigot")}, + {QStringLiteral("Marquesas"), i18nc("This is a city associated with particular time zone", "Marquesas")}, + {QStringLiteral("Martinique"), i18nc("This is a city associated with particular time zone", "Martinique")}, + {QStringLiteral("Maseru"), i18nc("This is a city associated with particular time zone", "Maseru")}, + {QStringLiteral("Matamoros"), i18nc("This is a city associated with particular time zone", "Matamoros")}, + {QStringLiteral("Mauritius"), i18nc("This is a city associated with particular time zone", "Mauritius")}, + {QStringLiteral("Mawson"), i18nc("This is a city associated with particular time zone", "Mawson")}, + {QStringLiteral("Mayotte"), i18nc("This is a city associated with particular time zone", "Mayotte")}, + {QStringLiteral("Mazatlan"), i18nc("This is a city associated with particular time zone", "Mazatlan")}, + {QStringLiteral("Mbabane"), i18nc("This is a city associated with particular time zone", "Mbabane")}, + {QStringLiteral("McMurdo"), i18nc("This is a city associated with particular time zone", "McMurdo")}, + {QStringLiteral("Melbourne"), i18nc("This is a city associated with particular time zone", "Melbourne")}, + {QStringLiteral("Mendoza"), i18nc("This is a city associated with particular time zone", "Mendoza")}, + {QStringLiteral("Menominee"), i18nc("This is a city associated with particular time zone", "Menominee")}, + {QStringLiteral("Merida"), i18nc("This is a city associated with particular time zone", "Merida")}, + {QStringLiteral("Metlakatla"), i18nc("This is a city associated with particular time zone", "Metlakatla")}, + {QStringLiteral("Mexico_City"), i18nc("This is a city associated with particular time zone", "Mexico City")}, + {QStringLiteral("Midway"), i18nc("This is a city associated with particular time zone", "Midway")}, + {QStringLiteral("Minsk"), i18nc("This is a city associated with particular time zone", "Minsk")}, + {QStringLiteral("Miquelon"), i18nc("This is a city associated with particular time zone", "Miquelon")}, + {QStringLiteral("Mogadishu"), i18nc("This is a city associated with particular time zone", "Mogadishu")}, + {QStringLiteral("Monaco"), i18nc("This is a city associated with particular time zone", "Monaco")}, + {QStringLiteral("Moncton"), i18nc("This is a city associated with particular time zone", "Moncton")}, + {QStringLiteral("Monrovia"), i18nc("This is a city associated with particular time zone", "Monrovia")}, + {QStringLiteral("Monterrey"), i18nc("This is a city associated with particular time zone", "Monterrey")}, + {QStringLiteral("Montevideo"), i18nc("This is a city associated with particular time zone", "Montevideo")}, + {QStringLiteral("Monticello"), i18nc("This is a city associated with particular time zone", "Monticello")}, + {QStringLiteral("Montreal"), i18nc("This is a city associated with particular time zone", "Montreal")}, + {QStringLiteral("Montserrat"), i18nc("This is a city associated with particular time zone", "Montserrat")}, + {QStringLiteral("Moscow"), i18nc("This is a city associated with particular time zone", "Moscow")}, + {QStringLiteral("Muscat"), i18nc("This is a city associated with particular time zone", "Muscat")}, + {QStringLiteral("Nairobi"), i18nc("This is a city associated with particular time zone", "Nairobi")}, + {QStringLiteral("Nassau"), i18nc("This is a city associated with particular time zone", "Nassau")}, + {QStringLiteral("Nauru"), i18nc("This is a city associated with particular time zone", "Nauru")}, + {QStringLiteral("Ndjamena"), i18nc("This is a city associated with particular time zone", "Ndjamena")}, + {QStringLiteral("New_Salem"), i18nc("This is a city associated with particular time zone", "New Salem")}, + {QStringLiteral("New_York"), i18nc("This is a city associated with particular time zone", "New York")}, + {QStringLiteral("Niamey"), i18nc("This is a city associated with particular time zone", "Niamey")}, + {QStringLiteral("Nicosia"), i18nc("This is a city associated with particular time zone", "Nicosia")}, + {QStringLiteral("Nipigon"), i18nc("This is a city associated with particular time zone", "Nipigon")}, + {QStringLiteral("Niue"), i18nc("This is a city associated with particular time zone", "Niue")}, + {QStringLiteral("Nome"), i18nc("This is a city associated with particular time zone", "Nome")}, + {QStringLiteral("Norfolk"), i18nc("This is a city associated with particular time zone", "Norfolk")}, + {QStringLiteral("Noronha"), i18nc("This is a city associated with particular time zone", "Noronha")}, + {QStringLiteral("Nouakchott"), i18nc("This is a city associated with particular time zone", "Nouakchott")}, + {QStringLiteral("Noumea"), i18nc("This is a city associated with particular time zone", "Noumea")}, + {QStringLiteral("Novokuznetsk"), i18nc("This is a city associated with particular time zone", "Novokuznetsk")}, + {QStringLiteral("Novosibirsk"), i18nc("This is a city associated with particular time zone", "Novosibirsk")}, + {QStringLiteral("Nuuk"), i18nc("This is a city associated with particular time zone", "Nuuk")}, + {QStringLiteral("Ojinaga"), i18nc("This is a city associated with particular time zone", "Ojinaga")}, + {QStringLiteral("Omsk"), i18nc("This is a city associated with particular time zone", "Omsk")}, + {QStringLiteral("Oral"), i18nc("This is a city associated with particular time zone", "Oral")}, + {QStringLiteral("Oslo"), i18nc("This is a city associated with particular time zone", "Oslo")}, + {QStringLiteral("Ouagadougou"), i18nc("This is a city associated with particular time zone", "Ouagadougou")}, + {QStringLiteral("Pago_Pago"), i18nc("This is a city associated with particular time zone", "Pago Pago")}, + {QStringLiteral("Palau"), i18nc("This is a city associated with particular time zone", "Palau")}, + {QStringLiteral("Palmer"), i18nc("This is a city associated with particular time zone", "Palmer")}, + {QStringLiteral("Panama"), i18nc("This is a city associated with particular time zone", "Panama")}, + {QStringLiteral("Pangnirtung"), i18nc("This is a city associated with particular time zone", "Pangnirtung")}, + {QStringLiteral("Paramaribo"), i18nc("This is a city associated with particular time zone", "Paramaribo")}, + {QStringLiteral("Paris"), i18nc("This is a city associated with particular time zone", "Paris")}, + {QStringLiteral("Perth"), i18nc("This is a city associated with particular time zone", "Perth")}, + {QStringLiteral("Petersburg"), i18nc("This is a city associated with particular time zone", "Petersburg")}, + {QStringLiteral("Phnom_Penh"), i18nc("This is a city associated with particular time zone", "Phnom Penh")}, + {QStringLiteral("Phoenix"), i18nc("This is a city associated with particular time zone", "Phoenix")}, + {QStringLiteral("Pitcairn"), i18nc("This is a city associated with particular time zone", "Pitcairn")}, + {QStringLiteral("Podgorica"), i18nc("This is a city associated with particular time zone", "Podgorica")}, + {QStringLiteral("Pohnpei"), i18nc("This is a city associated with particular time zone", "Pohnpei")}, + {QStringLiteral("Ponape"), i18nc("This is a city associated with particular time zone", "Ponape")}, + {QStringLiteral("Pontianak"), i18nc("This is a city associated with particular time zone", "Pontianak")}, + {QStringLiteral("Port-au-Prince"), i18nc("This is a city associated with particular time zone", "Port-au-Prince")}, + {QStringLiteral("Port_Moresby"), i18nc("This is a city associated with particular time zone", "Port Moresby")}, + {QStringLiteral("Port_of_Spain"), i18nc("This is a city associated with particular time zone", "Port of Spain")}, + {QStringLiteral("Porto-Novo"), i18nc("This is a city associated with particular time zone", "Porto-Novo")}, + {QStringLiteral("Porto_Acre"), i18nc("This is a city associated with particular time zone", "Porto Acre")}, + {QStringLiteral("Porto_Velho"), i18nc("This is a city associated with particular time zone", "Porto Velho")}, + {QStringLiteral("Prague"), i18nc("This is a city associated with particular time zone", "Prague")}, + {QStringLiteral("Puerto_Rico"), i18nc("This is a city associated with particular time zone", "Puerto Rico")}, + {QStringLiteral("Punta_Arenas"), i18nc("This is a city associated with particular time zone", "Punta Arenas")}, + {QStringLiteral("Pyongyang"), i18nc("This is a city associated with particular time zone", "Pyongyang")}, + {QStringLiteral("Qatar"), i18nc("This is a city associated with particular time zone", "Qatar")}, + {QStringLiteral("Qostanay"), i18nc("This is a city associated with particular time zone", "Qostanay")}, + {QStringLiteral("Queensland"), i18nc("This is a city associated with particular time zone", "Queensland")}, + {QStringLiteral("Qyzylorda"), i18nc("This is a city associated with particular time zone", "Qyzylorda")}, + {QStringLiteral("Rainy_River"), i18nc("This is a city associated with particular time zone", "Rainy River")}, + {QStringLiteral("Rangoon"), i18nc("This is a city associated with particular time zone", "Rangoon")}, + {QStringLiteral("Rankin_Inlet"), i18nc("This is a city associated with particular time zone", "Rankin Inlet")}, + {QStringLiteral("Rarotonga"), i18nc("This is a city associated with particular time zone", "Rarotonga")}, + {QStringLiteral("Recife"), i18nc("This is a city associated with particular time zone", "Recife")}, + {QStringLiteral("Regina"), i18nc("This is a city associated with particular time zone", "Regina")}, + {QStringLiteral("Resolute"), i18nc("This is a city associated with particular time zone", "Resolute")}, + {QStringLiteral("Reunion"), i18nc("This is a city associated with particular time zone", "Reunion")}, + {QStringLiteral("Reykjavik"), i18nc("This is a city associated with particular time zone", "Reykjavik")}, + {QStringLiteral("Riga"), i18nc("This is a city associated with particular time zone", "Riga")}, + {QStringLiteral("Rio_Branco"), i18nc("This is a city associated with particular time zone", "Rio Branco")}, + {QStringLiteral("Rio_Gallegos"), i18nc("This is a city associated with particular time zone", "Rio Gallegos")}, + {QStringLiteral("Riyadh"), i18nc("This is a city associated with particular time zone", "Riyadh")}, + {QStringLiteral("Rome"), i18nc("This is a city associated with particular time zone", "Rome")}, + {QStringLiteral("Rosario"), i18nc("This is a city associated with particular time zone", "Rosario")}, + {QStringLiteral("Rothera"), i18nc("This is a city associated with particular time zone", "Rothera")}, + {QStringLiteral("Saigon"), i18nc("This is a city associated with particular time zone", "Saigon")}, + {QStringLiteral("Saipan"), i18nc("This is a city associated with particular time zone", "Saipan")}, + {QStringLiteral("Sakhalin"), i18nc("This is a city associated with particular time zone", "Sakhalin")}, + {QStringLiteral("Salta"), i18nc("This is a city associated with particular time zone", "Salta")}, + {QStringLiteral("Samara"), i18nc("This is a city associated with particular time zone", "Samara")}, + {QStringLiteral("Samarkand"), i18nc("This is a city associated with particular time zone", "Samarkand")}, + {QStringLiteral("Samoa"), i18nc("This is a city associated with particular time zone", "Samoa")}, + {QStringLiteral("San_Juan"), i18nc("This is a city associated with particular time zone", "San Juan")}, + {QStringLiteral("San_Luis"), i18nc("This is a city associated with particular time zone", "San Luis")}, + {QStringLiteral("San_Marino"), i18nc("This is a city associated with particular time zone", "San Marino")}, + {QStringLiteral("Santa_Isabel"), i18nc("This is a city associated with particular time zone", "Santa Isabel")}, + {QStringLiteral("Santarem"), i18nc("This is a city associated with particular time zone", "Santarem")}, + {QStringLiteral("Santiago"), i18nc("This is a city associated with particular time zone", "Santiago")}, + {QStringLiteral("Santo_Domingo"), i18nc("This is a city associated with particular time zone", "Santo Domingo")}, + {QStringLiteral("Sao_Paulo"), i18nc("This is a city associated with particular time zone", "Sao Paulo")}, + {QStringLiteral("Sao_Tome"), i18nc("This is a city associated with particular time zone", "Sao Tome")}, + {QStringLiteral("Sarajevo"), i18nc("This is a city associated with particular time zone", "Sarajevo")}, + {QStringLiteral("Saratov"), i18nc("This is a city associated with particular time zone", "Saratov")}, + {QStringLiteral("Scoresbysund"), i18nc("This is a city associated with particular time zone", "Scoresbysund")}, + {QStringLiteral("Seoul"), i18nc("This is a city associated with particular time zone", "Seoul")}, + {QStringLiteral("Shanghai"), i18nc("This is a city associated with particular time zone", "Shanghai")}, + {QStringLiteral("Shiprock"), i18nc("This is a city associated with particular time zone", "Shiprock")}, + {QStringLiteral("Simferopol"), i18nc("This is a city associated with particular time zone", "Simferopol")}, + {QStringLiteral("Singapore"), i18nc("This is a city associated with particular time zone", "Singapore")}, + {QStringLiteral("Sitka"), i18nc("This is a city associated with particular time zone", "Sitka")}, + {QStringLiteral("Skopje"), i18nc("This is a city associated with particular time zone", "Skopje")}, + {QStringLiteral("Sofia"), i18nc("This is a city associated with particular time zone", "Sofia")}, + {QStringLiteral("South_Georgia"), i18nc("This is a city associated with particular time zone", "South Georgia")}, + {QStringLiteral("South_Pole"), i18nc("This is a city associated with particular time zone", "South Pole")}, + {QStringLiteral("Srednekolymsk"), i18nc("This is a city associated with particular time zone", "Srednekolymsk")}, + {QStringLiteral("St_Barthelemy"), i18nc("This is a city associated with particular time zone", "St Barthelemy")}, + {QStringLiteral("St_Helena"), i18nc("This is a city associated with particular time zone", "St Helena")}, + {QStringLiteral("St_Johns"), i18nc("This is a city associated with particular time zone", "St Johns")}, + {QStringLiteral("St_Kitts"), i18nc("This is a city associated with particular time zone", "St Kitts")}, + {QStringLiteral("St_Lucia"), i18nc("This is a city associated with particular time zone", "St Lucia")}, + {QStringLiteral("St_Thomas"), i18nc("This is a city associated with particular time zone", "St Thomas")}, + {QStringLiteral("St_Vincent"), i18nc("This is a city associated with particular time zone", "St Vincent")}, + {QStringLiteral("Stanley"), i18nc("This is a city associated with particular time zone", "Stanley")}, + {QStringLiteral("Stockholm"), i18nc("This is a city associated with particular time zone", "Stockholm")}, + {QStringLiteral("Swift_Current"), i18nc("This is a city associated with particular time zone", "Swift Current")}, + {QStringLiteral("Sydney"), i18nc("This is a city associated with particular time zone", "Sydney")}, + {QStringLiteral("Syowa"), i18nc("This is a city associated with particular time zone", "Syowa")}, + {QStringLiteral("Tahiti"), i18nc("This is a city associated with particular time zone", "Tahiti")}, + {QStringLiteral("Taipei"), i18nc("This is a city associated with particular time zone", "Taipei")}, + {QStringLiteral("Tallinn"), i18nc("This is a city associated with particular time zone", "Tallinn")}, + {QStringLiteral("Tarawa"), i18nc("This is a city associated with particular time zone", "Tarawa")}, + {QStringLiteral("Tashkent"), i18nc("This is a city associated with particular time zone", "Tashkent")}, + {QStringLiteral("Tasmania"), i18nc("This is a city associated with particular time zone", "Tasmania")}, + {QStringLiteral("Tbilisi"), i18nc("This is a city associated with particular time zone", "Tbilisi")}, + {QStringLiteral("Tegucigalpa"), i18nc("This is a city associated with particular time zone", "Tegucigalpa")}, + {QStringLiteral("Tehran"), i18nc("This is a city associated with particular time zone", "Tehran")}, + {QStringLiteral("Tel_Aviv"), i18nc("This is a city associated with particular time zone", "Tel Aviv")}, + {QStringLiteral("Tell_City"), i18nc("This is a city associated with particular time zone", "Tell City")}, + {QStringLiteral("Thimbu"), i18nc("This is a city associated with particular time zone", "Thimbu")}, + {QStringLiteral("Thimphu"), i18nc("This is a city associated with particular time zone", "Thimphu")}, + {QStringLiteral("Thule"), i18nc("This is a city associated with particular time zone", "Thule")}, + {QStringLiteral("Thunder_Bay"), i18nc("This is a city associated with particular time zone", "Thunder Bay")}, + {QStringLiteral("Tijuana"), i18nc("This is a city associated with particular time zone", "Tijuana")}, + {QStringLiteral("Timbuktu"), i18nc("This is a city associated with particular time zone", "Timbuktu")}, + {QStringLiteral("Tirane"), i18nc("This is a city associated with particular time zone", "Tirane")}, + {QStringLiteral("Tiraspol"), i18nc("This is a city associated with particular time zone", "Tiraspol")}, + {QStringLiteral("Tokyo"), i18nc("This is a city associated with particular time zone", "Tokyo")}, + {QStringLiteral("Tomsk"), i18nc("This is a city associated with particular time zone", "Tomsk")}, + {QStringLiteral("Tongatapu"), i18nc("This is a city associated with particular time zone", "Tongatapu")}, + {QStringLiteral("Toronto"), i18nc("This is a city associated with particular time zone", "Toronto")}, + {QStringLiteral("Tortola"), i18nc("This is a city associated with particular time zone", "Tortola")}, + {QStringLiteral("Tripoli"), i18nc("This is a city associated with particular time zone", "Tripoli")}, + {QStringLiteral("Troll"), i18nc("This is a city associated with particular time zone", "Troll")}, + {QStringLiteral("Truk"), i18nc("This is a city associated with particular time zone", "Truk")}, + {QStringLiteral("Tucuman"), i18nc("This is a city associated with particular time zone", "Tucuman")}, + {QStringLiteral("Tunis"), i18nc("This is a city associated with particular time zone", "Tunis")}, + {QStringLiteral("Ujung_Pandang"), i18nc("This is a city associated with particular time zone", "Ujung Pandang")}, + {QStringLiteral("Ulaanbaatar"), i18nc("This is a city associated with particular time zone", "Ulaanbaatar")}, + {QStringLiteral("Ulan_Bator"), i18nc("This is a city associated with particular time zone", "Ulan Bator")}, + {QStringLiteral("Ulyanovsk"), i18nc("This is a city associated with particular time zone", "Ulyanovsk")}, + {QStringLiteral("Urumqi"), i18nc("This is a city associated with particular time zone", "Urumqi")}, + {QStringLiteral("Ushuaia"), i18nc("This is a city associated with particular time zone", "Ushuaia")}, + {QStringLiteral("Ust-Nera"), i18nc("This is a city associated with particular time zone", "Ust-Nera")}, + {QStringLiteral("UTC"), i18nc("This is a generic time zone name, localize as needed", "UTC")}, + {QStringLiteral("UTC+00:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+00:00")}, + {QStringLiteral("UTC+01:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+01:00")}, + {QStringLiteral("UTC+02:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+02:00")}, + {QStringLiteral("UTC+03:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+03:00")}, + {QStringLiteral("UTC+03:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+03:30")}, + {QStringLiteral("UTC+04:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+04:00")}, + {QStringLiteral("UTC+04:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+04:30")}, + {QStringLiteral("UTC+05:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:00")}, + {QStringLiteral("UTC+05:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:30")}, + {QStringLiteral("UTC+05:45"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:45")}, + {QStringLiteral("UTC+06:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+06:00")}, + {QStringLiteral("UTC+06:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+06:30")}, + {QStringLiteral("UTC+07:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+07:00")}, + {QStringLiteral("UTC+08:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+08:00")}, + {QStringLiteral("UTC+09:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+09:00")}, + {QStringLiteral("UTC+09:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+09:30")}, + {QStringLiteral("UTC+10:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+10:00")}, + {QStringLiteral("UTC+11:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+11:00")}, + {QStringLiteral("UTC+12:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+12:00")}, + {QStringLiteral("UTC+13:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+13:00")}, + {QStringLiteral("UTC+14:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+14:00")}, + {QStringLiteral("UTC-00:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-00:00")}, + {QStringLiteral("UTC-01:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-01:00")}, + {QStringLiteral("UTC-02:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-02:00")}, + {QStringLiteral("UTC-03:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-03:00")}, + {QStringLiteral("UTC-03:30"), i18nc("This is a generic time zone name, localize as needed", "UTC-03:30")}, + {QStringLiteral("UTC-04:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-04:00")}, + {QStringLiteral("UTC-04:30"), i18nc("This is a generic time zone name, localize as needed", "UTC-04:30")}, + {QStringLiteral("UTC-05:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-05:00")}, + {QStringLiteral("UTC-06:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-06:00")}, + {QStringLiteral("UTC-07:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-07:00")}, + {QStringLiteral("UTC-08:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-08:00")}, + {QStringLiteral("UTC-09:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-09:00")}, + {QStringLiteral("UTC-10:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-10:00")}, + {QStringLiteral("UTC-11:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-11:00")}, + {QStringLiteral("UTC-12:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-12:00")}, + {QStringLiteral("UTC-13:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-13:00")}, + {QStringLiteral("UTC-14:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-14:00")}, + {QStringLiteral("Uzhgorod"), i18nc("This is a city associated with particular time zone", "Uzhgorod")}, + {QStringLiteral("Vaduz"), i18nc("This is a city associated with particular time zone", "Vaduz")}, + {QStringLiteral("Vancouver"), i18nc("This is a city associated with particular time zone", "Vancouver")}, + {QStringLiteral("Vatican"), i18nc("This is a city associated with particular time zone", "Vatican")}, + {QStringLiteral("Vevay"), i18nc("This is a city associated with particular time zone", "Vevay")}, + {QStringLiteral("Victoria"), i18nc("This is a city associated with particular time zone", "Victoria")}, + {QStringLiteral("Vienna"), i18nc("This is a city associated with particular time zone", "Vienna")}, + {QStringLiteral("Vientiane"), i18nc("This is a city associated with particular time zone", "Vientiane")}, + {QStringLiteral("Vilnius"), i18nc("This is a city associated with particular time zone", "Vilnius")}, + {QStringLiteral("Vincennes"), i18nc("This is a city associated with particular time zone", "Vincennes")}, + {QStringLiteral("Virgin"), i18nc("This is a city associated with particular time zone", "Virgin")}, + {QStringLiteral("Vladivostok"), i18nc("This is a city associated with particular time zone", "Vladivostok")}, + {QStringLiteral("Volgograd"), i18nc("This is a city associated with particular time zone", "Volgograd")}, + {QStringLiteral("Vostok"), i18nc("This is a city associated with particular time zone", "Vostok")}, + {QStringLiteral("Wake"), i18nc("This is a city associated with particular time zone", "Wake")}, + {QStringLiteral("Wallis"), i18nc("This is a city associated with particular time zone", "Wallis")}, + {QStringLiteral("Warsaw"), i18nc("This is a city associated with particular time zone", "Warsaw")}, + {QStringLiteral("Whitehorse"), i18nc("This is a city associated with particular time zone", "Whitehorse")}, + {QStringLiteral("Winamac"), i18nc("This is a city associated with particular time zone", "Winamac")}, + {QStringLiteral("Windhoek"), i18nc("This is a city associated with particular time zone", "Windhoek")}, + {QStringLiteral("Winnipeg"), i18nc("This is a city associated with particular time zone", "Winnipeg")}, + {QStringLiteral("Yakutat"), i18nc("This is a city associated with particular time zone", "Yakutat")}, + {QStringLiteral("Yakutsk"), i18nc("This is a city associated with particular time zone", "Yakutsk")}, + {QStringLiteral("Yancowinna"), i18nc("This is a city associated with particular time zone", "Yancowinna")}, + {QStringLiteral("Yangon"), i18nc("This is a city associated with particular time zone", "Yangon")}, + {QStringLiteral("Yap"), i18nc("This is a city associated with particular time zone", "Yap")}, + {QStringLiteral("Yekaterinburg"), i18nc("This is a city associated with particular time zone", "Yekaterinburg")}, + {QStringLiteral("Yellowknife"), i18nc("This is a city associated with particular time zone", "Yellowknife")}, + {QStringLiteral("Yerevan"), i18nc("This is a city associated with particular time zone", "Yerevan")}, + {QStringLiteral("Zagreb"), i18nc("This is a city associated with particular time zone", "Zagreb")}, + {QStringLiteral("Zaporozhye"), i18nc("This is a city associated with particular time zone", "Zaporozhye")}, + {QStringLiteral("Zurich"), i18nc("This is a city associated with particular time zone", "Zurich")}, + }; +} + +static TimezoneContinentToL10nMap timezoneContinentToL10nMap() +{ + return { + {QStringLiteral("Africa"), i18nc("This is a continent/area associated with a particular timezone", "Africa")}, + {QStringLiteral("America"), i18nc("This is a continent/area associated with a particular timezone", "America")}, + {QStringLiteral("Antarctica"), i18nc("This is a continent/area associated with a particular timezone", "Antarctica")}, + {QStringLiteral("Arctic"), i18nc("This is a continent/area associated with a particular timezone", "Arctic")}, + {QStringLiteral("Asia"), i18nc("This is a continent/area associated with a particular timezone", "Asia")}, + {QStringLiteral("Atlantic"), i18nc("This is a continent/area associated with a particular timezone", "Atlantic")}, + {QStringLiteral("Australia"), i18nc("This is a continent/area associated with a particular timezone", "Australia")}, + {QStringLiteral("Europe"), i18nc("This is a continent/area associated with a particular timezone", "Europe")}, + {QStringLiteral("Indian"), i18nc("This is a continent/area associated with a particular timezone", "Indian")}, + {QStringLiteral("Pacific"), i18nc("This is a continent/area associated with a particular timezone", "Pacific")}, + }; +} +} // namespace TimezonesI18nData diff --git a/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h.erb b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h.erb new file mode 100644 index 0000000000..a29ac15321 --- /dev/null +++ b/plasma/workspace/applets/digital-clock/plugin/timezonesi18n_generated.h.erb @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +// SPDX-FileCopyrightText: 2021 Harald Sitter + +// !!! This file was auto-generated. Do not edit it manually! !!! + +#pragma once + +#include +#include + +#include + +namespace TimezonesI18nData +{ +using TimezoneCityToL10nMap = QHash; +using TimezoneContinentToL10nMap = QHash; + +static TimezoneCityToL10nMap timezoneCityToL10nMap() +{ + return {<% cities.each do |city| %> + {QStringLiteral("<%= city %>"), i18nc("<%= city_description(city) %>", "<%= to_city_name(city) %>")},<%end%> + }; +} + +static TimezoneContinentToL10nMap timezoneContinentToL10nMap() +{ + return {<% continents.each do |continent| %> + {QStringLiteral("<%= continent %>"), i18nc("<%= continent_description(continent) %>", "<%= to_contient_name(continent) %>")},<%end%> + }; +} +} // namespace TimezonesI18nData diff --git a/plasma/workspace/applets/icon/CMakeLists.txt b/plasma/workspace/applets/icon/CMakeLists.txt new file mode 100644 index 0000000000..c9d74ca0ca --- /dev/null +++ b/plasma/workspace/applets/icon/CMakeLists.txt @@ -0,0 +1,14 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.icon\") + +kcoreaddons_add_plugin(plasma_applet_icon SOURCES iconapplet.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(plasma_applet_icon + KF5::I18n + KF5::KIOCore # for OpenFileManagerWindowJob + KF5::KIOGui # for FavIconRequestJob + KF5::KIOWidgets # for KRun + KF5::Notifications + KF5::Plasma + PW::LibTaskManager) + +plasma_install_package(package org.kde.plasma.icon) diff --git a/plasma/workspace/applets/icon/Messages.sh b/plasma/workspace/applets/icon/Messages.sh new file mode 100644 index 0000000000..1b16a7ac32 --- /dev/null +++ b/plasma/workspace/applets/icon/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.icon.pot diff --git a/plasma/workspace/applets/icon/iconapplet.cpp b/plasma/workspace/applets/icon/iconapplet.cpp new file mode 100644 index 0000000000..929b5c93a6 --- /dev/null +++ b/plasma/workspace/applets/icon/iconapplet.cpp @@ -0,0 +1,580 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "iconapplet.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +IconApplet::IconApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ +} + +IconApplet::~IconApplet() +{ + // in a handler connected to IconApplet::appletDeleted m_localPath will be empty?! + if (destroyed()) { + QFile::remove(m_localPath); + } +} + +void IconApplet::init() +{ + populate(); +} + +void IconApplet::configChanged() +{ + populate(); +} + +void IconApplet::populate() +{ + m_url = config().readEntry(QStringLiteral("url"), QUrl()); + + if (!m_url.isValid()) { + // the old applet that used a QML plugin and stored its url + // in plasmoid.configuration.url had its entries stored in [Configuration][General] + // so we look here as well to provide an upgrade path + m_url = config().group("General").readEntry(QStringLiteral("url"), QUrl()); + } + + // our backing desktop file already exists? just read all the things from it + const QString path = localPath(); + if (QFileInfo::exists(path)) { + populateFromDesktopFile(path); + return; + } + + if (!m_url.isValid()) { + // invalid url, use dummy data + populateFromDesktopFile(QString()); + return; + } + + const QString plasmaIconsFolderPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/plasma_icons"); + if (!QDir().mkpath(plasmaIconsFolderPath)) { + setLaunchErrorMessage(i18n("Failed to create icon widgets folder '%1'", plasmaIconsFolderPath)); + return; + } + + setBusy(true); // unset in populateFromDesktopFile where we'll end up in if all goes well + + auto *statJob = KIO::stat(m_url, KIO::HideProgressInfo); + connect(statJob, &KIO::StatJob::finished, this, [=] { + QString desiredDesktopFileName = m_url.fileName(); + + // in doubt, just encode the entire URL, e.g. https://www.kde.org/ has no filename + if (desiredDesktopFileName.isEmpty()) { + desiredDesktopFileName = KIO::encodeFileName(m_url.toDisplayString()); + } + + // We always want it to be a .desktop file (e.g. also for the "Type=Link" at the end) + if (!desiredDesktopFileName.endsWith(QLatin1String(".desktop"))) { + desiredDesktopFileName.append(QLatin1String(".desktop")); + } + + QString backingDesktopFile = plasmaIconsFolderPath + QLatin1Char('/'); + // KFileUtils::suggestName always appends a suffix, i.e. it expects that we already know the file already exists + if (QFileInfo::exists(backingDesktopFile + desiredDesktopFileName)) { + desiredDesktopFileName = KFileUtils::suggestName(QUrl::fromLocalFile(plasmaIconsFolderPath), desiredDesktopFileName); + } + backingDesktopFile.append(desiredDesktopFileName); + + QString name; // ends up as "Name" in the .desktop file for "Link" files below + + const QUrl url = statJob->mostLocalUrl(); + if (url.isLocalFile()) { + const QString localUrlString = url.toLocalFile(); + + // if desktop file just copy it over + if (KDesktopFile::isDesktopFile(localUrlString)) { + // if this restriction is set, KIO won't allow running desktop files from outside + // registered services, applications, and so on, in this case we'll use the original + // .desktop file and lose the ability to customize it + if (!KAuthorized::authorize(KAuthorized::RUN_DESKTOP_FILES)) { + populateFromDesktopFile(localUrlString); + // we don't call setLocalPath here as we don't want to store localPath to be a system-location + // so that the fact that we cannot edit is re-evaluated every time + return; + } + + if (!QFile::copy(localUrlString, backingDesktopFile)) { + setLaunchErrorMessage(i18n("Failed to copy icon widget desktop file from '%1' to '%2'", localUrlString, backingDesktopFile)); + setBusy(false); + return; + } + + // set executable flag on the desktop file so KIO doesn't complain about executing it + QFile file(backingDesktopFile); + file.setPermissions(file.permissions() | QFile::ExeOwner); + + populateFromDesktopFile(backingDesktopFile); + setLocalPath(backingDesktopFile); + + return; + } + } + + // in all other cases just make it a link + + QString iconName; + QString genericName; + + if (!statJob->error()) { + KFileItem item(statJob->statResult(), url); + + if (name.isEmpty()) { + name = item.text(); + } + + if (item.mimetype() != QLatin1String("application/octet-stream")) { + iconName = item.iconName(); + genericName = item.mimeComment(); + } + } + + // KFileItem might return "." as text for e.g. root folders + if (name == QLatin1Char('.')) { + name.clear(); + } + + if (name.isEmpty()) { + name = url.fileName(); + } + + if (name.isEmpty()) { + // TODO would be cool to just show the parent folder name instead of the full path + name = url.path(); + } + + // For websites the filename e.g. "index.php" is usually not what you want + // also "/" isn't very descript when it's not our local "root" folder + if (name.isEmpty() || url.scheme().startsWith(QLatin1String("http")) || (!url.isLocalFile() && name == QLatin1String("/"))) { + name = url.host(); + } + + if (iconName.isEmpty()) { + // In doubt ask KIO::iconNameForUrl, KFileItem can't cope with http:// URLs for instance + iconName = KIO::iconNameForUrl(url); + } + + bool downloadFavIcon = false; + + if (url.scheme().startsWith(QLatin1String("http"))) { + const QString favIcon = KIO::favIconForUrl(url); + + if (!favIcon.isEmpty()) { + iconName = favIcon; + } else { + downloadFavIcon = true; + } + } + + KDesktopFile linkDesktopFile(backingDesktopFile); + auto desktopGroup = linkDesktopFile.desktopGroup(); + + desktopGroup.writeEntry(QStringLiteral("Name"), name); + desktopGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Link")); + desktopGroup.writeEntry(QStringLiteral("URL"), url); + desktopGroup.writeEntry(QStringLiteral("Icon"), iconName); + if (!genericName.isEmpty()) { + desktopGroup.writeEntry(QStringLiteral("GenericName"), genericName); + } + + linkDesktopFile.sync(); + + populateFromDesktopFile(backingDesktopFile); + setLocalPath(backingDesktopFile); + + if (downloadFavIcon) { + KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(m_url); + connect(job, &KIO::FavIconRequestJob::result, this, [job, backingDesktopFile, this](KJob *) { + if (!job->error()) { + KDesktopFile(backingDesktopFile).desktopGroup().writeEntry(QStringLiteral("Icon"), job->iconFile()); + + m_iconName = job->iconFile(); + Q_EMIT iconNameChanged(m_iconName); + } + }); + } + }); +} + +void IconApplet::populateFromDesktopFile(const QString &path) +{ + // path empty? just set icon to "unknown" and call it a day + if (path.isEmpty()) { + setIconName({}); + return; + } + + KDesktopFile desktopFile(path); + + const QString &name = desktopFile.readName(); + if (m_name != name) { + m_name = name; + Q_EMIT nameChanged(name); + } + + const QString &genericName = desktopFile.readGenericName(); + if (m_genericName != genericName) { + m_genericName = genericName; + Q_EMIT genericNameChanged(genericName); + } + + setIconName(desktopFile.readIcon()); + + delete m_openContainingFolderAction; + m_openContainingFolderAction = nullptr; + m_openWithActions.clear(); + m_jumpListActions.clear(); + + m_localPath = path; + + setBusy(false); +} + +QUrl IconApplet::url() const +{ + return m_url; +} + +void IconApplet::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + Q_EMIT urlChanged(url); + + config().writeEntry(QStringLiteral("url"), url); + + populate(); + } +} + +void IconApplet::setIconName(const QString &iconName) +{ + const QString newIconName = (!iconName.isEmpty() ? iconName : QStringLiteral("unknown")); + if (m_iconName != newIconName) { + m_iconName = newIconName; + Q_EMIT iconNameChanged(newIconName); + } +} + +QString IconApplet::name() const +{ + return m_name; +} + +QString IconApplet::iconName() const +{ + return m_iconName; +} + +QString IconApplet::genericName() const +{ + return m_genericName; +} + +QList IconApplet::contextualActions() +{ + QList actions; + if (m_localPath.isEmpty()) { + return actions; + } + + KDesktopFile desktopFile(m_localPath); + + if (m_jumpListActions.isEmpty()) { + const KService service(m_localPath); + + const auto jumpListActions = service.actions(); + for (const KServiceAction &serviceAction : jumpListActions) { + if (serviceAction.noDisplay()) { + continue; + } + + QAction *action = new QAction(QIcon::fromTheme(serviceAction.icon()), serviceAction.text(), this); + if (serviceAction.isSeparator()) { + action->setSeparator(true); + } + + connect(action, &QAction::triggered, this, [serviceAction]() { + auto *job = new KIO::ApplicationLauncherJob(serviceAction); + auto *delegate = new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled); + job->setUiDelegate(delegate); + job->start(); + }); + + m_jumpListActions << action; + } + } + + actions << m_jumpListActions; + + if (!actions.isEmpty()) { + if (!m_separatorAction) { + m_separatorAction = new QAction(this); + m_separatorAction->setSeparator(true); + } + actions << m_separatorAction; + } + + if (desktopFile.hasLinkType()) { + const QUrl linkUrl = QUrl(desktopFile.readUrl()); + + if (linkUrl.isValid() && !linkUrl.scheme().isEmpty()) { + if (m_openWithActions.isEmpty()) { + if (!m_fileItemActions) { + m_fileItemActions = new KFileItemActions(this); + } + KFileItemListProperties itemProperties(KFileItemList({KFileItem(linkUrl)})); + m_fileItemActions->setItemListProperties(itemProperties); + + if (!m_openWithMenu) { + m_openWithMenu.reset(new QMenu()); + } + m_openWithMenu->clear(); + m_fileItemActions->insertOpenWithActionsTo(nullptr, m_openWithMenu.data(), QStringList()); + + m_openWithActions = m_openWithMenu->actions(); + } + + if (!m_openContainingFolderAction) { + if (KProtocolManager::supportsListing(linkUrl)) { + m_openContainingFolderAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("Open Containing Folder"), this); + connect(m_openContainingFolderAction, &QAction::triggered, this, [linkUrl] { + KIO::highlightInFileManager({linkUrl}); + }); + } + } + } + } + + actions << m_openWithActions; + + if (m_openContainingFolderAction) { + actions << m_openContainingFolderAction; + } + + return actions; +} + +void IconApplet::run() +{ + if (!m_startupTasksModel) { + m_startupTasksModel = new TaskManager::StartupTasksModel(this); + + auto handleRow = [this](bool busy, const QModelIndex &parent, int first, int last) { + Q_UNUSED(parent); + for (int i = first; i <= last; ++i) { + const QModelIndex idx = m_startupTasksModel->index(i, 0); + if (idx.data(TaskManager::AbstractTasksModel::LauncherUrlWithoutIcon).toUrl() == QUrl::fromLocalFile(m_localPath)) { + setBusy(busy); + break; + } + } + }; + + using namespace std::placeholders; + connect(m_startupTasksModel, &QAbstractItemModel::rowsInserted, this, std::bind(handleRow, true /*busy*/, _1, _2, _3)); + connect(m_startupTasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, std::bind(handleRow, false /*busy*/, _1, _2, _3)); + } + + KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(m_localPath)); + job->setRunExecutables(true); // so it can launch apps + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); +} + +void IconApplet::processDrop(QObject *dropEvent) +{ + Q_ASSERT(dropEvent); + Q_ASSERT(isAcceptableDrag(dropEvent)); + + const auto &urls = urlsFromDrop(dropEvent); + + if (urls.isEmpty()) { + return; + } + + const QString &localPath = m_url.toLocalFile(); + + if (KDesktopFile::isDesktopFile(localPath)) { + auto service = new KService(localPath); + + if (service->isApplication()) { + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(KService::Ptr(service)); + job->setUrls(urls); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); + return; + } + } + + QMimeDatabase db; + const QMimeType mimeType = db.mimeTypeForUrl(m_url); + + if (isExecutable(mimeType)) { // isAcceptableDrag has the KAuthorized check for this + QProcess::startDetached(m_url.toLocalFile(), QUrl::toStringList(urls)); + return; + } + + if (mimeType.inherits(QStringLiteral("inode/directory"))) { + QMimeData mimeData; + mimeData.setUrls(urls); + + // DeclarativeDropEvent isn't public + QDropEvent de(QPointF(dropEvent->property("x").toInt(), dropEvent->property("y").toInt()), + static_cast(dropEvent->property("proposedActions").toInt()), + &mimeData, + static_cast(dropEvent->property("buttons").toInt()), + static_cast(dropEvent->property("modifiers").toInt())); + + KIO::DropJob *dropJob = KIO::drop(&de, m_url); + KJobWidgets::setWindow(dropJob, QApplication::desktop()); + return; + } +} + +bool IconApplet::isAcceptableDrag(QObject *dropEvent) +{ + Q_ASSERT(dropEvent); + + const auto &urls = urlsFromDrop(dropEvent); + + if (urls.isEmpty()) { + return false; + } + + const QString &localPath = m_url.toLocalFile(); + if (KDesktopFile::isDesktopFile(localPath)) { + return true; + } + + QMimeDatabase db; + const QMimeType mimeType = db.mimeTypeForUrl(m_url); + + if (KAuthorized::authorize(KAuthorized::SHELL_ACCESS) && isExecutable(mimeType)) { + return true; + } + + if (mimeType.inherits(QStringLiteral("inode/directory"))) { + return true; + } + + return false; +} + +QList IconApplet::urlsFromDrop(QObject *dropEvent) +{ + // DeclarativeDropEvent and co aren't public + const QObject *mimeData = qvariant_cast(dropEvent->property("mimeData")); + Q_ASSERT(mimeData); + + const QJsonArray &droppedUrls = mimeData->property("urls").toJsonArray(); + + QList urls; + urls.reserve(droppedUrls.count()); + for (const QJsonValue &droppedUrl : droppedUrls) { + const QUrl url(droppedUrl.toString()); + if (url.isValid()) { + urls.append(url); + } + } + + return urls; +} + +bool IconApplet::isExecutable(const QMimeType &mimeType) +{ + return (mimeType.inherits(QStringLiteral("application/x-executable")) || mimeType.inherits(QStringLiteral("application/x-shellscript"))); +} + +void IconApplet::configure() +{ + KPropertiesDialog *dialog = m_configDialog.data(); + + if (dialog) { + dialog->show(); + dialog->raise(); + return; + } + + dialog = new KPropertiesDialog(QUrl::fromLocalFile(m_localPath)); + m_configDialog = dialog; + + connect(dialog, &KPropertiesDialog::applied, this, [this] { + KDesktopFile desktopFile(m_localPath); + if (desktopFile.hasLinkType()) { + const QUrl newUrl(desktopFile.readUrl()); + + if (m_url != newUrl) { + // make sure to fully repopulate in case the user changed the Link URL + QFile::remove(m_localPath); + + setUrl(newUrl); // calls populate() itself, but only if it changed + return; + } + } + + populate(); + }); + + dialog->setAttribute(Qt::WA_DeleteOnClose, true); + dialog->setFileNameReadOnly(true); + dialog->setWindowTitle(i18n("Properties for %1", m_name)); + dialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); + dialog->show(); +} + +QString IconApplet::localPath() const +{ + return config().readEntry(QStringLiteral("localPath")); +} + +void IconApplet::setLocalPath(const QString &localPath) +{ + m_localPath = localPath; + config().writeEntry(QStringLiteral("localPath"), localPath); +} + +K_PLUGIN_CLASS_WITH_JSON(IconApplet, "package/metadata.json") + +#include "iconapplet.moc" diff --git a/plasma/workspace/applets/icon/iconapplet.h b/plasma/workspace/applets/icon/iconapplet.h new file mode 100644 index 0000000000..3bc32f8cf5 --- /dev/null +++ b/plasma/workspace/applets/icon/iconapplet.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include + +#include + +class KFileItemActions; + +class QMenu; + +namespace TaskManager +{ +class StartupTasksModel; +} + +class IconApplet : public Plasma::Applet +{ + Q_OBJECT + + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged) + +public: + explicit IconApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~IconApplet() override; + + void init() override; + void configChanged() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QString name() const; + QString iconName() const; + QString genericName() const; + + QList contextualActions() override; + + Q_INVOKABLE void run(); + Q_INVOKABLE void processDrop(QObject *dropEvent); + Q_INVOKABLE void configure(); + + Q_INVOKABLE bool isAcceptableDrag(QObject *dropEvent); + +Q_SIGNALS: + void urlChanged(const QUrl &url); + + void nameChanged(const QString &name); + void iconNameChanged(const QString &iconName); + void genericNameChanged(const QString &genericName); + void jumpListActionsChanged(const QVariantList &jumpListActions); + +private: + void setIconName(const QString &iconName); + + static QList urlsFromDrop(QObject *dropEvent); + static bool isExecutable(const QMimeType &mimeType); + + void populate(); + void populateFromDesktopFile(const QString &path); + + QString localPath() const; + void setLocalPath(const QString &localPath); + + QUrl m_url; + QString m_localPath; + + QString m_name; + QString m_iconName; + QString m_genericName; + + QList m_jumpListActions; + QAction *m_separatorAction = nullptr; + QList m_openWithActions; + + QAction *m_openContainingFolderAction = nullptr; + + KFileItemActions *m_fileItemActions = nullptr; + QScopedPointer m_openWithMenu; + + QPointer m_configDialog; + + TaskManager::StartupTasksModel *m_startupTasksModel = nullptr; +}; diff --git a/plasma/workspace/applets/icon/package/contents/config/main.xml b/plasma/workspace/applets/icon/package/contents/config/main.xml new file mode 100644 index 0000000000..9b3069f42a --- /dev/null +++ b/plasma/workspace/applets/icon/package/contents/config/main.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/plasma/workspace/applets/icon/package/contents/ui/main.qml b/plasma/workspace/applets/icon/package/contents/ui/main.qml new file mode 100644 index 0000000000..b15e99e878 --- /dev/null +++ b/plasma/workspace/applets/icon/package/contents/ui/main.qml @@ -0,0 +1,169 @@ +/* + SPDX-FileCopyrightText: 2013 Bhushan Shah + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.draganddrop 2.0 as DragDrop + +MouseArea { + id: root + + readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge + || plasmoid.location === PlasmaCore.Types.RightEdge + || plasmoid.location === PlasmaCore.Types.BottomEdge + || plasmoid.location === PlasmaCore.Types.LeftEdge) + readonly property bool constrained: plasmoid.formFactor === PlasmaCore.Types.Vertical || plasmoid.formFactor === PlasmaCore.Types.Horizontal + property bool containsAcceptableDrag: false + + height: Math.round(PlasmaCore.Units.iconSizes.desktop + 2 * PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height) + width: Math.round(PlasmaCore.Units.iconSizes.desktop * 1.5) + + activeFocusOnTab: true + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + plasmoid.nativeInterface.run() + break; + } + } + Accessible.name: plasmoid.title + Accessible.description: plasmoid.nativeInterface.genericName !== mainText ? plasmoid.nativeInterface.genericName :"" + Accessible.role: Accessible.Button + + Layout.minimumWidth: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : PlasmaCore.Units.iconSizes.small + Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : (PlasmaCore.Units.iconSizes.small + 2 * PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height) + + Layout.maximumWidth: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1 + Layout.maximumHeight: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1 + + hoverEnabled: true + + onClicked: plasmoid.nativeInterface.run() + + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + Plasmoid.icon: plasmoid.nativeInterface.iconName + Plasmoid.title: plasmoid.nativeInterface.name + Plasmoid.backgroundHints: PlasmaCore.Types.NoBackground + + Plasmoid.onActivated: plasmoid.nativeInterface.run() + + Plasmoid.onContextualActionsAboutToShow: updateActions() + + Component.onCompleted: updateActions() + + function updateActions() { + plasmoid.clearActions() + + plasmoid.removeAction("configure"); + + if (plasmoid.immutability !== PlasmaCore.Types.SystemImmutable) { + plasmoid.setAction("configure", i18n("Properties"), "document-properties"); + } + } + + function action_configure() { + plasmoid.nativeInterface.configure() + } + + Connections { + target: plasmoid + function onExternalData(mimetype, data) { + plasmoid.nativeInterface.url = data + } + } + + DragDrop.DropArea { + id: dropArea + anchors.fill: parent + preventStealing: true + onDragEnter: { + var acceptable = plasmoid.nativeInterface.isAcceptableDrag(event); + root.containsAcceptableDrag = acceptable; + + if (!acceptable) { + event.ignore(); + } + } + onDragLeave: root.containsAcceptableDrag = false + onDrop: { + if (root.containsAcceptableDrag) { + plasmoid.nativeInterface.processDrop(event) + } else { + event.ignore(); + } + + root.containsAcceptableDrag = false + } + } + + PlasmaCore.IconItem { + id: icon + anchors{ + left: parent.left + right: parent.right + top: parent.top + bottom: constrained ? parent.bottom : text.top + } + source: plasmoid.icon + enabled: root.enabled + active: root.containsMouse || root.containsAcceptableDrag + usesPlasmaTheme: false + opacity: plasmoid.busy ? 0.6 : 1 + } + + DropShadow { + id: textShadow + + anchors.fill: text + + visible: !constrained + + horizontalOffset: 1 + verticalOffset: 1 + + radius: 4 + samples: 9 + spread: 0.35 + + color: "black" + + source: constrained ? null : text + } + + PlasmaComponents3.Label { + id : text + text : plasmoid.title + anchors { + left : parent.left + bottom : parent.bottom + right : parent.right + } + horizontalAlignment : Text.AlignHCenter + visible: false // rendered by DropShadow + maximumLineCount: 2 + color: "white" + elide: Text.ElideRight + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + textFormat: Text.PlainText + } + + PlasmaCore.ToolTipArea { + anchors.fill: parent + mainText: plasmoid.title + subText: plasmoid.nativeInterface.genericName !== mainText ? plasmoid.nativeInterface.genericName :"" + textFormat: Text.PlainText + } +} diff --git a/plasma/workspace/applets/icon/package/metadata.json b/plasma/workspace/applets/icon/package/metadata.json new file mode 100644 index 0000000000..725d1a493f --- /dev/null +++ b/plasma/workspace/applets/icon/package/metadata.json @@ -0,0 +1,172 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "plasma-devel@kde.org", + "Name": "The Plasma Team", + "Name[ar]": "فريق بلازما", + "Name[az]": "Plasma komandası", + "Name[ca]": "L'equip del Plasma", + "Name[cs]": "Team Plasma", + "Name[de]": "Das Plasma-Team", + "Name[en_GB]": "The Plasma Team", + "Name[es]": "El equipo de Plasma", + "Name[eu]": "Plasma taldea", + "Name[fi]": "Plasma-työryhmä", + "Name[fr]": "L'équipe de Plasma", + "Name[hu]": "A Plasma fejlesztői", + "Name[ia]": "Le equipa de Plasma", + "Name[it]": "La squadra di Plasma", + "Name[ko]": "Plasma 팀", + "Name[lt]": "Plasma komanda", + "Name[nl]": "Het team van Plasma", + "Name[nn]": "Utviklingslaget for Plasma", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਟੀਮ", + "Name[pl]": "Zespół Plazmy", + "Name[pt_BR]": "Temas do Plasma", + "Name[ro]": "Echipa Plasma", + "Name[ru]": "Команда разработчиков Plasma", + "Name[sk]": "Plasma Tím", + "Name[sl]": "Ekipa Plasme", + "Name[sv]": "Plasma-gruppen", + "Name[ta]": "பிளாஸ்மா குழு", + "Name[tr]": "Plazma Takımı", + "Name[uk]": "Команда розробників Плазми", + "Name[vi]": "Đội Plasma", + "Name[x-test]": "xxThe Plasma Teamxx", + "Name[zh_CN]": "Plasma 开发团队" + } + ], + "Category": "File System", + "Description": "A generic icon", + "Description[ar]": "أيقونة عامّة", + "Description[az]": "Ümumi nişan", + "Description[ca]": "Una icona genèrica", + "Description[cs]": "Obecná ikona", + "Description[de]": "Ein allgemeines Symbol", + "Description[en_GB]": "A generic icon", + "Description[es]": "Un icono genérico", + "Description[eu]": "Ikono generiko bat", + "Description[fi]": "Yleiskuvake", + "Description[fr]": "Une icône générique", + "Description[hu]": "Általános ikon", + "Description[ia]": "Un icone generic", + "Description[it]": "Un'icona generica", + "Description[ko]": "일반적인 아이콘", + "Description[lt]": "Bendrinė piktograma", + "Description[nl]": "Een generiek pictogram", + "Description[nn]": "Eit generisk ikon", + "Description[pa]": "ਆਮ ਆਈਕਾਨ", + "Description[pl]": "Zwykła ikona", + "Description[pt_BR]": "Um ícone genérico", + "Description[ro]": "O pictogramă generică", + "Description[ru]": "Значок запуска", + "Description[sk]": "Všeobecná ikona", + "Description[sl]": "Generična ikona", + "Description[sv]": "En generell ikon", + "Description[ta]": "பொதுவான சின்னம்", + "Description[tr]": "Genel bir simge", + "Description[uk]": "Загальна піктограма", + "Description[vi]": "Một biểu tượng chung", + "Description[x-test]": "xxA generic iconxx", + "Description[zh_CN]": "通用图标", + "EnabledByDefault": true, + "Id": "org.kde.plasma.icon", + "License": "GPL", + "Name": "Icon", + "Name[af]": "Ikoon", + "Name[ar]": "أيقونة", + "Name[as]": "আইকন", + "Name[ast]": "Iconu", + "Name[az]": "Nişan", + "Name[be@latin]": "Ikona", + "Name[bg]": "Икона", + "Name[bn]": "আইকন", + "Name[bn_IN]": "আইকন", + "Name[bs]": "Ikona", + "Name[ca@valencia]": "Icona", + "Name[ca]": "Icona", + "Name[cs]": "Ikona", + "Name[csb]": "Ikòna", + "Name[da]": "Ikon", + "Name[de]": "Symbol", + "Name[el]": "Εικονίδιο", + "Name[en_GB]": "Icon", + "Name[eo]": "Piktogramo", + "Name[es]": "Icono", + "Name[et]": "Ikoon", + "Name[eu]": "Ikonoa", + "Name[fa]": "شمایل", + "Name[fi]": "Kuvake", + "Name[fr]": "Icône", + "Name[fy]": "Bykdkaike", + "Name[ga]": "Deilbhín", + "Name[gl]": "Icona", + "Name[gu]": "ચિહ્ન", + "Name[he]": "סימלון", + "Name[hi]": "प्रतीक", + "Name[hne]": "चिनहा", + "Name[hr]": "Ikona", + "Name[hsb]": "Piktogram", + "Name[hu]": "Ikon", + "Name[ia]": "Icone", + "Name[id]": "Ikon", + "Name[is]": "Táknmynd", + "Name[it]": "Icona", + "Name[ja]": "アイコン", + "Name[kk]": "Таңбаша", + "Name[km]": "រូប​តំណាង", + "Name[kn]": "ಚಿಹ್ನೆ", + "Name[ko]": "아이콘", + "Name[ku]": "Îkon", + "Name[lt]": "Piktograma", + "Name[lv]": "Ikona", + "Name[mai]": "चिह्न", + "Name[mk]": "Икона", + "Name[ml]": "ചിഹ്നം", + "Name[mr]": "चिन्ह", + "Name[nb]": "Ikon", + "Name[nds]": "Lüttbild", + "Name[nl]": "Pictogram", + "Name[nn]": "Ikon", + "Name[oc]": "Icòna", + "Name[or]": "ଚିତ୍ର ସଂକେତ", + "Name[pa]": "ਆਈਕਾਨ", + "Name[pl]": "Ikona", + "Name[pt]": "Ícone", + "Name[pt_BR]": "Ícone", + "Name[ro]": "Pictogramă", + "Name[ru]": "Значок", + "Name[se]": "Govaš", + "Name[si]": "අයිකනය", + "Name[sk]": "Ikona", + "Name[sl]": "Ikona", + "Name[sr@ijekavian]": "икона", + "Name[sr@ijekavianlatin]": "ikona", + "Name[sr@latin]": "ikona", + "Name[sr]": "икона", + "Name[sv]": "Ikon", + "Name[ta]": "சின்னம்", + "Name[te]": "ప్రతిమ", + "Name[tg]": "Нишона", + "Name[th]": "ภาพไอคอน", + "Name[tr]": "Simge", + "Name[ug]": "سىنبەلگە", + "Name[uk]": "Піктограма", + "Name[uz@cyrillic]": "Нишонча", + "Name[uz]": "Nishoncha", + "Name[vi]": "Biểu tượng", + "Name[wa]": "Imådjete", + "Name[x-test]": "xxIconxx", + "Name[zh_CN]": "图标", + "Name[zh_TW]": "圖示", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "NoDisplay": true, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml" +} diff --git a/plasma/workspace/applets/kicker/CMakeLists.txt b/plasma/workspace/applets/kicker/CMakeLists.txt new file mode 100644 index 0000000000..7279c05eb9 --- /dev/null +++ b/plasma/workspace/applets/kicker/CMakeLists.txt @@ -0,0 +1,88 @@ +add_definitions( + -DQT_USE_QSTRINGBUILDER + -DQT_NO_CAST_TO_ASCII +# -DQT_NO_CAST_FROM_ASCII + -DQT_STRICT_ITERATORS + -DQT_NO_CAST_FROM_BYTEARRAY + -DQT_USE_FAST_OPERATOR_PLUS + -DTRANSLATION_DOMAIN=\"libkicker\" +) + +set(kickerplugin_SRCS + plugin/abstractentry.cpp + plugin/abstractmodel.cpp + plugin/actionlist.cpp + plugin/appentry.cpp + plugin/appsmodel.cpp + plugin/computermodel.cpp + plugin/contactentry.cpp + plugin/containmentinterface.cpp + plugin/draghelper.cpp + plugin/simplefavoritesmodel.cpp + plugin/kastatsfavoritesmodel.cpp + plugin/fileentry.cpp + plugin/forwardingmodel.cpp + plugin/placeholdermodel.cpp + plugin/funnelmodel.cpp + plugin/dashboardwindow.cpp + plugin/kickerplugin.cpp + plugin/menuentryeditor.cpp + plugin/processrunner.cpp + plugin/rootmodel.cpp + plugin/runnermodel.cpp + plugin/runnermatchesmodel.cpp + plugin/recentcontactsmodel.cpp + plugin/recentusagemodel.cpp + plugin/submenu.cpp + plugin/systementry.cpp + plugin/systemmodel.cpp + plugin/systemsettings.cpp + plugin/trianglemousefilter.cpp + plugin/wheelinterceptor.cpp + plugin/windowsystem.cpp + plugin/funnelmodel.cpp +) + +ecm_qt_declare_logging_category(kickerplugin_SRCS + HEADER debug.h + IDENTIFIER KICKER_DEBUG + CATEGORY_NAME org.kde.plasma.kicker) + +qt_add_dbus_interface(kickerplugin_SRCS ${CMAKE_SOURCE_DIR}/krunner/dbus/org.kde.krunner.App.xml krunner_interface) +qt_add_dbus_interface(kickerplugin_SRCS ${CMAKE_SOURCE_DIR}/ksmserver/org.kde.KSMServerInterface.xml ksmserver_interface) + +install(FILES plugin/qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/kicker) + +add_library(kickerplugin SHARED ${kickerplugin_SRCS}) + +target_link_libraries(kickerplugin + Qt::Core + Qt::Qml + Qt::Quick + Qt::X11Extras + KF5::Activities + KF5::ActivitiesStats + KF5::ConfigCore + KF5::CoreAddons + KF5::I18n + KF5::IconThemes + KF5::ItemModels + KF5::KIOCore + KF5::KIOWidgets + KF5::KIOFileWidgets + KF5::Notifications + KF5::People + KF5::PeopleWidgets + KF5::PlasmaQuick + KF5::Runner + KF5::Service + KF5::WindowSystem + PW::KWorkspace) + +if (${HAVE_APPSTREAMQT}) +target_link_libraries(kickerplugin AppStreamQt) +endif() + +add_subdirectory(plugin/autotests) + +install(TARGETS kickerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/kicker) diff --git a/plasma/workspace/applets/kicker/Messages.sh b/plasma/workspace/applets/kicker/Messages.sh new file mode 100644 index 0000000000..2a689fede5 --- /dev/null +++ b/plasma/workspace/applets/kicker/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/libkicker.pot diff --git a/plasma/workspace/applets/kicker/plugin/abstractentry.cpp b/plasma/workspace/applets/kicker/plugin/abstractentry.cpp new file mode 100644 index 0000000000..f64cce666d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/abstractentry.cpp @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "abstractentry.h" + +#include + +AbstractEntry::AbstractEntry(AbstractModel *owner) + : m_owner(owner) +{ +} + +AbstractEntry::~AbstractEntry() +{ +} + +AbstractModel *AbstractEntry::owner() const +{ + return m_owner; +} + +bool AbstractEntry::isValid() const +{ + return true; +} + +QIcon AbstractEntry::icon() const +{ + return QIcon(); +} + +QString AbstractEntry::name() const +{ + return QString(); +} + +QString AbstractEntry::group() const +{ + return QString(); +} + +QString AbstractEntry::description() const +{ + return QString(); +} + +QString AbstractEntry::id() const +{ + return QString(); +} + +QUrl AbstractEntry::url() const +{ + return QUrl(); +} + +bool AbstractEntry::hasChildren() const +{ + return false; +} + +AbstractModel *AbstractEntry::childModel() const +{ + return nullptr; +} + +bool AbstractEntry::hasActions() const +{ + return false; +} + +QVariantList AbstractEntry::actions() const +{ + return QVariantList(); +} + +bool AbstractEntry::run(const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(actionId) + Q_UNUSED(argument) + + return false; +} + +AbstractGroupEntry::AbstractGroupEntry(AbstractModel *owner) + : AbstractEntry(owner) +{ +} + +SeparatorEntry::SeparatorEntry(AbstractModel *owner) + : AbstractEntry(owner) +{ +} diff --git a/plasma/workspace/applets/kicker/plugin/abstractentry.h b/plasma/workspace/applets/kicker/plugin/abstractentry.h new file mode 100644 index 0000000000..eb91aedfb9 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/abstractentry.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include +#include + +class AbstractEntry +{ +public: + explicit AbstractEntry(AbstractModel *owner); + virtual ~AbstractEntry(); + + enum EntryType { + RunnableType, + GroupType, + SeparatorType, + }; + + virtual EntryType type() const = 0; + + AbstractModel *owner() const; + + virtual bool isValid() const; + + virtual QIcon icon() const; + virtual QString name() const; + virtual QString group() const; + virtual QString description() const; + + virtual QString id() const; + virtual QUrl url() const; + + virtual bool hasChildren() const; + virtual AbstractModel *childModel() const; + + virtual bool hasActions() const; + virtual QVariantList actions() const; + + virtual bool run(const QString &actionId = QString(), const QVariant &argument = QVariant()); + +protected: + AbstractModel *m_owner; +}; + +class AbstractGroupEntry : public AbstractEntry +{ +public: + explicit AbstractGroupEntry(AbstractModel *owner); + + EntryType type() const override + { + return GroupType; + } +}; + +class SeparatorEntry : public AbstractEntry +{ +public: + explicit SeparatorEntry(AbstractModel *owner); + + EntryType type() const override + { + return SeparatorType; + } +}; diff --git a/plasma/workspace/applets/kicker/plugin/abstractmodel.cpp b/plasma/workspace/applets/kicker/plugin/abstractmodel.cpp new file mode 100644 index 0000000000..44f81e3f8b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/abstractmodel.cpp @@ -0,0 +1,146 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "abstractmodel.h" +#include "actionlist.h" + +AbstractModel::AbstractModel(QObject *parent) + : QAbstractListModel(parent) + , m_favoritesModel(nullptr) + , m_iconSize(32) +{ +} + +AbstractModel::~AbstractModel() +{ +} + +QHash AbstractModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, "display"); + roles.insert(Qt::DecorationRole, "decoration"); + roles.insert(Kicker::GroupRole, "group"); + roles.insert(Kicker::DescriptionRole, "description"); + roles.insert(Kicker::FavoriteIdRole, "favoriteId"); + roles.insert(Kicker::IsParentRole, "isParent"); + roles.insert(Kicker::IsSeparatorRole, "isSeparator"); + roles.insert(Kicker::HasChildrenRole, "hasChildren"); + roles.insert(Kicker::HasActionListRole, "hasActionList"); + roles.insert(Kicker::ActionListRole, "actionList"); + roles.insert(Kicker::UrlRole, "url"); + roles.insert(Kicker::DisabledRole, "disabled"); + roles.insert(Kicker::IsMultilineTextRole, "isMultilineText"); + + return roles; +} + +int AbstractModel::count() const +{ + return rowCount(); +} + +int AbstractModel::separatorCount() const +{ + return 0; +} + +int AbstractModel::iconSize() const +{ + return m_iconSize; +} + +void AbstractModel::setIconSize(int iconSize) +{ + if (m_iconSize != iconSize) { + m_iconSize = iconSize; + refresh(); + } +} + +void AbstractModel::refresh() +{ +} + +QString AbstractModel::labelForRow(int row) +{ + return data(index(row, 0), Qt::DisplayRole).toString(); +} + +AbstractModel *AbstractModel::modelForRow(int row) +{ + Q_UNUSED(row) + + return nullptr; +} + +int AbstractModel::rowForModel(AbstractModel *model) +{ + Q_UNUSED(model) + + return -1; +} + +bool AbstractModel::hasActions() const +{ + return false; +} + +QVariantList AbstractModel::actions() const +{ + return QVariantList(); +} + +AbstractModel *AbstractModel::favoritesModel() +{ + if (m_favoritesModel) { + return m_favoritesModel; + } else { + AbstractModel *model = rootModel(); + + if (model && model != this) { + return model->favoritesModel(); + } + } + + return nullptr; +} + +void AbstractModel::setFavoritesModel(AbstractModel *model) +{ + if (m_favoritesModel != model) { + m_favoritesModel = model; + + Q_EMIT favoritesModelChanged(); + } +} + +AbstractModel *AbstractModel::rootModel() +{ + if (!parent()) { + return nullptr; + } + + QObject *p = this; + AbstractModel *rootModel = nullptr; + + while (p) { + if (qobject_cast(p)) { + rootModel = qobject_cast(p); + } else { + return rootModel; + } + + p = p->parent(); + } + + return rootModel; +} + +void AbstractModel::entryChanged(AbstractEntry *entry) +{ + Q_UNUSED(entry) +} diff --git a/plasma/workspace/applets/kicker/plugin/abstractmodel.h b/plasma/workspace/applets/kicker/plugin/abstractmodel.h new file mode 100644 index 0000000000..fe179d175b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/abstractmodel.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class AbstractEntry; + +class AbstractModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(int separatorCount READ separatorCount NOTIFY separatorCountChanged) + Q_PROPERTY(int iconSize READ iconSize WRITE setIconSize NOTIFY iconSizeChanged) + Q_PROPERTY(AbstractModel *favoritesModel READ favoritesModel WRITE setFavoritesModel NOTIFY favoritesModelChanged) + +public: + explicit AbstractModel(QObject *parent = nullptr); + ~AbstractModel() override; + + QHash roleNames() const override; + + virtual QString description() const = 0; + + int count() const; + virtual int separatorCount() const; + + int iconSize() const; + void setIconSize(int size); + + Q_INVOKABLE virtual bool trigger(int row, const QString &actionId, const QVariant &argument) = 0; + + Q_INVOKABLE virtual void refresh(); + + Q_INVOKABLE virtual QString labelForRow(int row); + + Q_INVOKABLE virtual AbstractModel *modelForRow(int row); + Q_INVOKABLE virtual int rowForModel(AbstractModel *model); + + virtual bool hasActions() const; + virtual QVariantList actions() const; + + virtual AbstractModel *favoritesModel(); + virtual void setFavoritesModel(AbstractModel *model); + AbstractModel *rootModel(); + + virtual void entryChanged(AbstractEntry *entry); + +Q_SIGNALS: + void descriptionChanged() const; + void countChanged() const; + void separatorCountChanged() const; + void iconSizeChanged() const; + void favoritesModelChanged() const; + +protected: + AbstractModel *m_favoritesModel; + +private: + int m_iconSize; +}; diff --git a/plasma/workspace/applets/kicker/plugin/actionlist.cpp b/plasma/workspace/applets/kicker/plugin/actionlist.cpp new file mode 100644 index 0000000000..177a6a36da --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/actionlist.cpp @@ -0,0 +1,466 @@ +/* + SPDX-FileCopyrightText: 2013 Aurélien Gâteau + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "actionlist.h" +#include "menuentryeditor.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "containmentinterface.h" + +#ifdef HAVE_APPSTREAMQT +#include +#endif + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +namespace Kicker +{ +QVariantMap createActionItem(const QString &label, const QString &icon, const QString &actionId, const QVariant &argument) +{ + QVariantMap map; + + map[QStringLiteral("text")] = label; + map[QStringLiteral("icon")] = icon; + map[QStringLiteral("actionId")] = actionId; + + if (argument.isValid()) { + map[QStringLiteral("actionArgument")] = argument; + } + + return map; +} + +QVariantMap createTitleActionItem(const QString &label) +{ + QVariantMap map; + + map[QStringLiteral("text")] = label; + map[QStringLiteral("type")] = QStringLiteral("title"); + + return map; +} + +QVariantMap createSeparatorActionItem() +{ + QVariantMap map; + + map[QStringLiteral("type")] = QStringLiteral("separator"); + + return map; +} + +QVariantList createActionListForFileItem(const KFileItem &fileItem) +{ + QVariantList list; + + const KService::List services = KApplicationTrader::queryByMimeType(fileItem.mimetype()); + + if (!services.isEmpty()) { + list << createTitleActionItem(i18n("Open with:")); + + for (const KService::Ptr service : services) { + const QString text = service->name().replace(QLatin1Char('&'), QStringLiteral("&&")); + QVariantMap item = createActionItem(text, service->icon(), QStringLiteral("_kicker_fileItem_openWith"), service->entryPath()); + + list << item; + } + + list << createSeparatorActionItem(); + } + + const QVariantMap &propertiesItem = + createActionItem(i18n("Properties"), QStringLiteral("document-properties"), QStringLiteral("_kicker_fileItem_properties")); + list << propertiesItem; + + return list; +} + +bool handleFileItemAction(const KFileItem &fileItem, const QString &actionId, const QVariant &argument, bool *close) +{ + if (actionId == QLatin1String("_kicker_fileItem_properties")) { + KPropertiesDialog *dlg = new KPropertiesDialog(fileItem, QApplication::activeWindow()); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->show(); + + *close = false; + + return true; + } + + if (actionId == QLatin1String("_kicker_fileItem_openWith")) { + const QString path = argument.toString(); + const KService::Ptr service = KService::serviceByDesktopPath(path); + + if (!service) { + return false; + } + + auto *job = new KIO::ApplicationLauncherJob(service); + job->setUrls({fileItem.url()}); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); + + *close = true; + + return true; + } + + return false; +} + +QVariantList createAddLauncherActionList(QObject *appletInterface, const KService::Ptr &service) +{ + QVariantList actionList; + if (!service) { + return actionList; + } + + if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Desktop)) { + QVariantMap addToDesktopAction = Kicker::createActionItem(i18n("Add to Desktop"), QStringLiteral("list-add"), QStringLiteral("addToDesktop")); + actionList << addToDesktopAction; + } + + if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Panel)) { + QVariantMap addToPanelAction = Kicker::createActionItem(i18n("Add to Panel (Widget)"), QStringLiteral("list-add"), QStringLiteral("addToPanel")); + actionList << addToPanelAction; + } + + if (service && ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::TaskManager, Kicker::resolvedServiceEntryPath(service))) { + QVariantMap addToTaskManagerAction = Kicker::createActionItem(i18n("Pin to Task Manager"), QStringLiteral("pin"), QStringLiteral("addToTaskManager")); + actionList << addToTaskManagerAction; + } + + return actionList; +} + +bool handleAddLauncherAction(const QString &actionId, QObject *appletInterface, const KService::Ptr &service) +{ + if (!service) { + return false; + } + + if (actionId == QLatin1String("addToDesktop")) { + if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Desktop)) { + ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::Desktop, Kicker::resolvedServiceEntryPath(service)); + } + return true; + } else if (actionId == QLatin1String("addToPanel")) { + if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::Panel)) { + ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::Panel, Kicker::resolvedServiceEntryPath(service)); + } + return true; + } else if (actionId == QLatin1String("addToTaskManager")) { + if (ContainmentInterface::mayAddLauncher(appletInterface, ContainmentInterface::TaskManager, Kicker::resolvedServiceEntryPath(service))) { + ContainmentInterface::addLauncher(appletInterface, ContainmentInterface::TaskManager, Kicker::resolvedServiceEntryPath(service)); + } + return true; + } + + return false; +} + +QString storageIdFromService(KService::Ptr service) +{ + QString storageId = service->storageId(); + + if (storageId.endsWith(QLatin1String(".desktop"))) { + storageId = storageId.left(storageId.length() - 8); + } + + return storageId; +} + +QVariantList jumpListActions(KService::Ptr service) +{ + QVariantList list; + + if (!service) { + return list; + } + + // Add frequently used settings modules similar to SystemSetting's overview page. + if (service->storageId() == QLatin1String("systemsettings.desktop")) { + list = systemSettingsActions(); + + if (!list.isEmpty()) { + return list; + } + } + + const auto &actions = service->actions(); + for (const KServiceAction &action : actions) { + if (action.text().isEmpty() || action.exec().isEmpty()) { + continue; + } + + QVariantMap item = createActionItem(action.text(), action.icon(), QStringLiteral("_kicker_jumpListAction"), action.exec()); + + list << item; + } + + return list; +} + +QVariantList systemSettingsActions() +{ + QVariantList list; + + auto query = AllResources | Agent(QStringLiteral("org.kde.systemsettings")) | HighScoredFirst | Limit(5); + + ResultSet results(query); + + QStringList ids; + for (const ResultSet::Result &result : results) { + ids << QUrl(result.resource()).path(); + } + + if (ids.count() < 5) { + // We'll load the default set of settings from its jump list actions. + return list; + } + + for (const QString &id : qAsConst(ids)) { + KService::Ptr service = KService::serviceByStorageId(id); + if (!service || !service->isValid()) { + continue; + } + + list << createActionItem(service->name(), service->icon(), QStringLiteral("_kicker_jumpListAction"), service->exec()); + } + + return list; +} + +QVariantList recentDocumentActions(KService::Ptr service) +{ + QVariantList list; + + if (!service) { + return list; + } + + const QString storageId = storageIdFromService(service); + + if (storageId.isEmpty()) { + return list; + } + + // clang-format off + auto query = UsedResources + | RecentlyUsedFirst + | Agent(storageId) + | Type::any() + | Activity::current() + | Url::file(); + // clang-format on + + ResultSet results(query); + + ResultSet::const_iterator resultIt; + resultIt = results.begin(); + + while (list.count() < 6 && resultIt != results.end()) { + const QString resource = (*resultIt).resource(); + const QString mimeType = (*resultIt).mimetype(); + ++resultIt; + + const QUrl url(resource); + + if (!url.isValid()) { + continue; + } + + const KFileItem fileItem(url); + + if (!fileItem.isFile()) { + continue; + } + + if (list.isEmpty()) { + list << createTitleActionItem(i18n("Recent Files")); + } + + QVariantMap item = createActionItem(url.fileName(), fileItem.iconName(), QStringLiteral("_kicker_recentDocument"), QStringList{resource, mimeType}); + + list << item; + } + + if (!list.isEmpty()) { + QVariantMap forgetAction = + createActionItem(i18n("Forget Recent Files"), QStringLiteral("edit-clear-history"), QStringLiteral("_kicker_forgetRecentDocuments")); + list << forgetAction; + } + + return list; +} + +bool handleRecentDocumentAction(KService::Ptr service, const QString &actionId, const QVariant &_argument) +{ + if (!service) { + return false; + } + + if (actionId == QLatin1String("_kicker_forgetRecentDocuments")) { + const QString storageId = storageIdFromService(service); + + if (storageId.isEmpty()) { + return false; + } + + // clang-format off + auto query = UsedResources + | Agent(storageId) + | Type::any() + | Activity::current() + | Url::file(); + // clang-format on + + KAStats::forgetResources(query); + + return false; + } + + const QStringList argument = _argument.toStringList(); + if (argument.isEmpty()) { + return false; + } + const auto resource = argument.at(0); + const auto mimeType = argument.at(1); + + // prevents using a service file that does not support opening a mime type for a file it created + // for instance a screenshot tool + if (!mimeType.isEmpty()) { + if (!service->hasMimeType(mimeType)) { + // needs to find the application that supports this mimetype + service = KApplicationTrader::preferredService(mimeType); + + if (!service) { + // no service found to handle the mimetype + return false; + } + } + } + + auto *job = new KIO::ApplicationLauncherJob(service); + job->setUrls({QUrl::fromUserInput(resource)}); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + return job->exec(); +} + +Q_GLOBAL_STATIC(MenuEntryEditor, menuEntryEditor) + +bool canEditApplication(const KService::Ptr &service) +{ + return (service->isApplication() && menuEntryEditor->canEdit(service->entryPath())); +} + +void editApplication(const QString &entryPath, const QString &menuId) +{ + menuEntryEditor->edit(entryPath, menuId); +} + +QVariantList editApplicationAction(const KService::Ptr &service) +{ + QVariantList actionList; + + if (canEditApplication(service)) { + // TODO: Using the KMenuEdit icon might be misleading. + QVariantMap editAction = Kicker::createActionItem(i18n("Edit Application…"), QStringLiteral("kmenuedit"), QStringLiteral("editApplication")); + actionList << editAction; + } + + return actionList; +} + +bool handleEditApplicationAction(const QString &actionId, const KService::Ptr &service) +{ + if (service && actionId == QLatin1String("editApplication") && canEditApplication(service)) { + Kicker::editApplication(service->entryPath(), service->menuId()); + + return true; + } + + return false; +} + +#ifdef HAVE_APPSTREAMQT +Q_GLOBAL_STATIC(AppStream::Pool, appstreamPool) +#endif + +QVariantList appstreamActions(const KService::Ptr &service) +{ +#ifdef HAVE_APPSTREAMQT + const KService::Ptr appStreamHandler = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/appstream")); + + // Don't show action if we can't find any app to handle appstream:// URLs. + if (!appStreamHandler) { + if (!KProtocolInfo::isHelperProtocol(QStringLiteral("appstream")) || KProtocolInfo::exec(QStringLiteral("appstream")).isEmpty()) { + return {}; + } + } + + if (!appstreamPool.exists()) { + appstreamPool->load(); + } + + const auto components = appstreamPool->componentsById(service->desktopEntryName() + QLatin1String(".desktop")); + for (const auto &component : components) { + const QString componentId = component.id(); + + QVariantMap appstreamAction = Kicker::createActionItem(i18nc("@action opens a software center with the application", "Uninstall or Manage Add-Ons…"), + appStreamHandler->icon(), + "manageApplication", + QVariant(QLatin1String("appstream://") + componentId)); + // Only process the first element. In case we have system provided and flatpack sources we would end up with duplicated entries + return {appstreamAction}; + } +#else + Q_UNUSED(service) +#endif + + return {}; +} + +bool handleAppstreamActions(const QString &actionId, const QVariant &argument) +{ + if (actionId == QLatin1String("manageApplication")) { + return QDesktopServices::openUrl(QUrl(argument.toString())); + } + + return false; +} + +QString resolvedServiceEntryPath(const KService::Ptr &service) +{ + QString path = service->entryPath(); + if (!QDir::isAbsolutePath(path)) { + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + path); + } + return path; +} + +} diff --git a/plasma/workspace/applets/kicker/plugin/actionlist.h b/plasma/workspace/applets/kicker/plugin/actionlist.h new file mode 100644 index 0000000000..32aabdc89d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/actionlist.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2013 Aurélien Gâteau + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class KFileItem; + +namespace Kicker +{ +enum { + DescriptionRole = Qt::UserRole + 1, + GroupRole, + FavoriteIdRole, + IsSeparatorRole, + IsDropPlaceholderRole, + IsParentRole, + HasChildrenRole, + HasActionListRole, + ActionListRole, + UrlRole, + DisabledRole, + IsMultilineTextRole, +}; + +QVariantMap createActionItem(const QString &label, const QString &icon, const QString &actionId, const QVariant &argument = QVariant()); + +QVariantMap createTitleActionItem(const QString &label); + +QVariantMap createSeparatorActionItem(); + +QVariantList createActionListForFileItem(const KFileItem &fileItem); +bool handleFileItemAction(const KFileItem &fileItem, const QString &actionId, const QVariant &argument, bool *close); + +QVariantList createAddLauncherActionList(QObject *appletInterface, const KService::Ptr &service); +bool handleAddLauncherAction(const QString &actionId, QObject *appletInterface, const KService::Ptr &service); + +QVariantList jumpListActions(KService::Ptr service); +QVariantList systemSettingsActions(); + +QVariantList recentDocumentActions(KService::Ptr service); +bool handleRecentDocumentAction(KService::Ptr service, const QString &actionId, const QVariant &argument); + +bool canEditApplication(const QString &entryPath); +void editApplication(const QString &entryPath, const QString &menuId); +QVariantList editApplicationAction(const KService::Ptr &service); +bool handleEditApplicationAction(const QString &actionId, const KService::Ptr &service); + +QVariantList appstreamActions(const KService::Ptr &service); +bool handleAppstreamActions(const QString &actionId, const QVariant &argument); + +QString resolvedServiceEntryPath(const KService::Ptr &service); + +} diff --git a/plasma/workspace/applets/kicker/plugin/appentry.cpp b/plasma/workspace/applets/kicker/plugin/appentry.cpp new file mode 100644 index 0000000000..249c18f473 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/appentry.cpp @@ -0,0 +1,334 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "appentry.h" +#include "actionlist.h" +#include "appsmodel.h" +#include "containmentinterface.h" +#include + +#include + +#include +#include +#include +#include +#if HAVE_X11 +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +AppEntry::AppEntry(AbstractModel *owner, KService::Ptr service, NameFormat nameFormat) + : AbstractEntry(owner) + , m_service(service) +{ + if (m_service) { + init(nameFormat); + } +} + +AppEntry::AppEntry(AbstractModel *owner, const QString &id) + : AbstractEntry(owner) +{ + const QUrl url(id); + + if (url.scheme() == QLatin1String("preferred")) { + m_service = defaultAppByName(url.host()); + m_id = id; + m_con = QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, owner, [this, owner, id]() { + KSharedConfig::openConfig()->reparseConfiguration(); + m_service = defaultAppByName(QUrl(id).host()); + if (m_service) { + init((NameFormat)owner->rootModel()->property("appNameFormat").toInt()); + m_icon = QIcon(); + Q_EMIT owner->layoutChanged(); + } + }); + } else { + m_service = KService::serviceByStorageId(id); + } + + if (m_service) { + init((NameFormat)owner->rootModel()->property("appNameFormat").toInt()); + } +} + +void AppEntry::init(NameFormat nameFormat) +{ + m_name = nameFromService(m_service, nameFormat); + + if (nameFormat == GenericNameOnly) { + m_description = nameFromService(m_service, NameOnly); + } else { + m_description = nameFromService(m_service, GenericNameOnly); + } +} + +bool AppEntry::isValid() const +{ + return m_service; +} + +QIcon AppEntry::icon() const +{ + if (m_icon.isNull()) { + if (QFileInfo::exists(m_service->icon())) { + m_icon = QIcon(m_service->icon()); + } else { + m_icon = QIcon::fromTheme(m_service->icon(), QIcon::fromTheme(QStringLiteral("unknown"))); + } + } + return m_icon; +} + +QString AppEntry::name() const +{ + return m_name; +} + +QString AppEntry::description() const +{ + return m_description; +} + +KService::Ptr AppEntry::service() const +{ + return m_service; +} + +QString AppEntry::id() const +{ + if (!m_id.isEmpty()) { + return m_id; + } + + return m_service->storageId(); +} + +QString AppEntry::menuId() const +{ + return m_service->menuId(); +} + +QUrl AppEntry::url() const +{ + return QUrl::fromLocalFile(Kicker::resolvedServiceEntryPath(m_service)); +} + +bool AppEntry::hasActions() const +{ + return true; +} + +QVariantList AppEntry::actions() const +{ + QVariantList actionList; + + actionList << Kicker::jumpListActions(m_service); + if (!actionList.isEmpty()) { + actionList << Kicker::createSeparatorActionItem(); + } + + QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value(); + + bool systemImmutable = false; + if (appletInterface) { + systemImmutable = (appletInterface->property("immutability").toInt() == Plasma::Types::SystemImmutable); + } + + const QVariantList &addLauncherActions = Kicker::createAddLauncherActionList(appletInterface, m_service); + if (!systemImmutable && !addLauncherActions.isEmpty()) { + actionList << addLauncherActions; + } + + const QVariantList &recentDocuments = Kicker::recentDocumentActions(m_service); + if (!recentDocuments.isEmpty()) { + actionList << recentDocuments << Kicker::createSeparatorActionItem(); + } + + // Don't allow adding launchers, editing, hiding, or uninstalling applications + // when system is immutable. + if (systemImmutable) { + return actionList; + } + + if (m_service->isApplication()) { + actionList << Kicker::createSeparatorActionItem(); + actionList << Kicker::editApplicationAction(m_service); + actionList << Kicker::appstreamActions(m_service); + } + + if (appletInterface) { + QQmlPropertyMap *appletConfig = qobject_cast(appletInterface->property("configuration").value()); + + if (appletConfig && appletConfig->contains(QLatin1String("hiddenApplications")) && qobject_cast(m_owner)) { + const QStringList &hiddenApps = appletConfig->value(QLatin1String("hiddenApplications")).toStringList(); + + if (!hiddenApps.contains(m_service->menuId())) { + QVariantMap hideAction = Kicker::createActionItem(i18n("Hide Application"), QStringLiteral("view-hidden"), QStringLiteral("hideApplication")); + actionList << hideAction; + } + } + } + + return actionList; +} + +bool AppEntry::run(const QString &actionId, const QVariant &argument) +{ + if (!m_service->isValid()) { + return false; + } + + if (actionId.isEmpty()) { + quint32 timeStamp = 0; + +#if HAVE_X11 + if (QX11Info::isPlatformX11()) { + timeStamp = QX11Info::appUserTime(); + } +#endif + + auto *job = new KIO::ApplicationLauncherJob(m_service); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); + job->setStartupId(KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); + job->start(); + + KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + m_service->storageId()), QStringLiteral("org.kde.plasma.kicker")); + + return true; + } + + QObject *appletInterface = m_owner->rootModel()->property("appletInterface").value(); + + if (Kicker::handleAddLauncherAction(actionId, appletInterface, m_service)) { + return false; // We don't want to close Kicker, BUG: 390585 + } else if (Kicker::handleEditApplicationAction(actionId, m_service)) { + return true; + } else if (Kicker::handleAppstreamActions(actionId, argument)) { + return true; + } else if (actionId == QLatin1String("_kicker_jumpListAction")) { + auto job = new KIO::CommandLauncherJob(argument.toString()); + job->setDesktopName(m_service->entryPath()); + job->setIcon(m_service->icon()); + return job->exec(); + } + + return Kicker::handleRecentDocumentAction(m_service, actionId, argument); +} + +QString AppEntry::nameFromService(const KService::Ptr service, NameFormat nameFormat) +{ + const QString &name = service->name(); + QString genericName = service->genericName(); + + if (genericName.isEmpty()) { + genericName = service->comment(); + } + + if (nameFormat == NameOnly || genericName.isEmpty() || name == genericName) { + return name; + } else if (nameFormat == GenericNameOnly) { + return genericName; + } else if (nameFormat == NameAndGenericName) { + return i18nc("App name (Generic name)", "%1 (%2)", name, genericName); + } else { + return i18nc("Generic name (App name)", "%1 (%2)", genericName, name); + } +} + +KService::Ptr AppEntry::defaultAppByName(const QString &name) +{ + if (name == QLatin1String("browser")) { + KConfigGroup config(KSharedConfig::openConfig(), "General"); + QString browser = config.readPathEntry("BrowserApplication", QString()); + + if (browser.isEmpty()) { + return KApplicationTrader::preferredService(QLatin1String("text/html")); + } else if (browser.startsWith(QLatin1Char('!'))) { + browser.remove(0, 1); + } + + return KService::serviceByStorageId(browser); + } + + return KService::Ptr(); +} + +AppEntry::~AppEntry() +{ + if (m_con) { + QObject::disconnect(m_con); + } +} + +AppGroupEntry::AppGroupEntry(AppsModel *parentModel, + KServiceGroup::Ptr group, + bool paginate, + int pageSize, + bool flat, + bool sorted, + bool separators, + int appNameFormat) + : AbstractGroupEntry(parentModel) + , m_group(group) +{ + AppsModel *model = new AppsModel(group->entryPath(), paginate, pageSize, flat, sorted, separators, parentModel); + model->setAppNameFormat(appNameFormat); + m_childModel = model; + + QObject::connect(parentModel, &AppsModel::cleared, model, &AppsModel::deleteLater); + + QObject::connect(model, &AppsModel::countChanged, [parentModel, this] { + if (parentModel) { + parentModel->entryChanged(this); + } + }); + + QObject::connect(model, &AppsModel::hiddenEntriesChanged, [parentModel, this] { + if (parentModel) { + parentModel->entryChanged(this); + } + }); +} + +QIcon AppGroupEntry::icon() const +{ + if (m_icon.isNull()) { + m_icon = QIcon::fromTheme(m_group->icon(), QIcon::fromTheme(QStringLiteral("unknown"))); + } + return m_icon; +} + +QString AppGroupEntry::name() const +{ + return m_group->caption(); +} + +bool AppGroupEntry::hasChildren() const +{ + return m_childModel && m_childModel->count() > 0; +} + +AbstractModel *AppGroupEntry::childModel() const +{ + return m_childModel; +} diff --git a/plasma/workspace/applets/kicker/plugin/appentry.h b/plasma/workspace/applets/kicker/plugin/appentry.h new file mode 100644 index 0000000000..01565d96de --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/appentry.h @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractentry.h" + +#include +#include + +class AppsModel; +class MenuEntryEditor; + +class AppEntry : public AbstractEntry +{ +public: + enum NameFormat { + NameOnly = 0, + GenericNameOnly, + NameAndGenericName, + GenericNameAndName, + }; + + explicit AppEntry(AbstractModel *owner, KService::Ptr service, NameFormat nameFormat); + explicit AppEntry(AbstractModel *owner, const QString &id); + ~AppEntry() override; + + EntryType type() const override + { + return RunnableType; + } + + bool isValid() const override; + + QIcon icon() const override; + QString name() const override; + QString description() const override; + KService::Ptr service() const; + + QString id() const override; + QUrl url() const override; + + bool hasActions() const override; + QVariantList actions() const override; + + bool run(const QString &actionId = QString(), const QVariant &argument = QVariant()) override; + + QString menuId() const; + + static QString nameFromService(const KService::Ptr service, NameFormat nameFormat); + static KService::Ptr defaultAppByName(const QString &name); + +private: + void init(NameFormat nameFormat); + + QString m_id; + QString m_name; + QString m_description; + mutable QIcon m_icon; + KService::Ptr m_service; + static MenuEntryEditor *m_menuEntryEditor; + QMetaObject::Connection m_con; +}; + +class AppGroupEntry : public AbstractGroupEntry +{ +public: + AppGroupEntry(AppsModel *parentModel, KServiceGroup::Ptr group, bool paginate, int pageSize, bool flat, bool sorted, bool separators, int appNameFormat); + + QIcon icon() const override; + QString name() const override; + + bool hasChildren() const override; + AbstractModel *childModel() const override; + +private: + KServiceGroup::Ptr m_group; + mutable QIcon m_icon; + QPointer m_childModel; +}; diff --git a/plasma/workspace/applets/kicker/plugin/appsmodel.cpp b/plasma/workspace/applets/kicker/plugin/appsmodel.cpp new file mode 100644 index 0000000000..73962a514f --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/appsmodel.cpp @@ -0,0 +1,717 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2013-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "appsmodel.h" +#include "actionlist.h" +#include "rootmodel.h" + +#include +#include +#include +#include + +#include +#include + +AppsModel::AppsModel(const QString &entryPath, bool paginate, int pageSize, bool flat, bool sorted, bool separators, QObject *parent) + : AbstractModel(parent) + , m_complete(false) + , m_paginate(paginate) + , m_pageSize(pageSize) + , m_deleteEntriesOnDestruction(true) + , m_separatorCount(0) + , m_showSeparators(separators) + , m_showTopLevelItems(false) + , m_appletInterface(nullptr) + , m_autoPopulate(true) + , m_description(i18n("Applications")) + , m_entryPath(entryPath) + , m_staticEntryList(false) + , m_changeTimer(nullptr) + , m_flat(flat) + , m_sorted(sorted) + , m_appNameFormat(AppEntry::NameOnly) +{ + if (!m_entryPath.isEmpty()) { + componentComplete(); + } +} + +AppsModel::AppsModel(const QList entryList, bool deleteEntriesOnDestruction, QObject *parent) + : AbstractModel(parent) + , m_complete(false) + , m_paginate(false) + , m_pageSize(24) + , m_deleteEntriesOnDestruction(deleteEntriesOnDestruction) + , m_separatorCount(0) + , m_showSeparators(false) + , m_showTopLevelItems(false) + , m_appletInterface(nullptr) + , m_autoPopulate(true) + , m_description(i18n("Applications")) + , m_entryPath(QString()) + , m_staticEntryList(true) + , m_changeTimer(nullptr) + , m_flat(true) + , m_sorted(true) + , m_appNameFormat(AppEntry::NameOnly) +{ + for (AbstractEntry *suggestedEntry : entryList) { + const auto sameStorageId = [=](const AbstractEntry *entry) { + return entry->type() == AbstractEntry::RunnableType + && static_cast(entry)->service()->storageId() == static_cast(suggestedEntry)->service()->storageId(); + }; + + const bool found = std::find_if(m_entryList.cbegin(), m_entryList.cend(), sameStorageId) != m_entryList.cend(); + + if (!found) { + m_entryList << suggestedEntry; + } + } + + sortEntries(); +} + +AppsModel::~AppsModel() +{ + if (m_deleteEntriesOnDestruction) { + qDeleteAll(m_entryList); + } +} + +bool AppsModel::autoPopulate() const +{ + return m_autoPopulate; +} + +void AppsModel::setAutoPopulate(bool populate) +{ + if (m_autoPopulate != populate) { + m_autoPopulate = populate; + + Q_EMIT autoPopulateChanged(); + } +} + +QString AppsModel::description() const +{ + return m_description; +} + +void AppsModel::setDescription(const QString &text) +{ + if (m_description != text) { + m_description = text; + + Q_EMIT descriptionChanged(); + } +} + +QVariant AppsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_entryList.count()) { + return QVariant(); + } + + const AbstractEntry *entry = m_entryList.at(index.row()); + + if (role == Qt::DisplayRole) { + return entry->name(); + } else if (role == Qt::DecorationRole) { + return entry->icon(); + } else if (role == Kicker::DescriptionRole) { + return entry->description(); + } else if (role == Kicker::FavoriteIdRole && entry->type() == AbstractEntry::RunnableType) { + return entry->id(); + } else if (role == Kicker::UrlRole && entry->type() == AbstractEntry::RunnableType) { + return entry->url(); + } else if (role == Kicker::IsParentRole) { + return (entry->type() == AbstractEntry::GroupType); + } else if (role == Kicker::IsSeparatorRole) { + return (entry->type() == AbstractEntry::SeparatorType); + } else if (role == Kicker::HasChildrenRole) { + return entry->hasChildren(); + } else if (role == Kicker::HasActionListRole) { + const AppsModel *appsModel = qobject_cast(entry->childModel()); + + return entry->hasActions() || (appsModel && !appsModel->hiddenEntries().isEmpty()); + } else if (role == Kicker::ActionListRole) { + QVariantList actionList = entry->actions(); + + if (!m_hiddenEntries.isEmpty()) { + actionList << Kicker::createSeparatorActionItem(); + QVariantMap unhideSiblingApplicationsAction = Kicker::createActionItem(i18n("Unhide Applications in this Submenu"), + QStringLiteral("view-visible"), + QStringLiteral("unhideSiblingApplications")); + actionList << unhideSiblingApplicationsAction; + } + + const AppsModel *appsModel = qobject_cast(entry->childModel()); + + if (appsModel && !appsModel->hiddenEntries().isEmpty()) { + QVariantMap unhideChildApplicationsAction = Kicker::createActionItem(i18n("Unhide Applications in '%1'", entry->name()), + QStringLiteral("view-visible"), + QStringLiteral("unhideChildApplications")); + actionList << unhideChildApplicationsAction; + } + + return actionList; + } + + return QVariant(); +} + +QModelIndex AppsModel::index(int row, int column, const QModelIndex &parent) const +{ + return hasIndex(row, column, parent) ? createIndex(row, column, m_entryList.at(row)) : QModelIndex(); +} + +int AppsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_entryList.count(); +} + +bool AppsModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (row < 0 || row >= m_entryList.count()) { + return false; + } + + AbstractEntry *entry = m_entryList.at(row); + + if (actionId == QLatin1String("hideApplication") && entry->type() == AbstractEntry::RunnableType) { + QObject *appletInterface = rootModel()->property("appletInterface").value(); + QQmlPropertyMap *appletConfig = nullptr; + if (appletInterface) { + appletConfig = qobject_cast(appletInterface->property("configuration").value()); + } + + if (appletConfig && appletConfig->contains(QLatin1String("hiddenApplications"))) { + QStringList hiddenApps = appletConfig->value(QLatin1String("hiddenApplications")).toStringList(); + + KService::Ptr service = static_cast(entry)->service(); + + if (!hiddenApps.contains(service->menuId())) { + hiddenApps << service->menuId(); + + appletConfig->insert(QLatin1String("hiddenApplications"), hiddenApps); + QMetaObject::invokeMethod(appletConfig, + "valueChanged", + Qt::DirectConnection, + Q_ARG(QString, QStringLiteral("hiddenApplications")), + Q_ARG(QVariant, hiddenApps)); + + refresh(); + + Q_EMIT hiddenEntriesChanged(); + } + } + + return false; + } else if (actionId == QLatin1String("unhideSiblingApplications")) { + QObject *appletInterface = rootModel()->property("appletInterface").value(); + QQmlPropertyMap *appletConfig = nullptr; + if (appletInterface) { + appletConfig = qobject_cast(appletInterface->property("configuration").value()); + } + + if (appletConfig && appletConfig->contains(QLatin1String("hiddenApplications"))) { + QStringList hiddenApps = appletConfig->value(QLatin1String("hiddenApplications")).toStringList(); + + for (const QString &app : std::as_const(m_hiddenEntries)) { + hiddenApps.removeOne(app); + } + + appletConfig->insert(QStringLiteral("hiddenApplications"), hiddenApps); + QMetaObject::invokeMethod(appletConfig, + "valueChanged", + Qt::DirectConnection, + Q_ARG(QString, QStringLiteral("hiddenApplications")), + Q_ARG(QVariant, hiddenApps)); + + m_hiddenEntries.clear(); + + refresh(); + + Q_EMIT hiddenEntriesChanged(); + } + + return false; + } else if (actionId == QLatin1String("unhideChildApplications")) { + QObject *appletInterface = rootModel()->property("appletInterface").value(); + QQmlPropertyMap *appletConfig = nullptr; + if (appletInterface) { + appletConfig = qobject_cast(appletInterface->property("configuration").value()); + } + + if (entry->type() == AbstractEntry::GroupType && appletConfig && appletConfig->contains(QLatin1String("hiddenApplications"))) { + const AppsModel *appsModel = qobject_cast(entry->childModel()); + + if (!appsModel) { + return false; + } + + QStringList hiddenApps = appletConfig->value(QLatin1String("hiddenApplications")).toStringList(); + + const QStringList hiddenEntries = appsModel->hiddenEntries(); + for (const QString &app : hiddenEntries) { + hiddenApps.removeOne(app); + } + + appletConfig->insert(QStringLiteral("hiddenApplications"), hiddenApps); + QMetaObject::invokeMethod(appletConfig, + "valueChanged", + Qt::DirectConnection, + Q_ARG(QString, QStringLiteral("hiddenApplications")), + Q_ARG(QVariant, hiddenApps)); + + refresh(); + + Q_EMIT hiddenEntriesChanged(); + } + + return false; + } + + return entry->run(actionId, argument); +} + +AbstractModel *AppsModel::modelForRow(int row) +{ + if (row < 0 || row >= m_entryList.count()) { + return nullptr; + } + + return m_entryList.at(row)->childModel(); +} + +int AppsModel::rowForModel(AbstractModel *model) +{ + for (int i = 0; i < m_entryList.count(); ++i) { + if (m_entryList.at(i)->childModel() == model) { + return i; + } + } + + return -1; +} + +int AppsModel::separatorCount() const +{ + return m_separatorCount; +} + +bool AppsModel::paginate() const +{ + return m_paginate; +} + +void AppsModel::setPaginate(bool paginate) +{ + if (m_paginate != paginate) { + m_paginate = paginate; + + refresh(); + + Q_EMIT paginateChanged(); + } +} + +int AppsModel::pageSize() const +{ + return m_pageSize; +} + +void AppsModel::setPageSize(int size) +{ + if (m_pageSize != size) { + m_pageSize = size; + + refresh(); + + Q_EMIT pageSizeChanged(); + } +} + +bool AppsModel::flat() const +{ + return m_flat; +} + +void AppsModel::setFlat(bool flat) +{ + if (m_flat != flat) { + m_flat = flat; + + refresh(); + + Q_EMIT flatChanged(); + } +} + +bool AppsModel::sorted() const +{ + return m_sorted; +} + +void AppsModel::setSorted(bool sorted) +{ + if (m_sorted != sorted) { + m_sorted = sorted; + + refresh(); + + Q_EMIT sortedChanged(); + } +} + +bool AppsModel::showSeparators() const +{ + return m_showSeparators; +} + +void AppsModel::setShowSeparators(bool showSeparators) +{ + if (m_showSeparators != showSeparators) { + m_showSeparators = showSeparators; + + refresh(); + + Q_EMIT showSeparatorsChanged(); + } +} + +bool AppsModel::showTopLevelItems() const +{ + return m_showTopLevelItems; +} + +void AppsModel::setShowTopLevelItems(bool showTopLevelItems) +{ + if (m_showTopLevelItems != showTopLevelItems) { + m_showTopLevelItems = showTopLevelItems; + + refresh(); + + Q_EMIT showTopLevelItemsChanged(); + } +} + +int AppsModel::appNameFormat() const +{ + return m_appNameFormat; +} + +void AppsModel::setAppNameFormat(int format) +{ + if (m_appNameFormat != (AppEntry::NameFormat)format) { + m_appNameFormat = (AppEntry::NameFormat)format; + + refresh(); + + Q_EMIT appNameFormatChanged(); + } +} + +QObject *AppsModel::appletInterface() const +{ + return m_appletInterface; +} + +void AppsModel::setAppletInterface(QObject *appletInterface) +{ + if (m_appletInterface != appletInterface) { + m_appletInterface = appletInterface; + + refresh(); + + Q_EMIT appletInterfaceChanged(); + } +} + +QStringList AppsModel::hiddenEntries() const +{ + return m_hiddenEntries; +} + +void AppsModel::refresh() +{ + if (!m_complete) { + return; + } + + if (m_staticEntryList) { + return; + } + + if (rootModel() == this && !m_appletInterface) { + return; + } + + beginResetModel(); + + refreshInternal(); + + endResetModel(); + + if (favoritesModel()) { + favoritesModel()->refresh(); + } + + Q_EMIT countChanged(); + Q_EMIT separatorCountChanged(); +} + +static bool containsSameStorageId(const QList &entryList, KService::Ptr service) +{ + return std::any_of(entryList.cbegin(), entryList.cend(), [=](const AbstractEntry *entry) { + return entry->type() == AbstractEntry::RunnableType && static_cast(entry)->service()->storageId() == service->storageId(); + }); +} + +void AppsModel::refreshInternal() +{ + if (m_staticEntryList) { + return; + } + + if (m_entryList.count()) { + qDeleteAll(m_entryList); + m_entryList.clear(); + Q_EMIT cleared(); + } + + m_hiddenEntries.clear(); + m_separatorCount = 0; + + if (m_entryPath.isEmpty()) { + KServiceGroup::Ptr group = KServiceGroup::root(); + if (!group) { + return; + } + + bool sortByGenericName = (appNameFormat() == AppEntry::GenericNameOnly || appNameFormat() == AppEntry::GenericNameAndName); + + KServiceGroup::List list = + group->entries(true /* sorted */, true /* excludeNoDisplay */, true /* allowSeparators */, sortByGenericName /* sortByGenericName */); + + for (KServiceGroup::List::ConstIterator it = list.constBegin(); it != list.constEnd(); it++) { + const KSycocaEntry::Ptr p = (*it); + + if (p->isType(KST_KServiceGroup)) { + KServiceGroup::Ptr subGroup(static_cast(p.data())); + + if (!subGroup->noDisplay() && subGroup->childCount() > 0) { + AppGroupEntry *groupEntry = new AppGroupEntry(this, subGroup, m_paginate, m_pageSize, m_flat, m_sorted, m_showSeparators, m_appNameFormat); + m_entryList << groupEntry; + } + } else if (p->isType(KST_KService) && m_showTopLevelItems) { + const KService::Ptr service(static_cast(p.data())); + + if (service->noDisplay()) { + continue; + } + + if (!containsSameStorageId(m_entryList, service)) { + m_entryList << new AppEntry(this, service, m_appNameFormat); + } + } else if (p->isType(KST_KServiceSeparator) && m_showSeparators && m_showTopLevelItems) { + if (!m_entryList.count()) { + continue; + } + + if (m_entryList.last()->type() == AbstractEntry::SeparatorType) { + continue; + } + + m_entryList << new SeparatorEntry(this); + ++m_separatorCount; + } + } + + if (m_entryList.count()) { + while (m_entryList.last()->type() == AbstractEntry::SeparatorType) { + m_entryList.removeLast(); + --m_separatorCount; + } + } + + if (m_sorted) { + sortEntries(); + } + + m_changeTimer = new QTimer(this); + m_changeTimer->setSingleShot(true); + m_changeTimer->setInterval(100); + connect(m_changeTimer, SIGNAL(timeout()), this, SLOT(refresh())); + + connect(KSycoca::self(), &KSycoca::databaseChanged, this, [this]() { + m_changeTimer->start(); + }); + } else { + KServiceGroup::Ptr group = KServiceGroup::group(m_entryPath); + processServiceGroup(group); + + if (m_entryList.count()) { + while (m_entryList.last()->type() == AbstractEntry::SeparatorType) { + m_entryList.removeLast(); + --m_separatorCount; + } + } + + if (m_sorted) { + sortEntries(); + } + + if (m_paginate) { + QList groups; + + int at = 0; + QList page; + + for (AbstractEntry *app : std::as_const(m_entryList)) { + page.append(app); + + if (at == (m_pageSize - 1)) { + at = 0; + AppsModel *model = new AppsModel(page, true, this); + groups.append(new GroupEntry(this, QString(), QString(), model)); + page.clear(); + } else { + ++at; + } + } + + if (page.count()) { + AppsModel *model = new AppsModel(page, true, this); + groups.append(new GroupEntry(this, QString(), QString(), model)); + } + + m_entryList = groups; + } + } +} + +void AppsModel::processServiceGroup(KServiceGroup::Ptr group) +{ + if (!group || !group->isValid()) { + return; + } + + bool hasSubGroups = false; + + const QList groupEntries = group->groupEntries(KServiceGroup::ExcludeNoDisplay); + for (KServiceGroup::Ptr subGroup : groupEntries) { + if (subGroup->childCount() > 0) { + hasSubGroups = true; + + break; + } + } + + bool sortByGenericName = (appNameFormat() == AppEntry::GenericNameOnly || appNameFormat() == AppEntry::GenericNameAndName); + + KServiceGroup::List list = group->entries(true /* sorted */, + true /* excludeNoDisplay */, + (!m_flat || (m_flat && !hasSubGroups)) /* allowSeparators */, + sortByGenericName /* sortByGenericName */); + + QStringList hiddenApps; + + QObject *appletInterface = rootModel()->property("appletInterface").value(); + QQmlPropertyMap *appletConfig = nullptr; + if (appletInterface) { + appletConfig = qobject_cast(appletInterface->property("configuration").value()); + } + if (appletConfig && appletConfig->contains(QLatin1String("hiddenApplications"))) { + hiddenApps = appletConfig->value(QLatin1String("hiddenApplications")).toStringList(); + } + + for (KServiceGroup::List::ConstIterator it = list.constBegin(); it != list.constEnd(); it++) { + const KSycocaEntry::Ptr p = (*it); + + if (p->isType(KST_KService)) { + const KService::Ptr service(static_cast(p.data())); + + if (service->noDisplay()) { + continue; + } + + if (hiddenApps.contains(service->menuId())) { + m_hiddenEntries << service->menuId(); + + continue; + } + + if (!containsSameStorageId(m_entryList, service)) { + m_entryList << new AppEntry(this, service, m_appNameFormat); + } + } else if (p->isType(KST_KServiceSeparator) && m_showSeparators) { + if (!m_entryList.count()) { + continue; + } + + if (m_entryList.last()->type() == AbstractEntry::SeparatorType) { + continue; + } + + m_entryList << new SeparatorEntry(this); + ++m_separatorCount; + } else if (p->isType(KST_KServiceGroup)) { + const KServiceGroup::Ptr subGroup(static_cast(p.data())); + + if (subGroup->childCount() == 0) { + continue; + } + + if (m_flat) { + m_sorted = true; + const KServiceGroup::Ptr serviceGroup(static_cast(p.data())); + processServiceGroup(serviceGroup); + } else { + AppGroupEntry *groupEntry = new AppGroupEntry(this, subGroup, m_paginate, m_pageSize, m_flat, m_sorted, m_showSeparators, m_appNameFormat); + m_entryList << groupEntry; + } + } + } +} + +void AppsModel::sortEntries() +{ + QCollator c; + + std::sort(m_entryList.begin(), m_entryList.end(), [&c](AbstractEntry *a, AbstractEntry *b) { + if (a->type() != b->type()) { + return a->type() > b->type(); + } else { + return c.compare(a->name(), b->name()) < 0; + } + }); +} + +void AppsModel::entryChanged(AbstractEntry *entry) +{ + int i = m_entryList.indexOf(entry); + + if (i != -1) { + QModelIndex idx = index(i, 0); + Q_EMIT dataChanged(idx, idx); + } +} + +void AppsModel::classBegin() +{ +} + +void AppsModel::componentComplete() +{ + m_complete = true; + + if (m_autoPopulate) { + refresh(); + } +} diff --git a/plasma/workspace/applets/kicker/plugin/appsmodel.h b/plasma/workspace/applets/kicker/plugin/appsmodel.h new file mode 100644 index 0000000000..f6989b515d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/appsmodel.h @@ -0,0 +1,143 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2013-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" +#include "appentry.h" + +#include + +#include + +class QTimer; + +class AppsModel : public AbstractModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(bool autoPopulate READ autoPopulate WRITE setAutoPopulate NOTIFY autoPopulateChanged) + + Q_PROPERTY(bool paginate READ paginate WRITE setPaginate NOTIFY paginateChanged) + Q_PROPERTY(int pageSize READ pageSize WRITE setPageSize NOTIFY pageSizeChanged) + Q_PROPERTY(bool flat READ flat WRITE setFlat NOTIFY flatChanged) + Q_PROPERTY(bool sorted READ sorted WRITE setSorted NOTIFY sortedChanged) + Q_PROPERTY(bool showSeparators READ showSeparators WRITE setShowSeparators NOTIFY showSeparatorsChanged) + Q_PROPERTY(bool showTopLevelItems READ showTopLevelItems WRITE setShowTopLevelItems NOTIFY showTopLevelItemsChanged) + Q_PROPERTY(int appNameFormat READ appNameFormat WRITE setAppNameFormat NOTIFY appNameFormatChanged) + Q_PROPERTY(QObject *appletInterface READ appletInterface WRITE setAppletInterface NOTIFY appletInterfaceChanged) + +public: + explicit AppsModel(const QString &entryPath = QString(), + bool paginate = false, + int pageSize = 24, + bool flat = false, + bool sorted = true, + bool separators = true, + QObject *parent = nullptr); + explicit AppsModel(const QList entryList, bool deleteEntriesOnDestruction, QObject *parent = nullptr); + ~AppsModel() override; + + QString description() const override; + void setDescription(const QString &text); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool autoPopulate() const; + void setAutoPopulate(bool populate); + + Q_INVOKABLE AbstractModel *modelForRow(int row) override; + Q_INVOKABLE int rowForModel(AbstractModel *model) override; + + int separatorCount() const override; + + bool paginate() const; + void setPaginate(bool paginate); + + int pageSize() const; + void setPageSize(int size); + + bool flat() const; + void setFlat(bool flat); + + bool sorted() const; + void setSorted(bool sorted); + + bool showSeparators() const; + void setShowSeparators(bool showSeparators); + + bool showTopLevelItems() const; + void setShowTopLevelItems(bool showTopLevelItems); + + int appNameFormat() const; + void setAppNameFormat(int format); + + QObject *appletInterface() const; + void setAppletInterface(QObject *appletInterface); + + QStringList hiddenEntries() const; + + void entryChanged(AbstractEntry *entry) override; + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void cleared() const; + void autoPopulateChanged() const; + void paginateChanged() const; + void pageSizeChanged() const; + void flatChanged() const; + void sortedChanged() const; + void showSeparatorsChanged() const; + void showTopLevelItemsChanged() const; + void appNameFormatChanged() const; + void appletInterfaceChanged() const; + void hiddenEntriesChanged() const; + +protected Q_SLOTS: + void refresh() override; + +protected: + void refreshInternal(); + + bool m_complete; + + bool m_paginate; + int m_pageSize; + + QList m_entryList; + bool m_deleteEntriesOnDestruction; + int m_separatorCount; + bool m_showSeparators; + bool m_showTopLevelItems; + + QObject *m_appletInterface; + +private: + void processServiceGroup(KServiceGroup::Ptr group); + void sortEntries(); + + bool m_autoPopulate; + + QString m_description; + QString m_entryPath; + bool m_staticEntryList; + QTimer *m_changeTimer; + bool m_flat; + bool m_sorted; + AppEntry::NameFormat m_appNameFormat; + QStringList m_hiddenEntries; + static MenuEntryEditor *m_menuEntryEditor; +}; diff --git a/plasma/workspace/applets/kicker/plugin/autotests/CMakeLists.txt b/plasma/workspace/applets/kicker/plugin/autotests/CMakeLists.txt new file mode 100644 index 0000000000..f6fa928093 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/autotests/CMakeLists.txt @@ -0,0 +1,29 @@ +find_package(Qt5QuickTest ${REQUIRED_QT_VERSION} CONFIG QUIET) + +if(NOT Qt5QuickTest_FOUND) + message(STATUS "Qt5QuickTest not found, autotests will not be built.") + return() +endif() + +add_executable(qmltest qmltest.cpp) +target_link_libraries(qmltest Qt5::QuickTest) + +macro(qtquick_add_tests) + if (WIN32) + set(_extra_args -platform offscreen) + endif() + + foreach(test ${ARGV}) + add_test(NAME ${test} + COMMAND qmltest + ${_extra_args} + -import ${CMAKE_BINARY_DIR}/bin + -input ${test}.qml + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endforeach() +endmacro() + +qtquick_add_tests( + tst_triangleFilter +) diff --git a/plasma/workspace/applets/kicker/plugin/autotests/qmltest.cpp b/plasma/workspace/applets/kicker/plugin/autotests/qmltest.cpp new file mode 100644 index 0000000000..71b4a7be62 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/autotests/qmltest.cpp @@ -0,0 +1,8 @@ +/* + SPDX-FileCopyrightText: 2020 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +QUICK_TEST_MAIN(Kicker) diff --git a/plasma/workspace/applets/kicker/plugin/autotests/tst_triangleFilter.qml b/plasma/workspace/applets/kicker/plugin/autotests/tst_triangleFilter.qml new file mode 100644 index 0000000000..2d0599a373 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/autotests/tst_triangleFilter.qml @@ -0,0 +1,70 @@ +import QtQuick 2.12 +import QtTest 1.0 +import org.kde.plasma.private.kicker 0.1 + +Item { + id: root + width: 400 + height: 400 + TriangleMouseFilter { + edge: Qt.RightEdge + // to simulate kicker's options at the bottom + height: 300 + width: 300 + Column { + anchors.fill: parent + MouseArea { + id: item1 + hoverEnabled: true + height: 100 + width: parent.width + } + MouseArea { + id: item2 + hoverEnabled: true + height: 100 + width: parent.width + } + MouseArea { + id: item3 + hoverEnabled: true + height: 100 + width: parent.width + } + } + } + TestCase { + when: windowShown + + name: "Triangle Mouse Filter tests" + + function test_triangle_filter() { + mouseMove(root, 100, 350); // under the list + compare(item3.containsMouse, false); + + mouseMove(root, 100, 290); // enter the last item + // the first entrance is filtered + compare(item3.containsMouse, false); + + // move up slightly + mouseMove(root, 100, 250); // still in the last + // but moved outside the filter triangle, accepted + compare(item3.containsMouse, true); + + // move near the border + mouseMove(root, 100, 205); // still in the last + compare(item3.containsMouse, true); + + // move diagonally into item2, item3 should still get the event + console.log("last"); + mouseMove(root, 290, 195); + //item 3 might not still have containMouse true, as we don't filter leave events + compare(item2.containsMouse, false); + + // but after a timeout it gets the mouse event + wait(500); + compare(item2.containsMouse, true); + } + + } +} diff --git a/plasma/workspace/applets/kicker/plugin/computermodel.cpp b/plasma/workspace/applets/kicker/plugin/computermodel.cpp new file mode 100644 index 0000000000..5daf938e0d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/computermodel.cpp @@ -0,0 +1,287 @@ +/* + SPDX-FileCopyrightText: 2007 Kevin Ottens + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "computermodel.h" +#include "actionlist.h" +#include "simplefavoritesmodel.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "krunner_interface.h" + +FilteredPlacesModel::FilteredPlacesModel(QObject *parent) + : QSortFilterProxyModel(parent) + , m_placesModel(new KFilePlacesModel(this)) +{ + setSourceModel(m_placesModel); + sort(0); +} + +FilteredPlacesModel::~FilteredPlacesModel() +{ +} + +QUrl FilteredPlacesModel::url(const QModelIndex &index) const +{ + return KFilePlacesModel::convertedUrl(m_placesModel->url(mapToSource(index))); +} + +bool FilteredPlacesModel::isDevice(const QModelIndex &index) const +{ + return m_placesModel->isDevice(mapToSource(index)); +} + +Solid::Device FilteredPlacesModel::deviceForIndex(const QModelIndex &index) const +{ + return m_placesModel->deviceForIndex(mapToSource(index)); +} + +bool FilteredPlacesModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const QModelIndex index = m_placesModel->index(sourceRow, 0, sourceParent); + + return !m_placesModel->isHidden(index) && !m_placesModel->data(index, KFilePlacesModel::FixedDeviceRole).toBool(); +} + +bool FilteredPlacesModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + bool lDevice = m_placesModel->isDevice(left); + bool rDevice = m_placesModel->isDevice(right); + + if (lDevice && !rDevice) { + return false; + } else if (!lDevice && rDevice) { + return true; + } + + return (left.row() < right.row()); +} + +RunCommandModel::RunCommandModel(QObject *parent) + : AbstractModel(parent) +{ +} + +RunCommandModel::~RunCommandModel() +{ +} + +QString RunCommandModel::description() const +{ + return QString(); +} + +QVariant RunCommandModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return i18n("Show KRunner"); + } else if (role == Qt::DecorationRole) { + return QIcon::fromTheme(QStringLiteral("plasma-search")); + } else if (role == Kicker::DescriptionRole) { + return i18n("Search, calculate, or run a command"); + } else if (role == Kicker::GroupRole) { + return i18n("Applications"); + } + + return QVariant(); +} + +int RunCommandModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : (KAuthorized::authorize(QStringLiteral("run_command")) ? 1 : 0); +} + +Q_INVOKABLE bool RunCommandModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(actionId) + Q_UNUSED(argument) + + if (row == 0 && KAuthorized::authorize(QStringLiteral("run_command"))) { + org::kde::krunner::App krunner(QStringLiteral("org.kde.krunner"), QStringLiteral("/App"), QDBusConnection::sessionBus()); + krunner.display(); + + return true; + } + + return false; +} + +ComputerModel::ComputerModel(QObject *parent) + : ForwardingModel(parent) + , m_concatProxy(new KConcatenateRowsProxyModel(this)) + , m_runCommandModel(new RunCommandModel(this)) + , m_systemAppsModel(new SimpleFavoritesModel(this)) + , m_filteredPlacesModel(new FilteredPlacesModel(this)) + , m_appNameFormat(AppEntry::NameOnly) + , m_appletInterface(nullptr) +{ + connect(m_systemAppsModel, &SimpleFavoritesModel::favoritesChanged, this, &ComputerModel::systemApplicationsChanged); + m_systemAppsModel->setFavorites(QStringList() << QStringLiteral("systemsettings.desktop")); + + m_concatProxy->addSourceModel(m_runCommandModel); + m_concatProxy->addSourceModel(m_systemAppsModel); + m_concatProxy->addSourceModel(m_filteredPlacesModel); + + setSourceModel(m_concatProxy); +} + +ComputerModel::~ComputerModel() +{ +} + +QString ComputerModel::description() const +{ + return i18n("Computer"); +} + +int ComputerModel::appNameFormat() const +{ + return m_appNameFormat; +} + +void ComputerModel::setAppNameFormat(int format) +{ + if (m_appNameFormat != (AppEntry::NameFormat)format) { + m_appNameFormat = (AppEntry::NameFormat)format; + + m_systemAppsModel->refresh(); + + Q_EMIT appNameFormatChanged(); + } +} + +QObject *ComputerModel::appletInterface() const +{ + return m_appletInterface; +} + +void ComputerModel::setAppletInterface(QObject *appletInterface) +{ + if (m_appletInterface != appletInterface) { + m_appletInterface = appletInterface; + + Q_EMIT appletInterfaceChanged(); + } +} + +QStringList ComputerModel::systemApplications() const +{ + return m_systemAppsModel->favorites(); +} + +void ComputerModel::setSystemApplications(const QStringList &apps) +{ + m_systemAppsModel->setFavorites(apps); +} + +QVariant ComputerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + const QModelIndex sourceIndex = m_concatProxy->mapToSource(m_concatProxy->index(index.row(), index.column())); + + bool isPlace = (sourceIndex.model() == m_filteredPlacesModel); + + if (isPlace) { + if (role == Kicker::DescriptionRole) { + if (m_filteredPlacesModel->isDevice(sourceIndex)) { + Solid::Device device = m_filteredPlacesModel->deviceForIndex(sourceIndex); + Solid::StorageAccess *access = device.as(); + + if (access) { + return access->filePath(); + } else { + return QString(); + } + } + } else if (role == Kicker::FavoriteIdRole) { + if (!m_filteredPlacesModel->isDevice(sourceIndex)) { + return m_filteredPlacesModel->url(sourceIndex); + } + } else if (role == Kicker::UrlRole) { + return m_filteredPlacesModel->url(sourceIndex); + } else if (role == Kicker::GroupRole) { + return sourceIndex.data(KFilePlacesModel::GroupRole).toString(); + } else if (role == Qt::DisplayRole || role == Qt::DecorationRole) { + return sourceIndex.data(role); + } + } else if (role == Kicker::GroupRole) { + return i18n("Applications"); + } else { + return sourceIndex.data(role); + } + + return QVariant(); +} + +bool ComputerModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + const QModelIndex sourceIndex = m_concatProxy->mapToSource(m_concatProxy->index(row, 0)); + + if (sourceIndex.model() == m_filteredPlacesModel) { + const QUrl &url = m_filteredPlacesModel->url(sourceIndex); + + if (url.isValid()) { + auto job = new KIO::OpenUrlJob(url); + job->start(); + + return true; + } + + Solid::Device device = m_filteredPlacesModel->deviceForIndex(sourceIndex); + Solid::StorageAccess *access = device.as(); + + if (access && !access->isAccessible()) { + connect(access, &Solid::StorageAccess::setupDone, this, &ComputerModel::onSetupDone); + access->setup(); + + return true; + } + } else { + AbstractModel *model = nullptr; + + if (sourceIndex.model() == m_systemAppsModel) { + model = m_systemAppsModel; + } else { + model = m_runCommandModel; + } + + return model->trigger(sourceIndex.row(), actionId, argument); + } + + return false; +} + +void ComputerModel::onSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi) +{ + Q_UNUSED(errorData); + + if (error != Solid::NoError) { + return; + } + + Solid::Device device(udi); + Solid::StorageAccess *access = device.as(); + + Q_ASSERT(access); + + auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(access->filePath())); + job->start(); +} diff --git a/plasma/workspace/applets/kicker/plugin/computermodel.h b/plasma/workspace/applets/kicker/plugin/computermodel.h new file mode 100644 index 0000000000..c25297e14b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/computermodel.h @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "appentry.h" +#include "forwardingmodel.h" + +#include +#include + +class SimpleFavoritesModel; + +class KConcatenateRowsProxyModel; +class KFilePlacesModel; + +namespace Solid +{ +class Device; +} + +class FilteredPlacesModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit FilteredPlacesModel(QObject *parent = nullptr); + ~FilteredPlacesModel() override; + + QUrl url(const QModelIndex &index) const; + bool isDevice(const QModelIndex &index) const; + Solid::Device deviceForIndex(const QModelIndex &index) const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + KFilePlacesModel *m_placesModel; +}; + +class RunCommandModel : public AbstractModel +{ + Q_OBJECT + +public: + RunCommandModel(QObject *parent = nullptr); + ~RunCommandModel() override; + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; +}; + +class ComputerModel : public ForwardingModel +{ + Q_OBJECT + + Q_PROPERTY(int appNameFormat READ appNameFormat WRITE setAppNameFormat NOTIFY appNameFormatChanged) + Q_PROPERTY(QObject *appletInterface READ appletInterface WRITE setAppletInterface NOTIFY appletInterfaceChanged) + Q_PROPERTY(QStringList systemApplications READ systemApplications WRITE setSystemApplications NOTIFY systemApplicationsChanged) + +public: + explicit ComputerModel(QObject *parent = nullptr); + ~ComputerModel() override; + + QString description() const override; + + int appNameFormat() const; + void setAppNameFormat(int format); + + QObject *appletInterface() const; + void setAppletInterface(QObject *appletInterface); + + QStringList systemApplications() const; + void setSystemApplications(const QStringList &apps); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + +Q_SIGNALS: + void appNameFormatChanged() const; + void appletInterfaceChanged() const; + void systemApplicationsChanged() const; + +private Q_SLOTS: + void onSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi); + +private: + KConcatenateRowsProxyModel *m_concatProxy; + RunCommandModel *m_runCommandModel; + SimpleFavoritesModel *m_systemAppsModel; + FilteredPlacesModel *m_filteredPlacesModel; + AppEntry::NameFormat m_appNameFormat; + QObject *m_appletInterface; +}; diff --git a/plasma/workspace/applets/kicker/plugin/contactentry.cpp b/plasma/workspace/applets/kicker/plugin/contactentry.cpp new file mode 100644 index 0000000000..acc074f606 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/contactentry.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "contactentry.h" +#include "actionlist.h" + +#include +#include + +#include +#include + +#include +#include +#include + +ContactEntry::ContactEntry(AbstractModel *owner, const QString &id) + : AbstractEntry(owner) + , m_personData(nullptr) +{ + if (!id.isEmpty()) { + m_personData = new KPeople::PersonData(id); + + QObject::connect(m_personData, &KPeople::PersonData::dataChanged, [this] { + if (m_owner) { + m_owner->entryChanged(this); + } + }); + } +} + +bool ContactEntry::isValid() const +{ + return m_personData; +} + +QIcon ContactEntry::icon() const +{ + if (m_personData) { + QPixmap photo = m_personData->photo(); + QBitmap mask(photo.size()); + QPainter painter(&mask); + mask.fill(Qt::white); + painter.setBrush(Qt::black); + painter.drawEllipse(0, 0, mask.width(), mask.height()); + photo.setMask(mask); + + photo = photo.scaled(m_owner->iconSize(), m_owner->iconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + KIconLoader::global()->drawOverlays(QStringList() << m_personData->presenceIconName(), photo, KIconLoader::Panel); + + return QIcon(photo); + } + + return QIcon::fromTheme(QStringLiteral("unknown")); +} + +QString ContactEntry::name() const +{ + if (m_personData) { + return m_personData->name(); + } + + return QString(); +} + +QString ContactEntry::id() const +{ + if (m_personData) { + const QString &id = m_personData->personUri(); + + if (id.isEmpty()) { + const QStringList uris = m_personData->contactUris(); + + if (!uris.isEmpty()) { + return uris.at(0); + } + } else { + return id; + } + } + + return QString(); +} + +QUrl ContactEntry::url() const +{ + if (m_personData) { + return QUrl(m_personData->personUri()); + } + + return QUrl(); +} + +bool ContactEntry::hasActions() const +{ + return m_personData; +} + +QVariantList ContactEntry::actions() const +{ + QVariantList actionList; + + actionList << Kicker::createActionItem(i18n("Show Contact Information…"), QStringLiteral("identity"), QStringLiteral("showContactInfo")); + + return actionList; +} + +bool ContactEntry::run(const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(argument) + + if (!m_personData) { + return false; + } + + if (actionId == QLatin1String("showContactInfo")) { + showPersonDetailsDialog(m_personData->personUri()); + } + + return false; +} + +void ContactEntry::showPersonDetailsDialog(const QString &id) +{ + KPeople::PersonDetailsDialog *view = new KPeople::PersonDetailsDialog(nullptr); + KPeople::PersonData *data = new KPeople::PersonData(id, view); + view->setPerson(data); + view->setAttribute(Qt::WA_DeleteOnClose); + view->show(); +} diff --git a/plasma/workspace/applets/kicker/plugin/contactentry.h b/plasma/workspace/applets/kicker/plugin/contactentry.h new file mode 100644 index 0000000000..5072a518bf --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/contactentry.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractentry.h" + +namespace KPeople +{ +class PersonData; +} + +class ContactEntry : public AbstractEntry +{ +public: + explicit ContactEntry(AbstractModel *owner, const QString &id); + + EntryType type() const override + { + return RunnableType; + } + + bool isValid() const override; + + QIcon icon() const override; + QString name() const override; + + QString id() const override; + QUrl url() const override; + + bool hasActions() const override; + QVariantList actions() const override; + + bool run(const QString &actionId = QString(), const QVariant &argument = QVariant()) override; + + static void showPersonDetailsDialog(const QString &id); + +private: + KPeople::PersonData *m_personData; +}; diff --git a/plasma/workspace/applets/kicker/plugin/containmentinterface.cpp b/plasma/workspace/applets/kicker/plugin/containmentinterface.cpp new file mode 100644 index 0000000000..23bcea67e9 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/containmentinterface.cpp @@ -0,0 +1,239 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "containmentinterface.h" + +#include +#include +#include + +#include + +#include + +// FIXME HACK TODO: Unfortunately we have no choice but to hard-code a list of +// applets we know to expose the correct interface right now -- this is slated +// for replacement with some form of generic service. +QStringList ContainmentInterface::m_knownTaskManagers{ + QLatin1String("org.kde.plasma.taskmanager"), + QLatin1String("org.kde.plasma.icontasks"), + QLatin1String("org.kde.plasma.expandingiconstaskmanager"), +}; + +ContainmentInterface::ContainmentInterface(QObject *parent) + : QObject(parent) +{ +} + +ContainmentInterface::~ContainmentInterface() +{ +} + +bool ContainmentInterface::mayAddLauncher(QObject *appletInterface, ContainmentInterface::Target target, const QString &entryPath) +{ + if (!appletInterface) { + return false; + } + + Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); + Plasma::Containment *containment = applet->containment(); + + if (!containment) { + return false; + } + + Plasma::Corona *corona = containment->corona(); + + if (!corona) { + return false; + } + + switch (target) { + case Desktop: { + containment = corona->containmentForScreen(containment->screen(), QString(), QString()); + + if (containment) { + return (containment->immutability() == Plasma::Types::Mutable); + } + + break; + } + case Panel: { + if (containment->pluginMetaData().pluginId() == QLatin1String("org.kde.panel")) { + return (containment->immutability() == Plasma::Types::Mutable); + } + + break; + } + case TaskManager: { + if (!entryPath.isEmpty() && containment->pluginMetaData().pluginId() == QLatin1String("org.kde.panel")) { + const Plasma::Applet *taskManager = findTaskManagerApplet(containment); + + if (!taskManager) { + return false; + } + + QQuickItem *rootItem = firstPlasmaGraphicObjectChild(taskManager); + + if (!rootItem) { + return false; + } + + QVariant ret; + QMetaObject::invokeMethod(rootItem, "hasLauncher", Q_RETURN_ARG(QVariant, ret), Q_ARG(QVariant, QUrl::fromLocalFile(entryPath))); + return !ret.toBool(); + } + + break; + } + } + + return false; +} + +void ContainmentInterface::addLauncher(QObject *appletInterface, ContainmentInterface::Target target, const QString &entryPath) +{ + if (!appletInterface) { + return; + } + + Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); + Plasma::Containment *containment = applet->containment(); + + if (!containment) { + return; + } + + Plasma::Corona *corona = containment->corona(); + + if (!corona) { + return; + } + + switch (target) { + case Desktop: { + containment = corona->containmentForScreen(containment->screen(), QString(), QString()); + + if (!containment) { + return; + } + + const QStringList &containmentProvides = containment->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList()); + + if (containmentProvides.contains(QLatin1String("org.kde.plasma.filemanagement"))) { + QQuickItem *rootItem = findPlasmaGraphicObjectChildIf(containment, [](QQuickItem *item) { + return item->objectName() == QLatin1String("folder"); + }); + + if (!rootItem) { + return; + } + + QMetaObject::invokeMethod(rootItem, "addLauncher", Q_ARG(QVariant, QUrl::fromLocalFile(entryPath))); + } else { + containment->createApplet(QStringLiteral("org.kde.plasma.icon"), QVariantList() << entryPath); + } + + break; + } + case Panel: { + if (containment->pluginMetaData().pluginId() == QLatin1String("org.kde.panel")) { + containment->createApplet(QStringLiteral("org.kde.plasma.icon"), QVariantList() << entryPath); + } + + break; + } + case TaskManager: { + if (containment->pluginMetaData().pluginId() == QLatin1String("org.kde.panel")) { + const Plasma::Applet *taskManager = findTaskManagerApplet(containment); + + if (!taskManager) { + return; + } + + QQuickItem *rootItem = firstPlasmaGraphicObjectChild(taskManager); + + if (!rootItem) { + return; + } + + QMetaObject::invokeMethod(rootItem, "addLauncher", Q_ARG(QVariant, QUrl::fromLocalFile(entryPath))); + } + + break; + } + } +} + +QObject *ContainmentInterface::screenContainment(QObject *appletInterface) +{ + if (!appletInterface) { + return nullptr; + } + + const Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); + Plasma::Containment *containment = applet->containment(); + + if (!containment) { + return nullptr; + } + + Plasma::Corona *corona = containment->corona(); + + if (!corona) { + return nullptr; + } + + return corona->containmentForScreen(containment->screen(), QString(), QString()); +} + +bool ContainmentInterface::screenContainmentMutable(QObject *appletInterface) +{ + const Plasma::Containment *containment = static_cast(screenContainment(appletInterface)); + + if (containment) { + return (containment->immutability() == Plasma::Types::Mutable); + } + + return false; +} + +void ContainmentInterface::ensureMutable(Plasma::Containment *containment) +{ + if (containment && containment->immutability() != Plasma::Types::Mutable) { + containment->actions()->action(QStringLiteral("lock widgets"))->trigger(); + } +} + +template +QQuickItem *ContainmentInterface::findPlasmaGraphicObjectChildIf(const Plasma::Applet *applet, UnaryPredicate predicate) +{ + QQuickItem *gObj = qobject_cast(applet->property("_plasma_graphicObject").value()); + + if (!gObj) { + return nullptr; + } + + const QList children = gObj->childItems(); + const auto found = std::find_if(children.cbegin(), children.cend(), predicate); + return found != children.cend() ? *found : nullptr; +} + +QQuickItem *ContainmentInterface::firstPlasmaGraphicObjectChild(const Plasma::Applet *applet) +{ + return findPlasmaGraphicObjectChildIf(applet, [](QQuickItem *) { + return true; + }); +} + +Plasma::Applet *ContainmentInterface::findTaskManagerApplet(Plasma::Containment *containment) +{ + const QList applets = containment->applets(); + const auto found = std::find_if(applets.cbegin(), applets.cend(), [](const Plasma::Applet *applet) { + return m_knownTaskManagers.contains(applet->pluginMetaData().pluginId()); + }); + return found != applets.cend() ? *found : nullptr; +} diff --git a/plasma/workspace/applets/kicker/plugin/containmentinterface.h b/plasma/workspace/applets/kicker/plugin/containmentinterface.h new file mode 100644 index 0000000000..b7acd11268 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/containmentinterface.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Plasma +{ +class Applet; +class Containment; +} + +class ContainmentInterface : public QObject +{ + Q_OBJECT + +public: + enum Target { + Desktop = 0, + Panel, + TaskManager, + }; + + Q_ENUM(Target) + + explicit ContainmentInterface(QObject *parent = nullptr); + ~ContainmentInterface() override; + + static Q_INVOKABLE bool mayAddLauncher(QObject *appletInterface, Target target, const QString &entryPath = QString()); + + static Q_INVOKABLE void addLauncher(QObject *appletInterface, Target target, const QString &entryPath); + + static Q_INVOKABLE QObject *screenContainment(QObject *appletInterface); + static Q_INVOKABLE bool screenContainmentMutable(QObject *appletInterface); + static Q_INVOKABLE void ensureMutable(Plasma::Containment *containment); + +private: + template + static QQuickItem *findPlasmaGraphicObjectChildIf(const Plasma::Applet *applet, UnaryPredicate predicate); + static QQuickItem *firstPlasmaGraphicObjectChild(const Plasma::Applet *applet); + + static Plasma::Applet *findTaskManagerApplet(Plasma::Containment *containment); + static QStringList m_knownTaskManagers; +}; diff --git a/plasma/workspace/applets/kicker/plugin/dashboardwindow.cpp b/plasma/workspace/applets/kicker/plugin/dashboardwindow.cpp new file mode 100644 index 0000000000..eae5022917 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/dashboardwindow.cpp @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "dashboardwindow.h" + +#include +#include +#include + +#include +#include + +DashboardWindow::DashboardWindow(QQuickItem *parent) + : QQuickWindow(parent ? parent->window() : nullptr) + , m_mainItem(nullptr) + , m_visualParentItem(nullptr) + , m_visualParentWindow(nullptr) +{ + setClearBeforeRendering(true); + setFlags(Qt::FramelessWindowHint); + + setIcon(QIcon::fromTheme(QStringLiteral("plasma"))); + + connect(&m_theme, &Plasma::Theme::themeChanged, this, &DashboardWindow::updateTheme); +} + +DashboardWindow::~DashboardWindow() +{ +} + +QQuickItem *DashboardWindow::mainItem() const +{ + return m_mainItem; +} + +void DashboardWindow::setMainItem(QQuickItem *item) +{ + if (m_mainItem != item) { + if (m_mainItem) { + m_mainItem->setVisible(false); + } + + m_mainItem = item; + + if (m_mainItem) { + m_mainItem->setVisible(isVisible()); + m_mainItem->setParentItem(contentItem()); + } + + Q_EMIT mainItemChanged(); + } +} + +QQuickItem *DashboardWindow::visualParent() const +{ + return m_visualParentItem; +} + +void DashboardWindow::setVisualParent(QQuickItem *item) +{ + if (m_visualParentItem != item) { + if (m_visualParentItem) { + disconnect(m_visualParentItem.data(), &QQuickItem::windowChanged, this, &DashboardWindow::visualParentWindowChanged); + } + + m_visualParentItem = item; + + if (m_visualParentItem) { + if (m_visualParentItem->window()) { + visualParentWindowChanged(m_visualParentItem->window()); + } + + connect(m_visualParentItem.data(), &QQuickItem::windowChanged, this, &DashboardWindow::visualParentWindowChanged); + } + + Q_EMIT visualParentChanged(); + } +} + +QColor DashboardWindow::backgroundColor() const +{ + return color(); +} + +void DashboardWindow::setBackgroundColor(const QColor &c) +{ + if (color() != c) { + setColor(c); + + Q_EMIT backgroundColorChanged(); + } +} + +QQuickItem *DashboardWindow::keyEventProxy() const +{ + return m_keyEventProxy; +} + +void DashboardWindow::setKeyEventProxy(QQuickItem *item) +{ + if (m_keyEventProxy != item) { + m_keyEventProxy = item; + + Q_EMIT keyEventProxyChanged(); + } +} + +void DashboardWindow::toggle() +{ + if (isVisible()) { + close(); + } else { + resize(screen()->size()); + showFullScreen(); + KWindowSystem::forceActiveWindow(winId()); + } +} + +bool DashboardWindow::event(QEvent *event) +{ + if (event->type() == QEvent::Expose) { + // FIXME TODO: We can remove this once we depend on Qt 5.6.1+. + // See: https://bugreports.qt.io/browse/QTBUG-26978 + KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager); + } else if (event->type() == QEvent::PlatformSurface) { + const QPlatformSurfaceEvent *pSEvent = static_cast(event); + + if (pSEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { + KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager); + } + } else if (event->type() == QEvent::Show) { + updateTheme(); + + if (m_mainItem) { + m_mainItem->setVisible(true); + } + } else if (event->type() == QEvent::Hide) { + if (m_mainItem) { + m_mainItem->setVisible(false); + } + } else if (event->type() == QEvent::FocusOut) { + if (isVisible()) { + KWindowSystem::raiseWindow(winId()); + KWindowSystem::forceActiveWindow(winId()); + } + } + + return QQuickWindow::event(event); +} + +void DashboardWindow::keyPressEvent(QKeyEvent *e) +{ + if (e->key() == Qt::Key_Escape) { + Q_EMIT keyEscapePressed(); + + return; + // clang-format off + } else if (m_keyEventProxy && !m_keyEventProxy->hasActiveFocus() + && !(e->key() == Qt::Key_Home) + && !(e->key() == Qt::Key_End) + && !(e->key() == Qt::Key_Left) + && !(e->key() == Qt::Key_Up) + && !(e->key() == Qt::Key_Right) + && !(e->key() == Qt::Key_Down) + && !(e->key() == Qt::Key_PageUp) + && !(e->key() == Qt::Key_PageDown) + && !(e->key() == Qt::Key_Enter) + && !(e->key() == Qt::Key_Return) + && !(e->key() == Qt::Key_Menu) + && !(e->key() == Qt::Key_Tab) + && !(e->key() == Qt::Key_Backtab)) { + // clang-format on + QPointer previousFocusItem = activeFocusItem(); + + m_keyEventProxy->forceActiveFocus(); + QEvent *eventCopy = new QKeyEvent(e->type(), + e->key(), + e->modifiers(), + e->nativeScanCode(), + e->nativeVirtualKey(), + e->nativeModifiers(), + e->text(), + e->isAutoRepeat(), + e->count()); + QCoreApplication::postEvent(this, eventCopy); + + // We _need_ to do it twice to make sure the event ping-pong needed + // for delivery happens before we sap focus again. + QCoreApplication::processEvents(); + QCoreApplication::processEvents(); + + if (previousFocusItem) { + previousFocusItem->forceActiveFocus(); + } + + return; + } + + QQuickWindow::keyPressEvent(e); +} + +void DashboardWindow::updateTheme() +{ + KWindowEffects::enableBlurBehind(this, true); +} + +void DashboardWindow::visualParentWindowChanged(QQuickWindow *window) +{ + if (m_visualParentWindow) { + disconnect(m_visualParentWindow.data(), &QQuickWindow::screenChanged, this, &DashboardWindow::visualParentScreenChanged); + } + + m_visualParentWindow = window; + + if (m_visualParentWindow) { + visualParentScreenChanged(m_visualParentWindow->screen()); + + connect(m_visualParentWindow.data(), &QQuickWindow::screenChanged, this, &DashboardWindow::visualParentScreenChanged); + } +} + +void DashboardWindow::visualParentScreenChanged(QScreen *screen) +{ + if (screen) { + setScreen(screen); + setGeometry(screen->geometry()); + } +} diff --git a/plasma/workspace/applets/kicker/plugin/dashboardwindow.h b/plasma/workspace/applets/kicker/plugin/dashboardwindow.h new file mode 100644 index 0000000000..96f4dbcaaa --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/dashboardwindow.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +class DashboardWindow : public QQuickWindow +{ + Q_OBJECT + + Q_PROPERTY(QQuickItem *mainItem READ mainItem WRITE setMainItem NOTIFY mainItemChanged) + Q_PROPERTY(QQuickItem *visualParent READ visualParent WRITE setVisualParent NOTIFY visualParentChanged) + Q_PROPERTY(QQuickItem *keyEventProxy READ keyEventProxy WRITE setKeyEventProxy NOTIFY keyEventProxyChanged) + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged) + + Q_CLASSINFO("DefaultProperty", "mainItem") + +public: + explicit DashboardWindow(QQuickItem *parent = nullptr); + ~DashboardWindow() override; + + QQuickItem *mainItem() const; + void setMainItem(QQuickItem *item); + + QQuickItem *visualParent() const; + void setVisualParent(QQuickItem *item); + + QQuickItem *keyEventProxy() const; + void setKeyEventProxy(QQuickItem *item); + + QColor backgroundColor() const; + void setBackgroundColor(const QColor &color); + + Q_INVOKABLE void toggle(); + +Q_SIGNALS: + void mainItemChanged() const; + void visualParentChanged() const; + void keyEventProxyChanged() const; + void backgroundColorChanged() const; + void keyEscapePressed() const; + +private Q_SLOTS: + void updateTheme(); + void visualParentWindowChanged(QQuickWindow *window); + void visualParentScreenChanged(QScreen *screen); + +protected: + bool event(QEvent *event) override; + void keyPressEvent(QKeyEvent *e) override; + +private: + QQuickItem *m_mainItem; + QPointer m_visualParentItem; + QPointer m_visualParentWindow; + QPointer m_keyEventProxy; + Plasma::Theme m_theme; +}; diff --git a/plasma/workspace/applets/kicker/plugin/draghelper.cpp b/plasma/workspace/applets/kicker/plugin/draghelper.cpp new file mode 100644 index 0000000000..f647f8566b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/draghelper.cpp @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2013 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "draghelper.h" + +#include +#include +#include +#include +#include +#include + +DragHelper::DragHelper(QObject *parent) + : QObject(parent) + , m_dragIconSize(32) + , m_dragging(false) +{ +} + +DragHelper::~DragHelper() +{ +} + +int DragHelper::dragIconSize() const +{ + return m_dragIconSize; +} + +void DragHelper::setDragIconSize(int size) +{ + if (m_dragIconSize != size) { + m_dragIconSize = size; + + Q_EMIT dragIconSizeChanged(); + } +} + +bool DragHelper::isDrag(int oldX, int oldY, int newX, int newY) const +{ + return ((QPoint(oldX, oldY) - QPoint(newX, newY)).manhattanLength() >= QApplication::startDragDistance()); +} + +void DragHelper::startDrag(QQuickItem *item, const QUrl &url, const QIcon &icon, const QString &extraMimeType, const QString &extraMimeData) +{ + // This allows the caller to return, making sure we don't crash if + // the caller is destroyed mid-drag (as can happen due to a sycoca + // change). + + QMetaObject::invokeMethod(this, + "doDrag", + Qt::QueuedConnection, + Q_ARG(QQuickItem *, item), + Q_ARG(QUrl, url), + Q_ARG(QIcon, icon), + Q_ARG(QString, extraMimeType), + Q_ARG(QString, extraMimeData)); +} + +void DragHelper::doDrag(QQuickItem *item, const QUrl &url, const QIcon &icon, const QString &extraMimeType, const QString &extraMimeData) +{ + setDragging(true); + + if (item && item->window() && item->window()->mouseGrabberItem()) { + item->window()->mouseGrabberItem()->ungrabMouse(); + } + + QDrag *drag = new QDrag(item); + + QMimeData *mimeData = new QMimeData(); + + if (!url.isEmpty()) { + mimeData->setUrls(QList() << url); + } + + if (!extraMimeType.isEmpty() && !extraMimeData.isEmpty()) { + mimeData->setData(extraMimeType, extraMimeData.toLatin1()); + } + + drag->setMimeData(mimeData); + + if (!icon.isNull()) { + drag->setPixmap(icon.pixmap(m_dragIconSize, m_dragIconSize)); + } + + drag->exec(); + + Q_EMIT dropped(); + + // Ensure dragging is still true when onRelease is called. + QTimer::singleShot(0, qApp, [this] { + setDragging(false); + }); +} + +void DragHelper::setDragging(bool dragging) +{ + if (m_dragging == dragging) + return; + m_dragging = dragging; + Q_EMIT draggingChanged(); +} diff --git a/plasma/workspace/applets/kicker/plugin/draghelper.h b/plasma/workspace/applets/kicker/plugin/draghelper.h new file mode 100644 index 0000000000..563a7b78ec --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/draghelper.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2013 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class QQuickItem; + +class DragHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(int dragIconSize READ dragIconSize WRITE setDragIconSize NOTIFY dragIconSizeChanged) + Q_PROPERTY(bool dragging READ isDragging NOTIFY draggingChanged) + +public: + explicit DragHelper(QObject *parent = nullptr); + ~DragHelper() override; + + int dragIconSize() const; + void setDragIconSize(int size); + bool isDragging() const + { + return m_dragging; + } + + Q_INVOKABLE bool isDrag(int oldX, int oldY, int newX, int newY) const; + Q_INVOKABLE void startDrag(QQuickItem *item, + const QUrl &url = QUrl(), + const QIcon &icon = QIcon(), + const QString &extraMimeType = QString(), + const QString &extraMimeData = QString()); + +Q_SIGNALS: + void dragIconSizeChanged() const; + void dropped() const; + void draggingChanged() const; + +private: + int m_dragIconSize; + bool m_dragging; + Q_INVOKABLE void doDrag(QQuickItem *item, + const QUrl &url = QUrl(), + const QIcon &icon = QIcon(), + const QString &extraMimeType = QString(), + const QString &extraMimeData = QString()); + void setDragging(bool dragging); +}; diff --git a/plasma/workspace/applets/kicker/plugin/fileentry.cpp b/plasma/workspace/applets/kicker/plugin/fileentry.cpp new file mode 100644 index 0000000000..54d1dbd412 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/fileentry.cpp @@ -0,0 +1,112 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fileentry.h" +#include "actionlist.h" + +#include +#include + +FileEntry::FileEntry(AbstractModel *owner, const QUrl &url) + : AbstractEntry(owner) + , m_fileItem(nullptr) +{ + if (url.isValid()) { + m_fileItem = new KFileItem(url); + m_fileItem->determineMimeType(); + } +} + +FileEntry::~FileEntry() +{ + delete m_fileItem; +} + +bool FileEntry::isValid() const +{ + return m_fileItem && (m_fileItem->isFile() || m_fileItem->isDir()); +} + +QIcon FileEntry::icon() const +{ + if (m_fileItem) { + return QIcon::fromTheme(m_fileItem->iconName(), QIcon::fromTheme(QStringLiteral("unknown"))); + } + + return QIcon::fromTheme(QStringLiteral("unknown")); +} + +QString FileEntry::name() const +{ + if (m_fileItem) { + return m_fileItem->text(); + } + + return QString(); +} + +QString FileEntry::description() const +{ + if (m_fileItem) { + return m_fileItem->url().toString(QUrl::PreferLocalFile); + } + + return QString(); +} + +QString FileEntry::id() const +{ + if (m_fileItem) { + return m_fileItem->url().toString(); + } + + return QString(); +} + +QUrl FileEntry::url() const +{ + if (m_fileItem) { + return m_fileItem->url(); + } + + return QUrl(); +} + +bool FileEntry::hasActions() const +{ + return m_fileItem && m_fileItem->isFile(); +} + +QVariantList FileEntry::actions() const +{ + if (m_fileItem) { + return Kicker::createActionListForFileItem(*m_fileItem); + } + + return QVariantList(); +} + +bool FileEntry::run(const QString &actionId, const QVariant &argument) +{ + if (!m_fileItem) { + return false; + } + + if (actionId.isEmpty()) { + auto job = new KIO::OpenUrlJob(m_fileItem->url()); + job->start(); + + return true; + } else { + bool close = false; + + if (Kicker::handleFileItemAction(*m_fileItem, actionId, argument, &close)) { + return close; + } + } + + return false; +} diff --git a/plasma/workspace/applets/kicker/plugin/fileentry.h b/plasma/workspace/applets/kicker/plugin/fileentry.h new file mode 100644 index 0000000000..200875b2af --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/fileentry.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractentry.h" + +class KFileItem; + +class FileEntry : public AbstractEntry +{ +public: + explicit FileEntry(AbstractModel *owner, const QUrl &url); + ~FileEntry() override; + + EntryType type() const override + { + return RunnableType; + } + + bool isValid() const override; + + QIcon icon() const override; + QString name() const override; + QString description() const override; + + QString id() const override; + QUrl url() const override; + + bool hasActions() const override; + QVariantList actions() const override; + + bool run(const QString &actionId = QString(), const QVariant &argument = QVariant()) override; + +private: + KFileItem *m_fileItem; +}; diff --git a/plasma/workspace/applets/kicker/plugin/forwardingmodel.cpp b/plasma/workspace/applets/kicker/plugin/forwardingmodel.cpp new file mode 100644 index 0000000000..b8616f71b7 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/forwardingmodel.cpp @@ -0,0 +1,226 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "forwardingmodel.h" + +ForwardingModel::ForwardingModel(QObject *parent) + : AbstractModel(parent) +{ +} + +ForwardingModel::~ForwardingModel() +{ +} + +QString ForwardingModel::description() const +{ + if (!m_sourceModel) { + return QString(); + } + + AbstractModel *abstractModel = qobject_cast(m_sourceModel); + + if (!abstractModel) { + return QString(); + } + + return abstractModel->description(); +} + +QAbstractItemModel *ForwardingModel::sourceModel() const +{ + return m_sourceModel; +} + +void ForwardingModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + disconnectSignals(); + + beginResetModel(); + + m_sourceModel = sourceModel; + + connectSignals(); + + endResetModel(); + + Q_EMIT countChanged(); + Q_EMIT sourceModelChanged(); + Q_EMIT descriptionChanged(); +} + +bool ForwardingModel::canFetchMore(const QModelIndex &parent) const +{ + if (!m_sourceModel) { + return false; + } + + return m_sourceModel->canFetchMore(indexToSourceIndex(parent)); +} + +void ForwardingModel::fetchMore(const QModelIndex &parent) +{ + if (m_sourceModel) { + m_sourceModel->fetchMore(indexToSourceIndex(parent)); + } +} + +QModelIndex ForwardingModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + if (!m_sourceModel) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex ForwardingModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + + return QModelIndex(); +} + +QVariant ForwardingModel::data(const QModelIndex &index, int role) const +{ + if (!m_sourceModel) { + return QVariant(); + } + + return m_sourceModel->data(indexToSourceIndex(index), role); +} + +int ForwardingModel::rowCount(const QModelIndex &parent) const +{ + if (!m_sourceModel) { + return 0; + } + + return m_sourceModel->rowCount(indexToSourceIndex(parent)); +} + +QModelIndex ForwardingModel::indexToSourceIndex(const QModelIndex &index) const +{ + if (!m_sourceModel || !index.isValid()) { + return QModelIndex(); + } + + return m_sourceModel->index(index.row(), index.column(), index.parent().isValid() ? indexToSourceIndex(index.parent()) : QModelIndex()); +} + +bool ForwardingModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (!m_sourceModel) { + return false; + } + + AbstractModel *abstractModel = qobject_cast(m_sourceModel); + + if (!abstractModel) { + return false; + } + + return abstractModel->trigger(row, actionId, argument); +} + +QString ForwardingModel::labelForRow(int row) +{ + if (!m_sourceModel) { + return QString(); + } + + AbstractModel *abstractModel = qobject_cast(m_sourceModel); + + if (!abstractModel) { + return QString(); + } + + return abstractModel->labelForRow(row); +} + +AbstractModel *ForwardingModel::modelForRow(int row) +{ + if (!m_sourceModel) { + return nullptr; + } + + AbstractModel *abstractModel = qobject_cast(m_sourceModel); + + if (!abstractModel) { + return nullptr; + } + + return abstractModel->modelForRow(row); +} + +AbstractModel *ForwardingModel::favoritesModel() +{ + AbstractModel *sourceModel = qobject_cast(m_sourceModel); + + if (sourceModel) { + return sourceModel->favoritesModel(); + } + + return AbstractModel::favoritesModel(); +} + +int ForwardingModel::separatorCount() const +{ + if (!m_sourceModel) { + return 0; + } + + AbstractModel *abstractModel = qobject_cast(m_sourceModel); + + if (!abstractModel) { + return 0; + } + + return abstractModel->separatorCount(); +} + +void ForwardingModel::reset() +{ + beginResetModel(); + endResetModel(); + + Q_EMIT countChanged(); + Q_EMIT separatorCountChanged(); +} + +void ForwardingModel::connectSignals() +{ + if (!m_sourceModel) { + return; + } + + connect(m_sourceModel, SIGNAL(destroyed()), this, SLOT(reset())); + connect(m_sourceModel.data(), &QAbstractItemModel::dataChanged, this, &QAbstractItemModel::dataChanged, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsAboutToBeInserted, this, &QAbstractItemModel::rowsAboutToBeInserted, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsAboutToBeMoved, this, &QAbstractItemModel::rowsAboutToBeMoved, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &QAbstractItemModel::rowsAboutToBeRemoved, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsInserted, this, &QAbstractItemModel::rowsInserted, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsInserted, this, &AbstractModel::countChanged, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsMoved, this, &QAbstractItemModel::rowsMoved, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &QAbstractItemModel::rowsRemoved, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::rowsRemoved, this, &AbstractModel::countChanged, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::modelAboutToBeReset, this, &QAbstractItemModel::modelAboutToBeReset, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::modelReset, this, &QAbstractItemModel::modelReset, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::modelReset, this, &AbstractModel::countChanged, Qt::UniqueConnection); + connect(m_sourceModel.data(), &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged, Qt::UniqueConnection); +} + +void ForwardingModel::disconnectSignals() +{ + if (!m_sourceModel) { + return; + } + + disconnect(m_sourceModel, nullptr, this, nullptr); +} diff --git a/plasma/workspace/applets/kicker/plugin/forwardingmodel.h b/plasma/workspace/applets/kicker/plugin/forwardingmodel.h new file mode 100644 index 0000000000..82df2725d7 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/forwardingmodel.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include + +class ForwardingModel : public AbstractModel +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) + +public: + explicit ForwardingModel(QObject *parent = nullptr); + ~ForwardingModel() override; + + QString description() const override; + + QAbstractItemModel *sourceModel() const; + virtual void setSourceModel(QAbstractItemModel *sourceModel); + + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + Q_INVOKABLE QString labelForRow(int row) override; + + Q_INVOKABLE AbstractModel *modelForRow(int row) override; + + AbstractModel *favoritesModel() override; + + int separatorCount() const override; + +public Q_SLOTS: + void reset(); + +Q_SIGNALS: + void sourceModelChanged() const; + +protected: + QModelIndex indexToSourceIndex(const QModelIndex &index) const; + + void connectSignals(); + void disconnectSignals(); + + QPointer m_sourceModel; +}; diff --git a/plasma/workspace/applets/kicker/plugin/funnelmodel.cpp b/plasma/workspace/applets/kicker/plugin/funnelmodel.cpp new file mode 100644 index 0000000000..1af6bbb383 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/funnelmodel.cpp @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "funnelmodel.h" + +FunnelModel::FunnelModel(QObject *parent) + : ForwardingModel(parent) +{ +} + +FunnelModel::~FunnelModel() +{ +} + +void FunnelModel::setSourceModel(QAbstractItemModel *model) +{ + if (model && m_sourceModel == model) { + return; + } + + if (!model) { + reset(); + + return; + } + + connect(model, SIGNAL(destroyed(QObject *)), this, SLOT(reset())); + + if (!m_sourceModel) { + beginResetModel(); + + m_sourceModel = model; + + connectSignals(); + + endResetModel(); + + Q_EMIT countChanged(); + + Q_EMIT sourceModelChanged(); + Q_EMIT descriptionChanged(); + + return; + } + + int oldCount = m_sourceModel->rowCount(); + int newCount = model->rowCount(); + + auto setNewModel = [this, model]() { + disconnectSignals(); + m_sourceModel = model; + connectSignals(); + }; + + if (newCount > oldCount) { + beginInsertRows(QModelIndex(), oldCount, newCount - 1); + setNewModel(); + endInsertRows(); + } else if (newCount < oldCount) { + if (newCount == 0) { + beginResetModel(); + setNewModel(); + endResetModel(); + } else { + beginRemoveRows(QModelIndex(), newCount, oldCount - 1); + setNewModel(); + endRemoveRows(); + } + } else { + setNewModel(); + } + + if (newCount > 0) { + Q_EMIT dataChanged(index(0, 0), index(qMin(oldCount, newCount) - 1, 0)); + } + + if (oldCount != newCount) { + Q_EMIT countChanged(); + } + + Q_EMIT sourceModelChanged(); + Q_EMIT descriptionChanged(); +} diff --git a/plasma/workspace/applets/kicker/plugin/funnelmodel.h b/plasma/workspace/applets/kicker/plugin/funnelmodel.h new file mode 100644 index 0000000000..2fd3b9820b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/funnelmodel.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "forwardingmodel.h" + +class FunnelModel : public ForwardingModel +{ + Q_OBJECT + +public: + explicit FunnelModel(QObject *parent = nullptr); + ~FunnelModel() override; + + void setSourceModel(QAbstractItemModel *model) override; +}; diff --git a/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.cpp b/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.cpp new file mode 100644 index 0000000000..9b289979c5 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.cpp @@ -0,0 +1,682 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + SPDX-FileCopyrightText: 2016-2017 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kastatsfavoritesmodel.h" +#include "actionlist.h" +#include "appentry.h" +#include "contactentry.h" +#include "debug.h" +#include "fileentry.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +#define AGENT_APPLICATIONS QStringLiteral("org.kde.plasma.favorites.applications") +#define AGENT_CONTACTS QStringLiteral("org.kde.plasma.favorites.contacts") +#define AGENT_DOCUMENTS QStringLiteral("org.kde.plasma.favorites.documents") + +QString agentForUrl(const QString &url) +{ + // clang-format off + return url.startsWith(QLatin1String("ktp:")) + ? AGENT_CONTACTS + : url.startsWith(QLatin1String("preferred:")) + ? AGENT_APPLICATIONS + : url.startsWith(QLatin1String("applications:")) + ? AGENT_APPLICATIONS + : (url.startsWith(QLatin1Char('/')) && !url.endsWith(QLatin1String(".desktop"))) + ? AGENT_DOCUMENTS + : (url.startsWith(QLatin1String("file:/")) && !url.endsWith(QLatin1String(".desktop"))) + ? AGENT_DOCUMENTS + // use applications as the default + : AGENT_APPLICATIONS; + // clang-format on +} + +class KAStatsFavoritesModel::Private : public QAbstractListModel +{ +public: + class NormalizedId + { + public: + NormalizedId() + { + } + + NormalizedId(const Private *parent, const QString &id) + { + if (id.isEmpty()) + return; + + QSharedPointer entry = nullptr; + + if (parent->m_itemEntries.contains(id)) { + entry = parent->m_itemEntries[id]; + } else { + // This entry is not cached - it is temporary, + // so let's clean up when we exit this function + entry = parent->entryForResource(id); + } + + if (!entry || !entry->isValid()) { + qCWarning(KICKER_DEBUG) << "Entry is not valid" << id << entry; + m_id = id; + return; + } + + const auto url = entry->url(); + + qCDebug(KICKER_DEBUG) << "Original id is: " << id << ", and the url is" << url; + + // Preferred applications need special handling + if (entry->id().startsWith(QLatin1String("preferred:"))) { + m_id = entry->id(); + return; + } + + // If this is an application, use the applications:-format url + auto appEntry = dynamic_cast(entry.data()); + if (appEntry && !appEntry->menuId().isEmpty()) { + m_id = QLatin1String("applications:") + appEntry->menuId(); + return; + } + + // We want to resolve symbolic links not to have two paths + // refer to the same .desktop file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + + if (file.exists()) { + m_id = QUrl::fromLocalFile(file.canonicalFilePath()).toString(); + return; + } + } + + // If this is a file, we should have already covered it + if (url.scheme() == QLatin1String("file")) { + return; + } + + m_id = url.toString(); + } + + const QString &value() const + { + return m_id; + } + + bool operator==(const NormalizedId &other) const + { + return m_id == other.m_id; + } + + private: + QString m_id; + }; + + NormalizedId normalizedId(const QString &id) const + { + return NormalizedId(this, id); + } + + QSharedPointer entryForResource(const QString &resource) const + { + using SP = QSharedPointer; + + const auto agent = agentForUrl(resource); + + if (agent == AGENT_CONTACTS) { + return SP(new ContactEntry(q, resource)); + + } else if (agent == AGENT_DOCUMENTS) { + if (resource.startsWith(QLatin1String("/"))) { + return SP(new FileEntry(q, QUrl::fromLocalFile(resource))); + } else { + return SP(new FileEntry(q, QUrl(resource))); + } + + } else if (agent == AGENT_APPLICATIONS) { + if (resource.startsWith(QLatin1String("applications:"))) { + return SP(new AppEntry(q, resource.mid(13))); + } else { + return SP(new AppEntry(q, resource)); + } + + } else { + return {}; + } + } + + Private(KAStatsFavoritesModel *parent, QString clientId) + : q(parent) + , m_query(LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::current() | Activity::global() + | Limit::all()) + , m_watcher(m_query) + , m_clientId(clientId) + { + // Connecting the watcher + connect(&m_watcher, &ResultWatcher::resultLinked, [this](const QString &resource) { + addResult(resource, -1); + }); + + connect(&m_watcher, &ResultWatcher::resultUnlinked, [this](const QString &resource) { + removeResult(resource); + }); + + // Loading the items order + const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc")); + + // We want first to check whether we have an ordering for this activity. + // If not, we will try to get a global one for this applet + + const QString thisGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + m_activities.currentActivity(); + const QString globalGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-global"); + + KConfigGroup thisCfgGroup(cfg, thisGroupName); + KConfigGroup globalCfgGroup(cfg, globalGroupName); + + QStringList ordering = thisCfgGroup.readEntry("ordering", QStringList()) + globalCfgGroup.readEntry("ordering", QStringList()); + + qCDebug(KICKER_DEBUG) << "Loading the ordering " << ordering; + + // Loading the results without emitting any model signals + qCDebug(KICKER_DEBUG) << "Query is" << m_query; + ResultSet results(m_query); + + for (const auto &result : results) { + qCDebug(KICKER_DEBUG) << "Got " << result.resource() << " -->"; + addResult(result.resource(), -1, false); + } + + // Normalizing all the ids + std::transform(ordering.begin(), ordering.end(), ordering.begin(), [&](const QString &item) { + return normalizedId(item).value(); + }); + + // Sorting the items in the cache + std::sort(m_items.begin(), m_items.end(), [&](const NormalizedId &left, const NormalizedId &right) { + auto leftIndex = ordering.indexOf(left.value()); + auto rightIndex = ordering.indexOf(right.value()); + // clang-format off + return (leftIndex == -1 && rightIndex == -1) ? + left.value() < right.value() : + + (leftIndex == -1) ? + false : + + (rightIndex == -1) ? + true : + + // otherwise + leftIndex < rightIndex; + // clang-format on + }); + + // Debugging: + QVector itemStrings(m_items.size()); + std::transform(m_items.cbegin(), m_items.cend(), itemStrings.begin(), [](const NormalizedId &item) { + return item.value(); + }); + qCDebug(KICKER_DEBUG) << "After ordering: " << itemStrings; + } + + void addResult(const QString &_resource, int index, bool notifyModel = true) + { + // We want even files to have a proper URL + const auto resource = _resource.startsWith(QLatin1Char('/')) ? QUrl::fromLocalFile(_resource).toString() : _resource; + + qCDebug(KICKER_DEBUG) << "Adding result" << resource << "already present?" << m_itemEntries.contains(resource); + + if (m_itemEntries.contains(resource)) + return; + + auto entry = entryForResource(resource); + + if (!entry || !entry->isValid()) { + qCDebug(KICKER_DEBUG) << "Entry is not valid!"; + return; + } + + if (index == -1) { + index = m_items.count(); + } + + if (notifyModel) { + beginInsertRows(QModelIndex(), index, index); + } + + auto url = entry->url(); + + m_itemEntries[resource] = m_itemEntries[entry->id()] = m_itemEntries[url.toString()] = m_itemEntries[url.toLocalFile()] = entry; + + auto normalized = normalizedId(resource); + m_items.insert(index, normalized); + m_itemEntries[normalized.value()] = entry; + + if (notifyModel) { + endInsertRows(); + saveOrdering(); + } + } + + void removeResult(const QString &resource) + { + auto normalized = normalizedId(resource); + + // If we know this item will not really be removed, + // but only that activities it is on have changed, + // lets leave it + if (m_ignoredItems.contains(normalized.value())) { + m_ignoredItems.removeAll(normalized.value()); + return; + } + + qCDebug(KICKER_DEBUG) << "Removing result" << resource; + + auto index = m_items.indexOf(normalizedId(resource)); + + if (index == -1) + return; + + beginRemoveRows(QModelIndex(), index, index); + auto entry = m_itemEntries[resource]; + m_items.removeAt(index); + + // Removing the entry from the cache + QMutableHashIterator> i(m_itemEntries); + while (i.hasNext()) { + i.next(); + if (i.value() == entry) { + i.remove(); + } + } + + endRemoveRows(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + if (parent.isValid()) + return 0; + + return m_items.count(); + } + + QVariant data(const QModelIndex &item, int role = Qt::DisplayRole) const override + { + if (item.parent().isValid()) + return QVariant(); + + const auto index = item.row(); + + const auto entry = m_itemEntries[m_items[index].value()]; + // clang-format off + return entry == nullptr ? QVariant() + : role == Qt::DisplayRole ? entry->name() + : role == Qt::DecorationRole ? entry->icon() + : role == Kicker::DescriptionRole ? entry->description() + : role == Kicker::FavoriteIdRole ? entry->id() + : role == Kicker::UrlRole ? entry->url() + : role == Kicker::HasActionListRole ? entry->hasActions() + : role == Kicker::ActionListRole ? entry->actions() + : QVariant(); + // clang-format on + } + + bool trigger(int row, const QString &actionId, const QVariant &argument) + { + if (row < 0 || row >= rowCount()) { + return false; + } + + const QString id = data(index(row, 0), Kicker::UrlRole).toString(); + if (m_itemEntries.contains(id)) { + return m_itemEntries[id]->run(actionId, argument); + } + // Entries with preferred:// can be changed by the user, BUG: 416161 + // then the list of entries could be out of sync + const auto entry = m_itemEntries[m_items[row].value()]; + if (QUrl(entry->id()).scheme() == QLatin1String("preferred")) { + return entry->run(actionId, argument); + } + return false; + } + + void move(int from, int to) + { + if (from < 0) + return; + if (from >= m_items.count()) + return; + if (to < 0) + return; + if (to >= m_items.count()) + return; + + if (from == to) + return; + + const int modelTo = to + (to > from ? 1 : 0); + + if (q->beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo)) { + m_items.move(from, to); + q->endMoveRows(); + + qCDebug(KICKER_DEBUG) << "Save ordering (from Private::move) -->"; + saveOrdering(); + } + } + + void saveOrdering() + { + QStringList ids; + + for (const auto &item : qAsConst(m_items)) { + ids << item.value(); + } + + qCDebug(KICKER_DEBUG) << "Save ordering (from Private::saveOrdering) -->"; + saveOrdering(ids, m_clientId, m_activities.currentActivity()); + } + + static void saveOrdering(const QStringList &ids, const QString &clientId, const QString ¤tActivity) + { + const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc")); + + QStringList activities{currentActivity, QStringLiteral("global")}; + + qCDebug(KICKER_DEBUG) << "Saving ordering for" << currentActivity << "and global" << ids; + + for (const auto &activity : activities) { + const QString groupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + activity; + + KConfigGroup cfgGroup(cfg, groupName); + + cfgGroup.writeEntry("ordering", ids); + } + + cfg->sync(); + } + + KAStatsFavoritesModel *const q; + KActivities::Consumer m_activities; + Query m_query; + ResultWatcher m_watcher; + QString m_clientId; + + QVector m_items; + QHash> m_itemEntries; + QStringList m_ignoredItems; +}; + +KAStatsFavoritesModel::KAStatsFavoritesModel(QObject *parent) + : PlaceholderModel(parent) + , d(nullptr) // we have no client id yet + , m_enabled(true) + , m_maxFavorites(-1) + , m_activities(new KActivities::Consumer(this)) +{ + connect(m_activities, &KActivities::Consumer::currentActivityChanged, this, [&](const QString ¤tActivity) { + qCDebug(KICKER_DEBUG) << "Activity just got changed to" << currentActivity; + Q_UNUSED(currentActivity); + if (d) { + auto clientId = d->m_clientId; + initForClient(clientId); + } + }); +} + +KAStatsFavoritesModel::~KAStatsFavoritesModel() +{ + delete d; +} + +void KAStatsFavoritesModel::initForClient(const QString &clientId) +{ + qCDebug(KICKER_DEBUG) << "initForClient" << clientId; + + setSourceModel(nullptr); + delete d; + d = new Private(this, clientId); + + setSourceModel(d); +} + +QString KAStatsFavoritesModel::description() const +{ + return i18n("Favorites"); +} + +bool KAStatsFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + return d && d->trigger(row, actionId, argument); +} + +bool KAStatsFavoritesModel::enabled() const +{ + return m_enabled; +} + +int KAStatsFavoritesModel::maxFavorites() const +{ + return m_maxFavorites; +} + +void KAStatsFavoritesModel::setMaxFavorites(int max) +{ + Q_UNUSED(max); +} + +void KAStatsFavoritesModel::setEnabled(bool enable) +{ + if (m_enabled != enable) { + m_enabled = enable; + + Q_EMIT enabledChanged(); + } +} + +QStringList KAStatsFavoritesModel::favorites() const +{ + qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::favorites returns nothing, it is here just to keep the API backwards-compatible"; + return QStringList(); +} + +void KAStatsFavoritesModel::setFavorites(const QStringList &favorites) +{ + Q_UNUSED(favorites); + qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::setFavorites is ignored"; +} + +bool KAStatsFavoritesModel::isFavorite(const QString &id) const +{ + return d && d->m_itemEntries.contains(id); +} + +void KAStatsFavoritesModel::portOldFavorites(const QStringList &ids) +{ + if (!d) + return; + qCDebug(KICKER_DEBUG) << "portOldFavorites" << ids; + + const QString activityId = QStringLiteral(":global"); + std::for_each(ids.begin(), ids.end(), [&](const QString &id) { + addFavoriteTo(id, activityId); + }); + + // Resetting the model + auto clientId = d->m_clientId; + setSourceModel(nullptr); + delete d; + d = nullptr; + + qCDebug(KICKER_DEBUG) << "Save ordering (from portOldFavorites) -->"; + Private::saveOrdering(ids, clientId, m_activities->currentActivity()); + + QTimer::singleShot(500, this, std::bind(&KAStatsFavoritesModel::initForClient, this, clientId)); +} + +void KAStatsFavoritesModel::addFavorite(const QString &id, int index) +{ + qCDebug(KICKER_DEBUG) << "addFavorite" << id << index << " -->"; + addFavoriteTo(id, QStringLiteral(":global"), index); +} + +void KAStatsFavoritesModel::removeFavorite(const QString &id) +{ + qCDebug(KICKER_DEBUG) << "removeFavorite" << id << " -->"; + removeFavoriteFrom(id, QStringLiteral(":any")); +} + +void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const QString &activityId, int index) +{ + qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activityId << index << " -->"; + addFavoriteTo(id, Activity(activityId), index); +} + +void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const QString &activityId) +{ + qCDebug(KICKER_DEBUG) << "removeFavoriteFrom" << id << activityId << " -->"; + removeFavoriteFrom(id, Activity(activityId)); +} + +void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const Activity &activity, int index) +{ + if (!d || id.isEmpty()) + return; + + Q_ASSERT(!activity.values.isEmpty()); + + setDropPlaceholderIndex(-1); + + QStringList matchers{d->m_activities.currentActivity(), QStringLiteral(":global"), QStringLiteral(":current")}; + if (std::find_first_of(activity.values.cbegin(), activity.values.cend(), matchers.cbegin(), matchers.cend()) != activity.values.cend()) { + d->addResult(id, index); + } + + const auto url = d->normalizedId(id).value(); + + qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << index << url << " (actual)"; + + if (url.isEmpty()) + return; + + d->m_watcher.linkToActivity(QUrl(url), activity, Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const Activity &activity) +{ + if (!d || id.isEmpty()) + return; + + const auto url = d->normalizedId(id).value(); + + Q_ASSERT(!activity.values.isEmpty()); + + qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << url << " (actual)"; + + if (url.isEmpty()) + return; + + d->m_watcher.unlinkFromActivity(QUrl(url), activity, Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::setFavoriteOn(const QString &id, const QString &activityId) +{ + if (!d || id.isEmpty()) + return; + + const auto url = d->normalizedId(id).value(); + + qCDebug(KICKER_DEBUG) << "setFavoriteOn" << id << activityId << url << " (actual)"; + + qCDebug(KICKER_DEBUG) << "%%%%%%%%%%% Activity is" << activityId; + if (activityId.isEmpty() || activityId == QLatin1String(":any") || activityId == QLatin1String(":global") + || activityId == m_activities->currentActivity()) { + d->m_ignoredItems << url; + } + + d->m_watcher.unlinkFromActivity(QUrl(url), Activity::any(), Agent(agentForUrl(url))); + d->m_watcher.linkToActivity(QUrl(url), activityId, Agent(agentForUrl(url))); +} + +void KAStatsFavoritesModel::moveRow(int from, int to) +{ + if (!d) + return; + + d->move(from, to); +} + +AbstractModel *KAStatsFavoritesModel::favoritesModel() +{ + return this; +} + +void KAStatsFavoritesModel::refresh() +{ +} + +QObject *KAStatsFavoritesModel::activities() const +{ + return m_activities; +} + +QString KAStatsFavoritesModel::activityNameForId(const QString &activityId) const +{ + // It is safe to use a short-lived object here, + // we are always synced with KAMD in plasma + KActivities::Info info(activityId); + return info.name(); +} + +QStringList KAStatsFavoritesModel::linkedActivitiesFor(const QString &id) const +{ + if (!d) { + qCDebug(KICKER_DEBUG) << "Linked for" << id << "is empty, no Private instance"; + return {}; + } + + auto url = d->normalizedId(id).value(); + + if (url.startsWith(QLatin1String("file:"))) { + url = QUrl(url).toLocalFile(); + } + + if (url.isEmpty()) { + qCDebug(KICKER_DEBUG) << "The url for" << id << "is empty"; + return {}; + } + + auto query = LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::any() | Url(url) | Limit::all(); + + ResultSet results(query); + + for (const auto &result : results) { + qCDebug(KICKER_DEBUG) << "Returning" << result.linkedActivities() << "for" << id << url; + return result.linkedActivities(); + } + + qCDebug(KICKER_DEBUG) << "Returning empty list of activities for" << id << url; + return {}; +} diff --git a/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.h b/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.h new file mode 100644 index 0000000000..3012985842 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/kastatsfavoritesmodel.h @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + SPDX-FileCopyrightText: 2016-2017 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "placeholdermodel.h" + +#include + +#include + +class PlaceholderModel; + +namespace KActivities +{ +class Consumer; +namespace Stats +{ +namespace Terms +{ +class Activity; +} // namespace Terms +} // namespace Stats +} // namespace KActivities + +class KAStatsFavoritesModel : public PlaceholderModel +{ + Q_OBJECT + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QStringList favorites READ favorites WRITE setFavorites NOTIFY favoritesChanged) + Q_PROPERTY(int maxFavorites READ maxFavorites WRITE setMaxFavorites NOTIFY maxFavoritesChanged) + + Q_PROPERTY(QObject *activities READ activities CONSTANT) + +public: + explicit KAStatsFavoritesModel(QObject *parent = nullptr); + ~KAStatsFavoritesModel() override; + + QString description() const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool enabled() const; + void setEnabled(bool enable); + + QStringList favorites() const; + void setFavorites(const QStringList &favorites); + + int maxFavorites() const; + void setMaxFavorites(int max); + + Q_INVOKABLE bool isFavorite(const QString &id) const; + + Q_INVOKABLE void addFavorite(const QString &id, int index = -1); + Q_INVOKABLE void removeFavorite(const QString &id); + + Q_INVOKABLE void addFavoriteTo(const QString &id, const QString &activityId, int index = -1); + Q_INVOKABLE void removeFavoriteFrom(const QString &id, const QString &activityId); + + Q_INVOKABLE void setFavoriteOn(const QString &id, const QString &activityId); + + Q_INVOKABLE void portOldFavorites(const QStringList &ids); + + Q_INVOKABLE QStringList linkedActivitiesFor(const QString &id) const; + + Q_INVOKABLE void moveRow(int from, int to); + + Q_INVOKABLE void initForClient(const QString &client); + + QObject *activities() const; + Q_INVOKABLE QString activityNameForId(const QString &activityId) const; + + AbstractModel *favoritesModel() override; + +public Q_SLOTS: + void refresh() override; + +Q_SIGNALS: + void enabledChanged() const; + void favoritesChanged() const; + void maxFavoritesChanged() const; + +private: + class Private; + Private *d; + + AbstractEntry *favoriteFromId(const QString &id) const; + + void addFavoriteTo(const QString &id, const KActivities::Stats::Terms::Activity &activityId, int index = -1); + void removeFavoriteFrom(const QString &id, const KActivities::Stats::Terms::Activity &activityId); + + bool m_enabled; + + int m_maxFavorites; + + KActivities::Consumer *m_activities; +}; diff --git a/plasma/workspace/applets/kicker/plugin/kickerplugin.cpp b/plasma/workspace/applets/kicker/plugin/kickerplugin.cpp new file mode 100644 index 0000000000..b1d53873fa --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/kickerplugin.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kickerplugin.h" +#include "abstractmodel.h" +#include "appsmodel.h" +#include "computermodel.h" +#include "containmentinterface.h" +#include "dashboardwindow.h" +#include "draghelper.h" +#include "funnelmodel.h" +#include "kastatsfavoritesmodel.h" +#include "processrunner.h" +#include "recentusagemodel.h" +#include "rootmodel.h" +#include "runnermodel.h" +#include "simplefavoritesmodel.h" +#include "submenu.h" +#include "systemmodel.h" +#include "systemsettings.h" +#include "trianglemousefilter.h" +#include "wheelinterceptor.h" +#include "windowsystem.h" + +void KickerPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.private.kicker")); + + qmlRegisterAnonymousType("", 1); + + qmlRegisterType(uri, 0, 1, "AppsModel"); + qmlRegisterType(uri, 0, 1, "ComputerModel"); + qmlRegisterType(uri, 0, 1, "ContainmentInterface"); + qmlRegisterType(uri, 0, 1, "DragHelper"); + qmlRegisterType(uri, 0, 1, "FavoritesModel"); + qmlRegisterType(uri, 0, 1, "KAStatsFavoritesModel"); + qmlRegisterType(uri, 0, 1, "DashboardWindow"); + qmlRegisterType(uri, 0, 1, "FunnelModel"); + qmlRegisterType(uri, 0, 1, "ProcessRunner"); + qmlRegisterType(uri, 0, 1, "RecentUsageModel"); + qmlRegisterType(uri, 0, 1, "RootModel"); + qmlRegisterType(uri, 0, 1, "RunnerModel"); + qmlRegisterType(uri, 0, 1, "SubMenu"); + qmlRegisterType(uri, 0, 1, "SystemModel"); + qmlRegisterType(uri, 0, 1, "SystemSettings"); + qmlRegisterType(uri, 0, 1, "TriangleMouseFilter"); + qmlRegisterType(uri, 0, 1, "WheelInterceptor"); + qmlRegisterType(uri, 0, 1, "WindowSystem"); +} diff --git a/plasma/workspace/applets/kicker/plugin/kickerplugin.h b/plasma/workspace/applets/kicker/plugin/kickerplugin.h new file mode 100644 index 0000000000..b45b048af4 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/kickerplugin.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class KickerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/applets/kicker/plugin/menuentryeditor.cpp b/plasma/workspace/applets/kicker/plugin/menuentryeditor.cpp new file mode 100644 index 0000000000..a1126fa981 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/menuentryeditor.cpp @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "menuentryeditor.h" + +#include +#include +#include + +#include + +MenuEntryEditor::MenuEntryEditor(QObject *parent) + : QObject(parent) +{ +} + +MenuEntryEditor::~MenuEntryEditor() +{ +} + +bool MenuEntryEditor::canEdit(const QString &entryPath) const +{ + KFileItemList itemList; + itemList << KFileItem(QUrl::fromLocalFile(entryPath)); + + return KPropertiesDialog::canDisplay(itemList); +} + +void MenuEntryEditor::edit(const QString &entryPath, const QString &menuId) +{ + const QString &appsPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); + const QUrl &entryUrl = QUrl::fromLocalFile(entryPath); + + if (!appsPath.isEmpty() && entryUrl.isValid()) { + const QDir appsDir(appsPath); + const QString &fileName = entryUrl.fileName(); + + if (appsDir.exists(fileName)) { + KPropertiesDialog::showDialog(entryUrl, nullptr, false); + } else { + if (!appsDir.exists()) { + if (!QDir::root().mkpath(appsPath)) { + return; + } + } + + KPropertiesDialog *dialog = new KPropertiesDialog(entryUrl, QUrl::fromLocalFile(appsPath), menuId); + // KPropertiesDialog deletes itself + dialog->show(); + } + } +} diff --git a/plasma/workspace/applets/kicker/plugin/menuentryeditor.h b/plasma/workspace/applets/kicker/plugin/menuentryeditor.h new file mode 100644 index 0000000000..780c3fa0d8 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/menuentryeditor.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class MenuEntryEditor : public QObject +{ + Q_OBJECT + +public: + explicit MenuEntryEditor(QObject *parent = nullptr); + ~MenuEntryEditor() override; + + bool canEdit(const QString &entryPath) const; + +public Q_SLOTS: + void edit(const QString &entryPath, const QString &menuId); +}; diff --git a/plasma/workspace/applets/kicker/plugin/placeholdermodel.cpp b/plasma/workspace/applets/kicker/plugin/placeholdermodel.cpp new file mode 100644 index 0000000000..41fd56cafa --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/placeholdermodel.cpp @@ -0,0 +1,358 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + SPDX-FileCopyrightText: 2017 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "placeholdermodel.h" +#include "actionlist.h" +#include "debug.h" + +PlaceholderModel::PlaceholderModel(QObject *parent) + : AbstractModel(parent) + , m_dropPlaceholderIndex(-1) + , m_isTriggerInhibited(false) +{ + connect(&m_triggerInhibitor, &QTimer::timeout, this, [&] { + qCDebug(KICKER_DEBUG) << "%%% Inhibit stopped"; + m_isTriggerInhibited = false; + }); + + m_triggerInhibitor.setInterval(500); + m_triggerInhibitor.setSingleShot(true); +} + +void PlaceholderModel::inhibitTriggering() +{ + qCDebug(KICKER_DEBUG) << "%%% Inhibit started"; + m_isTriggerInhibited = true; + m_triggerInhibitor.start(); +} + +PlaceholderModel::~PlaceholderModel() +{ +} + +QString PlaceholderModel::description() const +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->description(); + + } else { + return QString(); + } +} + +QAbstractItemModel *PlaceholderModel::sourceModel() const +{ + return m_sourceModel; +} + +void PlaceholderModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + disconnectSignals(); + + beginResetModel(); + + m_sourceModel = sourceModel; + + connectSignals(); + + endResetModel(); + + Q_EMIT countChanged(); + Q_EMIT sourceModelChanged(); + Q_EMIT descriptionChanged(); +} + +bool PlaceholderModel::canFetchMore(const QModelIndex &parent) const +{ + return m_sourceModel && m_sourceModel->canFetchMore(indexToSourceIndex(parent)); +} + +void PlaceholderModel::fetchMore(const QModelIndex &parent) +{ + if (m_sourceModel) { + m_sourceModel->fetchMore(indexToSourceIndex(parent)); + } +} + +QModelIndex PlaceholderModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_sourceModel ? createIndex(row, column) : QModelIndex(); +} + +QModelIndex PlaceholderModel::parent(const QModelIndex &index) const +{ + Q_UNUSED(index) + + return QModelIndex(); +} + +QVariant PlaceholderModel::data(const QModelIndex &index, int role) const +{ + const auto row = index.row(); + + if (m_dropPlaceholderIndex == row) { + switch (role) { + case Kicker::IsDropPlaceholderRole: + return true; + + // TODO: Maybe it would be nice to show something here? + // case Qt::DisplayRole: + // return "placeholder"; + // + // case Qt::DecorationRole: + // return "select"; + + default: + return QVariant(); + } + } + + return m_sourceModel ? m_sourceModel->data(indexToSourceIndex(index), role) : QVariant(); +} + +int PlaceholderModel::rowCount(const QModelIndex &parent) const +{ + if (!m_sourceModel || parent.isValid()) { + return 0; + } + + return m_sourceModel->rowCount() + (m_dropPlaceholderIndex != -1 ? 1 : 0); +} + +QModelIndex PlaceholderModel::indexToSourceIndex(const QModelIndex &index) const +{ + if (!m_sourceModel || !index.isValid()) { + return QModelIndex(); + } + + const auto row = index.row(); + const auto column = index.column(); + + return index.parent().isValid() ? + // We do not support tree models + QModelIndex() + : + + // If we are on top-level, lets add a placeholder + m_sourceModel->index(row - (m_dropPlaceholderIndex != -1 && row > m_dropPlaceholderIndex ? 1 : 0), column, QModelIndex()); +} + +int PlaceholderModel::sourceRowToRow(int sourceRow) const +{ + return sourceRow + (m_dropPlaceholderIndex != -1 && sourceRow >= m_dropPlaceholderIndex ? 1 : 0); +} + +int PlaceholderModel::rowToSourceRow(int row) const +{ + return row == m_dropPlaceholderIndex ? -1 : row - (m_dropPlaceholderIndex != -1 && row > m_dropPlaceholderIndex ? 1 : 0); +} + +QModelIndex PlaceholderModel::sourceIndexToIndex(const QModelIndex &sourceIndex) const +{ + if (!m_sourceModel || !sourceIndex.isValid()) { + return QModelIndex(); + } + + const auto sourceRow = sourceIndex.row(); + const auto sourceColumn = sourceIndex.column(); + + return sourceIndex.parent().isValid() ? + // We do not support tree-models + QModelIndex() + : + + // If we are on top-level, lets add a placeholder + index(sourceRowToRow(sourceRow), sourceColumn, QModelIndex()); +} + +bool PlaceholderModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (m_isTriggerInhibited) + return false; + + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->trigger(rowToSourceRow(row), actionId, argument); + + } else { + return false; + } +} + +QString PlaceholderModel::labelForRow(int row) +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->labelForRow(rowToSourceRow(row)); + + } else { + return QString(); + } +} + +AbstractModel *PlaceholderModel::modelForRow(int row) +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->modelForRow(rowToSourceRow(row)); + + } else { + return nullptr; + } +} + +AbstractModel *PlaceholderModel::favoritesModel() +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->favoritesModel(); + + } else { + return AbstractModel::favoritesModel(); + } +} + +int PlaceholderModel::separatorCount() const +{ + if (auto abstractModel = qobject_cast(m_sourceModel)) { + return abstractModel->separatorCount(); + + } else { + return 0; + } +} + +void PlaceholderModel::reset() +{ + beginResetModel(); + endResetModel(); + Q_EMIT countChanged(); + Q_EMIT separatorCountChanged(); +} + +void PlaceholderModel::connectSignals() +{ + if (!m_sourceModel) { + return; + } + + const auto sourceModelPtr = m_sourceModel.data(); + + connect(sourceModelPtr, SIGNAL(destroyed()), this, SLOT(reset())); + + connect(sourceModelPtr, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &from, const QModelIndex &to, const QVector &roles) { + Q_EMIT dataChanged(sourceIndexToIndex(from), sourceIndexToIndex(to), roles); + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsAboutToBeInserted, this, [this](const QModelIndex &parent, int from, int to) { + if (parent.isValid()) { + qCWarning(KICKER_DEBUG) << "We do not support tree models"; + + } else { + beginInsertRows(QModelIndex(), sourceRowToRow(from), sourceRowToRow(to)); + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsInserted, this, [this] { + endInsertRows(); + Q_EMIT countChanged(); + }); + + connect(sourceModelPtr, + &QAbstractItemModel::rowsAboutToBeMoved, + this, + [this](const QModelIndex &source, int from, int to, const QModelIndex &dest, int destRow) { + if (source.isValid() || dest.isValid()) { + qCWarning(KICKER_DEBUG) << "We do not support tree models"; + + } else { + beginMoveRows(QModelIndex(), sourceRowToRow(from), sourceRowToRow(to), QModelIndex(), sourceRowToRow(destRow)); + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsMoved, this, [this] { + endMoveRows(); + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int from, int to) { + if (parent.isValid()) { + qCWarning(KICKER_DEBUG) << "We do not support tree models"; + + } else { + beginRemoveRows(QModelIndex(), sourceRowToRow(from), sourceRowToRow(to)); + } + }); + + connect(sourceModelPtr, &QAbstractItemModel::rowsRemoved, this, [this] { + endRemoveRows(); + Q_EMIT countChanged(); + }); + + connect(sourceModelPtr, &QAbstractItemModel::modelAboutToBeReset, this, [this] { + beginResetModel(); + }); + + connect(sourceModelPtr, &QAbstractItemModel::modelReset, this, [this] { + endResetModel(); + Q_EMIT countChanged(); + }); + + // We do not have persistant indices + // connect(sourceModelPtr, &QAbstractItemModel::layoutAboutToBeChanged), + // this, &PlaceholderModel::layoutAboutToBeChanged); + // connect(sourceModelPtr, &QAbstractItemModel::layoutChanged), + // this, SIGNAL(layoutChanged(QList,QAbstractItemModel::LayoutChangeHint)), + // Qt::UniqueConnection); +} + +void PlaceholderModel::disconnectSignals() +{ + if (!m_sourceModel) { + return; + } + + disconnect(m_sourceModel, nullptr, this, nullptr); +} + +int PlaceholderModel::dropPlaceholderIndex() const +{ + return m_dropPlaceholderIndex; +} + +void PlaceholderModel::setDropPlaceholderIndex(int index) +{ + if (index == m_dropPlaceholderIndex) + return; + + inhibitTriggering(); + + if (index == -1 && m_dropPlaceholderIndex != -1) { + // Removing the placeholder + beginRemoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex); + m_dropPlaceholderIndex = index; + endRemoveRows(); + + Q_EMIT countChanged(); + + } else if (index != -1 && m_dropPlaceholderIndex == -1) { + // Creating the placeholder + beginInsertRows(QModelIndex(), index, index); + m_dropPlaceholderIndex = index; + endInsertRows(); + + Q_EMIT countChanged(); + + } else if (m_dropPlaceholderIndex != index) { + // Moving the placeholder + int modelTo = index + (index > m_dropPlaceholderIndex ? 1 : 0); + + if (beginMoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex, QModelIndex(), modelTo)) { + m_dropPlaceholderIndex = index; + endMoveRows(); + } + } + + Q_EMIT dropPlaceholderIndexChanged(); +} diff --git a/plasma/workspace/applets/kicker/plugin/placeholdermodel.h b/plasma/workspace/applets/kicker/plugin/placeholdermodel.h new file mode 100644 index 0000000000..522ab7e33e --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/placeholdermodel.h @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + SPDX-FileCopyrightText: 2017 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include +#include + +class PlaceholderModel : public AbstractModel +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) + Q_PROPERTY(int dropPlaceholderIndex READ dropPlaceholderIndex WRITE setDropPlaceholderIndex NOTIFY dropPlaceholderIndexChanged) + +public: + explicit PlaceholderModel(QObject *parent = nullptr); + ~PlaceholderModel() override; + + QString description() const override; + + QAbstractItemModel *sourceModel() const; + virtual void setSourceModel(QAbstractItemModel *sourceModel); + + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + Q_INVOKABLE QString labelForRow(int row) override; + + Q_INVOKABLE AbstractModel *modelForRow(int row) override; + + AbstractModel *favoritesModel() override; + + int separatorCount() const override; + + int dropPlaceholderIndex() const; + void setDropPlaceholderIndex(int index); + +public Q_SLOTS: + void reset(); + +Q_SIGNALS: + void sourceModelChanged() const; + void dropPlaceholderIndexChanged(); + +protected: + void inhibitTriggering(); + +private: + QModelIndex indexToSourceIndex(const QModelIndex &index) const; + QModelIndex sourceIndexToIndex(const QModelIndex &index) const; + int sourceRowToRow(int sourceRow) const; + int rowToSourceRow(int row) const; + + void connectSignals(); + void disconnectSignals(); + + QPointer m_sourceModel; + + int m_dropPlaceholderIndex; + bool m_isTriggerInhibited; + QTimer m_triggerInhibitor; +}; diff --git a/plasma/workspace/applets/kicker/plugin/processrunner.cpp b/plasma/workspace/applets/kicker/plugin/processrunner.cpp new file mode 100644 index 0000000000..073249d490 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/processrunner.cpp @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2013 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "processrunner.h" + +#include + +ProcessRunner::ProcessRunner(QObject *parent) + : QObject(parent) +{ +} + +ProcessRunner::~ProcessRunner() +{ +} + +void ProcessRunner::runMenuEditor() +{ + KProcess::startDetached(QStringLiteral("kmenuedit")); +} diff --git a/plasma/workspace/applets/kicker/plugin/processrunner.h b/plasma/workspace/applets/kicker/plugin/processrunner.h new file mode 100644 index 0000000000..e288ab8cc1 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/processrunner.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2013 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class ProcessRunner : public QObject +{ + Q_OBJECT + +public: + explicit ProcessRunner(QObject *parent = nullptr); + ~ProcessRunner() override; + + Q_INVOKABLE void runMenuEditor(); +}; diff --git a/plasma/workspace/applets/kicker/plugin/qmldir b/plasma/workspace/applets/kicker/plugin/qmldir new file mode 100644 index 0000000000..c07aed3f8f --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/qmldir @@ -0,0 +1,2 @@ +module org.kde.plasma.private.kicker +plugin kickerplugin diff --git a/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.cpp b/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.cpp new file mode 100644 index 0000000000..7956f3ce17 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.cpp @@ -0,0 +1,232 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "recentcontactsmodel.h" +#include "actionlist.h" +#include "contactentry.h" + +#include + +#include + +#include +#include + +#include +#include //FIXME TODO: Pretty include in KPeople broken. +#include + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +RecentContactsModel::RecentContactsModel(QObject *parent) + : ForwardingModel(parent) +{ + refresh(); +} + +RecentContactsModel::~RecentContactsModel() +{ +} + +QString RecentContactsModel::description() const +{ + return i18n("Contacts"); +} + +QVariant RecentContactsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + QString id = sourceModel()->data(index, ResultModel::ResourceRole).toString(); + + KPeople::PersonData *data = nullptr; + + if (m_idToData.contains(id)) { + data = m_idToData[id]; + } + + if (!data) { + const_cast(this)->insertPersonData(id, index.row()); + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return data->name(); + } else if (role == Qt::DecorationRole) { + return data->presenceIconName(); + } else if (role == Kicker::FavoriteIdRole) { + return id; + } else if (role == Kicker::HasActionListRole) { + return true; + } else if (role == Kicker::ActionListRole) { + QVariantList actionList; + + const QVariantMap &forgetAction = Kicker::createActionItem(i18n("Forget Contact"), QStringLiteral("edit-clear-history"), QStringLiteral("forget")); + actionList << forgetAction; + + const QVariantMap &forgetAllAction = + Kicker::createActionItem(i18n("Forget All Contacts"), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); + actionList << forgetAllAction; + + actionList << Kicker::createSeparatorActionItem(); + + actionList << Kicker::createActionItem(i18n("Show Contact Information…"), QStringLiteral("identity"), QStringLiteral("showContactInfo")); + + return actionList; + } else if (role == Kicker::DescriptionRole) { + return QString(); + } + + return QVariant(); +} + +bool RecentContactsModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(argument) + + bool withinBounds = row >= 0 && row < rowCount(); + + if (actionId.isEmpty() && withinBounds) { + QString id = sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString(); + + const QList actionList = KPeople::actionsForPerson(id, this); + + if (!actionList.isEmpty()) { + QAction *chat = nullptr; + + for (QAction *action : actionList) { + const QVariant &actionType = action->property("actionType"); + + if (!actionType.isNull() && actionType.toInt() == KPeople::ActionType::TextChatAction) { + chat = action; + } + } + + if (chat) { + chat->trigger(); + + return true; + } + } + + return false; + } else if (actionId == QLatin1String("showContactInfo") && withinBounds) { + ContactEntry::showPersonDetailsDialog(sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString()); + } else if (actionId == QLatin1String("forget") && withinBounds) { + if (sourceModel()) { + ResultModel *resultModel = static_cast(sourceModel()); + resultModel->forgetResource(row); + } + + return false; + } else if (actionId == QLatin1String("forgetAll")) { + if (sourceModel()) { + ResultModel *resultModel = static_cast(sourceModel()); + resultModel->forgetAllResources(); + } + + return false; + } + + return false; +} + +bool RecentContactsModel::hasActions() const +{ + return rowCount(); +} + +QVariantList RecentContactsModel::actions() const +{ + QVariantList actionList; + + if (rowCount()) { + actionList << Kicker::createActionItem(i18n("Forget All Contacts"), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); + } + + return actionList; +} + +void RecentContactsModel::refresh() +{ + QObject *oldModel = sourceModel(); + + // clang-format off + auto query = UsedResources + | RecentlyUsedFirst + | Agent(QStringLiteral("KTp")) + | Type::any() + | Activity::current() + | Url::startsWith(QStringLiteral("ktp")) + | Limit(15); + // clang-format on + + ResultModel *model = new ResultModel(query); + + QModelIndex index; + + if (model->canFetchMore(index)) { + model->fetchMore(index); + } + + // FIXME TODO: Don't wipe entire cache on transactions. + connect(model, &QAbstractItemModel::rowsInserted, this, &RecentContactsModel::buildCache, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsRemoved, this, &RecentContactsModel::buildCache, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::rowsMoved, this, &RecentContactsModel::buildCache, Qt::UniqueConnection); + connect(model, &QAbstractItemModel::modelReset, this, &RecentContactsModel::buildCache, Qt::UniqueConnection); + + setSourceModel(model); + + buildCache(); + + delete oldModel; +} + +void RecentContactsModel::buildCache() +{ + qDeleteAll(m_idToData); + m_idToData.clear(); + m_dataToRow.clear(); + + QString id; + + for (int i = 0; i < sourceModel()->rowCount(); ++i) { + id = sourceModel()->data(sourceModel()->index(i, 0), ResultModel::ResourceRole).toString(); + + if (!m_idToData.contains(id)) { + insertPersonData(id, i); + } + } +} + +void RecentContactsModel::insertPersonData(const QString &id, int row) +{ + KPeople::PersonData *data = new KPeople::PersonData(id); + + m_idToData[id] = data; + m_dataToRow[data] = row; + + connect(data, &KPeople::PersonData::dataChanged, this, &RecentContactsModel::personDataChanged); +} + +void RecentContactsModel::personDataChanged() +{ + KPeople::PersonData *data = static_cast(sender()); + + if (m_dataToRow.contains(data)) { + int row = m_dataToRow[data]; + + QModelIndex idx = sourceModel()->index(row, 0); + + Q_EMIT dataChanged(idx, idx); + } +} diff --git a/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.h b/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.h new file mode 100644 index 0000000000..43d9a14e26 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/recentcontactsmodel.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "forwardingmodel.h" + +namespace KPeople +{ +class PersonData; +} + +class RecentContactsModel : public ForwardingModel +{ + Q_OBJECT + +public: + explicit RecentContactsModel(QObject *parent = nullptr); + ~RecentContactsModel() override; + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool hasActions() const override; + QVariantList actions() const override; + +private Q_SLOTS: + void refresh() override; + void buildCache(); + void personDataChanged(); + +private: + void insertPersonData(const QString &id, int row); + + QHash m_idToData; + QHash m_dataToRow; +}; diff --git a/plasma/workspace/applets/kicker/plugin/recentusagemodel.cpp b/plasma/workspace/applets/kicker/plugin/recentusagemodel.cpp new file mode 100644 index 0000000000..ad523925c1 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/recentusagemodel.cpp @@ -0,0 +1,564 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "recentusagemodel.h" +#include "actionlist.h" +#include "appentry.h" +#include "appsmodel.h" +#include "debug.h" +#include "kastatsfavoritesmodel.h" +#include + +#include + +#include +#include +#include +#include +#include +#if HAVE_X11 +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace KAStats = KActivities::Stats; + +using namespace KAStats; +using namespace KAStats::Terms; + +GroupSortProxy::GroupSortProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel) + : QSortFilterProxyModel(parentModel) +{ + sourceModel->setParent(this); + setSourceModel(sourceModel); + sort(0); +} + +GroupSortProxy::~GroupSortProxy() +{ +} + +InvalidAppsFilterProxy::InvalidAppsFilterProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel) + : QSortFilterProxyModel(parentModel) + , m_parentModel(parentModel) +{ + connect(parentModel, &AbstractModel::favoritesModelChanged, this, &InvalidAppsFilterProxy::connectNewFavoritesModel); + connectNewFavoritesModel(); + + sourceModel->setParent(this); + setSourceModel(sourceModel); +} + +InvalidAppsFilterProxy::~InvalidAppsFilterProxy() +{ +} + +void InvalidAppsFilterProxy::connectNewFavoritesModel() +{ + KAStatsFavoritesModel *favoritesModel = static_cast(m_parentModel->favoritesModel()); + if (favoritesModel) { + connect(favoritesModel, &KAStatsFavoritesModel::favoritesChanged, this, &QSortFilterProxyModel::invalidate); + } + + invalidate(); +} + +bool InvalidAppsFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_parent); + + const QString resource = sourceModel()->index(source_row, 0).data(ResultModel::ResourceRole).toString(); + + if (resource.startsWith(QLatin1String("applications:"))) { + KService::Ptr service = KService::serviceByStorageId(resource.section(QLatin1Char(':'), 1)); + + KAStatsFavoritesModel *favoritesModel = m_parentModel ? static_cast(m_parentModel->favoritesModel()) : nullptr; + + return (service && (!favoritesModel || !favoritesModel->isFavorite(service->storageId()))); + } + + return true; +} + +bool InvalidAppsFilterProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + return (left.row() < right.row()); +} + +bool GroupSortProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + const QString &lResource = sourceModel()->data(left, ResultModel::ResourceRole).toString(); + const QString &rResource = sourceModel()->data(right, ResultModel::ResourceRole).toString(); + + if (lResource.startsWith(QLatin1String("applications:")) && !rResource.startsWith(QLatin1String("applications:"))) { + return true; + } else if (!lResource.startsWith(QLatin1String("applications:")) && rResource.startsWith(QLatin1String("applications:"))) { + return false; + } + + return (left.row() < right.row()); +} + +RecentUsageModel::RecentUsageModel(QObject *parent, IncludeUsage usage, int ordering) + : ForwardingModel(parent) + , m_usage(usage) + , m_ordering((Ordering)ordering) + , m_complete(false) + , m_placesModel(new KFilePlacesModel(this)) +{ + refresh(); +} + +RecentUsageModel::~RecentUsageModel() +{ +} + +void RecentUsageModel::setShownItems(IncludeUsage usage) +{ + if (m_usage == usage) { + return; + } + + m_usage = usage; + + Q_EMIT shownItemsChanged(); + refresh(); +} + +RecentUsageModel::IncludeUsage RecentUsageModel::shownItems() const +{ + return m_usage; +} + +QString RecentUsageModel::description() const +{ + switch (m_usage) { + case AppsAndDocs: + return i18n("Recently Used"); + case OnlyApps: + return i18n("Applications"); + case OnlyDocs: + default: + return i18n("Files"); + } +} + +QString RecentUsageModel::resourceAt(int row) const +{ + return rowValueAt(row, ResultModel::ResourceRole).toString(); +} + +QVariant RecentUsageModel::rowValueAt(int row, ResultModel::Roles role) const +{ + QSortFilterProxyModel *sourceProxy = qobject_cast(sourceModel()); + + if (sourceProxy) { + return sourceProxy->sourceModel()->data(sourceProxy->mapToSource(sourceProxy->index(row, 0)), role).toString(); + } + + return sourceModel()->data(index(row, 0), role); +} + +QVariant RecentUsageModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + const QString &resource = resourceAt(index.row()); + + if (resource.startsWith(QLatin1String("applications:"))) { + return appData(resource, role); + } else { + return docData(resource, role); + } +} + +QVariant RecentUsageModel::appData(const QString &resource, int role) const +{ + const QString storageId = resource.section(QLatin1Char(':'), 1); + KService::Ptr service = KService::serviceByStorageId(storageId); + + QStringList allowedTypes({QLatin1String("Service"), QLatin1String("Application")}); + + if (!service || !allowedTypes.contains(service->property(QLatin1String("Type")).toString()) || service->exec().isEmpty()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + AppsModel *parentModel = qobject_cast(QObject::parent()); + + if (parentModel) { + return AppEntry::nameFromService(service, (AppEntry::NameFormat)qobject_cast(QObject::parent())->appNameFormat()); + } else { + return AppEntry::nameFromService(service, AppEntry::NameOnly); + } + } else if (role == Qt::DecorationRole) { + return service->icon(); + } else if (role == Kicker::DescriptionRole) { + return service->comment(); + } else if (role == Kicker::GroupRole) { + return i18n("Applications"); + } else if (role == Kicker::FavoriteIdRole) { + return service->storageId(); + } else if (role == Kicker::HasActionListRole) { + return true; + } else if (role == Kicker::ActionListRole) { + QVariantList actionList; + + const QVariantList &jumpList = Kicker::jumpListActions(service); + if (!jumpList.isEmpty()) { + actionList << jumpList << Kicker::createSeparatorActionItem(); + } + + const QVariantList &recentDocuments = Kicker::recentDocumentActions(service); + if (!recentDocuments.isEmpty()) { + actionList << recentDocuments << Kicker::createSeparatorActionItem(); + } + + const QVariantMap &forgetAction = Kicker::createActionItem(i18n("Forget Application"), QStringLiteral("edit-clear-history"), QStringLiteral("forget")); + actionList << forgetAction; + + const QVariantMap &forgetAllAction = Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); + actionList << forgetAllAction; + + return actionList; + } + + return QVariant(); +} + +QModelIndex RecentUsageModel::findPlaceForKFileItem(const KFileItem &fileItem) const +{ + const auto index = m_placesModel->closestItem(fileItem.url()); + if (index.isValid()) { + const auto parentUrl = m_placesModel->url(index); + if (parentUrl == fileItem.url()) { + return index; + } + } + return QModelIndex(); +} + +QVariant RecentUsageModel::docData(const QString &resource, int role) const +{ + QUrl url(resource); + + if (url.scheme().isEmpty()) { + url.setScheme(QStringLiteral("file")); + } + + auto getFileItem = [=]() { + // Avoid calling QT_LSTAT and accessing recent documents + return KFileItem(url, KFileItem::SkipMimeTypeFromContent); + }; + + if (!url.isValid()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + auto fileItem = getFileItem(); + const auto index = findPlaceForKFileItem(fileItem); + if (index.isValid()) { + return m_placesModel->text(index); + } + return fileItem.text(); + } else if (role == Qt::DecorationRole) { + auto fileItem = getFileItem(); + const auto index = findPlaceForKFileItem(fileItem); + if (index.isValid()) { + return m_placesModel->icon(index); + } + return QIcon::fromTheme(fileItem.iconName(), QIcon::fromTheme(QStringLiteral("unknown"))); + } else if (role == Kicker::GroupRole) { + return i18n("Files"); + } else if (role == Kicker::FavoriteIdRole || role == Kicker::UrlRole) { + return url.toString(); + } else if (role == Kicker::DescriptionRole) { + auto fileItem = getFileItem(); + QString desc = fileItem.localPath(); + + const auto index = m_placesModel->closestItem(fileItem.url()); + if (index.isValid()) { + // the current file has a parent in placesModel + const auto parentUrl = m_placesModel->url(index); + if (parentUrl == fileItem.url()) { + // if the current item is a place + return QString(); + } + desc.truncate(desc.lastIndexOf(QChar('/'))); + const auto text = m_placesModel->text(index); + desc.replace(0, parentUrl.path().length(), text); + } else { + // remove filename + desc.truncate(desc.lastIndexOf(QChar('/'))); + } + return desc; + } else if (role == Kicker::UrlRole) { + return url; + } else if (role == Kicker::HasActionListRole) { + return true; + } else if (role == Kicker::ActionListRole) { + auto fileItem = getFileItem(); + QVariantList actionList = Kicker::createActionListForFileItem(fileItem); + + actionList << Kicker::createSeparatorActionItem(); + + QVariantMap openParentFolder = + Kicker::createActionItem(i18n("Open Containing Folder"), QStringLiteral("folder-open"), QStringLiteral("openParentFolder")); + actionList << openParentFolder; + + QVariantMap forgetAction = Kicker::createActionItem(i18n("Forget File"), QStringLiteral("edit-clear-history"), QStringLiteral("forget")); + actionList << forgetAction; + + QVariantMap forgetAllAction = Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); + actionList << forgetAllAction; + + return actionList; + } + + return QVariant(); +} + +bool RecentUsageModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(argument) + + bool withinBounds = row >= 0 && row < rowCount(); + + if (actionId.isEmpty() && withinBounds) { + const QString &resource = resourceAt(row); + const QString &mimeType = rowValueAt(row, ResultModel::MimeType).toString(); + + if (!resource.startsWith(QLatin1String("applications:"))) { + const QUrl resourceUrl = docData(resource, Kicker::UrlRole).toUrl(); + + auto job = new KIO::OpenUrlJob(resourceUrl); + job->setRunExecutables(false); + job->start(); + + return true; + } + + const QString storageId = resource.section(QLatin1Char(':'), 1); + KService::Ptr service = KService::serviceByStorageId(storageId); + + if (!service) { + return false; + } + + quint32 timeStamp = 0; + +#if HAVE_X11 + if (QX11Info::isPlatformX11()) { + timeStamp = QX11Info::appUserTime(); + } +#endif + + // prevents using a service file that does not support opening a mime type for a file it created + // for instance a screenshot tool + if (!mimeType.simplified().isEmpty()) { + if (!service->hasMimeType(mimeType)) { + // needs to find the application that supports this mimetype + service = KApplicationTrader::preferredService(mimeType); + + if (!service) { + // no service found to handle the mimetype + return false; + } else { + qCWarning(KICKER_DEBUG) << "Preventing the file to open with " << service->desktopEntryName() << "no alternative found"; + } + } + } + + auto *job = new KIO::ApplicationLauncherJob(service); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->setStartupId(KStartupInfo::createNewStartupIdForTimestamp(timeStamp)); + job->start(); + + KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + storageId), QStringLiteral("org.kde.plasma.kicker")); + + return true; + } else if (actionId == QLatin1String("forget") && withinBounds) { + if (m_activitiesModel) { + QModelIndex idx = sourceModel()->index(row, 0); + QSortFilterProxyModel *sourceProxy = qobject_cast(sourceModel()); + + while (sourceProxy) { + idx = sourceProxy->mapToSource(idx); + sourceProxy = qobject_cast(sourceProxy->sourceModel()); + } + + static_cast(m_activitiesModel.data())->forgetResource(idx.row()); + } + + return false; + } else if (actionId == QLatin1String("openParentFolder") && withinBounds) { + const auto url = QUrl::fromUserInput(resourceAt(row)); + KIO::highlightInFileManager({url}); + } else if (actionId == QLatin1String("forgetAll")) { + if (m_activitiesModel) { + static_cast(m_activitiesModel.data())->forgetAllResources(); + } + + return false; + } else if (actionId == QLatin1String("_kicker_jumpListAction")) { + const QString storageId = sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString().section(QLatin1Char(':'), 1); + KService::Ptr service = KService::serviceByStorageId(storageId); + service->setExec(argument.toString()); + KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service); + job->start(); + return true; + } else if (withinBounds) { + const QString &resource = resourceAt(row); + + if (resource.startsWith(QLatin1String("applications:"))) { + const QString storageId = sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString().section(QLatin1Char(':'), 1); + KService::Ptr service = KService::serviceByStorageId(storageId); + + if (service) { + return Kicker::handleRecentDocumentAction(service, actionId, argument); + } + } else { + bool close = false; + + QUrl url(sourceModel()->data(sourceModel()->index(row, 0), ResultModel::ResourceRole).toString()); + + KFileItem item(url); + + if (Kicker::handleFileItemAction(item, actionId, argument, &close)) { + return close; + } + } + } + + return false; +} + +bool RecentUsageModel::hasActions() const +{ + return rowCount(); +} + +QVariantList RecentUsageModel::actions() const +{ + QVariantList actionList; + + if (rowCount()) { + actionList << Kicker::createActionItem(forgetAllActionName(), QStringLiteral("edit-clear-history"), QStringLiteral("forgetAll")); + } + + return actionList; +} + +QString RecentUsageModel::forgetAllActionName() const +{ + switch (m_usage) { + case AppsAndDocs: + return i18n("Forget All"); + case OnlyApps: + return i18n("Forget All Applications"); + case OnlyDocs: + default: + return i18n("Forget All Files"); + } +} + +void RecentUsageModel::setOrdering(int ordering) +{ + if (ordering == m_ordering) + return; + + m_ordering = (Ordering)ordering; + refresh(); + + Q_EMIT orderingChanged(ordering); +} + +int RecentUsageModel::ordering() const +{ + return m_ordering; +} + +void RecentUsageModel::classBegin() +{ +} + +void RecentUsageModel::componentComplete() +{ + m_complete = true; + + refresh(); +} + +void RecentUsageModel::refresh() +{ + if (qmlEngine(this) && !m_complete) { + return; + } + + QAbstractItemModel *oldModel = sourceModel(); + disconnectSignals(); + setSourceModel(nullptr); + delete oldModel; + + // clang-format off + auto query = UsedResources + | (m_ordering == Recent ? RecentlyUsedFirst : HighScoredFirst) + | Agent::any() + | (m_usage == OnlyDocs ? Type::files() : Type::any()) + | Activity::current(); + // clang-format on + + switch (m_usage) { + case AppsAndDocs: { + query = query | Url::startsWith(QStringLiteral("applications:")) | Url::file() | Limit(30); + break; + } + case OnlyApps: { + query = query | Url::startsWith(QStringLiteral("applications:")) | Limit(15); + break; + } + case OnlyDocs: + default: { + query = query | Url::file() | Limit(15); + } + } + + m_activitiesModel = new ResultModel(query); + QAbstractItemModel *model = m_activitiesModel; + + QModelIndex index; + + if (model->canFetchMore(index)) { + model->fetchMore(index); + } + + if (m_usage != OnlyDocs) { + model = new InvalidAppsFilterProxy(this, model); + } + + if (m_usage == AppsAndDocs) { + model = new GroupSortProxy(this, model); + } + + setSourceModel(model); +} diff --git a/plasma/workspace/applets/kicker/plugin/recentusagemodel.h b/plasma/workspace/applets/kicker/plugin/recentusagemodel.h new file mode 100644 index 0000000000..64ddc7fed9 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/recentusagemodel.h @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "forwardingmodel.h" + +#include +#include +#include +#include + +class QModelIndex; +class KFileItem; + +class GroupSortProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit GroupSortProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel); + ~GroupSortProxy() override; + +protected: + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; +}; + +class InvalidAppsFilterProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit InvalidAppsFilterProxy(AbstractModel *parentModel, QAbstractItemModel *sourceModel); + ~InvalidAppsFilterProxy() override; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private Q_SLOTS: + void connectNewFavoritesModel(); + +private: + QPointer m_parentModel; +}; + +class RecentUsageModel : public ForwardingModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(int ordering READ ordering WRITE setOrdering NOTIFY orderingChanged) + Q_PROPERTY(IncludeUsage shownItems READ shownItems WRITE setShownItems NOTIFY shownItemsChanged) + +public: + enum IncludeUsage { + AppsAndDocs, + OnlyApps, + OnlyDocs, + }; + Q_ENUM(IncludeUsage) + + enum Ordering { + Recent, + Popular, + }; + + explicit RecentUsageModel(QObject *parent = nullptr, IncludeUsage usage = AppsAndDocs, int ordering = Recent); + ~RecentUsageModel() override; + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool hasActions() const override; + QVariantList actions() const override; + + void setShownItems(IncludeUsage usage); + IncludeUsage shownItems() const; + + void setOrdering(int ordering); + int ordering() const; + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void orderingChanged(int ordering); + void shownItemsChanged(); + +private Q_SLOTS: + void refresh() override; + +private: + QVariant appData(const QString &resource, int role) const; + QVariant docData(const QString &resource, int role) const; + + QString resourceAt(int row) const; + QVariant rowValueAt(int row, KActivities::Stats::ResultModel::Roles role) const; + + QString forgetAllActionName() const; + + QModelIndex findPlaceForKFileItem(const KFileItem &fileItem) const; + + IncludeUsage m_usage; + QPointer m_activitiesModel; + + Ordering m_ordering; + + bool m_complete; + KFilePlacesModel *m_placesModel; +}; diff --git a/plasma/workspace/applets/kicker/plugin/rootmodel.cpp b/plasma/workspace/applets/kicker/plugin/rootmodel.cpp new file mode 100644 index 0000000000..4ec3236839 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/rootmodel.cpp @@ -0,0 +1,466 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "rootmodel.h" +#include "actionlist.h" +#include "kastatsfavoritesmodel.h" +#include "recentcontactsmodel.h" +#include "recentusagemodel.h" +#include "systemmodel.h" + +#include + +#include + +GroupEntry::GroupEntry(AppsModel *parentModel, const QString &name, const QString &iconName, AbstractModel *childModel) + : AbstractGroupEntry(parentModel) + , m_name(name) + , m_iconName(iconName) + , m_childModel(childModel) +{ + QObject::connect(parentModel, &RootModel::cleared, childModel, &AbstractModel::deleteLater); + + QObject::connect(childModel, &AbstractModel::countChanged, [parentModel, this] { + if (parentModel) { + parentModel->entryChanged(this); + } + }); +} + +QString GroupEntry::name() const +{ + return m_name; +} + +QIcon GroupEntry::icon() const +{ + return QIcon::fromTheme(m_iconName, QIcon::fromTheme(QStringLiteral("unknown"))); +} + +bool GroupEntry::hasChildren() const +{ + return m_childModel && m_childModel->count() > 0; +} + +AbstractModel *GroupEntry::childModel() const +{ + return m_childModel; +} + +RootModel::RootModel(QObject *parent) + : AppsModel(QString(), parent) + , m_favorites(new KAStatsFavoritesModel(this)) + , m_systemModel(nullptr) + , m_showAllApps(false) + , m_showAllAppsCategorized(false) + , m_showRecentApps(true) + , m_showRecentDocs(true) + , m_showRecentContacts(false) + , m_recentOrdering(RecentUsageModel::Recent) + , m_showPowerSession(true) + , m_showFavoritesPlaceholder(false) + , m_recentAppsModel(nullptr) + , m_recentDocsModel(nullptr) + , m_recentContactsModel(nullptr) +{ +} + +RootModel::~RootModel() +{ +} + +QVariant RootModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_entryList.count()) { + return QVariant(); + } + + if (role == Kicker::HasActionListRole || role == Kicker::ActionListRole) { + const AbstractEntry *entry = m_entryList.at(index.row()); + + if (entry->type() == AbstractEntry::GroupType) { + const GroupEntry *group = static_cast(entry); + AbstractModel *model = group->childModel(); + + if (model == m_recentAppsModel || model == m_recentDocsModel || model == m_recentContactsModel) { + if (role == Kicker::HasActionListRole) { + return true; + } else if (role == Kicker::ActionListRole) { + QVariantList actionList; + actionList << model->actions(); + actionList << Kicker::createSeparatorActionItem(); + actionList << Kicker::createActionItem(i18n("Hide %1", group->name()), QStringLiteral("view-hidden"), QStringLiteral("hideCategory")); + return actionList; + } + } + } + } + + return AppsModel::data(index, role); +} + +bool RootModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + const AbstractEntry *entry = m_entryList.at(row); + + if (entry->type() == AbstractEntry::GroupType) { + if (actionId == QLatin1String("hideCategory")) { + AbstractModel *model = entry->childModel(); + + if (model == m_recentAppsModel) { + setShowRecentApps(false); + + return true; + } else if (model == m_recentDocsModel) { + setShowRecentDocs(false); + + return true; + } else if (model == m_recentContactsModel) { + setShowRecentContacts(false); + + return true; + } + } else if (entry->childModel()->hasActions()) { + return entry->childModel()->trigger(-1, actionId, QVariant()); + } + } + + return AppsModel::trigger(row, actionId, argument); +} + +bool RootModel::showAllApps() const +{ + return m_showAllApps; +} + +void RootModel::setShowAllApps(bool show) +{ + if (m_showAllApps != show) { + m_showAllApps = show; + + refresh(); + + Q_EMIT showAllAppsChanged(); + } +} + +bool RootModel::showAllAppsCategorized() const +{ + return m_showAllAppsCategorized; +} + +void RootModel::setShowAllAppsCategorized(bool showCategorized) +{ + if (m_showAllAppsCategorized != showCategorized) { + m_showAllAppsCategorized = showCategorized; + + refresh(); + + Q_EMIT showAllAppsCategorizedChanged(); + } +} + +bool RootModel::showRecentApps() const +{ + return m_showRecentApps; +} + +void RootModel::setShowRecentApps(bool show) +{ + if (show != m_showRecentApps) { + m_showRecentApps = show; + + refresh(); + + Q_EMIT showRecentAppsChanged(); + } +} + +bool RootModel::showRecentDocs() const +{ + return m_showRecentDocs; +} + +void RootModel::setShowRecentDocs(bool show) +{ + if (show != m_showRecentDocs) { + m_showRecentDocs = show; + + refresh(); + + Q_EMIT showRecentDocsChanged(); + } +} + +bool RootModel::showRecentContacts() const +{ + return m_showRecentContacts; +} + +void RootModel::setShowRecentContacts(bool show) +{ + if (show != m_showRecentContacts) { + m_showRecentContacts = show; + + refresh(); + + Q_EMIT showRecentContactsChanged(); + } +} + +int RootModel::recentOrdering() const +{ + return m_recentOrdering; +} + +void RootModel::setRecentOrdering(int ordering) +{ + if (ordering != m_recentOrdering) { + m_recentOrdering = ordering; + + refresh(); + + Q_EMIT recentOrderingChanged(); + } +} + +bool RootModel::showPowerSession() const +{ + return m_showPowerSession; +} + +void RootModel::setShowPowerSession(bool show) +{ + if (show != m_showPowerSession) { + m_showPowerSession = show; + + refresh(); + + Q_EMIT showPowerSessionChanged(); + } +} + +bool RootModel::showFavoritesPlaceholder() const +{ + return m_showFavoritesPlaceholder; +} + +void RootModel::setShowFavoritesPlaceholder(bool show) +{ + if (show != m_showFavoritesPlaceholder) { + m_showFavoritesPlaceholder = show; + + refresh(); + + Q_EMIT showFavoritesPlaceholderChanged(); + } +} + +AbstractModel *RootModel::favoritesModel() +{ + return m_favorites; +} + +AbstractModel *RootModel::systemFavoritesModel() +{ + if (m_systemModel) { + return m_systemModel->favoritesModel(); + } + + return nullptr; +} + +void RootModel::refresh() +{ + if (!m_complete) { + return; + } + + beginResetModel(); + + AppsModel::refreshInternal(); + + AppsModel *allModel = nullptr; + m_recentAppsModel = nullptr; + m_recentDocsModel = nullptr; + m_recentContactsModel = nullptr; + + if (m_showAllApps) { + QHash appsHash; + + std::function processEntry = [&](AbstractEntry *entry) { + if (entry->type() == AbstractEntry::RunnableType) { + AppEntry *appEntry = static_cast(entry); + appsHash.insert(appEntry->service()->menuId(), appEntry); + } else if (entry->type() == AbstractEntry::GroupType) { + GroupEntry *groupEntry = static_cast(entry); + AbstractModel *model = groupEntry->childModel(); + + if (!model) { + return; + } + + for (int i = 0; i < model->count(); ++i) { + processEntry(static_cast(model->index(i, 0).internalPointer())); + } + } + }; + + for (AbstractEntry *entry : qAsConst(m_entryList)) { + processEntry(entry); + } + + QList apps(appsHash.values()); + QCollator c; + + std::sort(apps.begin(), apps.end(), [&c](AbstractEntry *a, AbstractEntry *b) { + if (a->type() != b->type()) { + return a->type() > b->type(); + } else { + return c.compare(a->name(), b->name()) < 0; + } + }); + + if (!m_showAllAppsCategorized && !m_paginate) { // The app list built above goes into a model. + allModel = new AppsModel(apps, false, this); + } else if (m_paginate) { // We turn the apps list into a subtree of pages. + m_favorites = new KAStatsFavoritesModel(this); + Q_EMIT favoritesModelChanged(); + + QList groups; + + int at = 0; + QList page; + page.reserve(m_pageSize); + + for (AbstractEntry *app : std::as_const(apps)) { + page.append(app); + + if (at == (m_pageSize - 1)) { + at = 0; + AppsModel *model = new AppsModel(page, false, this); + groups.append(new GroupEntry(this, QString(), QString(), model)); + page.clear(); + } else { + ++at; + } + } + + if (!page.isEmpty()) { + AppsModel *model = new AppsModel(page, false, this); + groups.append(new GroupEntry(this, QString(), QString(), model)); + } + + groups.prepend(new GroupEntry(this, QString(), QString(), m_favorites)); + + allModel = new AppsModel(groups, true, this); + } else { // We turn the apps list into a subtree of apps by starting letter. + QList groups; + QHash> m_categoryHash; + + for (const AbstractEntry *groupEntry : std::as_const(m_entryList)) { + AbstractModel *model = groupEntry->childModel(); + + if (!model) + continue; + + for (int i = 0; i < model->count(); ++i) { + AbstractEntry *appEntry = static_cast(model->index(i, 0).internalPointer()); + + if (appEntry->name().isEmpty()) { + continue; + } + + const QChar &first = appEntry->name().at(0).toUpper(); + m_categoryHash[first.isDigit() ? QStringLiteral("0-9") : first].append(appEntry); + } + } + + QHashIterator> i(m_categoryHash); + + while (i.hasNext()) { + i.next(); + AppsModel *model = new AppsModel(i.value(), false, this); + model->setDescription(i.key()); + groups.append(new GroupEntry(this, i.key(), QString(), model)); + } + + allModel = new AppsModel(groups, true, this); + } + + allModel->setDescription(QStringLiteral("KICKER_ALL_MODEL")); // Intentionally no i18n. + } + + int separatorPosition = 0; + + if (allModel) { + m_entryList.prepend(new GroupEntry(this, i18n("All Applications"), QStringLiteral("applications-all"), allModel)); + ++separatorPosition; + } + + if (m_showFavoritesPlaceholder) { + // This entry is a placeholder and shouldn't ever be visible + QList placeholderList; + AppsModel *placeholderModel = new AppsModel(placeholderList, false, this); + + // Favorites group containing a placeholder entry, so it would be considered as a group, not an entry + QList placeholderEntry; + placeholderEntry.append(new GroupEntry(this, // + i18n("This shouldn't be visible! Use KICKER_FAVORITES_MODEL"), + QStringLiteral("dialog-warning"), + placeholderModel)); + AppsModel *favoritesPlaceholderModel = new AppsModel(placeholderEntry, false, this); + + favoritesPlaceholderModel->setDescription(QStringLiteral("KICKER_FAVORITES_MODEL")); // Intentionally no i18n. + m_entryList.prepend(new GroupEntry(this, i18n("Favorites"), QStringLiteral("bookmarks"), favoritesPlaceholderModel)); + ++separatorPosition; + } + + if (m_showRecentContacts) { + m_recentContactsModel = new RecentContactsModel(this); + m_entryList.prepend(new GroupEntry(this, i18n("Recent Contacts"), QStringLiteral("view-history"), m_recentContactsModel)); + ++separatorPosition; + } + + if (m_showRecentDocs) { + m_recentDocsModel = new RecentUsageModel(this, RecentUsageModel::OnlyDocs, m_recentOrdering); + m_entryList.prepend(new GroupEntry(this, + m_recentOrdering == RecentUsageModel::Recent ? i18n("Recent Files") : i18n("Often Used Files"), + m_recentOrdering == RecentUsageModel::Recent ? QStringLiteral("view-history") : QStringLiteral("office-chart-pie"), + m_recentDocsModel)); + ++separatorPosition; + } + + if (m_showRecentApps) { + m_recentAppsModel = new RecentUsageModel(this, RecentUsageModel::OnlyApps, m_recentOrdering); + m_entryList.prepend(new GroupEntry(this, + m_recentOrdering == RecentUsageModel::Recent ? i18n("Recent Applications") : i18n("Often Used Applications"), + m_recentOrdering == RecentUsageModel::Recent ? QStringLiteral("view-history") : QStringLiteral("office-chart-pie"), + m_recentAppsModel)); + ++separatorPosition; + } + + if (m_showSeparators && separatorPosition > 0) { + m_entryList.insert(separatorPosition, new SeparatorEntry(this)); + ++m_separatorCount; + } + + m_systemModel = new SystemModel(this); + + if (m_showPowerSession) { + m_entryList << new GroupEntry(this, i18n("Power / Session"), QStringLiteral("system-log-out"), m_systemModel); + } + + endResetModel(); + + m_favorites->refresh(); + + Q_EMIT systemFavoritesModelChanged(); + Q_EMIT countChanged(); + Q_EMIT separatorCountChanged(); + + Q_EMIT refreshed(); +} diff --git a/plasma/workspace/applets/kicker/plugin/rootmodel.h b/plasma/workspace/applets/kicker/plugin/rootmodel.h new file mode 100644 index 0000000000..97c831da23 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/rootmodel.h @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "appsmodel.h" + +class KAStatsFavoritesModel; +class RecentContactsModel; +class RecentUsageModel; +class SystemModel; + +class RootModel; + +class GroupEntry : public AbstractGroupEntry +{ +public: + GroupEntry(AppsModel *parentModel, const QString &name, const QString &iconName, AbstractModel *childModel); + + QIcon icon() const override; + QString name() const override; + + bool hasChildren() const override; + AbstractModel *childModel() const override; + +private: + QString m_name; + QString m_iconName; + QPointer m_childModel; +}; + +class RootModel : public AppsModel +{ + Q_OBJECT + + Q_PROPERTY(QObject *systemFavoritesModel READ systemFavoritesModel NOTIFY systemFavoritesModelChanged) + Q_PROPERTY(bool showAllApps READ showAllApps WRITE setShowAllApps NOTIFY showAllAppsChanged) + Q_PROPERTY(bool showAllAppsCategorized READ showAllAppsCategorized WRITE setShowAllAppsCategorized NOTIFY showAllAppsCategorizedChanged) + Q_PROPERTY(bool showRecentApps READ showRecentApps WRITE setShowRecentApps NOTIFY showRecentAppsChanged) + Q_PROPERTY(bool showRecentDocs READ showRecentDocs WRITE setShowRecentDocs NOTIFY showRecentDocsChanged) + Q_PROPERTY(bool showRecentContacts READ showRecentContacts WRITE setShowRecentContacts NOTIFY showRecentContactsChanged) + Q_PROPERTY(int recentOrdering READ recentOrdering WRITE setRecentOrdering NOTIFY recentOrderingChanged) + Q_PROPERTY(bool showPowerSession READ showPowerSession WRITE setShowPowerSession NOTIFY showPowerSessionChanged) + Q_PROPERTY(bool showFavoritesPlaceholder READ showFavoritesPlaceholder WRITE setShowFavoritesPlaceholder NOTIFY showFavoritesPlaceholderChanged) + +public: + explicit RootModel(QObject *parent = nullptr); + ~RootModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool showAllApps() const; + void setShowAllApps(bool show); + + bool showAllAppsCategorized() const; + void setShowAllAppsCategorized(bool showCategorized); + + bool showRecentApps() const; + void setShowRecentApps(bool show); + + bool showRecentDocs() const; + void setShowRecentDocs(bool show); + + bool showRecentContacts() const; + void setShowRecentContacts(bool show); + + int recentOrdering() const; + void setRecentOrdering(int ordering); + + bool showPowerSession() const; + void setShowPowerSession(bool show); + + bool showFavoritesPlaceholder() const; + void setShowFavoritesPlaceholder(bool show); + + AbstractModel *favoritesModel() override; + AbstractModel *systemFavoritesModel(); + +Q_SIGNALS: + void refreshed() const; + void systemFavoritesModelChanged() const; + void showAllAppsChanged() const; + void showAllAppsCategorizedChanged() const; + void showRecentAppsChanged() const; + void showRecentDocsChanged() const; + void showRecentContactsChanged() const; + void showPowerSessionChanged() const; + void recentOrderingChanged() const; + void recentAppsModelChanged() const; + void showFavoritesPlaceholderChanged() const; + +protected Q_SLOTS: + void refresh() override; + +private: + KAStatsFavoritesModel *m_favorites; + SystemModel *m_systemModel; + + bool m_showAllApps; + bool m_showAllAppsCategorized; + bool m_showRecentApps; + bool m_showRecentDocs; + bool m_showRecentContacts; + int m_recentOrdering; + bool m_showPowerSession; + bool m_showFavoritesPlaceholder; + + RecentUsageModel *m_recentAppsModel; + RecentUsageModel *m_recentDocsModel; + RecentContactsModel *m_recentContactsModel; +}; diff --git a/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.cpp b/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.cpp new file mode 100644 index 0000000000..4804d2e126 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.cpp @@ -0,0 +1,256 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "runnermatchesmodel.h" +#include "actionlist.h" +#include "runnermodel.h" + +#include +#include +#include + +#include +#include +#include + +#include + +RunnerMatchesModel::RunnerMatchesModel(const QString &runnerId, const QString &name, Plasma::RunnerManager *manager, QObject *parent) + : AbstractModel(parent) + , m_runnerId(runnerId) + , m_name(name) + , m_runnerManager(manager) +{ +} + +QString RunnerMatchesModel::description() const +{ + return m_name; +} + +QVariant RunnerMatchesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_matches.count()) { + return QVariant(); + } + + Plasma::QueryMatch match = m_matches.at(index.row()); + + if (role == Qt::DisplayRole) { + return match.text(); + } else if (role == Qt::DecorationRole) { + if (!match.iconName().isEmpty()) { + return match.iconName(); + } + + return match.icon(); + } else if (role == Kicker::DescriptionRole) { + return match.subtext(); + } else if (role == Kicker::FavoriteIdRole) { + if (match.runner()->id() == QLatin1String("services")) { + return match.data().toString(); + } + } else if (role == Kicker::UrlRole) { + const QString &runnerId = match.runner()->id(); + if (runnerId == QLatin1String("baloosearch") || runnerId == QLatin1String("bookmarks")) { + return QUrl(match.data().toString()); + } else if (runnerId == QLatin1String("recentdocuments") || runnerId == QLatin1String("services")) { + KService::Ptr service = KService::serviceByStorageId(match.data().toString()); + if (service) { + return QUrl::fromLocalFile(Kicker::resolvedServiceEntryPath(service)); + } + } + } else if (role == Kicker::HasActionListRole) { + return match.runner()->id() == QLatin1String("services") || !match.runner()->findChildren().isEmpty(); + } else if (role == Kicker::IsMultilineTextRole) { + return match.isMultiLine(); + } else if (role == Kicker::ActionListRole) { + QVariantList actionList; + const QList actions = m_runnerManager->actionsForMatch(match); + for (QAction *action : actions) { + QVariantMap item = Kicker::createActionItem(action->text(), // + action->icon().name(), + QStringLiteral("runnerAction"), + QVariant::fromValue(action)); + + actionList << item; + } + + // Only try to get a KService for matches from the services and systemsettings runner. Assuming + // that any other runner returns something we want to turn into a KService is + // unsafe, e.g. files from the Baloo runner might match a storageId just by + // accident, creating a dangerous false positive. + if (match.runner()->id() != QLatin1String("services") && match.runner()->id() != QLatin1String("krunner_systemsettings")) { + return actionList; + } + + QUrl dataUrl(match.data().toUrl()); + if (dataUrl.isEmpty() && !match.urls().isEmpty()) { + // needed for systemsettigs runner + dataUrl = match.urls().constFirst(); + } + if (dataUrl.scheme() != QLatin1String("applications")) { + return actionList; + } + + // Don't offer jump list actions on a jump list action. + const QString actionName = QUrlQuery(dataUrl).queryItemValue(QStringLiteral("action")); + if (!actionName.isEmpty()) { + return actionList; + } + + const KService::Ptr service = KService::serviceByStorageId(dataUrl.path()); + if (service) { + if (!actionList.isEmpty()) { + actionList << Kicker::createSeparatorActionItem(); + } + + const QVariantList &jumpListActions = Kicker::jumpListActions(service); + if (!jumpListActions.isEmpty()) { + actionList << jumpListActions << Kicker::createSeparatorActionItem(); + } + + QObject *appletInterface = static_cast(parent())->appletInterface(); + + bool systemImmutable = false; + if (appletInterface) { + systemImmutable = (appletInterface->property("immutability").toInt() == Plasma::Types::SystemImmutable); + } + + const QVariantList &addLauncherActions = Kicker::createAddLauncherActionList(appletInterface, service); + if (!systemImmutable && !addLauncherActions.isEmpty()) { + actionList << addLauncherActions << Kicker::createSeparatorActionItem(); + } + + const QVariantList &recentDocuments = Kicker::recentDocumentActions(service); + if (!recentDocuments.isEmpty()) { + actionList << recentDocuments << Kicker::createSeparatorActionItem(); + } + + // Don't allow adding launchers, editing, hiding, or uninstalling applications + // when system is immutable. + if (systemImmutable) { + return actionList; + } + + if (service->isApplication()) { + actionList << Kicker::editApplicationAction(service); + actionList << Kicker::appstreamActions(service); + } + } + + return actionList; + } + + return QVariant(); +} + +int RunnerMatchesModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_matches.count(); +} + +bool RunnerMatchesModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (row < 0 || row >= m_matches.count()) { + return false; + } + + Plasma::QueryMatch match = m_matches.at(row); + + if (!match.isEnabled()) { + return false; + } + + QObject *appletInterface = static_cast(parent())->appletInterface(); + + KService::Ptr service = KService::serviceByStorageId(match.data().toUrl().toString(QUrl::RemoveScheme)); + if (!service && !match.urls().isEmpty()) { + // needed for systemsettigs runner + service = KService::serviceByStorageId(match.urls().constFirst().toString(QUrl::RemoveScheme)); + } + + if (Kicker::handleAddLauncherAction(actionId, appletInterface, service)) { + return false; // We don't want to close Kicker, BUG: 390585 + } else if (Kicker::handleEditApplicationAction(actionId, service)) { + return true; + } else if (Kicker::handleAppstreamActions(actionId, argument)) { + return true; + } else if (actionId == QLatin1String("_kicker_jumpListAction")) { + auto job = new KIO::CommandLauncherJob(argument.toString()); + job->setDesktopName(service->entryPath()); + job->setIcon(service->icon()); + return job->exec(); + } else if (actionId == QLatin1String("_kicker_recentDocument") || actionId == QLatin1String("_kicker_forgetRecentDocuments")) { + return Kicker::handleRecentDocumentAction(service, actionId, argument); + } + + if (!actionId.isEmpty()) { + QObject *obj = argument.value(); + + if (!obj) { + return false; + } + + QAction *action = qobject_cast(obj); + + if (!action) { + return false; + } + + match.setSelectedAction(action); + } + + m_runnerManager->run(match); + + return true; +} + +void RunnerMatchesModel::setMatches(const QList &matches) +{ + int oldCount = m_matches.count(); + int newCount = matches.count(); + + bool emitCountChange = (oldCount != newCount); + + int ceiling = qMin(oldCount, newCount); + bool emitDataChange = false; + + for (int row = 0; row < ceiling; ++row) { + if (!(m_matches.at(row) == matches.at(row))) { + emitDataChange = true; + m_matches[row] = matches.at(row); + } + } + + if (emitDataChange) { + Q_EMIT dataChanged(index(0, 0), index(ceiling - 1, 0)); + } + + if (newCount > oldCount) { + beginInsertRows(QModelIndex(), oldCount, newCount - 1); + + m_matches = matches; + + endInsertRows(); + } else if (newCount < oldCount) { + beginRemoveRows(QModelIndex(), newCount, oldCount - 1); + + m_matches = matches; + + endRemoveRows(); + } + + if (emitCountChange) { + Q_EMIT countChanged(); + } +} + +AbstractModel *RunnerMatchesModel::favoritesModel() +{ + return static_cast(parent())->favoritesModel(); +} diff --git a/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.h b/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.h new file mode 100644 index 0000000000..5b90d068d8 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/runnermatchesmodel.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include + +namespace Plasma +{ +class RunnerManager; +} + +class RunnerMatchesModel : public AbstractModel +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name CONSTANT) + +public: + explicit RunnerMatchesModel(const QString &runnerId, const QString &name, Plasma::RunnerManager *manager, QObject *parent = nullptr); + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + QString runnerId() const + { + return m_runnerId; + } + QString name() const + { + return m_name; + } + + void setMatches(const QList &matches); + + AbstractModel *favoritesModel() override; + +private: + QString m_runnerId; + QString m_name; + Plasma::RunnerManager *m_runnerManager; + QList m_matches; +}; diff --git a/plasma/workspace/applets/kicker/plugin/runnermodel.cpp b/plasma/workspace/applets/kicker/plugin/runnermodel.cpp new file mode 100644 index 0000000000..18518f5413 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/runnermodel.cpp @@ -0,0 +1,352 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "runnermodel.h" +#include "runnermatchesmodel.h" + +#include + +#include +#include +#include + +RunnerModel::RunnerModel(QObject *parent) + : QAbstractListModel(parent) + , m_favoritesModel(nullptr) + , m_appletInterface(nullptr) + , m_runnerManager(nullptr) + , m_mergeResults(false) + , m_deleteWhenEmpty(false) +{ + m_queryTimer.setSingleShot(true); + m_queryTimer.setInterval(10); + connect(&m_queryTimer, &QTimer::timeout, this, &RunnerModel::startQuery); +} + +RunnerModel::~RunnerModel() +{ +} + +QHash RunnerModel::roleNames() const +{ + return {{Qt::DisplayRole, "display"}}; +} + +AbstractModel *RunnerModel::favoritesModel() const +{ + return m_favoritesModel; +} + +void RunnerModel::setFavoritesModel(AbstractModel *model) +{ + if (m_favoritesModel != model) { + m_favoritesModel = model; + + clear(); + + if (!m_query.isEmpty()) { + m_queryTimer.start(); + } + + Q_EMIT favoritesModelChanged(); + } +} + +QObject *RunnerModel::appletInterface() const +{ + return m_appletInterface; +} + +void RunnerModel::setAppletInterface(QObject *appletInterface) +{ + if (m_appletInterface != appletInterface) { + m_appletInterface = appletInterface; + + clear(); + + if (!m_query.isEmpty()) { + m_queryTimer.start(); + } + + Q_EMIT appletInterfaceChanged(); + } +} + +bool RunnerModel::deleteWhenEmpty() const +{ + return m_deleteWhenEmpty; +} + +void RunnerModel::setDeleteWhenEmpty(bool deleteWhenEmpty) +{ + if (m_deleteWhenEmpty != deleteWhenEmpty) { + m_deleteWhenEmpty = deleteWhenEmpty; + + clear(); + + if (!m_query.isEmpty()) { + m_queryTimer.start(); + } + + Q_EMIT deleteWhenEmptyChanged(); + } +} + +bool RunnerModel::mergeResults() const +{ + return m_mergeResults; +} + +void RunnerModel::setMergeResults(bool merge) +{ + if (m_mergeResults != merge) { + m_mergeResults = merge; + + clear(); + + if (!m_query.isEmpty()) { + m_queryTimer.start(); + } + + Q_EMIT mergeResultsChanged(); + } +} + +QVariant RunnerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_models.count()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return m_models.at(index.row())->name(); + } + + return QVariant(); +} + +int RunnerModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_models.count(); +} + +int RunnerModel::count() const +{ + return rowCount(); +} + +QObject *RunnerModel::modelForRow(int row) +{ + if (row < 0 || row >= m_models.count()) { + return nullptr; + } + + return m_models.at(row); +} + +QStringList RunnerModel::runners() const +{ + return m_runners; +} + +void RunnerModel::setRunners(const QStringList &runners) +{ + if (QSet(runners.cbegin(), runners.cend()) != QSet(m_runners.cbegin(), m_runners.cend())) { + m_runners = runners; + + if (m_runnerManager) { + m_runnerManager->setAllowedRunners(runners); + } + + Q_EMIT runnersChanged(); + } +} + +QString RunnerModel::query() const +{ + return m_query; +} + +void RunnerModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + + m_queryTimer.start(); + + Q_EMIT queryChanged(); + } +} + +void RunnerModel::startQuery() +{ + if (m_query.isEmpty()) { + clear(); + } + + if (m_query.isEmpty() && m_runnerManager) { + return; + } + + createManager(); + + m_runnerManager->launchQuery(m_query); +} + +void RunnerModel::matchesChanged(const QList &matches) +{ + // Group matches by runner. + // We do not use a QMultiHash here because it keeps values in LIFO order, while we want FIFO. + QHash> matchesForRunner; + + for (const Plasma::QueryMatch &match : matches) { + auto it = matchesForRunner.find(match.runner()->id()); + + if (it == matchesForRunner.end()) { + it = matchesForRunner.insert(match.runner()->id(), QList()); + } + + it.value().append(match); + } + + // Sort matches for all runners in descending order, note the reverse iterators. This allows the best + // match to win whilest preserving order between runners. + for (auto &list : matchesForRunner) { + std::sort(list.rbegin(), list.rend()); + } + + if (m_mergeResults) { + RunnerMatchesModel *matchesModel = nullptr; + + if (m_models.isEmpty()) { + matchesModel = new RunnerMatchesModel(QString(), i18n("Search results"), m_runnerManager, this); + + beginInsertRows(QModelIndex(), 0, 0); + m_models.append(matchesModel); + endInsertRows(); + Q_EMIT countChanged(); + } else { + matchesModel = m_models.at(0); + } + + QList matches; + // To preserve the old behavior when allowing all runners we use static sorting + const static QStringList runnerIds = { + QStringLiteral("desktopsessions"), + QStringLiteral("services"), + QStringLiteral("places"), + QStringLiteral("PowerDevil"), + QStringLiteral("calculator"), + QStringLiteral("unitconverter"), + QStringLiteral("shell"), + QStringLiteral("bookmarks"), + QStringLiteral("recentdocuments"), + QStringLiteral("locations"), + }; + if (m_runners.isEmpty()) { + const auto baloo = matchesForRunner.take(QStringLiteral("baloosearch")); + const auto appstream = matchesForRunner.take(QStringLiteral("krunner_appstream")); + for (const QString &runnerId : runnerIds) { + matches.append(matchesForRunner.take(runnerId)); + } + for (const auto &match : matchesForRunner) { + matches.append(match); + } + matches.append(baloo); + matches.append(appstream); + } else { + for (const QString &runnerId : qAsConst(m_runners)) { + matches.append(matchesForRunner.take(runnerId)); + } + } + + matchesModel->setMatches(matches); + + return; + } + + // Assign matches to existing models. If there is no match for a model, delete it. + for (int row = m_models.count() - 1; row >= 0; --row) { + RunnerMatchesModel *matchesModel = m_models.at(row); + QList matches = matchesForRunner.take(matchesModel->runnerId()); + + if (m_deleteWhenEmpty && matches.isEmpty()) { + beginRemoveRows(QModelIndex(), row, row); + m_models.removeAt(row); + delete matchesModel; + endRemoveRows(); + Q_EMIT countChanged(); + } else { + matchesModel->setMatches(matches); + } + } + + // At this point, matchesForRunner contains only matches for runners which + // do not have a model yet. Create new models for them. + if (!matchesForRunner.isEmpty()) { + auto it = matchesForRunner.constBegin(); + auto end = matchesForRunner.constEnd(); + int appendCount = 0; + + for (; it != end; ++it) { + QList matches = it.value(); + Q_ASSERT(!matches.isEmpty()); + RunnerMatchesModel *matchesModel = new RunnerMatchesModel(it.key(), matches.first().runner()->name(), m_runnerManager, this); + matchesModel->setMatches(matches); + + if (it.key() == QLatin1String("services")) { + beginInsertRows(QModelIndex(), 0, 0); + m_models.prepend(matchesModel); + endInsertRows(); + Q_EMIT countChanged(); + } else { + m_models.append(matchesModel); + ++appendCount; + } + } + + if (appendCount > 0) { + beginInsertRows(QModelIndex(), rowCount() - appendCount, rowCount() - 1); + endInsertRows(); + Q_EMIT countChanged(); + } + } +} + +void RunnerModel::createManager() +{ + if (!m_runnerManager) { + m_runnerManager = new Plasma::RunnerManager(QStringLiteral("krunnerrc"), this); + if (m_runners.isEmpty()) { + m_runnerManager->enableKNotifyPluginWatcher(); + } else { + m_runnerManager->setAllowedRunners(m_runners); + } + connect(m_runnerManager, &Plasma::RunnerManager::matchesChanged, this, &RunnerModel::matchesChanged); + } +} + +void RunnerModel::clear() +{ + if (m_runnerManager) { + m_runnerManager->reset(); + m_runnerManager->matchSessionComplete(); + } + + if (m_models.isEmpty()) { + return; + } + + beginResetModel(); + + qDeleteAll(m_models); + m_models.clear(); + + endResetModel(); + + Q_EMIT countChanged(); +} diff --git a/plasma/workspace/applets/kicker/plugin/runnermodel.h b/plasma/workspace/applets/kicker/plugin/runnermodel.h new file mode 100644 index 0000000000..f6d1231954 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/runnermodel.h @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2012 Aurélien Gâteau + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include +#include + +#include + +namespace Plasma +{ +class RunnerManager; +} + +class AbstractModel; +class RunnerMatchesModel; + +class RunnerModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(AbstractModel *favoritesModel READ favoritesModel WRITE setFavoritesModel NOTIFY favoritesModelChanged) + Q_PROPERTY(QObject *appletInterface READ appletInterface WRITE setAppletInterface NOTIFY appletInterfaceChanged) + Q_PROPERTY(QStringList runners READ runners WRITE setRunners NOTIFY runnersChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(bool mergeResults READ mergeResults WRITE setMergeResults NOTIFY mergeResultsChanged) + Q_PROPERTY(bool deleteWhenEmpty READ deleteWhenEmpty WRITE setDeleteWhenEmpty NOTIFY deleteWhenEmptyChanged) + +public: + explicit RunnerModel(QObject *parent = nullptr); + ~RunnerModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int count() const; + + Q_INVOKABLE QObject *modelForRow(int row); + + QStringList runners() const; + void setRunners(const QStringList &runners); + + QString query() const; + void setQuery(const QString &query); + + AbstractModel *favoritesModel() const; + void setFavoritesModel(AbstractModel *model); + + QObject *appletInterface() const; + void setAppletInterface(QObject *appletInterface); + + bool mergeResults() const; + void setMergeResults(bool merge); + + bool deleteWhenEmpty() const; + void setDeleteWhenEmpty(bool deleteWhenEmpty); + +Q_SIGNALS: + void countChanged() const; + void favoritesModelChanged() const; + void appletInterfaceChanged() const; + void runnersChanged() const; + void queryChanged() const; + void mergeResultsChanged() const; + void deleteWhenEmptyChanged(); + +private Q_SLOTS: + void startQuery(); + void matchesChanged(const QList &matches); + +private: + void createManager(); + void clear(); + + AbstractModel *m_favoritesModel; + QObject *m_appletInterface; + Plasma::RunnerManager *m_runnerManager; + QStringList m_runners; + QList m_models; + QString m_query; + QTimer m_queryTimer; + bool m_mergeResults; + bool m_deleteWhenEmpty; +}; diff --git a/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.cpp b/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.cpp new file mode 100644 index 0000000000..e27660930c --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.cpp @@ -0,0 +1,323 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "simplefavoritesmodel.h" +#include "actionlist.h" +#include "appentry.h" +#include "contactentry.h" +#include "fileentry.h" +#include "systementry.h" + +#include + +SimpleFavoritesModel::SimpleFavoritesModel(QObject *parent) + : AbstractModel(parent) + , m_enabled(true) + , m_maxFavorites(-1) + , m_dropPlaceholderIndex(-1) +{ +} + +SimpleFavoritesModel::~SimpleFavoritesModel() +{ + qDeleteAll(m_entryList); +} + +QString SimpleFavoritesModel::description() const +{ + return i18n("Favorites"); +} + +QVariant SimpleFavoritesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= rowCount()) { + return QVariant(); + } + + if (index.row() == m_dropPlaceholderIndex) { + if (role == Kicker::IsDropPlaceholderRole) { + return true; + } else { + return QVariant(); + } + } + + int mappedIndex = index.row(); + + if (m_dropPlaceholderIndex != -1 && mappedIndex > m_dropPlaceholderIndex) { + --mappedIndex; + } + + const AbstractEntry *entry = m_entryList.at(mappedIndex); + + if (role == Qt::DisplayRole) { + return entry->name(); + } else if (role == Qt::DecorationRole) { + if (entry->icon().name() != "") + return entry->icon().name(); + else + return entry->icon(); + } else if (role == Kicker::DescriptionRole) { + return entry->description(); + } else if (role == Kicker::FavoriteIdRole) { + return entry->id(); + } else if (role == Kicker::UrlRole) { + return entry->url(); + } else if (role == Kicker::HasActionListRole) { + return entry->hasActions(); + } else if (role == Kicker::ActionListRole) { + return entry->actions(); + } + + return QVariant(); +} + +int SimpleFavoritesModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_entryList.count() + (m_dropPlaceholderIndex != -1 ? 1 : 0); +} + +bool SimpleFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (row < 0 || row >= m_entryList.count()) { + return false; + } + + return m_entryList.at(row)->run(actionId, argument); +} + +bool SimpleFavoritesModel::enabled() const +{ + return m_enabled; +} + +void SimpleFavoritesModel::setEnabled(bool enable) +{ + if (m_enabled != enable) { + m_enabled = enable; + + Q_EMIT enabledChanged(); + } +} + +QStringList SimpleFavoritesModel::favorites() const +{ + return m_favorites; +} + +void SimpleFavoritesModel::setFavorites(const QStringList &favorites) +{ + QStringList _favorites(favorites); + _favorites.removeDuplicates(); + + if (_favorites != m_favorites) { + m_favorites = _favorites; + refresh(); + } +} + +int SimpleFavoritesModel::maxFavorites() const +{ + return m_maxFavorites; +} + +void SimpleFavoritesModel::setMaxFavorites(int max) +{ + if (m_maxFavorites != max) { + m_maxFavorites = max; + + if (m_maxFavorites != -1 && m_favorites.count() > m_maxFavorites) { + refresh(); + } + + Q_EMIT maxFavoritesChanged(); + } +} + +bool SimpleFavoritesModel::isFavorite(const QString &id) const +{ + return m_favorites.contains(id); +} + +void SimpleFavoritesModel::addFavorite(const QString &id, int index) +{ + if (!m_enabled || id.isEmpty()) { + return; + } + + if (m_maxFavorites != -1 && m_favorites.count() == m_maxFavorites) { + return; + } + + AbstractEntry *entry = favoriteFromId(id); + + if (!entry || !entry->isValid()) { + delete entry; + return; + } + + setDropPlaceholderIndex(-1); + + int insertIndex = (index != -1) ? index : m_entryList.count(); + + beginInsertRows(QModelIndex(), insertIndex, insertIndex); + + m_entryList.insert(insertIndex, entry); + m_favorites.insert(insertIndex, entry->id()); + + endInsertRows(); + + Q_EMIT countChanged(); + Q_EMIT favoritesChanged(); +} + +void SimpleFavoritesModel::removeFavorite(const QString &id) +{ + if (!m_enabled || id.isEmpty()) { + return; + } + + int index = m_favorites.indexOf(id); + + if (index != -1) { + setDropPlaceholderIndex(-1); + + beginRemoveRows(QModelIndex(), index, index); + + delete m_entryList[index]; + m_entryList.removeAt(index); + m_favorites.removeAt(index); + + endRemoveRows(); + + Q_EMIT countChanged(); + Q_EMIT favoritesChanged(); + } +} + +void SimpleFavoritesModel::moveRow(int from, int to) +{ + if (from >= m_favorites.count() || to >= m_favorites.count()) { + return; + } + + if (from == to) { + return; + } + + setDropPlaceholderIndex(-1); + + int modelTo = to + (to > from ? 1 : 0); + + bool ok = beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo); + + if (ok) { + m_entryList.move(from, to); + m_favorites.move(from, to); + + endMoveRows(); + + Q_EMIT favoritesChanged(); + } +} + +int SimpleFavoritesModel::dropPlaceholderIndex() const +{ + return m_dropPlaceholderIndex; +} + +void SimpleFavoritesModel::setDropPlaceholderIndex(int index) +{ + if (index == -1 && m_dropPlaceholderIndex != -1) { + beginRemoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex); + + m_dropPlaceholderIndex = index; + + endRemoveRows(); + + Q_EMIT countChanged(); + } else if (index != -1 && m_dropPlaceholderIndex == -1) { + beginInsertRows(QModelIndex(), index, index); + + m_dropPlaceholderIndex = index; + + endInsertRows(); + + Q_EMIT countChanged(); + } else if (m_dropPlaceholderIndex != index) { + int modelTo = index + (index > m_dropPlaceholderIndex ? 1 : 0); + + bool ok = beginMoveRows(QModelIndex(), m_dropPlaceholderIndex, m_dropPlaceholderIndex, QModelIndex(), modelTo); + + if (ok) { + m_dropPlaceholderIndex = index; + + endMoveRows(); + } + } +} + +AbstractModel *SimpleFavoritesModel::favoritesModel() +{ + return this; +} + +void SimpleFavoritesModel::refresh() +{ + beginResetModel(); + + setDropPlaceholderIndex(-1); + + int oldCount = m_entryList.count(); + + qDeleteAll(m_entryList); + m_entryList.clear(); + + QStringList newFavorites; + + for (const QString &id : std::as_const(m_favorites)) { + AbstractEntry *entry = favoriteFromId(id); + + if (entry && entry->isValid()) { + m_entryList << entry; + newFavorites << entry->id(); + + if (m_maxFavorites != -1 && newFavorites.count() == m_maxFavorites) { + break; + } + } else if (entry) { + delete entry; + } + } + + m_favorites = newFavorites; + + endResetModel(); + + if (oldCount != m_entryList.count()) { + Q_EMIT countChanged(); + } + + Q_EMIT favoritesChanged(); +} + +AbstractEntry *SimpleFavoritesModel::favoriteFromId(const QString &id) +{ + const QUrl url(id); + const QString &s = url.scheme(); + + if ((s.isEmpty() && id.contains(QLatin1String(".desktop"))) || s == QLatin1String("preferred")) { + return new AppEntry(this, id); + } else if (s == QLatin1String("ktp")) { + return new ContactEntry(this, id); + } else if (url.isValid() && !url.scheme().isEmpty()) { + return new FileEntry(this, url); + } else { + return new SystemEntry(this, id); + } + + return nullptr; +} diff --git a/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.h b/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.h new file mode 100644 index 0000000000..60029b6d58 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/simplefavoritesmodel.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" + +#include + +class SimpleFavoritesModel : public AbstractModel +{ + Q_OBJECT + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QStringList favorites READ favorites WRITE setFavorites NOTIFY favoritesChanged) + Q_PROPERTY(int maxFavorites READ maxFavorites WRITE setMaxFavorites NOTIFY maxFavoritesChanged) + Q_PROPERTY(int dropPlaceholderIndex READ dropPlaceholderIndex WRITE setDropPlaceholderIndex NOTIFY dropPlaceholderIndexChanged) + +public: + explicit SimpleFavoritesModel(QObject *parent = nullptr); + ~SimpleFavoritesModel() override; + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + + bool enabled() const; + void setEnabled(bool enable); + + QStringList favorites() const; + void setFavorites(const QStringList &favorites); + + int maxFavorites() const; + void setMaxFavorites(int max); + + Q_INVOKABLE bool isFavorite(const QString &id) const; + Q_INVOKABLE void addFavorite(const QString &id, int index = -1); + Q_INVOKABLE void removeFavorite(const QString &id); + + Q_INVOKABLE void moveRow(int from, int to); + + int dropPlaceholderIndex() const; + void setDropPlaceholderIndex(int index); + + AbstractModel *favoritesModel() override; + +public Q_SLOTS: + void refresh() override; + +Q_SIGNALS: + void enabledChanged() const; + void favoritesChanged() const; + void maxFavoritesChanged() const; + void dropPlaceholderIndexChanged(); + +private: + AbstractEntry *favoriteFromId(const QString &id); + + bool m_enabled; + + QList m_entryList; + QStringList m_favorites; + int m_maxFavorites; + + int m_dropPlaceholderIndex; +}; diff --git a/plasma/workspace/applets/kicker/plugin/submenu.cpp b/plasma/workspace/applets/kicker/plugin/submenu.cpp new file mode 100644 index 0000000000..081c7bcccb --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/submenu.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2014 David Edmundson + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "submenu.h" + +#include + +#include + +#include + +SubMenu::SubMenu(QQuickItem *parent) + : PlasmaQuick::Dialog(parent) + , m_offset(0) + , m_facingLeft(false) +{ + setType(PopupMenu); +} + +SubMenu::~SubMenu() +{ +} + +int SubMenu::offset() const +{ + return m_offset; +} + +void SubMenu::setOffset(int offset) +{ + if (m_offset != offset) { + m_offset = offset; + + Q_EMIT offsetChanged(); + } +} + +QPoint SubMenu::popupPosition(QQuickItem *item, const QSize &size) +{ + if (!item || !item->window()) { + return QPoint(0, 0); + } + + QPointF pos = item->mapToScene(QPointF(0, 0)); + pos = item->window()->mapToGlobal(pos.toPoint()); + + pos.setX(pos.x() + m_offset + item->width()); + + QRect avail = availableScreenRectForItem(item); + + if (pos.x() + size.width() > avail.right()) { + pos.setX(pos.x() - m_offset - item->width() - size.width()); + + m_facingLeft = true; + Q_EMIT facingLeftChanged(); + } + + pos.setY(pos.y() - margins()->property("top").toInt()); + + if (pos.y() + size.height() > avail.bottom()) { + int overshoot = std::ceil(((avail.bottom() - (pos.y() + size.height())) * -1) / item->height()) * item->height(); + + pos.setY(pos.y() - overshoot); + } + + return pos.toPoint(); +} + +QRect SubMenu::availableScreenRectForItem(QQuickItem *item) const +{ + QScreen *screen = QGuiApplication::primaryScreen(); + + const QPoint globalPosition = item->window()->mapToGlobal(item->position().toPoint()); + + const QList screens = QGuiApplication::screens(); + for (QScreen *s : screens) { + if (s->geometry().contains(globalPosition)) { + screen = s; + } + } + + return screen->availableGeometry(); +} diff --git a/plasma/workspace/applets/kicker/plugin/submenu.h b/plasma/workspace/applets/kicker/plugin/submenu.h new file mode 100644 index 0000000000..8a2d5a90e6 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/submenu.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2014 David Edmundson + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class SubMenu : public PlasmaQuick::Dialog +{ + Q_OBJECT + + Q_PROPERTY(int offset READ offset WRITE setOffset NOTIFY offsetChanged) + Q_PROPERTY(bool facingLeft READ facingLeft NOTIFY facingLeftChanged) + +public: + explicit SubMenu(QQuickItem *parent = nullptr); + ~SubMenu() override; + + Q_INVOKABLE QRect availableScreenRectForItem(QQuickItem *item) const; + + QPoint popupPosition(QQuickItem *item, const QSize &size) override; + + int offset() const; + void setOffset(int offset); + + bool facingLeft() const + { + return m_facingLeft; + } + +Q_SIGNALS: + void offsetChanged() const; + void facingLeftChanged() const; + +private: + int m_offset; + bool m_facingLeft; +}; diff --git a/plasma/workspace/applets/kicker/plugin/systementry.cpp b/plasma/workspace/applets/kicker/plugin/systementry.cpp new file mode 100644 index 0000000000..fdd86ece34 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systementry.cpp @@ -0,0 +1,366 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systementry.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +int SystemEntry::s_instanceCount = 0; +SessionManagement *SystemEntry::s_sessionManagement = nullptr; + +SystemEntry::SystemEntry(AbstractModel *owner, Action action) + : AbstractEntry(owner) + , m_initialized(false) + , m_action(action) + , m_valid(false) +{ + refresh(); + ++s_instanceCount; + m_initialized = true; +} + +SystemEntry::SystemEntry(AbstractModel *owner, const QString &id) + : AbstractEntry(owner) + , m_initialized(false) + , m_action(NoAction) + , m_valid(false) +{ + if (id == QLatin1String("lock-screen")) { + m_action = LockSession; + } else if (id == QLatin1String("logout")) { + m_action = LogoutSession; + } else if (id == QLatin1String("save-session")) { + m_action = SaveSession; + } else if (id == QLatin1String("switch-user")) { + m_action = SwitchUser; + } else if (id == QLatin1String("suspend")) { + m_action = Suspend; + } else if (id == QLatin1String("hibernate")) { + m_action = Hibernate; + } else if (id == QLatin1String("reboot")) { + m_action = Reboot; + } else if (id == QLatin1String("shutdown")) { + m_action = Shutdown; + } + + refresh(); + ++s_instanceCount; + m_initialized = true; +} + +SystemEntry::~SystemEntry() +{ + --s_instanceCount; + + if (!s_instanceCount) { + delete s_sessionManagement; + s_sessionManagement = nullptr; + } +} + +SystemEntry::Action SystemEntry::action() const +{ + return m_action; +} + +void SystemEntry::refresh() +{ + if (!s_sessionManagement) { + s_sessionManagement = new SessionManagement(); + } + + bool valid = false; + + switch (m_action) { + case LockSession: { + valid = s_sessionManagement->canLock(); + QObject::connect(s_sessionManagement, &SessionManagement::canLockChanged, this, &SystemEntry::refresh); + break; + } + case LogoutSession: { + valid = s_sessionManagement->canLogout(); + QObject::connect(s_sessionManagement, &SessionManagement::canLogoutChanged, this, &SystemEntry::refresh); + break; + } + case SaveSession: { + valid = s_sessionManagement->canSaveSession(); + QObject::connect(s_sessionManagement, &SessionManagement::canSaveSessionChanged, this, &SystemEntry::refresh); + break; + } + case SwitchUser: { + valid = s_sessionManagement->canSwitchUser(); + QObject::connect(s_sessionManagement, &SessionManagement::canSwitchUserChanged, this, &SystemEntry::refresh); + break; + } + case Suspend: { + valid = s_sessionManagement->canSuspend(); + QObject::connect(s_sessionManagement, &SessionManagement::canSuspendChanged, this, &SystemEntry::refresh); + break; + } + case Hibernate: { + valid = s_sessionManagement->canHibernate(); + QObject::connect(s_sessionManagement, &SessionManagement::canHibernateChanged, this, &SystemEntry::refresh); + break; + } + case Reboot: { + valid = s_sessionManagement->canReboot(); + QObject::connect(s_sessionManagement, &SessionManagement::canRebootChanged, this, &SystemEntry::refresh); + break; + } + case Shutdown: { + valid = s_sessionManagement->canShutdown(); + QObject::connect(s_sessionManagement, &SessionManagement::canShutdownChanged, this, &SystemEntry::refresh); + break; + } + default: + break; + } + + if (m_valid != valid) { + m_valid = valid; + + if (m_initialized) { + Q_EMIT isValidChanged(); + } + } +} + +bool SystemEntry::isValid() const +{ + return m_valid; +} + +QIcon SystemEntry::icon() const +{ + const QString &name = iconName(); + + if (!name.isEmpty()) { + return QIcon::fromTheme(name, QIcon::fromTheme(QStringLiteral("unknown"))); + } + + return QIcon::fromTheme(QStringLiteral("unknown")); +} + +QString SystemEntry::iconName() const +{ + switch (m_action) { + case LockSession: + return QStringLiteral("system-lock-screen"); + break; + case LogoutSession: + return QStringLiteral("system-log-out"); + break; + case SaveSession: + return QStringLiteral("system-save-session"); + break; + case SwitchUser: + return QStringLiteral("system-switch-user"); + break; + case Suspend: + return QStringLiteral("system-suspend"); + break; + case Hibernate: + return QStringLiteral("system-suspend-hibernate"); + break; + case Reboot: + return QStringLiteral("system-reboot"); + break; + case Shutdown: + return QStringLiteral("system-shutdown"); + break; + default: + break; + } + + return QString(); +} + +QString SystemEntry::name() const +{ + switch (m_action) { + case LockSession: + return i18n("Lock"); + break; + case LogoutSession: + return i18n("Log Out"); + break; + case SaveSession: + return i18n("Save Session"); + break; + case SwitchUser: + return i18n("Switch User"); + break; + case Suspend: + return i18nc("Suspend to RAM", "Sleep"); + break; + case Hibernate: + return i18n("Hibernate"); + break; + case Reboot: + return i18n("Restart"); + break; + case Shutdown: + return i18n("Shut Down"); + break; + default: + break; + } + + return QString(); +} + +QString SystemEntry::group() const +{ + switch (m_action) { + case LockSession: + return i18n("Session"); + break; + case LogoutSession: + return i18n("Session"); + break; + case SaveSession: + return i18n("Session"); + break; + case SwitchUser: + return i18n("Session"); + break; + case Suspend: + return i18n("System"); + break; + case Hibernate: + return i18n("System"); + break; + case Reboot: + return i18n("System"); + break; + case Shutdown: + return i18n("System"); + break; + default: + break; + } + + return QString(); +} + +QString SystemEntry::description() const +{ + switch (m_action) { + case LockSession: + return i18n("Lock screen"); + break; + case LogoutSession: + return i18n("End session"); + break; + case SaveSession: + return i18n("Save Session"); + break; + case SwitchUser: + return i18n("Start a parallel session as a different user"); + break; + case Suspend: + return i18n("Suspend to RAM"); + break; + case Hibernate: + return i18n("Suspend to disk"); + break; + case Reboot: + return i18n("Restart computer"); + break; + case Shutdown: + return i18n("Turn off computer"); + break; + default: + break; + } + + return QString(); +} + +QString SystemEntry::id() const +{ + switch (m_action) { + case LockSession: + return QStringLiteral("lock-screen"); + break; + case LogoutSession: + return QStringLiteral("logout"); + break; + case SaveSession: + return QStringLiteral("save-session"); + break; + case SwitchUser: + return QStringLiteral("switch-user"); + break; + case Suspend: + return QStringLiteral("suspend"); + break; + case Hibernate: + return QStringLiteral("hibernate"); + break; + case Reboot: + return QStringLiteral("reboot"); + break; + case Shutdown: + return QStringLiteral("shutdown"); + break; + + default: + break; + } + + return QString(); +} + +bool SystemEntry::run(const QString &actionId, const QVariant &argument) +{ + Q_UNUSED(actionId) + Q_UNUSED(argument) + + if (!m_valid) { + return false; + } + + switch (m_action) { + case LockSession: + s_sessionManagement->lock(); + break; + case LogoutSession: + s_sessionManagement->requestLogout(); + break; + case SaveSession: + s_sessionManagement->saveSession(); + break; + case SwitchUser: + s_sessionManagement->switchUser(); + break; + case Suspend: + s_sessionManagement->suspend(); + break; + case Hibernate: + s_sessionManagement->hibernate(); + break; + case Reboot: + s_sessionManagement->requestReboot(); + break; + case Shutdown: + s_sessionManagement->requestShutdown(); + break; + default: + break; + } + + return true; +} diff --git a/plasma/workspace/applets/kicker/plugin/systementry.h b/plasma/workspace/applets/kicker/plugin/systementry.h new file mode 100644 index 0000000000..a0a258024d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systementry.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractentry.h" + +#include + +class SessionManagement; + +class SystemEntry : public QObject, public AbstractEntry +{ + Q_OBJECT + +public: + enum Action { + NoAction = 0, + LockSession, + LogoutSession, + SaveSession, + SwitchUser, + Suspend, + Hibernate, + Reboot, + Shutdown, + }; + + explicit SystemEntry(AbstractModel *owner, Action action); + explicit SystemEntry(AbstractModel *owner, const QString &id); + ~SystemEntry(); + + Action action() const; + + EntryType type() const override + { + return RunnableType; + } + + bool isValid() const override; + + QIcon icon() const override; + QString iconName() const; + QString name() const override; + QString group() const override; + QString description() const override; + + QString id() const override; + + bool run(const QString &actionId = QString(), const QVariant &argument = QVariant()) override; + +Q_SIGNALS: + void isValidChanged() const; + +private Q_SLOTS: + void refresh(); + +private: + bool m_initialized; + + Action m_action; + bool m_valid; + + static int s_instanceCount; + static SessionManagement *s_sessionManagement; +}; diff --git a/plasma/workspace/applets/kicker/plugin/systemmodel.cpp b/plasma/workspace/applets/kicker/plugin/systemmodel.cpp new file mode 100644 index 0000000000..656bbdc4e3 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systemmodel.cpp @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemmodel.h" +#include "actionlist.h" +#include "simplefavoritesmodel.h" + +#include + +#include +#include + +SystemModel::SystemModel(QObject *parent) + : AbstractModel(parent) +{ + m_favoritesModel = new SimpleFavoritesModel(this); + + populate(); +} + +SystemModel::~SystemModel() +{ + qDeleteAll(m_entries); +} + +QString SystemModel::description() const +{ + return i18n("System actions"); +} + +QVariant SystemModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_entries.count()) { + return QVariant(); + } + + const SystemEntry *entry = m_entries.value(index.row()); + + if (role == Qt::DisplayRole) { + return entry->name(); + } else if (role == Qt::DecorationRole) { + return entry->iconName(); + } else if (role == Kicker::DescriptionRole) { + return entry->description(); + } else if (role == Kicker::GroupRole) { + return entry->group(); + } else if (role == Kicker::FavoriteIdRole) { + return entry->id(); + } else if (role == Kicker::HasActionListRole) { + return entry->hasActions(); + } else if (role == Kicker::ActionListRole) { + return entry->actions(); + } else if (role == Kicker::DisabledRole) { + return !entry->isValid(); + } + + return QVariant(); +} + +int SystemModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_entries.count(); +} + +bool SystemModel::trigger(int row, const QString &actionId, const QVariant &argument) +{ + if (row >= 0 && row < m_entries.count()) { + m_entries.at(row)->run(actionId, argument); + + return true; + } + + return false; +} + +void SystemModel::refresh() +{ + beginResetModel(); + populate(); + endResetModel(); + + m_favoritesModel->refresh(); +} + +void SystemModel::populate() +{ + qDeleteAll(m_entries); + m_entries.clear(); + + auto addIfValid = [=](const SystemEntry::Action action) { + SystemEntry *entry = new SystemEntry(this, action); + + if (entry->isValid()) { + m_entries << entry; + } + QObject::connect(entry, &SystemEntry::isValidChanged, this, &AbstractModel::refresh, Qt::UniqueConnection); + }; + + addIfValid(SystemEntry::LockSession); + addIfValid(SystemEntry::LogoutSession); + addIfValid(SystemEntry::SaveSession); + addIfValid(SystemEntry::SwitchUser); + addIfValid(SystemEntry::Suspend); + addIfValid(SystemEntry::Hibernate); + addIfValid(SystemEntry::Reboot); + addIfValid(SystemEntry::Shutdown); +} diff --git a/plasma/workspace/applets/kicker/plugin/systemmodel.h b/plasma/workspace/applets/kicker/plugin/systemmodel.h new file mode 100644 index 0000000000..cd9048de1b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systemmodel.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "abstractmodel.h" +#include "systementry.h" + +class SystemModel : public AbstractModel +{ + Q_OBJECT + +public: + explicit SystemModel(QObject *parent = nullptr); + ~SystemModel() override; + + QString description() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_INVOKABLE bool trigger(int row, const QString &actionId, const QVariant &argument) override; + +protected Q_SLOTS: + void refresh() override; + +private: + void populate(); + + QVector m_entries; +}; diff --git a/plasma/workspace/applets/kicker/plugin/systemsettings.cpp b/plasma/workspace/applets/kicker/plugin/systemsettings.cpp new file mode 100644 index 0000000000..589067b87c --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systemsettings.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemsettings.h" + +#include + +SystemSettings::SystemSettings(QObject *parent) + : QObject(parent) +{ +} + +SystemSettings::~SystemSettings() +{ +} + +QString SystemSettings::picturesLocation() const +{ + QString path; + + const QStringList &locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); + + if (!locations.isEmpty()) { + path = locations.at(0); + } else { + // HomeLocation is guaranteed not to be empty. + path = QStandardPaths::standardLocations(QStandardPaths::HomeLocation).at(0); + } + + return path; +} diff --git a/plasma/workspace/applets/kicker/plugin/systemsettings.h b/plasma/workspace/applets/kicker/plugin/systemsettings.h new file mode 100644 index 0000000000..866b225497 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/systemsettings.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class SystemSettings : public QObject +{ + Q_OBJECT + +public: + explicit SystemSettings(QObject *parent = nullptr); + ~SystemSettings() override; + + Q_INVOKABLE QString picturesLocation() const; +}; diff --git a/plasma/workspace/applets/kicker/plugin/trianglemousefilter.cpp b/plasma/workspace/applets/kicker/plugin/trianglemousefilter.cpp new file mode 100644 index 0000000000..858df676a7 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/trianglemousefilter.cpp @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2021 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "trianglemousefilter.h" + +#include + +TriangleMouseFilter::TriangleMouseFilter(QQuickItem *parent) + : QQuickItem(parent) +{ + setFiltersChildMouseEvents(true); + + m_resetTimer.setSingleShot(true); + connect(&m_resetTimer, &QTimer::timeout, this, [this]() { + m_interceptionPos.reset(); + if (!m_interceptedHoverItem) { + return; + } + if (m_interceptedHoverEnterPosition) { + const auto targetPosition = mapToItem(m_interceptedHoverItem, m_interceptedHoverEnterPosition.value()); + QHoverEvent e(QEvent::HoverEnter, targetPosition, targetPosition); + qApp->sendEvent(m_interceptedHoverItem, &e); + m_interceptedHoverEnterPosition.reset(); + } + const auto targetPosition = mapToItem(m_interceptedHoverItem, m_lastCursorPosition); + QHoverEvent e(QEvent::HoverMove, targetPosition, targetPosition); + qApp->sendEvent(m_interceptedHoverItem, &e); + }); +}; + +bool TriangleMouseFilter::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + switch (event->type()) { + case QEvent::HoverLeave: + if (!m_interceptedHoverItem) { + return false; + } + if (item == m_interceptedHoverItem.data()) { + m_interceptedHoverItem.clear(); + return false; + } + return true; + case QEvent::HoverEnter: + case QEvent::HoverMove: { + QHoverEvent *he = static_cast(event); + + const QPointF position = item->mapToItem(this, he->posF()); + + if (filterContains(position)) { + if (event->type() == QEvent::HoverEnter) { + m_interceptedHoverEnterPosition = position; + m_interceptedHoverItem = item; + } + + if (m_filterTimeout > 0) { + m_resetTimer.start(m_filterTimeout); + } + + m_lastCursorPosition = position; + event->setAccepted(true); + + return true; + } else { + // this clause means that we block focus when first entering a given position + // in the case of kickoff it's so that we can move the mouse from the bottom tabbar to the side view + // if using this in a more general setting we might want to make this guarded by an option + if (event->type() == QEvent::HoverEnter && !m_interceptionPos) { + m_interceptedHoverItem = item; + m_interceptedHoverEnterPosition = position; + if (m_filterTimeout > 0) { + m_resetTimer.start(m_filterTimeout); + } + event->setAccepted(true); + m_lastCursorPosition = position; + m_interceptionPos = position; + return true; + } + + m_interceptionPos = position; + m_lastCursorPosition = position; + + // if we are no longer inhibiting events and have previously intercepted a hover enter + // we manually send the hover enter to that item + if (event->type() == QEvent::HoverMove && m_interceptedHoverItem) { + const auto targetPosition = mapToItem(m_interceptedHoverItem, position); + QHoverEvent e(QEvent::HoverEnter, targetPosition, targetPosition); + qApp->sendEvent(m_interceptedHoverItem, &e); + m_interceptedHoverItem.clear(); + } + + event->setAccepted(false); + return false; + } + } + default: + return false; + } +} + +bool TriangleMouseFilter::filterContains(const QPointF &p) const +{ + if (!m_interceptionPos) { + return false; + } + + // We add some jitter protection by extending our triangle out slight past the mouse position in the opposite direction of the edge; + const int jitterThreshold = 3; + + // QPolygonF.contains returns false if we're on the edge, so we pad our main item + const QRectF shape = QRect(-1, -1, width() + 1, height() + 1); + + QPolygonF poly; + + switch (m_edge) { + case Qt::RightEdge: + poly << m_interceptionPos.value() + QPointF(-jitterThreshold, 0) << shape.topRight() << shape.bottomRight(); + break; + case Qt::TopEdge: + poly << m_interceptionPos.value() + QPointF(0, -jitterThreshold) << shape.topLeft() << shape.topRight(); + break; + case Qt::LeftEdge: + poly << m_interceptionPos.value() + QPointF(jitterThreshold, 0) << shape.topLeft() << shape.bottomLeft(); + break; + case Qt::BottomEdge: + poly << m_interceptionPos.value() + QPointF(0, jitterThreshold) << shape.bottomLeft() << shape.bottomRight(); + } + + return poly.containsPoint(p, Qt::OddEvenFill); +} diff --git a/plasma/workspace/applets/kicker/plugin/trianglemousefilter.h b/plasma/workspace/applets/kicker/plugin/trianglemousefilter.h new file mode 100644 index 0000000000..7c2b684c1b --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/trianglemousefilter.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2021 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +/** + * This class filters child mouse events that move from a current location towards a given edge. + * + * The primary use-case being where we have a list of actions that trigger on hover on one side + * adjacent to a large hit area where a user will want to interact with. + * + * Without this filter a user moving their mouse towards the target area will trigger other hover events + * + * This item distinguishes mouse moves towards an edge from attempts to select another item in the combo + * tree. + * + * See: https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown + */ +class TriangleMouseFilter : public QQuickItem +{ + Q_OBJECT + /** + * The timeout in ms after which the filter is disabled and the current item is selected + * regardless. + * + * The default is 300 + * Setting a negative value disables the timeout + */ + Q_PROPERTY(int filterTimeOut MEMBER m_filterTimeout NOTIFY filterTimoutChanged) + + /** + * The edge that we want to filter mouse actions towards. + * i.e if we have a listview on the left with a submenu on the right, the value + * will be Qt.RightEdge + * + * RTL configurations must be handled explicitly by the caller + */ + Q_PROPERTY(Qt::Edge edge MEMBER m_edge NOTIFY edgeChanged) + +public: + TriangleMouseFilter(QQuickItem *parent = nullptr); + ~TriangleMouseFilter() = default; + +Q_SIGNALS: + void filterTimoutChanged(); + void edgeChanged(); + +protected: + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + +private: + bool filterContains(const QPointF &p) const; + QTimer m_resetTimer; + std::optional m_interceptionPos; // point where we started intercepting + QPointF m_lastCursorPosition; + QPointer m_interceptedHoverItem; // item newly entered but the enter event was intercepted + std::optional m_interceptedHoverEnterPosition; // position of intercepted enter events + Qt::Edge m_edge = Qt::RightEdge; + int m_filterTimeout = 300; +}; diff --git a/plasma/workspace/applets/kicker/plugin/wheelinterceptor.cpp b/plasma/workspace/applets/kicker/plugin/wheelinterceptor.cpp new file mode 100644 index 0000000000..0668fb5d8d --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/wheelinterceptor.cpp @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wheelinterceptor.h" + +#include + +WheelInterceptor::WheelInterceptor(QQuickItem *parent) + : QQuickItem(parent) +{ +} + +WheelInterceptor::~WheelInterceptor() +{ +} + +QQuickItem *WheelInterceptor::destination() const +{ + return m_destination; +} + +void WheelInterceptor::setDestination(QQuickItem *destination) +{ + if (m_destination != destination) { + m_destination = destination; + + Q_EMIT destinationChanged(); + } +} + +void WheelInterceptor::wheelEvent(QWheelEvent *event) +{ + if (m_destination) { + QCoreApplication::sendEvent(m_destination, event); + } + + Q_EMIT wheelMoved(event->angleDelta()); +} + +QQuickItem *WheelInterceptor::findWheelArea(QQuickItem *parent) const +{ + if (!parent) { + return nullptr; + } + + const QList childItems = parent->childItems(); + for (QQuickItem *child : childItems) { + // HACK: ScrollView adds the WheelArea below its flickableItem with + // z==-1. This is reasonable non-risky considering we know about + // everything else in there, and worst case we break the mouse wheel. + if (child->z() == -1) { + return child; + } + } + + return nullptr; +} diff --git a/plasma/workspace/applets/kicker/plugin/wheelinterceptor.h b/plasma/workspace/applets/kicker/plugin/wheelinterceptor.h new file mode 100644 index 0000000000..0e24d6b0a1 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/wheelinterceptor.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class WheelInterceptor : public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(QQuickItem *destination READ destination WRITE setDestination NOTIFY destinationChanged) + +public: + explicit WheelInterceptor(QQuickItem *parent = nullptr); + ~WheelInterceptor() override; + + QQuickItem *destination() const; + void setDestination(QQuickItem *destination); + + Q_INVOKABLE QQuickItem *findWheelArea(QQuickItem *parent) const; + +Q_SIGNALS: + void destinationChanged() const; + void wheelMoved(QPoint delta) const; + +protected: + void wheelEvent(QWheelEvent *event) override; + +private: + QPointer m_destination; +}; diff --git a/plasma/workspace/applets/kicker/plugin/windowsystem.cpp b/plasma/workspace/applets/kicker/plugin/windowsystem.cpp new file mode 100644 index 0000000000..7d04580c86 --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/windowsystem.cpp @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowsystem.h" + +#include + +#include + +WindowSystem::WindowSystem(QObject *parent) + : QObject(parent) +{ +} + +WindowSystem::~WindowSystem() +{ +} + +bool WindowSystem::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::FocusIn) { + removeEventFilter(watched); + Q_EMIT focusIn(qobject_cast(watched)); + } + + return false; +} + +void WindowSystem::forceActive(QQuickItem *item) +{ + if (!item || !item->window()) { + return; + } + + KWindowSystem::forceActiveWindow(item->window()->winId()); + KWindowSystem::raiseWindow(item->window()->winId()); +} + +bool WindowSystem::isActive(QQuickItem *item) +{ + if (!item || !item->window()) { + return false; + } + + return item->window()->isActive(); +} + +void WindowSystem::monitorWindowFocus(QQuickItem *item) +{ + if (!item || !item->window()) { + return; + } + + item->window()->installEventFilter(this); +} + +void WindowSystem::monitorWindowVisibility(QQuickItem *item) +{ + if (!item || !item->window()) { + return; + } + + connect(item->window(), &QQuickWindow::visibilityChanged, this, &WindowSystem::monitoredWindowVisibilityChanged, Qt::UniqueConnection); +} + +void WindowSystem::monitoredWindowVisibilityChanged(QWindow::Visibility visibility) const +{ + bool visible = (visibility != QWindow::Hidden); + QQuickWindow *w = static_cast(QObject::sender()); + + if (!visible) { + Q_EMIT hidden(w); + } +} diff --git a/plasma/workspace/applets/kicker/plugin/windowsystem.h b/plasma/workspace/applets/kicker/plugin/windowsystem.h new file mode 100644 index 0000000000..6fca50032f --- /dev/null +++ b/plasma/workspace/applets/kicker/plugin/windowsystem.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2014 Eike Hein + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +class QQuickItem; + +class WindowSystem : public QObject +{ + Q_OBJECT + +public: + explicit WindowSystem(QObject *parent = nullptr); + ~WindowSystem() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + + Q_INVOKABLE void forceActive(QQuickItem *item); + + Q_INVOKABLE bool isActive(QQuickItem *item); + + Q_INVOKABLE void monitorWindowFocus(QQuickItem *item); + + Q_INVOKABLE void monitorWindowVisibility(QQuickItem *item); + +Q_SIGNALS: + void focusIn(QQuickWindow *window) const; + void hidden(QQuickWindow *window) const; + +private Q_SLOTS: + void monitoredWindowVisibilityChanged(QWindow::Visibility visibility) const; +}; diff --git a/plasma/workspace/applets/lock_logout/Messages.sh b/plasma/workspace/applets/lock_logout/Messages.sh new file mode 100644 index 0000000000..061867a8c9 --- /dev/null +++ b/plasma/workspace/applets/lock_logout/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.lock_logout.pot diff --git a/plasma/workspace/applets/lock_logout/contents/config/config.qml b/plasma/workspace/applets/lock_logout/contents/config/config.qml new file mode 100644 index 0000000000..0461385b1d --- /dev/null +++ b/plasma/workspace/applets/lock_logout/contents/config/config.qml @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("General") + icon: "preferences-desktop-plasma" + source: "ConfigGeneral.qml" + } +} diff --git a/plasma/workspace/applets/lock_logout/contents/config/main.xml b/plasma/workspace/applets/lock_logout/contents/config/main.xml new file mode 100644 index 0000000000..5074247f09 --- /dev/null +++ b/plasma/workspace/applets/lock_logout/contents/config/main.xml @@ -0,0 +1,39 @@ + + + + + + + + true + + + + false + + + + false + + + + true + + + + false + + + + false + + + + false + + + + diff --git a/plasma/workspace/applets/lock_logout/contents/ui/ConfigGeneral.qml b/plasma/workspace/applets/lock_logout/contents/ui/ConfigGeneral.qml new file mode 100644 index 0000000000..cae5fb603c --- /dev/null +++ b/plasma/workspace/applets/lock_logout/contents/ui/ConfigGeneral.qml @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.5 as QtControls +import org.kde.kirigami 2.5 as Kirigami +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.sessions 2.0 + +Kirigami.FormLayout { + id: iconsPage + anchors.left: parent.left + anchors.right: parent.right + + readonly property int checkedOptions: logout.checked + shutdown.checked + reboot.checked + lock.checked + switchUser.checked + hibernate.checked + sleep.checked + + property alias cfg_show_requestLogout: logout.checked + property alias cfg_show_requestShutDown: shutdown.checked + property alias cfg_show_requestReboot: reboot.checked + + property alias cfg_show_lockScreen: lock.checked + property alias cfg_show_switchUser: switchUser.checked + property alias cfg_show_suspendToDisk: hibernate.checked + property alias cfg_show_suspendToRam: sleep.checked + + SessionManagement { + id: session + } + + QtControls.CheckBox { + id: logout + Kirigami.FormData.label: i18nc("Heading for a list of actions (leave, lock, switch user, hibernate, suspend)", "Show actions:") + text: i18n("Logout") + // ensure user cannot have all options unchecked + enabled: session.canLogout && (checkedOptions > 1 || !checked) + } + QtControls.CheckBox { + id: shutdown + text: i18n("Shutdown") + enabled: session.canShutdown && (checkedOptions > 1 || !checked) + } + QtControls.CheckBox { + id: reboot + text: i18n("Reboot") + enabled: session.canReboot && (checkedOptions > 1 || !checked) + } + QtControls.CheckBox { + id: lock + text: i18n("Lock") + enabled: session.canLock && (checkedOptions > 1 || !checked) + } + QtControls.CheckBox { + id: switchUser + text: i18n("Switch User") + enabled: checkedOptions > 1 || !checked + } + QtControls.CheckBox { + id: hibernate + text: i18n("Hibernate") + enabled: session.canHibernate && (checkedOptions > 1 || !checked) + } + QtControls.CheckBox { + id: sleep + text: i18nc("Suspend to RAM", "Sleep") + enabled: session.canSuspend && (checkedOptions > 1 || !checked) + } +} diff --git a/plasma/workspace/applets/lock_logout/contents/ui/data.js b/plasma/workspace/applets/lock_logout/contents/ui/data.js new file mode 100644 index 0000000000..60c82ce5b4 --- /dev/null +++ b/plasma/workspace/applets/lock_logout/contents/ui/data.js @@ -0,0 +1,49 @@ +var data = [{ + icon: "system-lock-screen", + operation: "lock", + configKey: "lockScreen", + tooltip_mainText: i18n("Lock"), + tooltip_subText: i18n("Lock the screen"), + requires: "Lock" +}, { + icon: "system-switch-user", + operation: "switchUser", + configKey: "switchUser", + tooltip_mainText: i18n("Switch user"), + tooltip_subText: i18n("Start a parallel session as a different user") +}, { + icon: "system-shutdown", + operation: "requestShutdown", + configKey: "requestShutDown", + tooltip_mainText: i18n("Shutdown…"), + tooltip_subText: i18n("Turn off the computer"), + requires: "Shutdown" +}, { + icon: "system-reboot", + operation: "requestReboot", + configKey: "requestReboot", + tooltip_mainText: i18n("Restart…"), + tooltip_subText: i18n("Reboot the computer"), + requires: "Reboot" +}, { + icon: "system-log-out", + operation: "requestLogout", + configKey: "requestLogout", + tooltip_mainText: i18n("Logout…"), + tooltip_subText: i18n("End the session"), + requires: "Logout" +}, { + icon: "system-suspend", + operation: "suspend", + configKey: "suspendToRam", + tooltip_mainText: i18nc("Suspend to RAM", "Sleep"), + tooltip_subText: i18n("Sleep (suspend to RAM)"), + requires: "Suspend" +}, { + icon: "system-suspend-hibernate", + operation: "hibernate", + configKey: "suspendToDisk", + tooltip_mainText: i18n("Hibernate"), + tooltip_subText: i18n("Hibernate (suspend to disk)"), + requires: "Hibernate" +}] diff --git a/plasma/workspace/applets/lock_logout/contents/ui/lockout.qml b/plasma/workspace/applets/lock_logout/contents/ui/lockout.qml new file mode 100644 index 0000000000..78266120e8 --- /dev/null +++ b/plasma/workspace/applets/lock_logout/contents/ui/lockout.qml @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.0 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kquickcontrolsaddons 2.0 +import "data.js" as Data +import org.kde.plasma.private.sessions 2.0 + +Flow { + id: lockout + Layout.minimumWidth: { + if (plasmoid.formFactor === PlasmaCore.Types.Vertical) { + return 0 + } else if (plasmoid.formFactor === PlasmaCore.Types.Horizontal) { + return height < minButtonSize * visibleButtons ? height * visibleButtons : height / visibleButtons - 1; + } else { + return width > height ? minButtonSize * visibleButtons : minButtonSize + } + } + Layout.minimumHeight: { + if (plasmoid.formFactor === PlasmaCore.Types.Vertical) { + return width >= minButtonSize * visibleButtons ? width / visibleButtons - 1 : width * visibleButtons + } else if (plasmoid.formFactor === PlasmaCore.Types.Horizontal) { + return 0 + } else { + return width > height ? minButtonSize : minButtonSize * visibleButtons + } + } + + Layout.preferredWidth: Layout.minimumWidth + Layout.preferredHeight: Layout.minimumHeight + + readonly property int minButtonSize: PlasmaCore.Units.iconSizes.small + + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + readonly property int visibleButtons: { + var count = 0 + for (var i = 0, j = items.count; i < j; ++i) { + if (items.itemAt(i).visible) { + ++count + } + } + return count + } + + flow: { + if ((plasmoid.formFactor === PlasmaCore.Types.Vertical && width >= minButtonSize * visibleButtons) || + (plasmoid.formFactor === PlasmaCore.Types.Horizontal && height < minButtonSize * visibleButtons) || + (width > height)) { + return Flow.LeftToRight // horizontal + } else { + return Flow.TopToBottom // vertical + } + } + + SessionManagement { + id: session + } + + Repeater { + id: items + property int itemWidth: parent.flow==Flow.LeftToRight ? Math.floor(parent.width/visibleButtons) : parent.width + property int itemHeight: parent.flow==Flow.TopToBottom ? Math.floor(parent.height/visibleButtons) : parent.height + property int iconSize: Math.min(itemWidth, itemHeight) + + model: Data.data + + delegate: Item { + id: iconDelegate + visible: plasmoid.configuration["show_" + modelData.configKey] && (!modelData.hasOwnProperty("requires") || session["can" + modelData.requires]) + width: items.itemWidth + height: items.itemHeight + + PlasmaCore.IconItem { + id: iconButton + width: items.iconSize + height: items.iconSize + anchors.centerIn: parent + source: modelData.icon + scale: mouseArea.pressed ? 0.9 : 1 + active: mouseArea.containsMouse + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onReleased: clickHandler(modelData.operation, this) + activeFocusOnTab: true + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + clickHandler(modelData.operation, this) + break; + } + } + Accessible.name: modelData.tooltip_mainText + Accessible.description: modelData.tooltip_subText + Accessible.role: Accessible.Button + + PlasmaCore.ToolTipArea { + anchors.fill: parent + mainText: modelData.tooltip_mainText + subText: modelData.tooltip_subText + } + } + } + } + } + + function clickHandler(what, button) { + performOperation(what); + } + + function performOperation(operation) { + session[operation]() + } +} + diff --git a/plasma/workspace/applets/lock_logout/metadata.json b/plasma/workspace/applets/lock_logout/metadata.json new file mode 100644 index 0000000000..4e0140f0f3 --- /dev/null +++ b/plasma/workspace/applets/lock_logout/metadata.json @@ -0,0 +1,225 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "viranch.mehta@gmail.com", + "Name": "Viranch Mehta", + "Name[ar]": "Viranch Mehta", + "Name[az]": "Viranch Mehta", + "Name[ca]": "Viranch Mehta", + "Name[cs]": "Viranch Mehta", + "Name[de]": "Viranch Mehta", + "Name[en_GB]": "Viranch Mehta", + "Name[es]": "Viranch Mehta", + "Name[eu]": "Viranch Mehta", + "Name[fi]": "Viranch Mehta", + "Name[fr]": "Viranch Mehta", + "Name[hu]": "Viranch Mehta", + "Name[ia]": "Viranch Mehta", + "Name[it]": "Viranch Mehta", + "Name[ko]": "Viranch Mehta", + "Name[lt]": "Viranch Mehta", + "Name[nl]": "Viranch Mehta", + "Name[nn]": "Viranch Mehta", + "Name[pa]": "ਵਿਰਾਂਚ ਮਹਿਤਾ", + "Name[pl]": "Viranch Mehta", + "Name[pt_BR]": "Viranch Mehta", + "Name[ro]": "Viranch Mehta", + "Name[ru]": "Viranch Mehta", + "Name[sk]": "Viranch Mehta", + "Name[sl]": "Viranch Mehta", + "Name[sv]": "Viranch Mehta", + "Name[tr]": "Viranch Mehta", + "Name[uk]": "Viranch Mehta", + "Name[vi]": "Viranch Mehta", + "Name[x-test]": "xxViranch Mehtaxx", + "Name[zh_CN]": "Viranch Mehta" + } + ], + "Category": "System Information", + "Description": "Lock the screen or log out", + "Description[ar]": "اقفل الشاشة أو اخرج", + "Description[az]": "Ekranı kilidləmək və seansdan çıxış", + "Description[ca]": "Bloqueja la pantalla o desconnecta", + "Description[cs]": "Uzamknout obrazovku nebo se odhlásit", + "Description[de]": "Den Bildschirm sperren oder abmelden", + "Description[en_GB]": "Lock the screen or log out", + "Description[es]": "Bloquear el escritorio o salir", + "Description[eu]": "Giltzatu pantaila edo saio-itxi", + "Description[fi]": "Lukitse näyttö tai kirjaudu ulos", + "Description[fr]": "Verrouiller l'écran ou se déconnecter", + "Description[hu]": "Képernyőzárolás vagy kijelentkezés", + "Description[ia]": "Bloca le schermo o claude session", + "Description[it]": "Blocca lo schermo o esci", + "Description[ko]": "화면을 잠그거나 로그아웃합니다", + "Description[lt]": "Užrakinti ekraną arba atsijungti", + "Description[nl]": "Vergrendel het scherm of meldt u af", + "Description[nn]": "Lås skjermen eller logg ut", + "Description[pa]": "ਸਕਰੀਨ ਲਾਕ ਜਾਂ ਲਾਗ ਆਉਟ ਕਰੋ", + "Description[pl]": "Blokuje ekran lub wylogowuje", + "Description[pt_BR]": "Bloqueia a tela ou desliga", + "Description[ro]": "Blochează ecranul sau iese din sistem", + "Description[ru]": "Кнопки блокирования экрана и завершения сеанса", + "Description[sk]": "Zamknutie obrazovky alebo odhlásenie", + "Description[sl]": "Zaklenite zaslon ali pa se odjavite", + "Description[sv]": "Lås skärmen eller logga ut", + "Description[ta]": "திரையைப் பூட்டு அல்லது வெளியேறு", + "Description[tr]": "Ekranı kilitleyin veya çıkın", + "Description[uk]": "Заблокуйте екран або вийдіть", + "Description[vi]": "Khoá màn hình hoặc đăng xuất", + "Description[x-test]": "xxLock the screen or log outxx", + "Description[zh_CN]": "锁屏或注销", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "system-shutdown", + "Id": "org.kde.plasma.lock_logout", + "License": "LGPL", + "Name": "Lock/Logout", + "Name[af]": "Sluit/Teken af", + "Name[ar]": "اقفل/اخرج", + "Name[az]": "Ekran kilidləmə və Çıxış", + "Name[be@latin]": "Blok na ekran i vychad z systemy", + "Name[bg]": "Заключване/Изход", + "Name[bn]": "লক/লগ-আউট", + "Name[bn_IN]": "লক/লগ-আউট করুন", + "Name[bs]": "Zaključavanje/odjava", + "Name[ca@valencia]": "Bloqueja/ix", + "Name[ca]": "Bloqueja/surt", + "Name[cs]": "Odhlášení/uzamčení", + "Name[csb]": "Blokòwanié ekranu/Wëlogòwanié", + "Name[da]": "Lås/Log ud", + "Name[de]": "Bildschirmsperre/Abmeldung", + "Name[el]": "Κλείδωμα/αποσύνδεση", + "Name[en_GB]": "Lock/Logout", + "Name[eo]": "Ŝloso/adiaŭo", + "Name[es]": "Bloquear/Terminar", + "Name[et]": "Lukustamine/väljalogimine", + "Name[eu]": "Giltzatu/Saio-itxi", + "Name[fi]": "Lukitus/uloskirjautuminen", + "Name[fr]": "Verrouillage / Déconnexion", + "Name[fy]": "Beskoattelje/ôfmelde", + "Name[ga]": "Cuir Faoi Ghlas/Logáil Amach", + "Name[gl]": "Trancar/Saír", + "Name[gu]": "તાળું/બહાર નીકળો", + "Name[he]": "נעילה/יציאה", + "Name[hi]": "तालाबंद/लॉगआउट", + "Name[hne]": "ताला/लागआउट", + "Name[hr]": "Zaključavanje/odjavljivanje", + "Name[hsb]": "Zamknjenje/Wotzjewjenje", + "Name[hu]": "Zárolás/kijelentkezés", + "Name[ia]": "Bloca/Claude session", + "Name[id]": "Kunci/Logout", + "Name[is]": "Læsa/stimpla út", + "Name[it]": "Blocca/Esci", + "Name[ja]": "ロック/ログアウト", + "Name[kk]": "Бұғаттау/Шығу", + "Name[km]": "ចាក់សោ/ចេញ", + "Name[kn]": "ಬಂಧಿಸು/ನಿರ್ಗಮಿಸು (ಲಾಗೌಟ್)", + "Name[ko]": "잠금/로그아웃", + "Name[ku]": "Kilît Bike/Derkeve", + "Name[lt]": "Užrakinti/atsijungti", + "Name[lv]": "Slēgt/Atteikties", + "Name[mk]": "Копчиња „Заклучи/Одјави се“", + "Name[ml]": "പൂട്ടുക/പുറത്തിറങ്ങുക", + "Name[mr]": "कुलूपबंद/बाहेर पडा", + "Name[nb]": "Lås/Logg ut", + "Name[nds]": "Afsluten/Afmellen", + "Name[ne]": "ताल्चा लगाउनुहोस्/लगआउट गर्नुहोस्", + "Name[nl]": "Vergrendelen/Afmelden", + "Name[nn]": "Lås / logg ut", + "Name[or]": "ଅପରିବର୍ତ୍ତନୀୟ/ଲଗଆଉଟ କରନ୍ତୁ", + "Name[pa]": "ਲਾਕ/ਲਾਗ ਆਉਟ", + "Name[pl]": "Blokowanie/wylogowywanie", + "Name[pt]": "Bloquear/Sair", + "Name[pt_BR]": "Bloquear/encerrar sessão", + "Name[ro]": "Blocare/Ieșire", + "Name[ru]": "Блокирование экрана и выход", + "Name[se]": "Lohkadeapmi/olggosčáliheapmi", + "Name[si]": "අගුලු දමන්න/ඉවත් වන්න", + "Name[sk]": "Zamknutie/odhlásenie", + "Name[sl]": "Zaklepanje/odjava", + "Name[sr@ijekavian]": "закључавање/одјава", + "Name[sr@ijekavianlatin]": "zaključavanje/odjava", + "Name[sr@latin]": "zaključavanje/odjava", + "Name[sr]": "закључавање/одјава", + "Name[sv]": "Låsning eller utloggning", + "Name[ta]": "பூட்டு/வெளியேறு", + "Name[te]": "లాక్/లాగ్అవుట్", + "Name[tg]": "Қулф/Баромад", + "Name[th]": "ล็อคหน้าจอ/ออกจากระบบ", + "Name[tr]": "Kilitle/Çık", + "Name[ug]": "قۇلۇپلا/تىزىمدىن چىق", + "Name[uk]": "Блокування/Вихід", + "Name[uz@cyrillic]": "Қулфлаш/Чиқиш", + "Name[uz]": "Qulflash/Chiqish", + "Name[vi]": "Khoá/Đăng xuất", + "Name[wa]": "Serer/Dislodjî", + "Name[x-test]": "xxLock/Logoutxx", + "Name[zh_CN]": "锁屏/注销", + "Name[zh_TW]": "鎖定/登出", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "Keywords": "Lock;Logout;Sleep;Hibernate;Switch User;", + "Keywords[ar]": "اقفل;اخرج;نم;أسبت;بدل المستخدم;", + "Keywords[ast]": "Bloquiar;Bloquéu;Suspender;Suspensión;Dormir;Dormición;Ivernar;Ivernación;Hibernar;Hibernación;Cambiar d'usuariu;Cambéu d'usuariu;Cambéu d'usuarios;", + "Keywords[az]": "Kilid;Sistemdən Çıxış;Yuxu Rejimi;Gözləmə Rejimi;İstifadəçini Dəyişmək;Lock;Logout;Sleep;Hibernate;Switch User;", + "Keywords[bs]": "Zaključavanje;Odjava;Spavanje;Hibernacija;Promjena korisnika;", + "Keywords[ca@valencia]": "Bloqueig;Eixida;Adorm;Hiberna;Canvi d'usuari;", + "Keywords[ca]": "Bloqueig;Sortida;Adorm;Hiberna;Canvi d'usuari;", + "Keywords[da]": "Lås;Log ud;Slumre;Dvale;Suspend;Skift bruger;suspender;", + "Keywords[de]": "Sperren;Abmelden;Standby;Ruhezustand;Tiefschlaf;Benutzer wechseln;", + "Keywords[el]": "Κλείδωμα;έξοδος;ύπνωση;νάρκη;εναλλαγήχρήστη;", + "Keywords[en_GB]": "Lock;Logout;Sleep;Hibernate;Switch User;", + "Keywords[es]": "Bloquear;Terminar la sesión;Dormir;Hibernar;Cambiar de usuario;", + "Keywords[et]": "Lukustamine;Väljalogimine;Uni;Talveuni;Kasutaja vahetamine;", + "Keywords[eu]": "giltzatu;blokeatu;saio-itxi;egin lo;hibernatu;aldatu erabiltzailea;", + "Keywords[fi]": "Lukitse;kirjaudu ulos;valmiustila;lepotila;vaihda käyttäjää;", + "Keywords[fr]": "Verrouillage;Déconnexion;Hibernation;Changement d'utilisateur;", + "Keywords[gl]": "Trancar;saír;durmir;hibernar;cambiar de usuario;", + "Keywords[he]": "Lock;Logout;Sleep;Hibernate;Switch User;נעילה;יציא;שינה;מצב שינה;החלפת משתמש", + "Keywords[hi]": "तालाबंद;लॉगआउट;निद्रा;सुप्तावस्था;उपयोक्ता बदलें;", + "Keywords[hsb]": "Lock;Logout;Sleep;Hibernate;Switch User;Hasnyć;Spinkać;Zymski spar;So Wotzjewić;", + "Keywords[hu]": "Zárolás;Kijelentkezés;Alvó állapot;Hibernálás;Felhasználóváltás;", + "Keywords[ia]": "Bloca;Claude session;Dormi; Hiberna;Cambia Usator;", + "Keywords[id]": "Kunci;Logout;Tidur;Hibernasi;Alihkan Pengguna;", + "Keywords[is]": "Læsa;útskrá;svæfa:leggja í dvala;Skipta um notanda;", + "Keywords[it]": "Blocca;Esci;Sospendi;Iberna;Cambia utente;", + "Keywords[ja]": "ロック;ログアウト;スリープ;ハイバネート;ユーザの切り替え;", + "Keywords[kk]": "Lock;Logout;Sleep;Hibernate;Switch User;", + "Keywords[ko]": "Lock;Logout;Sleep;Hibernate;Switch User;잠금;로그아웃;대기;대기 모드;최대 절전;최대 절전 모드;사용자 전환;", + "Keywords[lt]": "Užrakinti;Uzrakinti;Atsijungti;Pristabdyti;Pristabdyti į RAM;Miegoti;Miegas;Miego veiksena;Miego režimas;Miego rezimas;Pristabdyti i RAM;Pristabdyti i diska;Pristabdyti į diską;Sustabdyti;Užmigdyti;Uzmigdyti;Hibernuoti;Perjungti naudotoją;Perjungti naudotoja;", + "Keywords[ml]": "പൂട്ടുക;പുറത്തിറങ്ങുക;ഉറങ്ങുക;നിഷ്ക്രിയമാക്കുക;ഉപയോക്താവിനെ മാറ്റുക;", + "Keywords[mr]": "कुलूप; बाहेर पडा; झोप; हायबरनेट; वापरकर्ता बदला;", + "Keywords[nb]": "Lås; Logg ut; Hvile; Dvale; Bytt bruker;", + "Keywords[nds]": "Slott,afsluten,afmellen,slapen,infreren,Bruker wesseln;Brukerwessel", + "Keywords[nl]": "Vergrendelen;Afmelden;Slapen;Slaapstand naar schijf;Gebruiker wisselen;", + "Keywords[nn]": "lås;logg ut;utlogging;kvilemodus;dvalemodus;byt brukar;", + "Keywords[pa]": "ਲਾਕ;ਲਾਗਆਉਟ;ਸਲੀਪ;ਹਾਈਬਰਨੇਟ;ਵਰਤੋਂਕਾਰ ਬਦਲੋ;", + "Keywords[pl]": "Zablokuj;Wyloguj;Uśpij;Hibernuj;Przełącz użytkownika;", + "Keywords[pt]": "Bloquear;Encerrar;Suspender;Hibernar;Mudar de Utilizador;", + "Keywords[pt_BR]": "Bloquear;Encerrar;Suspender;Hibernar;Trocar usuário;", + "Keywords[ro]": "blochează;ieși;adormire;dormi;hibernare;schimbă utilizatorul;comutare utilizatori;", + "Keywords[ru]": "Lock;Logout;Sleep;Hibernate;Switch User;блокировать;блокировка;выход;выход из системы;ждущий режим;спящий режим;сон;режим гибернации;гибернация;сменить пользователя;смена пользователя;", + "Keywords[sk]": "Zamknúť;Odhlásiť;Uspať;Hibernovať;Prepnúť používateľa;", + "Keywords[sl]": "Zaklep;Odjava;Pripravljenost;Mirovanje;Zamenjava uporabnika;", + "Keywords[sr@ijekavian]": "Lock;Logout;Sleep;Hibernate;Switch User;закључавање;одјављивање;спавање;хибернација;пребацивање;", + "Keywords[sr@ijekavianlatin]": "Lock;Logout;Sleep;Hibernate;Switch User;zaključavanje;odjavljivanje;spavanje;hibernacija;prebacivanje;", + "Keywords[sr@latin]": "Lock;Logout;Sleep;Hibernate;Switch User;zaključavanje;odjavljivanje;spavanje;hibernacija;prebacivanje;", + "Keywords[sr]": "Lock;Logout;Sleep;Hibernate;Switch User;закључавање;одјављивање;спавање;хибернација;пребацивање;", + "Keywords[sv]": "Lås;Utloggning;Viloläge;Dvala;Byt användare;", + "Keywords[ta]": "Lock;Logout;Sleep;Hibernate;Switch User;பூட்டு;வெளியேறு;தூங்கு;உறங்கு;உறக்கம்;தூக்கம்;பயனர் மாற்றம்;", + "Keywords[tr]": "Kilitle;Oturumu Kapat;Beklet;Askıya Al;Kullanıcı Değiştir;", + "Keywords[uk]": "Lock;Logout;Sleep;Hibernate;Switch User;блокування;вихід;присипляння;сон;перемикання;користувач;", + "Keywords[vi]": "Lock;Logout;Sleep;Hibernate;Switch User;Khoá;Đăng xuất;Ngủ;Ngủ đông;Chuyển người dùng;", + "Keywords[x-test]": "xxLockxx;xxLogoutxx;xxSleepxx;xxHibernatexx;xxSwitch Userxx;", + "Keywords[zh_CN]": "锁定;注销;待机;睡眠;休眠;切换用户;", + "Keywords[zh_TW]": "Lock;Logout;Sleep;Hibernate;Switch User;", + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/lockout.qml" +} diff --git a/plasma/workspace/applets/manage-inputmethod/Messages.sh b/plasma/workspace/applets/manage-inputmethod/Messages.sh new file mode 100644 index 0000000000..49e6e038ac --- /dev/null +++ b/plasma/workspace/applets/manage-inputmethod/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.manageinputmethod.pot diff --git a/plasma/workspace/applets/manage-inputmethod/contents/ui/manage-inputmethod.qml b/plasma/workspace/applets/manage-inputmethod/contents/ui/manage-inputmethod.qml new file mode 100644 index 0000000000..17ff4ff143 --- /dev/null +++ b/plasma/workspace/applets/manage-inputmethod/contents/ui/manage-inputmethod.qml @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.plasma.workspace.keyboardlayout 1.0 as Keyboards +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.kirigami 2.5 as Kirigami // For Settings.tabletMode + +Item { + id: root + property var overlays: [] + + Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation + Plasmoid.fullRepresentation: Plasmoid.compactRepresentation + Plasmoid.compactRepresentation: PlasmaCore.IconItem { + source: plasmoid.icon + active: compactMouse.containsMouse + overlays: root.overlays + + MouseArea { + id: compactMouse + anchors.fill: parent + hoverEnabled: true + onClicked: if (!Keyboards.KWinVirtualKeyboard.available) { + root.action_settings() + } else if (Keyboards.KWinVirtualKeyboard.visible) { + Keyboards.KWinVirtualKeyboard.active = false + } else { + Keyboards.KWinVirtualKeyboard.enabled = !Keyboards.KWinVirtualKeyboard.enabled + } + } + } + + Component.onCompleted: { + plasmoid.setAction("settings", i18nc("Opens the system settings module", "Configure Virtual Keyboards..."), + "settings-configure") + } + + function action_settings() { + KCMShell.openSystemSettings("kcm_virtualkeyboard"); + } + + states: [ + State { + name: "available" + when: !Keyboards.KWinVirtualKeyboard.available + PropertyChanges { + target: plasmoid + icon: "input-keyboard-virtual-off" + toolTipSubText: i18n("Virtual Keyboard: unavailable") + status: PlasmaCore.Types.HiddenStatus + } + PropertyChanges { target: root; overlays: [ "emblem-unavailable" ] } + }, + State { + name: "disabled" + when: Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.enabled + PropertyChanges { + target: plasmoid + icon: "input-keyboard-virtual-off" + toolTipSubText: i18n("Virtual Keyboard: disabled") + status: PlasmaCore.Types.ActiveStatus + } + PropertyChanges { target: root; overlays: [] } + }, + State { + name: "visible" + when: Keyboards.KWinVirtualKeyboard.available && Keyboards.KWinVirtualKeyboard.visible + PropertyChanges { target: plasmoid + icon: "arrow-down" + toolTipSubText: i18n("Virtual Keyboard: visible") + // It's only relevant in tablet mode + status: Kirigami.Settings.tabletMode ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus + } + PropertyChanges { target: root; overlays: [] } + }, + State { + name: "idle" + when: Keyboards.KWinVirtualKeyboard.available && Keyboards.KWinVirtualKeyboard.enabled && !Keyboards.KWinVirtualKeyboard.visible + PropertyChanges { target: plasmoid + icon: "input-keyboard-virtual-on" + toolTipSubText: i18n("Virtual Keyboard: enabled") + // It's only relevant in tablet mode + status: Kirigami.Settings.tabletMode ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus + } + PropertyChanges { target: root; overlays: [] } + } + ] +} diff --git a/plasma/workspace/applets/manage-inputmethod/metadata.json b/plasma/workspace/applets/manage-inputmethod/metadata.json new file mode 100644 index 0000000000..2c4efb8841 --- /dev/null +++ b/plasma/workspace/applets/manage-inputmethod/metadata.json @@ -0,0 +1,128 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "aleixpol@kde.org", + "Name": "Aleix Pol Gonzalez", + "Name[ar]": "Aleix Pol Gonzalez", + "Name[az]": "Aleix Pol Gonzalez", + "Name[ca]": "Aleix Pol Gonzalez", + "Name[cs]": "Aleix Pol Gonzalez", + "Name[de]": "Aleix Pol Gonzalez", + "Name[en_GB]": "Aleix Pol Gonzalez", + "Name[es]": "Aleix Pol Gonzalez", + "Name[eu]": "Aleix Pol Gonzalez", + "Name[fi]": "Aleix Pol Gonzalez", + "Name[fr]": "Aleix Pol Gonzalez", + "Name[hu]": "Aleix Pol Gonzalez", + "Name[ia]": "Aleix Pol Gonzalez", + "Name[it]": "Aleix Pol Gonzalez", + "Name[ko]": "Aleix Pol Gonzalez", + "Name[lt]": "Aleix Pol Gonzalez", + "Name[nl]": "Aleix Pol Gonzalez", + "Name[nn]": "Aleix Pol Gonzalez", + "Name[pa]": "ਐਲਿਕਸ ਪੋਲ ਗੋਨਜ਼ਾਵੇਜ", + "Name[pl]": "Aleix Pol Gonzalez", + "Name[pt_BR]": "Aleix Pol Gonzalez", + "Name[ro]": "Aleix Pol Gonzalez", + "Name[ru]": "Aleix Pol Gonzalez", + "Name[sk]": "Aleix Pol Gonzalez", + "Name[sl]": "Aleix Pol Gonzalez", + "Name[sv]": "Aleix Pol Gonzalez", + "Name[tr]": "Aleix Pol Gonzalez", + "Name[uk]": "Aleix Pol Gonzalez", + "Name[vi]": "Aleix Pol Gonzalez", + "Name[x-test]": "xxAleix Pol Gonzalezxx", + "Name[zh_CN]": "Aleix Pol Gonzalez" + } + ], + "Category": "Accessibility", + "Description": "Manage your Input Methods", + "Description[ar]": "أدر طريقة إدخالك للنص", + "Description[az]": "Daxiletmə üsulunu idarə edin", + "Description[ca]": "Gestioneu els mètodes d'entrada", + "Description[cs]": "Spravujte svoje metody vstupu", + "Description[de]": "Eingabemethoden verwalten", + "Description[en_GB]": "Manage your Input Methods", + "Description[es]": "Gestión de métodos de entrada", + "Description[eu]": "Kudeatu zure sarrerako metodoak", + "Description[fi]": "Syötemenetelmien hallinta", + "Description[fr]": "Gérer vos méthodes de saisie", + "Description[hu]": "Beviteli módok kezelése", + "Description[ia]": "Gere tu methodos de insertar", + "Description[it]": "Gestire i metodi di inserimento", + "Description[ko]": "입력기 관리", + "Description[lt]": "Tvarkyti įvesties metodus", + "Description[nl]": "Uw invoermethoden beheren", + "Description[nn]": "Handsam skrivemetodar", + "Description[pa]": "ਆਪਣੇ ਇਨਪੁੱਟ ਢੰਗਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ", + "Description[pl]": "Zarządzaj swoimi metodami wprowadzania", + "Description[pt_BR]": "Gerencia os seus métodos de entrada", + "Description[ro]": "Gestionați metodele de introducere", + "Description[ru]": "Настройка методов ввода", + "Description[sk]": "Spravujte svoje metódy vstupu", + "Description[sl]": "Upravlja z vašimi vhodnimi metodami", + "Description[sv]": "Hantera dina inmatningsmetoder", + "Description[ta]": "உள்ளீடு முறைகளை நிர்வகியுங்கள்", + "Description[tr]": "Giriş yöntemlerinizi yönetin", + "Description[uk]": "Керування вашими способами введення", + "Description[vi]": "Quản lí các phương thức nhập", + "Description[x-test]": "xxManage your Input Methodsxx", + "Description[zh_CN]": "管理输入法", + "EnabledByDefault": true, + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "input-keyboard-virtual", + "Id": "org.kde.plasma.manage-inputmethod", + "License": "GPL-2.0+", + "Name": "Input Method", + "Name[ar]": "طريقة الإدخال", + "Name[az]": "Daxiletmə üsulu", + "Name[ca]": "Mètode d'entrada", + "Name[cs]": "Metoda vstupu", + "Name[da]": "Input-metode", + "Name[de]": "Eingabemethoden", + "Name[en_GB]": "Input Method", + "Name[es]": "Método de entrada", + "Name[eu]": "Sarrerako metodoa", + "Name[fi]": "Syötemenetelmä", + "Name[fr]": "Méthode de saisie", + "Name[hi]": "इनपुट विधि", + "Name[hsb]": "Metoda zapodawanja", + "Name[hu]": "Beviteli mód", + "Name[ia]": "Methodo de insertar", + "Name[it]": "Metodo di inserimento", + "Name[ko]": "입력기", + "Name[lt]": "Įvesties metodai", + "Name[ml]": "നിവേശന രീതി", + "Name[nl]": "Invoermethode", + "Name[nn]": "Skrivemetode", + "Name[pa]": "ਇਨਪੁ਼ੱਟ ਢੰਗ", + "Name[pl]": "Metoda wprowadzania", + "Name[pt]": "Método de Entrada", + "Name[pt_BR]": "Método de entrada", + "Name[ro]": "Metodă de introducere", + "Name[ru]": "Метод ввода", + "Name[sk]": "Metóda vstupu", + "Name[sl]": "Vhodna metoda", + "Name[sv]": "Inmatningsmetod", + "Name[ta]": "உள்ளீடு முறை", + "Name[tr]": "Giriş Yöntemi", + "Name[uk]": "Спосіб введення", + "Name[vi]": "Phương thức nhập", + "Name[x-test]": "xxInput Methodxx", + "Name[zh_CN]": "输入法", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "3.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/manage-inputmethod.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "SystemServices" +} diff --git a/plasma/workspace/applets/mediacontroller/Messages.sh b/plasma/workspace/applets/mediacontroller/Messages.sh new file mode 100644 index 0000000000..ae1b008db9 --- /dev/null +++ b/plasma/workspace/applets/mediacontroller/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.mediacontroller.pot diff --git a/plasma/workspace/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml b/plasma/workspace/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml new file mode 100644 index 0000000000..2693fc0f6e --- /dev/null +++ b/plasma/workspace/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml @@ -0,0 +1,604 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2014, 2016 Kai Uwe Broulik + SPDX-FileCopyrightText: 2020 Carson Black + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.kcoreaddons 1.0 as KCoreAddons +import org.kde.kirigami 2.4 as Kirigami +import QtGraphicalEffects 1.0 + +PlasmaExtras.Representation { + id: expandedRepresentation + + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 14 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 14 + Layout.preferredWidth: Layout.minimumWidth * 1.5 + Layout.preferredHeight: Layout.minimumHeight * 1.5 + + collapseMarginsHint: true + + readonly property int controlSize: PlasmaCore.Units.iconSizes.medium + + property double position: (mpris2Source.currentData && mpris2Source.currentData.Position) || 0 + readonly property real rate: (mpris2Source.currentData && mpris2Source.currentData.Rate) || 1 + readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 + readonly property bool canSeek: (mpris2Source.currentData && mpris2Source.currentData.CanSeek) || false + readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software + + // only show hours (the default for KFormat) when track is actually longer than an hour + readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours + + property bool disablePositionUpdate: false + property bool keyPressed: false + + function retrievePosition() { + var service = mpris2Source.serviceForSource(mpris2Source.current); + var operation = service.operationDescription("GetPosition"); + service.startOperationCall(operation); + } + + Connections { + target: plasmoid + function onExpandedChanged() { + if (plasmoid.expanded) { + retrievePosition(); + } + } + } + + onPositionChanged: { + // we don't want to interrupt the user dragging the slider + if (!seekSlider.pressed && !keyPressed) { + // we also don't want passive position updates + disablePositionUpdate = true + seekSlider.value = position + disablePositionUpdate = false + } + } + + onLengthChanged: { + disablePositionUpdate = true + // When reducing maximumValue, value is clamped to it, however + // when increasing it again it gets its old value back. + // To keep us from seeking to the end of the track when moving + // to a new track, we'll reset the value to zero and ask for the position again + seekSlider.value = 0 + seekSlider.to = length + retrievePosition() + disablePositionUpdate = false + } + + Keys.onPressed: keyPressed = true + + Keys.onReleased: { + keyPressed = false + + if ((event.key == Qt.Key_Tab || event.key == Qt.Key_Backtab) && event.modifiers & Qt.ControlModifier) { + event.accepted = true; + if (root.mprisSourcesModel.length > 2) { + var nextIndex = playerSelector.currentIndex + 1; + if (event.key == Qt.Key_Backtab || event.modifiers & Qt.ShiftModifier) { + nextIndex -= 2; + } + if (nextIndex == root.mprisSourcesModel.length) { + nextIndex = 0; + } + if (nextIndex < 0) { + nextIndex = root.mprisSourcesModel.length - 1; + } + playerSelector.currentIndex = nextIndex; + disablePositionUpdate = true; + mpris2Source.current = root.mprisSourcesModel[nextIndex]["source"]; + disablePositionUpdate = false; + } + } + + if (!event.modifiers) { + event.accepted = true + + if (event.key === Qt.Key_Space || event.key === Qt.Key_K) { + // K is YouTube's key for "play/pause" :) + root.togglePlaying() + } else if (event.key === Qt.Key_P) { + root.action_previous() + } else if (event.key === Qt.Key_N) { + root.action_next() + } else if (event.key === Qt.Key_S) { + root.action_stop() + } else if (event.key === Qt.Key_Left || event.key === Qt.Key_J) { // TODO ltr languages + // seek back 5s + seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds + seekSlider.moved(); + } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { + // seek forward 5s + seekSlider.value = Math.min(seekSlider.to, seekSlider.value + 5000000) + seekSlider.moved(); + } else if (event.key === Qt.Key_Home) { + seekSlider.value = 0 + seekSlider.moved(); + } else if (event.key === Qt.Key_End) { + seekSlider.value = seekSlider.to + seekSlider.moved(); + } else if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) { + // jump to percentage, ie. 0 = beginnign, 1 = 10% of total length etc + seekSlider.value = seekSlider.to * (event.key - Qt.Key_0) / 10 + seekSlider.moved(); + } else { + event.accepted = false + } + } + } + + Item { // Album Art Background + Details + anchors.fill: parent + clip: true + + ShaderEffect { + id: backgroundImage + property real scaleFactor: 1.0 + property Image source: albumArt + + anchors.centerIn: parent + visible: !!root.track && source.status === Image.Ready && !softwareRendering + + layer.enabled: !softwareRendering + layer.effect: HueSaturation { + cached: true + + lightness: -0.5 + saturation: 0.9 + + layer.enabled: true + layer.effect: GaussianBlur { + cached: true + + radius: 128 + deviation: 12 + samples: 63 + + transparentBorder: false + } + } + // use State to avoid unnecessary reevaluation of width and height + states: State { + name: "albumArtReady" + when: plasmoid.expanded && backgroundImage.visible && albumArt.paintedWidth > 0 + PropertyChanges { + target: backgroundImage + scaleFactor: Math.max(parent.width / source.paintedWidth, parent.height / source.paintedHeight) + width: Math.round(source.paintedWidth * scaleFactor) + height: Math.round(source.paintedHeight * scaleFactor) + } + } + } + RowLayout { // Album Art + Details + id: albumRow + + anchors { + fill: parent + leftMargin: PlasmaCore.Units.largeSpacing + rightMargin: PlasmaCore.Units.largeSpacing + } + + spacing: PlasmaCore.Units.largeSpacing + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 50 + + Image { // Album Art + id: albumArt + + anchors.fill: parent + + visible: !!root.track && status === Image.Ready + + asynchronous: true + + horizontalAlignment: Image.AlignRight + verticalAlignment: Image.AlignVCenter + fillMode: Image.PreserveAspectFit + + source: root.albumArt + } + + Loader { + // When albumArt is shown, the icon is unloaded to reduce memory usage. + readonly property string icon: (mpris2Source.currentData && mpris2Source.currentData["Desktop Icon Name"]) || "media-album-cover" + active: !albumArt.visible + anchors.fill: parent + + sourceComponent: root.track ? fallbackIconItem : placeholderMessage + + Component { + id: fallbackIconItem + + PlasmaCore.IconItem { // Fallback + source: icon + anchors { + fill: parent + margins: PlasmaCore.Units.largeSpacing * 2 + } + } + } + + Component { + id: placeholderMessage + Item { // Put PlaceholderMessage in Item so PlaceholderMessage will not fill its parent. + anchors.fill: parent + + PlasmaExtras.PlaceholderMessage { // "No media playing" placeholder message + width: parent.width // For text wrap + anchors.centerIn: parent + iconName: icon + text: i18n("No media playing") + } + } + } + } + } + + ColumnLayout { // Details Column + visible: root.track + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 50 + + /* + * We use Kirigami.Heading instead of PlasmaExtras.Heading + * to prevent a binding loop caused by the PC2 Label component + * used by PlasmaExtras.Heading + */ + Kirigami.Heading { // Song Title + id: songTitle + level: 1 + + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" + + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight + + text: root.track + + Layout.fillWidth: true + Layout.maximumHeight: PlasmaCore.Units.gridUnit*5 + } + Kirigami.Heading { // Song Artist + id: songArtist + visible: root.artist + level: 2 + + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" + + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight + + text: root.artist + Layout.fillWidth: true + Layout.maximumHeight: PlasmaCore.Units.gridUnit*2 + } + Kirigami.Heading { // Song Album + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" + + level: 3 + opacity: 0.6 + + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight + + visible: text.length !== 0 + text: { + var metadata = root.currentMetadata + if (!metadata) { + return "" + } + var xesamAlbum = metadata["xesam:album"] + if (xesamAlbum) { + return xesamAlbum + } + + // if we play a local file without title and artist, show its containing folder instead + if (metadata["xesam:title"] || root.artist) { + return "" + } + + var xesamUrl = (metadata["xesam:url"] || "").toString() + if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" + return "" + } + + var urlParts = xesamUrl.split("/") + if (urlParts.length < 3) { + return "" + } + + var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename + if (lastFolderPath) { + return lastFolderPath + } + + return "" + } + Layout.fillWidth: true + Layout.maximumHeight: PlasmaCore.Units.gridUnit*2 + } + } + } + } + + footer: PlasmaExtras.PlasmoidHeading { + id: footerItem + location: PlasmaExtras.PlasmoidHeading.Location.Footer + ColumnLayout { // Main Column Layout + anchors.fill: parent + RowLayout { // Seek Bar + spacing: PlasmaCore.Units.smallSpacing + + // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case + enabled: !root.noPlayer && root.track && expandedRepresentation.length > 0 ? true : false + opacity: enabled ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: PlasmaCore.Units.longDuration } + } + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.maximumWidth: Math.min(PlasmaCore.Units.gridUnit*45, Math.round(expandedRepresentation.width*(7/10))) + + // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song + TextMetrics { + id: timeMetrics + text: i18nc("Remaining time for song e.g -5:42", "-%1", + KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions)) + font: PlasmaCore.Theme.smallestFont + } + + PlasmaComponents3.Label { // Time Elapsed + Layout.preferredWidth: timeMetrics.width + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) + opacity: 0.9 + font: PlasmaCore.Theme.smallestFont + color: PlasmaCore.ColorScope.textColor + } + + PlasmaComponents3.Slider { // Slider + id: seekSlider + Layout.fillWidth: true + z: 999 + value: 0 + visible: canSeek + + onMoved: { + if (!disablePositionUpdate) { + // delay setting the position to avoid race conditions + queuedPositionUpdate.restart() + } + } + + Timer { + id: seekTimer + interval: 1000 / expandedRepresentation.rate + repeat: true + running: root.state === "playing" && plasmoid.expanded && !keyPressed && interval > 0 && seekSlider.to >= 1000000 + onTriggered: { + // some players don't continuously update the seek slider position via mpris + // add one second; value in microseconds + if (!seekSlider.pressed) { + disablePositionUpdate = true + if (seekSlider.value == seekSlider.to) { + retrievePosition(); + } else { + seekSlider.value += 1000000 + } + disablePositionUpdate = false + } + } + } + } + + RowLayout { + visible: !canSeek + + Layout.fillWidth: true + Layout.preferredHeight: seekSlider.height + + PlasmaComponents3.ProgressBar { // Time Remaining + value: seekSlider.value + from: seekSlider.from + to: seekSlider.to + + Layout.fillWidth: true + Layout.fillHeight: false + Layout.alignment: Qt.AlignVCenter + } + } + + PlasmaComponents3.Label { + Layout.preferredWidth: timeMetrics.width + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + text: i18nc("Remaining time for song e.g -5:42", "-%1", + KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) + opacity: 0.9 + font: PlasmaCore.Theme.smallestFont + color: PlasmaCore.ColorScope.textColor + } + } + + RowLayout { // Player Controls + id: playerControls + + property bool enabled: root.canControl + property int controlsSize: PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height * 3 + + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents3.ToolButton { + Layout.rightMargin: LayoutMirroring.enabled ? 0 : PlasmaCore.Units.largeSpacing - playerControls.spacing + Layout.leftMargin: LayoutMirroring.enabled ? PlasmaCore.Units.largeSpacing - playerControls.spacing : 0 + icon.name: "media-playlist-shuffle" + icon.width: expandedRepresentation.controlSize + icon.height: expandedRepresentation.controlSize + checked: root.shuffle === true + enabled: root.canControl && root.shuffle !== undefined + Accessible.name: i18n("Shuffle") + onClicked: { + const service = mpris2Source.serviceForSource(mpris2Source.current); + let operation = service.operationDescription("SetShuffle"); + operation.on = !root.shuffle; + service.startOperationCall(operation); + } + + PlasmaComponents3.ToolTip { + text: parent.Accessible.name + } + } + + PlasmaComponents3.ToolButton { // Previous + icon.width: expandedRepresentation.controlSize + icon.height: expandedRepresentation.controlSize + Layout.alignment: Qt.AlignVCenter + enabled: playerControls.enabled && root.canGoPrevious + icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" + onClicked: { + seekSlider.value = 0 // Let the media start from beginning. Bug 362473 + root.action_previous() + } + } + + PlasmaComponents3.ToolButton { // Pause/Play + icon.width: expandedRepresentation.controlSize + icon.height: expandedRepresentation.controlSize + Layout.alignment: Qt.AlignVCenter + enabled: root.state == "playing" ? root.canPause : root.canPlay + icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start" + onClicked: root.togglePlaying() + } + + PlasmaComponents3.ToolButton { // Next + icon.width: expandedRepresentation.controlSize + icon.height: expandedRepresentation.controlSize + Layout.alignment: Qt.AlignVCenter + enabled: playerControls.enabled && root.canGoNext + icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" + onClicked: { + seekSlider.value = 0 // Let the media start from beginning. Bug 362473 + root.action_next() + } + } + + PlasmaComponents3.ToolButton { + Layout.leftMargin: LayoutMirroring.enabled ? 0 : PlasmaCore.Units.largeSpacing - playerControls.spacing + Layout.rightMargin: LayoutMirroring.enabled ? PlasmaCore.Units.largeSpacing - playerControls.spacing : 0 + icon.name: root.loopStatus === "Track" ? "media-playlist-repeat-song" : "media-playlist-repeat" + icon.width: expandedRepresentation.controlSize + icon.height: expandedRepresentation.controlSize + checked: root.loopStatus !== undefined && root.loopStatus !== "None" + enabled: root.canControl && root.loopStatus !== undefined + Accessible.name: root.loopStatus === "Track" ? i18n("Repeat Track") : i18n("Repeat") + onClicked: { + const service = mpris2Source.serviceForSource(mpris2Source.current); + let operation = service.operationDescription("SetLoopStatus"); + switch (root.loopStatus) { + case "Playlist": + operation.status = "Track"; + break; + case "Track": + operation.status = "None"; + break; + default: + operation.status = "Playlist"; + } + service.startOperationCall(operation); + } + + PlasmaComponents3.ToolTip { + text: parent.Accessible.name + } + } + } + } + } + + header: PlasmaExtras.PlasmoidHeading { + id: headerItem + location: PlasmaExtras.PlasmoidHeading.Location.Header + visible: playerList.model.length > 2 // more than one player, @multiplex is always there + //this removes top padding to allow tabbar to touch the edge + topPadding: topInset + bottomPadding: -bottomInset + implicitHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaComponents3.TabBar { + id: playerSelector + position: PlasmaComponents3.TabBar.Header + + anchors.fill: parent + + implicitHeight: contentHeight + + Repeater { + id: playerList + model: root.mprisSourcesModel + + delegate: PlasmaComponents3.TabButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + icon.name: modelData["icon"] + icon.height: PlasmaCore.Units.iconSizes.smallMedium + Accessible.name: modelData["text"] + PlasmaComponents3.ToolTip { + text: modelData["text"] + } + // Keep the delegate centered by offsetting the padding removed in the parent + bottomPadding: verticalPadding + headerItem.bottomPadding + topPadding: verticalPadding - headerItem.bottomPadding + onClicked: { + disablePositionUpdate = true + mpris2Source.current = modelData["source"]; + disablePositionUpdate = false + } + } + + onModelChanged: { + playerSelector.currentIndex = model.findIndex( + (data) => { return data.source === mpris2Source.current } + ) + } + } + } + } + + Timer { + id: queuedPositionUpdate + interval: 100 + onTriggered: { + if (position == seekSlider.value) { + return; + } + var service = mpris2Source.serviceForSource(mpris2Source.current) + var operation = service.operationDescription("SetPosition") + operation.microseconds = seekSlider.value + service.startOperationCall(operation) + } + } +} diff --git a/plasma/workspace/applets/mediacontroller/contents/ui/main.qml b/plasma/workspace/applets/mediacontroller/contents/ui/main.qml new file mode 100644 index 0000000000..fc98d99c4b --- /dev/null +++ b/plasma/workspace/applets/mediacontroller/contents/ui/main.qml @@ -0,0 +1,333 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + id: root + + property var currentMetadata: mpris2Source.currentData ? mpris2Source.currentData.Metadata : undefined + property string track: { + if (!currentMetadata) { + return "" + } + var xesamTitle = currentMetadata["xesam:title"] + if (xesamTitle) { + return xesamTitle + } + // if no track title is given, print out the file name + var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" + if (!xesamUrl) { + return "" + } + var lastSlashPos = xesamUrl.lastIndexOf('/') + if (lastSlashPos < 0) { + return "" + } + var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) + return decodeURIComponent(lastUrlPart) + } + property string artist: { + if (!currentMetadata) { + return "" + } + var xesamArtist = currentMetadata["xesam:artist"] + if (!xesamArtist) { + return ""; + } + + if (typeof xesamArtist == "string") { + return xesamArtist + } else { + return xesamArtist.join(", ") + } + } + property string albumArt: currentMetadata ? currentMetadata["mpris:artUrl"] || "" : "" + + readonly property string identity: !root.noPlayer ? mpris2Source.currentData.Identity || mpris2Source.current : "" + + property bool noPlayer: mpris2Source.sources.length <= 1 + + property var mprisSourcesModel: [] + + readonly property bool canControl: (!root.noPlayer && mpris2Source.currentData.CanControl) || false + readonly property bool canGoPrevious: (canControl && mpris2Source.currentData.CanGoPrevious) || false + readonly property bool canGoNext: (canControl && mpris2Source.currentData.CanGoNext) || false + readonly property bool canPlay: (canControl && mpris2Source.currentData.CanPlay) || false + readonly property bool canPause: (canControl && mpris2Source.currentData.CanPause) || false + + // var instead of bool so we can use "undefined" for "shuffle not supported" + readonly property var shuffle: !root.noPlayer && typeof mpris2Source.currentData.Shuffle === "boolean" + ? mpris2Source.currentData.Shuffle : undefined + readonly property var loopStatus: !root.noPlayer && typeof mpris2Source.currentData.LoopStatus === "string" + ? mpris2Source.currentData.LoopStatus : undefined + + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 14 + Plasmoid.switchHeight: PlasmaCore.Units.gridUnit * 10 + Plasmoid.icon: "media-playback-playing" + Plasmoid.toolTipMainText: i18n("No media playing") + Plasmoid.toolTipSubText: identity + Plasmoid.toolTipTextFormat: Text.PlainText + Plasmoid.status: PlasmaCore.Types.PassiveStatus + + Plasmoid.onContextualActionsAboutToShow: { + plasmoid.clearActions() + + if (root.noPlayer) { + return + } + + if (mpris2Source.currentData.CanRaise) { + var icon = mpris2Source.currentData["Desktop Icon Name"] || "" + plasmoid.setAction("open", i18nc("Open player window or bring it to the front if already open", "Open"), icon) + } + + if (canControl) { + plasmoid.setAction("previous", i18nc("Play previous track", "Previous Track"), + Qt.application.layoutDirection === Qt.RightToLeft ? "media-skip-forward" : "media-skip-backward"); + plasmoid.action("previous").enabled = Qt.binding(function() { + return root.canGoPrevious + }) + + // if CanPause, toggle the menu entry between Play & Pause, otherwise always use Play + if (root.state == "playing" && root.canPause) { + plasmoid.setAction("pause", i18nc("Pause playback", "Pause"), "media-playback-pause") + plasmoid.action("pause").enabled = Qt.binding(function() { + return root.state === "playing" && root.canPause; + }); + } else { + plasmoid.setAction("play", i18nc("Start playback", "Play"), "media-playback-start") + plasmoid.action("play").enabled = Qt.binding(function() { + return root.state !== "playing" && root.canPlay; + }); + } + + plasmoid.setAction("next", i18nc("Play next track", "Next Track"), + Qt.application.layoutDirection === Qt.RightToLeft ? "media-skip-backward" : "media-skip-forward") + plasmoid.action("next").enabled = Qt.binding(function() { + return root.canGoNext + }) + + plasmoid.setAction("stop", i18nc("Stop playback", "Stop"), "media-playback-stop") + plasmoid.action("stop").enabled = Qt.binding(function() { + return root.state === "playing" || root.state === "paused"; + }) + } + + if (mpris2Source.currentData.CanQuit) { + plasmoid.setActionSeparator("quitseparator"); + plasmoid.setAction("quit", i18nc("Quit player", "Quit"), "application-exit") + } + } + + // HACK Some players like Amarok take quite a while to load the next track + // this avoids having the plasmoid jump between popup and panel + onStateChanged: { + if (state != "") { + plasmoid.status = PlasmaCore.Types.ActiveStatus + } else { + updatePlasmoidStatusTimer.restart() + } + } + + Timer { + id: updatePlasmoidStatusTimer + interval: 3000 + onTriggered: { + if (state != "") { + plasmoid.status = PlasmaCore.Types.ActiveStatus + } else { + plasmoid.status = PlasmaCore.Types.PassiveStatus + } + } + } + + Plasmoid.fullRepresentation: ExpandedRepresentation {} + + Plasmoid.compactRepresentation: PlasmaCore.IconItem { + source: { + if (root.state === "playing") { + return "media-playback-playing"; + } else if (root.state === "paused") { + return "media-playback-paused"; + } else { + return "media-playback-stopped"; + } + } + active: compactMouse.containsMouse + + MouseArea { + id: compactMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton + + onWheel: { + var service = mpris2Source.serviceForSource(mpris2Source.current) + var operation = service.operationDescription("ChangeVolume") + operation.delta = (wheel.angleDelta.y / 120) * 0.03 + operation.showOSD = true + service.startOperationCall(operation) + } + + onClicked: { + switch (mouse.button) { + case Qt.MiddleButton: + root.togglePlaying() + break + case Qt.BackButton: + root.action_previous() + break + case Qt.ForwardButton: + root.action_next() + break + default: + plasmoid.expanded = !plasmoid.expanded + } + } + } + } + + PlasmaCore.DataSource { + id: mpris2Source + + readonly property string multiplexSource: "@multiplex" + property string current: multiplexSource + + readonly property var currentData: data[current] + + engine: "mpris2" + connectedSources: sources + + onSourceAdded: { + updateMprisSourcesModel() + } + + onSourceRemoved: { + // if player is closed, reset to multiplex source + if (source === current) { + current = multiplexSource + } + updateMprisSourcesModel() + } + } + + Component.onCompleted: { + mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts() + updateMprisSourcesModel() + } + + function togglePlaying() { + if (root.state === "playing") { + if (root.canPause) { + root.action_pause(); + } + } else { + if (root.canPlay) { + root.action_play(); + } + } + } + + function action_open() { + serviceOp(mpris2Source.current, "Raise"); + } + function action_quit() { + serviceOp(mpris2Source.current, "Quit"); + } + + function action_play() { + serviceOp(mpris2Source.current, "Play"); + } + + function action_pause() { + serviceOp(mpris2Source.current, "Pause"); + } + + function action_playPause() { + serviceOp(mpris2Source.current, "PlayPause"); + } + + function action_previous() { + serviceOp(mpris2Source.current, "Previous"); + } + + function action_next() { + serviceOp(mpris2Source.current, "Next"); + } + + function action_stop() { + serviceOp(mpris2Source.current, "Stop"); + } + + function serviceOp(src, op) { + var service = mpris2Source.serviceForSource(src); + var operation = service.operationDescription(op); + return service.startOperationCall(operation); + } + + function updateMprisSourcesModel () { + + var model = [{ + 'text': i18n("Choose player automatically"), + 'icon': 'emblem-favorite', + 'source': mpris2Source.multiplexSource + }] + + var sources = mpris2Source.sources + for (var i = 0, length = sources.length; i < length; ++i) { + var source = sources[i] + if (source === mpris2Source.multiplexSource) { + continue + } + + const playerData = mpris2Source.data[source]; + // source data is removed before its name is removed from the list + if (!playerData) { + continue; + } + + model.push({ + 'text': playerData["Identity"], + 'icon': playerData["Desktop Icon Name"] || playerData["DesktopEntry"] || "emblem-music-symbolic", + 'source': source + }); + } + + root.mprisSourcesModel = model; + } + + states: [ + State { + name: "playing" + when: !root.noPlayer && mpris2Source.currentData.PlaybackStatus === "Playing" + + PropertyChanges { + target: plasmoid + icon: "media-playback-playing" + toolTipMainText: track + toolTipSubText: artist ? i18nc("by Artist (player name)", "by %1 (%2)", artist, identity) : identity + } + }, + State { + name: "paused" + when: !root.noPlayer && mpris2Source.currentData.PlaybackStatus === "Paused" + + PropertyChanges { + target: plasmoid + icon: "media-playback-paused" + toolTipMainText: track + toolTipSubText: artist ? i18nc("by Artist (paused, player name)", "by %1 (paused, %2)", artist, identity) : i18nc("Paused (player name)", "Paused (%1)", identity) + } + } + ] +} diff --git a/plasma/workspace/applets/mediacontroller/metadata.json b/plasma/workspace/applets/mediacontroller/metadata.json new file mode 100644 index 0000000000..31a0fa776c --- /dev/null +++ b/plasma/workspace/applets/mediacontroller/metadata.json @@ -0,0 +1,151 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "sebas@kde.org", + "Name": "Sebastian Kügler", + "Name[ar]": "Sebastian Kügler", + "Name[az]": "Sebastian Kügler", + "Name[ca]": "Sebastian Kügler", + "Name[cs]": "Sebastian Kügler", + "Name[de]": "Sebastian Kügler", + "Name[en_GB]": "Sebastian Kügler", + "Name[es]": "Sebastian Kügler", + "Name[eu]": "Sebastian Kügler", + "Name[fi]": "Sebastian Kügler", + "Name[fr]": "Sebastian Kügler", + "Name[hu]": "Sebastian Kügler", + "Name[ia]": "Sebastian Kügler", + "Name[it]": "Sebastian Kügler", + "Name[ko]": "Sebastian Kügler", + "Name[lt]": "Sebastian Kügler", + "Name[nl]": "Sebastian Kügler", + "Name[nn]": "Sebastian Kügler", + "Name[pa]": "Sebastian Kügler", + "Name[pl]": "Sebastian Kügler", + "Name[pt_BR]": "Sebastian Kügler", + "Name[ro]": "Sebastian Kügler", + "Name[ru]": "Sebastian Kügler", + "Name[sk]": "Sebastian Kügler", + "Name[sl]": "Sebastian Kügler", + "Name[sv]": "Sebastian Kügler", + "Name[ta]": "ஸெபாஸ்டியன் கூக்லர்", + "Name[tr]": "Sebastian Kügler", + "Name[uk]": "Sebastian Kügler", + "Name[vi]": "Sebastian Kügler", + "Name[x-test]": "xxSebastian Küglerxx", + "Name[zh_CN]": "Sebastian Kügler" + } + ], + "Category": "Multimedia", + "Description": "Media Player Controls", + "Description[ar]": "تحكّمات مشغّل الوسائط", + "Description[az]": "Media Pleyer İdarəsi", + "Description[ca]": "Controls del reproductor multimèdia", + "Description[cs]": "Ovládání přehrávače médií", + "Description[de]": "Steuerung der Medienwiedergabe", + "Description[en_GB]": "Media Player Controls", + "Description[es]": "Controles del reproductor multimedia", + "Description[eu]": "Euskarri jotzailearen aginteak", + "Description[fi]": "Mediasoittimen säätimet", + "Description[fr]": "Contrôles du lecteur multimédia", + "Description[hu]": "Médialejátszó vezérlők", + "Description[ia]": "Controlos de Reproductor de Multimedia", + "Description[it]": "Controlli del lettore multimediale", + "Description[ko]": "미디어 재생기 제어", + "Description[lt]": "Medijos leistuvės valdikliai", + "Description[nl]": "Besturing van mediaspeler", + "Description[nn]": "Mediespelar­kontrollar", + "Description[pa]": "ਮੀਡਿਆ ਪਲੇਅਰ ਕੰਟਰੋਲ", + "Description[pl]": "Obsługa odtwarzacza multimedialnego", + "Description[pt_BR]": "Controles do reprodutor de mídia", + "Description[ro]": "Controale redare multimedia", + "Description[ru]": "Управление мультимедийным проигрывателем", + "Description[sk]": "Ovládanie prehrávača médií", + "Description[sl]": "Nadzor predstavnostnega predvajalnika", + "Description[sv]": "Kontroller för mediaspelare", + "Description[ta]": "ஊடக இயக்கி கட்டுப்பாடு", + "Description[tr]": "Ortam Yürütücüsü Denetimleri", + "Description[uk]": "Керування мультимедійним програвачем", + "Description[vi]": "Các điều khiển của trình phát phương tiện", + "Description[x-test]": "xxMedia Player Controlsxx", + "Description[zh_CN]": "媒体播放器控件", + "EnabledByDefault": true, + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "applications-multimedia", + "Id": "org.kde.plasma.mediacontroller", + "License": "GPL-2.0+", + "Name": "Media Player", + "Name[ar]": "مشغّل وسائط", + "Name[az]": "Mediya Pleyer", + "Name[bs]": "Izvođač medija", + "Name[ca@valencia]": "Reproductor multimèdia", + "Name[ca]": "Reproductor multimèdia", + "Name[cs]": "Přehrávač médií", + "Name[da]": "Medieafspiller", + "Name[de]": "Medienwiedergabe", + "Name[el]": "Αναπαραγωγέας πολυμέσων", + "Name[en_GB]": "Media Player", + "Name[es]": "Reproductor multimedia", + "Name[et]": "Meediamängija", + "Name[eu]": "Euskarri jotzailea", + "Name[fi]": "Mediasoitin", + "Name[fr]": "Lecteur multimédia", + "Name[gl]": "Reprodutor multimedia", + "Name[he]": "נגן מדיה", + "Name[hi]": "मीडिया-प्लेयर", + "Name[hsb]": "Medijowy wothrawak", + "Name[hu]": "Médialejátszó", + "Name[ia]": "Media Player (Reproductor de Media)", + "Name[id]": "Pemutar Media", + "Name[is]": "Margmiðlunarspilari", + "Name[it]": "Lettore multimediale", + "Name[ja]": "メディアプレーヤー", + "Name[ko]": "미디어 재생기", + "Name[lt]": "Medijos leistuvė", + "Name[ml]": "മീഡിയ പ്ലെയര്‍", + "Name[nb]": "Mediespiller", + "Name[nds]": "Medienafspeler", + "Name[nl]": "Mediaspeler", + "Name[nn]": "Mediespelar", + "Name[pa]": "ਮੀਡਿਆ ਪਲੇਅਰ", + "Name[pl]": "Odtwarzacz multimedialny", + "Name[pt]": "Reprodutor Multimédia", + "Name[pt_BR]": "Reprodutor de mídia", + "Name[ro]": "Redare multimedia", + "Name[ru]": "Проигрыватель", + "Name[sk]": "Prehrávač médií", + "Name[sl]": "Predstavnostni predvajalnik", + "Name[sr@ijekavian]": "медија плејер", + "Name[sr@ijekavianlatin]": "medija plejer", + "Name[sr@latin]": "medija plejer", + "Name[sr]": "медија плејер", + "Name[sv]": "Mediaspelare", + "Name[ta]": "ஊடக இயக்கி", + "Name[tg]": "Плеери медиа", + "Name[tr]": "Ortam Yürütücüsü", + "Name[uk]": "Програвач", + "Name[vi]": "Trình phát phương tiện", + "Name[x-test]": "xxMedia Playerxx", + "Name[zh_CN]": "媒体播放器", + "Name[zh_TW]": "媒體播放器", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-DBusActivationService": "org.mpris.MediaPlayer2.*", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "ApplicationStatus", + "X-Plasma-Provides": [ + "org.kde.plasma.multimediacontrols" + ], + "X-Plasma-StandAloneApp": true +} diff --git a/plasma/workspace/applets/notifications/CMakeLists.txt b/plasma/workspace/applets/notifications/CMakeLists.txt new file mode 100644 index 0000000000..ea922c7942 --- /dev/null +++ b/plasma/workspace/applets/notifications/CMakeLists.txt @@ -0,0 +1,32 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.notifications\") + +set(notificationapplet_SRCS + notificationapplet.cpp + fileinfo.cpp + filemenu.cpp + globalshortcuts.cpp + texteditclickhandler.cpp + thumbnailer.cpp +) + +kcoreaddons_add_plugin(plasma_applet_notifications SOURCES ${notificationapplet_SRCS} INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(plasma_applet_notifications + Qt::Gui + Qt::Quick # for QQmlParserStatus + KF5::ConfigWidgets # for KStandardAction + KF5::I18n + KF5::Plasma + KF5::PlasmaQuick + KF5::GlobalAccel + KF5::KIOGui + KF5::KIOWidgets # for PreviewJob + KF5::Notifications # for KNotificationJobUiDelegate + ) + +ecm_qt_declare_logging_category(plasma_applet_notifications + HEADER notifications_debug.h + IDENTIFIER PLASMA_APPLET_NOTIFICATIONS_DEBUG + CATEGORY_NAME org.kde.plasma.notifications) + +plasma_install_package(package org.kde.plasma.notifications) diff --git a/plasma/workspace/applets/notifications/Messages.sh b/plasma/workspace/applets/notifications/Messages.sh new file mode 100644 index 0000000000..bdf2187f94 --- /dev/null +++ b/plasma/workspace/applets/notifications/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.notifications.pot diff --git a/plasma/workspace/applets/notifications/fileinfo.cpp b/plasma/workspace/applets/notifications/fileinfo.cpp new file mode 100644 index 0000000000..47d4582284 --- /dev/null +++ b/plasma/workspace/applets/notifications/fileinfo.cpp @@ -0,0 +1,187 @@ +/* + SPDX-FileCopyrightText: 2021 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "fileinfo.h" +#include "notifications_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +FileInfo::FileInfo(QObject *parent) + : QObject(parent) +{ +} + +FileInfo::~FileInfo() = default; + +QUrl FileInfo::url() const +{ + return m_url; +} + +void FileInfo::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + reload(); + Q_EMIT urlChanged(url); + } +} + +bool FileInfo::busy() const +{ + return m_busy; +} + +void FileInfo::setBusy(bool busy) +{ + if (m_busy != busy) { + m_busy = busy; + Q_EMIT busyChanged(busy); + } +} + +int FileInfo::error() const +{ + return m_error; +} + +void FileInfo::setError(int error) +{ + if (m_error != error) { + m_error = error; + Q_EMIT errorChanged(error); + } +} + +QString FileInfo::mimeType() const +{ + return m_mimeType; +} + +QString FileInfo::iconName() const +{ + return m_iconName; +} + +QAction *FileInfo::openAction() const +{ + return m_openAction; +} + +QString FileInfo::openActionIconName() const +{ + return m_openAction ? m_openAction->icon().name() : QString(); +} + +void FileInfo::reload() +{ + if (!m_url.isValid()) { + return; + } + + if (m_job) { + m_job->kill(); + } + + setError(0); + + // Do a quick guess by file name while we wait for the job to find the mime type + QString guessedMimeType; + + // NOTE using QUrl::path() for API that accepts local files is usually wrong + // but here we really only care about the file name and its extension. + const auto type = QMimeDatabase().mimeTypeForFile(m_url.path(), QMimeDatabase::MatchExtension); + if (!type.isDefault()) { + guessedMimeType = type.name(); + } + + mimeTypeFound(guessedMimeType); + + m_job = new KIO::MimeTypeFinderJob(m_url); + m_job->setAuthenticationPromptEnabled(false); + + const QUrl url = m_url; + connect(m_job, &KIO::MimeTypeFinderJob::result, this, [this, url] { + setError(m_job->error()); + if (m_job->error()) { + qCWarning(PLASMA_APPLET_NOTIFICATIONS_DEBUG) << "Failed to determine mime type for" << url << m_job->errorString(); + } else { + mimeTypeFound(m_job->mimeType()); + } + setBusy(false); + }); + + setBusy(true); + m_job->start(); +} + +void FileInfo::mimeTypeFound(const QString &mimeType) +{ + if (m_mimeType == mimeType) { + return; + } + + const QString oldOpenActionIconName = openActionIconName(); + + bool emitOpenActionChanged = false; + if (!m_openAction) { + m_openAction = new QAction(this); + connect(m_openAction, &QAction::triggered, this, [this] { + auto *job = new KIO::ApplicationLauncherJob(m_preferredApplication); + if (m_preferredApplication) { + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + } else { + // needs KIO::JobUiDelegate for open with handler + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled, nullptr /*widget*/)); + } + job->setUrls({m_url}); + job->start(); + }); + emitOpenActionChanged = true; + } + + m_mimeType = mimeType; + + m_preferredApplication.reset(); + + if (!mimeType.isEmpty()) { + const auto type = QMimeDatabase().mimeTypeForName(mimeType); + m_iconName = type.iconName(); + + m_preferredApplication = KApplicationTrader::preferredService(mimeType); + } else { + m_iconName.clear(); + } + + if (m_preferredApplication) { + m_openAction->setText(i18n("Open with %1", m_preferredApplication->name())); + m_openAction->setIcon(QIcon::fromTheme(m_preferredApplication->icon())); + m_openAction->setEnabled(true); + } else { + m_openAction->setText(i18n("Open with…")); + m_openAction->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); + m_openAction->setEnabled(KAuthorized::authorizeAction(KAuthorized::OPEN_WITH)); + } + + Q_EMIT mimeTypeChanged(); + + if (emitOpenActionChanged) { + Q_EMIT openActionChanged(); + } + if (oldOpenActionIconName != openActionIconName()) { + Q_EMIT openActionIconNameChanged(); + } +} diff --git a/plasma/workspace/applets/notifications/fileinfo.h b/plasma/workspace/applets/notifications/fileinfo.h new file mode 100644 index 0000000000..2ac67fe204 --- /dev/null +++ b/plasma/workspace/applets/notifications/fileinfo.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2021 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +class QAction; + +namespace KIO +{ +class MimeTypeFinderJob; +} + +class FileInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(int error READ error NOTIFY errorChanged) + + Q_PROPERTY(QString mimeType READ mimeType NOTIFY mimeTypeChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY mimeTypeChanged) + + Q_PROPERTY(QAction *openAction READ openAction NOTIFY openActionChanged) + // QML can't deal with QIcon... + Q_PROPERTY(QString openActionIconName READ openActionIconName NOTIFY openActionIconNameChanged) + +public: + explicit FileInfo(QObject *parent = nullptr); + ~FileInfo() override; + + QUrl url() const; + void setUrl(const QUrl &url); + Q_SIGNAL void urlChanged(const QUrl &url); + + bool busy() const; + Q_SIGNAL void busyChanged(bool busy); + + int error() const; + Q_SIGNAL void errorChanged(bool error); + + QString mimeType() const; + Q_SIGNAL void mimeTypeChanged(); + + QString iconName() const; + Q_SIGNAL void iconNameChanged(const QString &iconName); + + QAction *openAction() const; + Q_SIGNAL void openActionChanged(); + + QString openActionIconName() const; + Q_SIGNAL void openActionIconNameChanged(); + +private: + void reload(); + void mimeTypeFound(const QString &mimeType); + + void setBusy(bool busy); + void setError(int error); + + QUrl m_url; + + QPointer m_job; + bool m_busy = false; + int m_error = 0; + + QString m_mimeType; + QString m_iconName; + + KService::Ptr m_preferredApplication; + QAction *m_openAction = nullptr; +}; diff --git a/plasma/workspace/applets/notifications/filemenu.cpp b/plasma/workspace/applets/notifications/filemenu.cpp new file mode 100644 index 0000000000..8d0f8ff590 --- /dev/null +++ b/plasma/workspace/applets/notifications/filemenu.cpp @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "filemenu.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for KIO::trash +#include +#include +#include +#include + +FileMenu::FileMenu(QObject *parent) + : QObject(parent) +{ +} + +FileMenu::~FileMenu() = default; + +QUrl FileMenu::url() const +{ + return m_url; +} + +void FileMenu::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + Q_EMIT urlChanged(); + } +} + +QQuickItem *FileMenu::visualParent() const +{ + return m_visualParent.data(); +} + +void FileMenu::setVisualParent(QQuickItem *visualParent) +{ + if (m_visualParent.data() == visualParent) { + return; + } + + if (m_visualParent) { + disconnect(m_visualParent.data(), nullptr, this, nullptr); + } + m_visualParent = visualParent; + if (m_visualParent) { + connect(m_visualParent.data(), &QObject::destroyed, this, &FileMenu::visualParentChanged); + } + Q_EMIT visualParentChanged(); +} + +bool FileMenu::visible() const +{ + return m_visible; +} + +void FileMenu::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + + if (visible) { + open(0, 0); + } else { + // TODO warning or close? + } +} + +void FileMenu::open(int x, int y) +{ + if (!m_visualParent || !m_visualParent->window()) { + return; + } + + if (!m_url.isValid()) { + return; + } + + KFileItem fileItem(m_url); + + QMenu *menu = new QMenu(); + menu->setAttribute(Qt::WA_DeleteOnClose, true); + connect(menu, &QMenu::triggered, this, &FileMenu::actionTriggered); + + connect(menu, &QMenu::aboutToHide, this, [this] { + m_visible = false; + Q_EMIT visibleChanged(); + }); + + if (KProtocolManager::supportsListing(m_url)) { + QAction *openContainingFolderAction = menu->addAction(QIcon::fromTheme(QStringLiteral("folder-open")), i18n("Open Containing Folder")); + connect(openContainingFolderAction, &QAction::triggered, [this] { + KIO::highlightInFileManager({m_url}); + }); + } + + KFileItemActions *actions = new KFileItemActions(menu); + KFileItemListProperties itemProperties(KFileItemList({fileItem})); + actions->setItemListProperties(itemProperties); + actions->setParentWidget(menu); + + actions->insertOpenWithActionsTo(nullptr, menu, QStringList()); + + // KStandardAction? But then the Ctrl+C shortcut makes no sense in this context + QAction *copyAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy")); + connect(copyAction, &QAction::triggered, this, [fileItem] { + // inspired by KDirModel::mimeData() + QMimeData *data = new QMimeData(); // who cleans it up? + KUrlMimeData::setUrls({fileItem.url()}, {fileItem.mostLocalUrl()}, data); + QApplication::clipboard()->setMimeData(data); + }); + + QAction *copyPathAction = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy-path")), i18nc("@action:incontextmenu", "Copy Location")); + connect(copyPathAction, &QAction::triggered, this, [fileItem] { + QString path = fileItem.localPath(); + if (path.isEmpty()) { + path = fileItem.url().toDisplayString(); + } + QApplication::clipboard()->setText(path); + }); + + menu->addSeparator(); + + const bool canTrash = itemProperties.isLocal() && itemProperties.supportsMoving(); + if (canTrash) { + auto moveToTrashLambda = [this] { + const QList urls{m_url}; + + KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { + auto *job = KIO::trash(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, urls, QUrl(QStringLiteral("trash:/")), job); + } + }; + QAction *moveToTrashAction = KStandardAction::moveToTrash(this, moveToTrashLambda, menu); + moveToTrashAction->setShortcut({}); // Can't focus notification to press Delete + menu->addAction(moveToTrashAction); + } + + KConfigGroup cg(KSharedConfig::openConfig(), "KDE"); + const bool showDeleteCommand = cg.readEntry("ShowDeleteCommand", false); + + if (itemProperties.supportsDeleting() && (!canTrash || showDeleteCommand)) { + auto deleteLambda = [this] { + const QList urls{m_url}; + + KIO::JobUiDelegate uiDelegate; + if (uiDelegate.askDeleteConfirmation(urls, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) { + auto *job = KIO::del(urls); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + } + }; + QAction *deleteAction = KStandardAction::deleteFile(this, deleteLambda, menu); + deleteAction->setShortcut({}); + menu->addAction(deleteAction); + } + + menu->addSeparator(); + + actions->addActionsTo(menu); + + menu->addSeparator(); + + QAction *propertiesAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties")); + connect(propertiesAction, &QAction::triggered, [fileItem] { + KPropertiesDialog *dialog = new KPropertiesDialog(fileItem.url()); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); + }); + + // 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 = [this]() { + if (m_visualParent && m_visualParent->window() && m_visualParent->window()->mouseGrabberItem()) { + m_visualParent->window()->mouseGrabberItem()->ungrabMouse(); + } + }; + + QTimer::singleShot(0, m_visualParent, ungrabMouseHack); + // end workaround + + QPoint pos; + if (x == -1 && y == -1) { // align "bottom left of visualParent" + menu->adjustSize(); + + pos = m_visualParent->mapToGlobal(QPointF(0, m_visualParent->height())).toPoint(); + + if (!qApp->isRightToLeft()) { + pos.rx() += m_visualParent->width(); + pos.rx() -= menu->width(); + } + } else { + pos = m_visualParent->mapToGlobal(QPointF(x, y)).toPoint(); + } + + menu->setAttribute(Qt::WA_TranslucentBackground); + menu->winId(); + menu->windowHandle()->setTransientParent(m_visualParent->window()); + menu->popup(pos); + + m_visible = true; + Q_EMIT visibleChanged(); +} diff --git a/plasma/workspace/applets/notifications/filemenu.h b/plasma/workspace/applets/notifications/filemenu.h new file mode 100644 index 0000000000..975e12144f --- /dev/null +++ b/plasma/workspace/applets/notifications/filemenu.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class QAction; + +class FileMenu : public QObject +{ + Q_OBJECT + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QQuickItem *visualParent READ visualParent WRITE setVisualParent NOTIFY visualParentChanged) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged) + +public: + explicit FileMenu(QObject *parent = nullptr); + ~FileMenu() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QQuickItem *visualParent() const; + void setVisualParent(QQuickItem *visualParent); + + bool visible() const; + void setVisible(bool visible); + + Q_INVOKABLE void open(int x, int y); + +Q_SIGNALS: + void actionTriggered(QAction *action); + + void urlChanged(); + void visualParentChanged(); + void visibleChanged(); + +private: + QUrl m_url; + QPointer m_visualParent; + bool m_visible = false; +}; diff --git a/plasma/workspace/applets/notifications/globalshortcuts.cpp b/plasma/workspace/applets/notifications/globalshortcuts.cpp new file mode 100644 index 0000000000..eed1ef6569 --- /dev/null +++ b/plasma/workspace/applets/notifications/globalshortcuts.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "globalshortcuts.h" + +#include +#include +#include + +#include + +#include + +GlobalShortcuts::GlobalShortcuts(QObject *parent) + : QObject(parent) + , m_toggleDoNotDisturbAction(new QAction(this)) +{ + m_toggleDoNotDisturbAction->setObjectName(QStringLiteral("toggle do not disturb")); + m_toggleDoNotDisturbAction->setProperty("componentName", QStringLiteral("plasmashell")); + m_toggleDoNotDisturbAction->setText(i18n("Toggle do not disturb")); + m_toggleDoNotDisturbAction->setIcon(QIcon::fromTheme(QStringLiteral("notifications-disabled"))); + m_toggleDoNotDisturbAction->setShortcutContext(Qt::ApplicationShortcut); + connect(m_toggleDoNotDisturbAction, &QAction::triggered, this, &GlobalShortcuts::toggleDoNotDisturbTriggered); + + KGlobalAccel::self()->setGlobalShortcut(m_toggleDoNotDisturbAction, QKeySequence()); +} + +GlobalShortcuts::~GlobalShortcuts() = default; + +void GlobalShortcuts::showDoNotDisturbOsd(bool doNotDisturb) const +{ + QDBusMessage msg = QDBusMessage::createMethodCall( // + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + + const QString iconName = doNotDisturb ? QStringLiteral("notifications-disabled") : QStringLiteral("notifications"); + const QString text = doNotDisturb ? i18nc("OSD popup, keep short", "Notifications Off") // + : i18nc("OSD popup, keep short", "Notifications On"); + + msg.setArguments({iconName, text}); + + QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); +} diff --git a/plasma/workspace/applets/notifications/globalshortcuts.h b/plasma/workspace/applets/notifications/globalshortcuts.h new file mode 100644 index 0000000000..989af70e61 --- /dev/null +++ b/plasma/workspace/applets/notifications/globalshortcuts.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class QAction; + +class GlobalShortcuts : public QObject +{ + Q_OBJECT + +public: + explicit GlobalShortcuts(QObject *parent = nullptr); + ~GlobalShortcuts() override; + + Q_INVOKABLE void showDoNotDisturbOsd(bool doNotDisturb) const; + +Q_SIGNALS: + void toggleDoNotDisturbTriggered(); + +private: + QAction *m_toggleDoNotDisturbAction; +}; diff --git a/plasma/workspace/applets/notifications/notificationapplet.cpp b/plasma/workspace/applets/notifications/notificationapplet.cpp new file mode 100644 index 0000000000..8674c53776 --- /dev/null +++ b/plasma/workspace/applets/notifications/notificationapplet.cpp @@ -0,0 +1,181 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "notificationapplet.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "fileinfo.h" +#include "filemenu.h" +#include "globalshortcuts.h" +#include "texteditclickhandler.h" +#include "thumbnailer.h" + +NotificationApplet::NotificationApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ + static bool s_typesRegistered = false; + if (!s_typesRegistered) { + const char uri[] = "org.kde.plasma.private.notifications"; + qmlRegisterType(uri, 2, 0, "FileInfo"); + qmlRegisterType(uri, 2, 0, "FileMenu"); + qmlRegisterType(uri, 2, 0, "GlobalShortcuts"); + qmlRegisterType(uri, 2, 0, "TextEditClickHandler"); + qmlRegisterType(uri, 2, 0, "Thumbnailer"); + qmlProtectModule(uri, 2); + s_typesRegistered = true; + } + + connect(qApp, &QGuiApplication::focusWindowChanged, this, &NotificationApplet::focussedPlasmaDialogChanged); +} + +NotificationApplet::~NotificationApplet() = default; + +void NotificationApplet::init() +{ +} + +void NotificationApplet::configChanged() +{ +} + +bool NotificationApplet::dragActive() const +{ + return m_dragActive; +} + +int NotificationApplet::dragPixmapSize() const +{ + return m_dragPixmapSize; +} + +void NotificationApplet::setDragPixmapSize(int dragPixmapSize) +{ + if (m_dragPixmapSize != dragPixmapSize) { + m_dragPixmapSize = dragPixmapSize; + Q_EMIT dragPixmapSizeChanged(); + } +} + +bool NotificationApplet::isDrag(int oldX, int oldY, int newX, int newY) const +{ + return ((QPoint(oldX, oldY) - QPoint(newX, newY)).manhattanLength() >= qApp->styleHints()->startDragDistance()); +} + +void NotificationApplet::startDrag(QQuickItem *item, const QUrl &url, const QString &iconName) +{ + startDrag(item, url, QIcon::fromTheme(iconName).pixmap(m_dragPixmapSize, m_dragPixmapSize)); +} + +void NotificationApplet::startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap) +{ + // This allows the caller to return, making sure we don't crash if + // the caller is destroyed mid-drag + + QMetaObject::invokeMethod(this, "doDrag", Qt::QueuedConnection, Q_ARG(QQuickItem *, item), Q_ARG(QUrl, url), Q_ARG(QPixmap, pixmap)); +} + +void NotificationApplet::doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap) +{ + if (item && item->window() && item->window()->mouseGrabberItem()) { + item->window()->mouseGrabberItem()->ungrabMouse(); + } + + QDrag *drag = new QDrag(item); + + QMimeData *mimeData = new QMimeData(); + + if (!url.isEmpty()) { + mimeData->setUrls(QList() << url); + } + + drag->setMimeData(mimeData); + + if (!pixmap.isNull()) { + drag->setPixmap(pixmap); + } + + m_dragActive = true; + Q_EMIT dragActiveChanged(); + + drag->exec(); + + m_dragActive = false; + Q_EMIT dragActiveChanged(); +} + +QWindow *NotificationApplet::focussedPlasmaDialog() const +{ + auto *focusWindow = qApp->focusWindow(); + if (qobject_cast(focusWindow)) { + return focusWindow; + } + + if (focusWindow) { + return qobject_cast(focusWindow->transientParent()); + } + + return nullptr; +} + +QQuickItem *NotificationApplet::systemTrayRepresentation() const +{ + auto *c = containment(); + if (!c) { + return nullptr; + } + + if (strcmp(c->metaObject()->className(), "SystemTray") != 0) { + return nullptr; + } + + return c->property("_plasma_graphicObject").value(); +} + +void NotificationApplet::setSelectionClipboardText(const QString &text) +{ + // FIXME KDeclarative Clipboard item uses QClipboard::Mode for "mode" + // which is an enum inaccessible from QML + QGuiApplication::clipboard()->setText(text, QClipboard::Selection); +} + +bool NotificationApplet::isPrimaryScreen(const QRect &rect) const +{ + QScreen *screen = QGuiApplication::primaryScreen(); + if (!screen) { + return false; + } + + // HACK + return rect == screen->geometry(); +} + +void NotificationApplet::forceActivateWindow(QWindow *window) +{ + if (window && window->winId()) { + KWindowSystem::forceActiveWindow(window->winId()); + } +} + +K_PLUGIN_CLASS_WITH_JSON(NotificationApplet, "package/metadata.json") + +#include "notificationapplet.moc" diff --git a/plasma/workspace/applets/notifications/notificationapplet.h b/plasma/workspace/applets/notifications/notificationapplet.h new file mode 100644 index 0000000000..a085ea2424 --- /dev/null +++ b/plasma/workspace/applets/notifications/notificationapplet.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include + +#include + +class QString; +class QRect; + +class NotificationApplet : public Plasma::Applet +{ + Q_OBJECT + + Q_PROPERTY(bool dragActive READ dragActive NOTIFY dragActiveChanged) + Q_PROPERTY(int dragPixmapSize READ dragPixmapSize WRITE setDragPixmapSize NOTIFY dragPixmapSizeChanged) + + Q_PROPERTY(QWindow *focussedPlasmaDialog READ focussedPlasmaDialog NOTIFY focussedPlasmaDialogChanged) + Q_PROPERTY(QQuickItem *systemTrayRepresentation READ systemTrayRepresentation CONSTANT) + +public: + explicit NotificationApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~NotificationApplet() override; + + void init() override; + void configChanged() override; + + bool dragActive() const; + + int dragPixmapSize() const; + void setDragPixmapSize(int dragPixmapSize); + + Q_INVOKABLE bool isDrag(int oldX, int oldY, int newX, int newY) const; + Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QString &iconName); + Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap); + + QWindow *focussedPlasmaDialog() const; + QQuickItem *systemTrayRepresentation() const; + + Q_INVOKABLE void setSelectionClipboardText(const QString &text); + + Q_INVOKABLE bool isPrimaryScreen(const QRect &rect) const; + + Q_INVOKABLE void forceActivateWindow(QWindow *window); + +Q_SIGNALS: + void dragActiveChanged(); + void dragPixmapSizeChanged(); + void focussedPlasmaDialogChanged(); + +private Q_SLOTS: + void doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap); + +private: + bool m_dragActive = false; + int m_dragPixmapSize = 48; // Bound to units.iconSizes.large in main.qml +}; diff --git a/plasma/workspace/applets/notifications/package/contents/ui/CompactRepresentation.qml b/plasma/workspace/applets/notifications/package/contents/ui/CompactRepresentation.qml new file mode 100644 index 0000000000..8ab0f50310 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/CompactRepresentation.qml @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import org.kde.quickcharts 1.0 as Charts + +import "global" + +MouseArea { + id: compactRoot + + readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge + || plasmoid.location === PlasmaCore.Types.RightEdge + || plasmoid.location === PlasmaCore.Types.BottomEdge + || plasmoid.location === PlasmaCore.Types.LeftEdge) + + Layout.minimumWidth: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : PlasmaCore.Units.iconSizes.small + Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : (PlasmaCore.Units.iconSizes.small + 2 * PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height) + + Layout.maximumWidth: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1 + Layout.maximumHeight: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1 + + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + + property int activeCount: 0 + property int unreadCount: 0 + + property int jobsCount: 0 + property int jobsPercentage: 0 + + property bool inhibited: false + + property bool wasExpanded: false + onPressed: wasExpanded = plasmoid.expanded + onClicked: { + if (mouse.button === Qt.MiddleButton) { + Globals.toggleDoNotDisturbMode(); + } else { + plasmoid.expanded = !wasExpanded; + } + } + + PlasmaCore.Svg { + id: notificationSvg + imagePath: "icons/notification" + colorGroup: PlasmaCore.ColorScope.colorGroup + } + + PlasmaCore.SvgItem { + id: notificationIcon + anchors.centerIn: parent + width: PlasmaCore.Units.roundToIconSize(Math.min(parent.width, parent.height)) + height: width + svg: notificationSvg + visible: opacity > 0 + + elementId: "notification-inactive" + + Charts.PieChart { + id: chart + + anchors.fill: parent + + visible: false + + range { from: 0; to: 100; automatic: false } + + valueSources: Charts.SingleValueSource { value: compactRoot.jobsPercentage } + colorSource: Charts.SingleValueSource { value: PlasmaCore.Theme.highlightColor } + + thickness: PlasmaCore.Units.devicePixelRatio * 5 + } + + PlasmaComponents3.Label { + id: countLabel + anchors.centerIn: parent + width: Math.round(Math.min(parent.width, parent.height) * (text.length > 1 ? 0.67 : 0.75)) + height: width + fontSizeMode: Text.Fit + font.pointSize: 1024 + font.pixelSize: -1 + minimumPointSize: 5//PlasmaCore.Theme.smallestFont.pointSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: compactRoot.unreadCount || "" + renderType: Text.QtRendering + visible: false + } + + PlasmaComponents3.BusyIndicator { + id: busyIndicator + anchors.fill: parent + visible: false + running: visible + } + } + + PlasmaCore.IconItem { + id: dndIcon + anchors.fill: parent + source: "notifications-disabled" + opacity: 0 + scale: 2 + visible: opacity > 0 + } + + states: [ + State { // active process + when: compactRoot.jobsCount > 0 + PropertyChanges { + target: notificationIcon + elementId: "notification-progress-inactive" + } + PropertyChanges { + target: countLabel + text: compactRoot.jobsCount + visible: true + } + PropertyChanges { + target: busyIndicator + visible: compactRoot.jobsPercentage == 0 + } + PropertyChanges { + target: chart + visible: true + } + }, + State { // do not disturb + when: compactRoot.inhibited + PropertyChanges { + target: dndIcon + scale: 1 + opacity: 1 + } + PropertyChanges { + target: notificationIcon + scale: 0 + opacity: 0 + } + }, + State { // unread notifications + name: "UNREAD" + when: compactRoot.unreadCount > 0 + PropertyChanges { + target: notificationIcon + elementId: "notification-active" + } + } + ] + + transitions: [ + Transition { + to: "*" // any state + NumberAnimation { + targets: [notificationIcon, dndIcon] + properties: "opacity,scale" + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + }, + Transition { + from: "" + to: "UNREAD" + SequentialAnimation { + RotationAnimation { + target: notificationIcon + to: 30 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration + } + RotationAnimation { + target: notificationIcon + to: -30 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration * 2 // twice the swing distance, keep speed uniform + } + RotationAnimation { + target: notificationIcon + to: 0 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration + } + } + } + ] + +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/DraggableDelegate.qml b/plasma/workspace/applets/notifications/package/contents/ui/DraggableDelegate.qml new file mode 100644 index 0000000000..a097f3bb66 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/DraggableDelegate.qml @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.10 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.11 as Kirigami + +MouseArea { + id: delegate + + property Item contentItem + property bool draggable: false + signal dismissRequested + + implicitWidth: contentItem ? contentItem.implicitWidth : 0 + implicitHeight: contentItem ? contentItem.implicitHeight : 0 + opacity: 1 - Math.min(1, 1.5 * Math.abs(x) / width) + + drag { + axis: Drag.XAxis + target: draggable && Kirigami.Settings.tabletMode ? this : null + } + + onReleased: { + if (Math.abs(x) > width / 2) { + delegate.dismissRequested(); + } else { + slideAnim.restart(); + } + } + + NumberAnimation { + id: slideAnim + target: delegate + property:"x" + to: 0 + duration: PlasmaCore.Units.longDuration + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/DraggableFileArea.qml b/plasma/workspace/applets/notifications/package/contents/ui/DraggableFileArea.qml new file mode 100644 index 0000000000..a75ce8e7d7 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/DraggableFileArea.qml @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2016, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.8 + +MouseArea { + id: area + + signal activated + signal contextMenuRequested(int x, int y) + + property Item dragParent + property url dragUrl + property var dragPixmap + + readonly property bool dragging: plasmoid.nativeInterface.dragActive + + property int _pressX: -1 + property int _pressY: -1 + + preventStealing: true + cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: { + if (mouse.button === Qt.LeftButton) { + area.activated(); + } + } + onPressed: { + if (mouse.button === Qt.LeftButton) { + _pressX = mouse.x; + _pressY = mouse.y; + } else if (mouse.button === Qt.RightButton) { + area.contextMenuRequested(mouse.x, mouse.y); + } + } + onPositionChanged: { + if (_pressX !== -1 && _pressY !== -1 && plasmoid.nativeInterface.isDrag(_pressX, _pressY, mouse.x, mouse.y)) { + plasmoid.nativeInterface.startDrag(area.dragParent, area.dragUrl, area.dragPixmap); + _pressX = -1; + _pressY = -1; + } + } + onReleased: { + _pressX = -1; + _pressY = -1; + } + onContainsMouseChanged: { + if (!containsMouse) { + _pressX = -1; + _pressY = -1; + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/EditContextMenu.qml b/plasma/workspace/applets/notifications/package/contents/ui/EditContextMenu.qml new file mode 100644 index 0000000000..853c6e55d4 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/EditContextMenu.qml @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For ContextMenu + +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +PlasmaComponents.ContextMenu { + id: contextMenu + + signal closed + + property QtObject __clipboard: KQCAddons.Clipboard { } + + // can be a Text or TextEdit + property Item target + + property string link + + onStatusChanged: { + if (status === PlasmaComponents.DialogStatus.Closed) { + closed(); + } + } + + PlasmaComponents.MenuItem { + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Copy Link Address") + onClicked: __clipboard.content = contextMenu.link + visible: contextMenu.link !== "" + } + + PlasmaComponents.MenuItem { + separator: true + visible: contextMenu.link !== "" + } + + PlasmaComponents.MenuItem { + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Copy") + icon: "edit-copy" + enabled: typeof target.selectionStart !== "undefined" + ? target.selectionStart !== target.selectionEnd + : (target.text || "").length > 0 + onClicked: { + if (typeof target.copy === "function") { + target.copy(); + } else { + __clipboard.content = target.text; + } + } + } + + PlasmaComponents.MenuItem { + id: selectAllAction + icon: "edit-select-all" + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Select All") + onClicked: target.selectAll() + visible: typeof target.selectAll === "function" + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/FullRepresentation.qml b/plasma/workspace/applets/notifications/package/contents/ui/FullRepresentation.qml new file mode 100644 index 0000000000..1a62eef03e --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -0,0 +1,591 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.10 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For ModelContextMenu +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import org.kde.notificationmanager 1.0 as NotificationManager + +import "global" + +PlasmaExtras.Representation { + // TODO these should be configurable in the future + readonly property int dndMorningHour: 6 + readonly property int dndEveningHour: 20 + + implicitWidth: PlasmaCore.Units.gridUnit * 18 + implicitHeight: PlasmaCore.Units.gridUnit * 24 + + Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical + + collapseMarginsHint: true + + // HACK forward focus to the list + onActiveFocusChanged: { + if (activeFocus) { + list.forceActiveFocus(); + } + } + + Connections { + target: plasmoid + function onExpandedChanged() { + if (plasmoid.expanded) { + list.positionViewAtBeginning(); + list.currentIndex = -1; + } + } + } + + PlasmaCore.Svg { + id: lineSvg + imagePath: "widgets/line" + } + + header: PlasmaExtras.PlasmoidHeading { + ColumnLayout { + anchors { + fill: parent + leftMargin: PlasmaCore.Units.smallSpacing + } + id: header + spacing: 0 + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + PlasmaComponents3.CheckBox { + id: dndCheck + enabled: NotificationManager.Server.valid + text: i18n("Do not disturb") + icon.name: "notifications-disabled" + checkable: true + checked: Globals.inhibited + + // Let the menu open on press + onPressed: { + if (!Globals.inhibited) { + dndMenu.date = new Date(); + // shows ontop of CheckBox to hide the fact that it's unchecked + // until you actually select something :) + dndMenu.open(0, 0); + } + } + // but disable only on click + onClicked: { + if (Globals.inhibited) { + Globals.revokeInhibitions(); + } + } + + + PlasmaComponents.ModelContextMenu { + id: dndMenu + property date date + visualParent: dndCheck + + onClicked: { + notificationSettings.notificationsInhibitedUntil = model.date; + notificationSettings.save(); + } + + model: { + var model = []; + + // For 1 hour + var d = dndMenu.date; + d.setHours(d.getHours() + 1); + d.setSeconds(0); + model.push({date: d, text: i18n("For 1 hour")}); + + d = dndMenu.date; + d.setHours(d.getHours() + 4); + d.setSeconds(0); + model.push({date: d, text: i18n("For 4 hours")}); + + // Until this evening + if (dndMenu.date.getHours() < dndEveningHour) { + d = dndMenu.date; + // TODO make the user's preferred time schedule configurable + d.setHours(dndEveningHour); + d.setMinutes(0); + d.setSeconds(0); + model.push({date: d, text: i18n("Until this evening")}); + } + + // Until next morning + if (dndMenu.date.getHours() > dndMorningHour) { + d = dndMenu.date; + d.setDate(d.getDate() + 1); + d.setHours(dndMorningHour); + d.setMinutes(0); + d.setSeconds(0); + model.push({date: d, text: i18n("Until tomorrow morning")}); + } + + // Until Monday + // show Friday and Saturday, Sunday is "0" but for that you can use "until tomorrow morning" + if (dndMenu.date.getDay() >= 5) { + d = dndMenu.date; + d.setHours(dndMorningHour); + // wraps around if necessary + d.setDate(d.getDate() + (7 - d.getDay() + 1)); + d.setMinutes(0); + d.setSeconds(0); + model.push({date: d, text: i18n("Until Monday")}); + } + + // Until "turned off" + d = dndMenu.date; + // Just set it to one year in the future so we don't need yet another "do not disturb enabled" property + d.setFullYear(d.getFullYear() + 1); + model.push({date: d, text: i18n("Until manually disabled")}); + + return model; + } + } + } + + Item { + Layout.fillWidth: true + } + + PlasmaComponents3.ToolButton { + visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) + + Accessible.name: plasmoid.action("clearHistory").text + icon.name: "edit-clear-history" + enabled: plasmoid.action("clearHistory").visible + onClicked: action_clearHistory() + + PlasmaComponents3.ToolTip { + text: parent.Accessible.name + } + } + } + + PlasmaExtras.DescriptiveLabel { + Layout.leftMargin: dndCheck.mirrored ? 0 : dndCheck.indicator.width + 2 * dndCheck.spacing + PlasmaCore.Units.iconSizes.smallMedium + Layout.rightMargin: dndCheck.mirrored ? dndCheck.indicator.width + 2 * dndCheck.spacing + PlasmaCore.Units.iconSizes.smallMedium : 0 + Layout.fillWidth: true + wrapMode: Text.WordWrap + textFormat: Text.PlainText + text: { + if (!Globals.inhibited) { + return ""; + } + + var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; + var inhibitedByApp = notificationSettings.notificationsInhibitedByApplication; + var inhibitedByMirroredScreens = notificationSettings.inhibitNotificationsWhenScreensMirrored + && notificationSettings.screensMirrored; + + var sections = []; + + // Show until time if valid but not if too far int he future + if (!isNaN(inhibitedUntil.getTime()) && inhibitedUntil.getTime() - Date.now() < 100 * 24 * 60 * 60 * 1000 /* 1 year*/) { + sections.push(i18nc("Do not disturb until date", "Until %1", + KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat))); + } + + if (inhibitedByApp) { + var inhibitionAppNames = notificationSettings.notificationInhibitionApplications; + var inhibitionAppReasons = notificationSettings.notificationInhibitionReasons; + + for (var i = 0, length = inhibitionAppNames.length; i < length; ++i) { + var name = inhibitionAppNames[i]; + var reason = inhibitionAppReasons[i]; + + if (reason) { + sections.push(i18nc("Do not disturb until app has finished (reason)", "While %1 is active (%2)", name, reason)); + } else { + sections.push(i18nc("Do not disturb until app has finished", "While %1 is active", name)); + } + } + } + + if (inhibitedByMirroredScreens) { + sections.push(i18nc("Do not disturb because external mirrored screens connected", "Screens are mirrored")) + } + + return sections.join(" · "); + } + visible: text !== "" + } + } + } + + PlasmaComponents3.ScrollView { + id: scrollView + anchors.fill: parent + background: null + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + contentItem: ListView { + id: list + model: historyModel + currentIndex: -1 + + topMargin: PlasmaCore.Units.smallSpacing * 2 + bottomMargin: PlasmaCore.Units.smallSpacing * 2 + leftMargin: PlasmaCore.Units.smallSpacing * 2 + rightMargin: PlasmaCore.Units.smallSpacing * 2 + spacing: PlasmaCore.Units.smallSpacing + + Keys.onDeletePressed: { + var idx = historyModel.index(currentIndex, 0); + if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { + historyModel.close(idx); + // TODO would be nice to stay inside the current group when deleting an item + } + } + Keys.onEnterPressed: Keys.onReturnPressed(event) + Keys.onReturnPressed: { + // Trigger default action, if any + var idx = historyModel.index(currentIndex, 0); + if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { + historyModel.invokeDefaultAction(idx); + return; + } + + // Trigger thumbnail URL if there's one + var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); + if (urls && urls.length === 1) { + Qt.openUrlExternally(urls[0]); + return; + } + + // TODO for finished jobs trigger "Open" or "Open Containing Folder" action + } + Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) + Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Home: + currentIndex = 0; + break; + case Qt.Key_End: + currentIndex = count - 1; + break; + } + } + + function isRowExpanded(row) { + var idx = historyModel.index(row, 0); + return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); + } + + function setGroupExpanded(row, expanded) { + var rowIdx = historyModel.index(row, 0); + var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); + var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); + + historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); + + // If the current item went away when the group collapsed, scroll to the group heading + if (!persistentRowIdx || !persistentRowIdx.valid) { + if (persistentGroupIdx && persistentGroupIdx.valid) { + list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); + // When closed via keyboard, also set a sane current index + if (list.currentIndex > -1) { + list.currentIndex = persistentGroupIdx.row; + } + } + } + } + + highlightMoveDuration: 0 + highlightResizeDuration: 0 + // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus + highlight: PlasmaCore.FrameSvgItem { + imagePath: "widgets/listitem" + prefix: "pressed" + } + + add: Transition { + SequentialAnimation { + PropertyAction { property: "opacity"; value: 0 } + PauseAnimation { duration: PlasmaCore.Units.longDuration } + ParallelAnimation { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: PlasmaCore.Units.longDuration } + NumberAnimation { property: "height"; from: 0; duration: PlasmaCore.Units.longDuration } + } + } + } + addDisplaced: Transition { + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + + remove: Transition { + id: removeTransition + ParallelAnimation { + NumberAnimation { property: "opacity"; to: 0; duration: PlasmaCore.Units.longDuration } + NumberAnimation { + id: removeXAnimation + property: "x" + to: list.width - (scrollView.PlasmaComponents3.ScrollBar.vertical.visible ? PlasmaCore.Units.smallSpacing * 4 : 0) + duration: PlasmaCore.Units.longDuration + } + } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: PlasmaCore.Units.longDuration } + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + } + + // This is so the delegates can detect the change in "isInGroup" and show a separator + section { + property: "isInGroup" + criteria: ViewSection.FullString + } + + delegate: DraggableDelegate { + id: delegate + width: ListView.view.width - PlasmaCore.Units.smallSpacing * 4 + contentItem: delegateLoader + + draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType + + onDismissRequested: { + // Setting the animation target explicitly before removing the notification: + // Using ViewTransition.item.x to get the x position in the animation + // causes random crash in attached property access (cf. Bug 414066) + if (x < 0) { + removeXAnimation.to = -delegate.width; + } + + historyModel.close(historyModel.index(index, 0)); + } + + Loader { + id: delegateLoader + width: delegate.width + sourceComponent: model.isGroup ? groupDelegate : notificationDelegate + + Component { + id: groupDelegate + NotificationHeader { + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" + + // don't show timestamp for group + + configurable: model.configurable + closable: model.closable + closeButtonTooltip: i18n("Close Group") + + onCloseClicked: historyModel.close(historyModel.index(index, 0)) + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) + } + } + + Component { + id: notificationDelegate + ColumnLayout { + spacing: PlasmaCore.Units.smallSpacing + + RowLayout { + Item { + id: groupLineContainer + Layout.fillHeight: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + width: PlasmaCore.Units.iconSizes.small + visible: model.isInGroup + + PlasmaCore.SvgItem { + elementId: "vertical-line" + svg: lineSvg + anchors.horizontalCenter: parent.horizontalCenter + // Want a thicker than default bar + width: Math.min(groupLineContainer.width, naturalSize.width * PlasmaCore.Units.devicePixelRatio * 3) + height: parent.height + } + } + + NotificationItem { + Layout.fillWidth: true + + notificationType: model.type + + inGroup: model.isInGroup + inHistory: true + listViewParent: list + + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" + + time: model.updated || model.created + + // configure button on every single notifications is bit overwhelming + configurable: !inGroup && model.configurable + + dismissable: model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + && model.dismissed + // TODO would be nice to be able to undismiss jobs even when they autohide + && notificationSettings.permanentJobPopups + dismissed: model.dismissed || false + closable: model.closable + + summary: model.summary + body: model.body || "" + icon: model.image || model.iconName + + urls: model.urls || [] + + jobState: model.jobState || 0 + percentage: model.percentage || 0 + jobError: model.jobError || 0 + suspendable: !!model.suspendable + killable: !!model.killable + jobDetails: model.jobDetails || null + + configureActionLabel: model.configureActionLabel || "" + // In the popup the default action is triggered by clicking on the popup + // however in the list this is undesirable, so instead show a clickable button + // in case you have a non-expired notification in history (do not disturb mode) + // unless it has the same label as an action + readonly property bool addDefaultAction: (model.hasDefaultAction + && model.defaultActionLabel + && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false + actionNames: { + var actions = (model.actionNames || []); + if (addDefaultAction) { + actions.unshift("default"); // prepend + } + return actions; + } + actionLabels: { + var labels = (model.actionLabels || []); + if (addDefaultAction) { + labels.unshift(model.defaultActionLabel); + } + return labels; + } + + onCloseClicked: close() + + onDismissClicked: { + model.dismissed = false; + root.closePlasmoid(); + } + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) + + onActionInvoked: { + if (actionName === "default") { + historyModel.invokeDefaultAction(historyModel.index(index, 0)); + } else { + historyModel.invokeAction(historyModel.index(index, 0), actionName); + } + + expire(); + } + onOpenUrl: { + Qt.openUrlExternally(url); + expire(); + } + onFileActionInvoked: { + if (action.objectName === "movetotrash" || action.objectName === "deletefile") { + close(); + } else { + expire(); + } + } + + onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) + onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) + onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) + + function expire() { + if (model.resident) { + model.expired = true; + } else { + historyModel.expire(historyModel.index(index, 0)); + } + } + + function close() { + historyModel.close(historyModel.index(index, 0)); + } + } + } + + PlasmaComponents3.ToolButton { + icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down" + text: model.isGroupExpanded ? i18n("Show Fewer") + : i18nc("Expand to show n more notifications", + "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) + visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) + && delegate.ListView.nextSection !== delegate.ListView.section + onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) + } + + PlasmaCore.SvgItem { + Layout.fillWidth: true + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + elementId: "horizontal-line" + svg: lineSvg + + // property is only atached to the delegate itself (the Loader in our case) + visible: (!model.isInGroup || delegate.ListView.nextSection !== delegate.ListView.section) + && delegate.ListView.nextSection !== "" // don't show after last item + } + } + } + } + } + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + + text: i18n("No unread notifications") + visible: list.count === 0 && NotificationManager.Server.valid + } + + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + + text: i18n("Notification service not available") + visible: list.count === 0 && !NotificationManager.Server.valid + + // TODO: port to using the subtitle property once it exists + PlasmaComponents3.Label { + // Checking valid to avoid creating ServerInfo object if everything is alright + readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner + : null + + // PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, + // so we can use Layout.whatever properties here + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: currentOwner ? i18nc("Vendor and product name", + "Notifications are currently provided by '%1 %2'", + currentOwner.vendor, + currentOwner.name) + : "" + visible: currentOwner && currentOwner.vendor && currentOwner.name + } + } + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/JobDetails.qml b/plasma/workspace/applets/notifications/package/contents/ui/JobDetails.qml new file mode 100644 index 0000000000..46f88aab17 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/JobDetails.qml @@ -0,0 +1,151 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import org.kde.notificationmanager 1.0 as NotificationManager + +GridLayout { + id: detailsGrid + + property QtObject jobDetails + + columns: 2 + rowSpacing: Math.round(PlasmaCore.Units.smallSpacing / 2) + columnSpacing: PlasmaCore.Units.smallSpacing + + // once you use Layout.column/Layout.row *all* of the items in the Layout have to use them + Repeater { + model: [1, 2] + + PlasmaExtras.DescriptiveLabel { + Layout.column: 0 + Layout.row: index + Layout.alignment: Qt.AlignTop | Qt.AlignRight + text: jobDetails["descriptionLabel" + modelData] && jobDetails["descriptionValue" + modelData] + ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Row description, e.g. Source", "%1:", jobDetails["descriptionLabel" + modelData]) : "" + font: PlasmaCore.Theme.smallestFont + textFormat: Text.PlainText + visible: text !== "" + } + } + + Repeater { + model: [1, 2] + + PlasmaExtras.DescriptiveLabel { + id: descriptionValueLabel + Layout.column: 1 + Layout.row: index + Layout.fillWidth: true + font: PlasmaCore.Theme.smallestFont + elide: Text.ElideMiddle + textFormat: Text.PlainText + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + verticalAlignment: Text.AlignTop + maximumLineCount: 5 + visible: text !== "" + + // Only let the label grow, never shrink, to avoid repeatedly resizing the dialog when copying many files + onImplicitHeightChanged: { + if (implicitHeight > Layout.preferredHeight) { + Layout.preferredHeight = implicitHeight; + } + } + + Component.onCompleted: bindText() + function bindText() { + text = Qt.binding(function() { + return jobDetails["descriptionValue" + modelData] || ""; + }); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onPressed: { + // break binding so it doesn't update while the menu is opened + descriptionValueLabel.text = descriptionValueLabel.text; + descriptionValueMenu.open(mouse.x, mouse.y) + } + } + + EditContextMenu { + id: descriptionValueMenu + target: descriptionValueLabel + // defer re-binding until after the "Copy" action in the menu has triggered + onClosed: Qt.callLater(descriptionValueLabel.bindText) + } + } + } + + Repeater { + model: ["Bytes", "Files", "Directories", "Items"] + + PlasmaExtras.DescriptiveLabel { + Layout.column: 1 + Layout.row: 2 + index + Layout.fillWidth: true + text: { + var processed = jobDetails["processed" + modelData]; + var total = jobDetails["total" + modelData]; + + if (processed > 0 || total > 1) { + if (processed > 0 && total > 0 && processed <= total) { + switch(modelData) { + case "Bytes": + return i18ndc("plasma_applet_org.kde.plasma.notifications", "How many bytes have been copied", "%2 of %1", + KCoreAddons.Format.formatByteSize(total), + KCoreAddons.Format.formatByteSize(processed)) + case "Files": + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many files have been copied", "%2 of %1 file", "%2 of %1 files", + total, processed); + case "Directories": + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many dirs have been copied", "%2 of %1 folder", "%2 of %1 folders", + total, processed); + case "Items": + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "How many items (that includes files and dirs) have been copied", "%2 of %1 item", "%2 of %1 items", + total, processed); + } + } else { + switch(modelData) { + case "Bytes": + return KCoreAddons.Format.formatByteSize(processed || total) + case "Files": + return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 file", "%1 files", (processed || total)); + case "Directories": + return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 folder", "%1 folders", (processed || total)); + case "Items": + return i18ndp("plasma_applet_org.kde.plasma.notifications", "%1 item", "%1 items", (processed || total)); + } + } + } + + return ""; + } + font: PlasmaCore.Theme.smallestFont + textFormat: Text.PlainText + visible: text !== "" + } + } + + PlasmaExtras.DescriptiveLabel { + Layout.column: 1 + Layout.row: 2 + 3 + Layout.fillWidth: true + text: jobDetails.speed > 0 ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Bytes per second", "%1/s", + KCoreAddons.Format.formatByteSize(jobDetails.speed)) : "" + font: PlasmaCore.Theme.smallestFont + textFormat: Text.PlainText + visible: text !== "" + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/JobItem.qml b/plasma/workspace/applets/notifications/package/contents/ui/JobItem.qml new file mode 100644 index 0000000000..63e1c172f2 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/JobItem.qml @@ -0,0 +1,302 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Window 2.2 +import QtQuick.Layouts 1.1 +import QtQml 2.15 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.plasma.private.notifications 2.0 as Notifications + +ColumnLayout { + id: jobItem + + property int jobState + property int jobError + + property alias percentage: progressBar.value + property alias suspendable: suspendButton.visible + property alias killable: killButton.visible + + property bool hovered + property QtObject jobDetails + + readonly property int totalFiles: jobItem.jobDetails && jobItem.jobDetails.totalFiles || 0 + readonly property var url: { + if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped + || jobItem.jobError) { + return null; + } + + // For a single file show actions for it + // Otherwise the destination folder all of them were copied into + const url = totalFiles === 1 ? jobItem.jobDetails.descriptionUrl + : jobItem.jobDetails.destUrl; + + // Don't offer opening files in Trash + if (url && url.toString().startsWith("trash:")) { + return null; + } + + return url; + } + + property alias iconContainerItem: jobDragIconItem.parent + + readonly property alias dragging: jobDragArea.dragging + readonly property alias menuOpen: otherFileActionsMenu.visible + + signal suspendJobClicked + signal resumeJobClicked + signal killJobClicked + + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + spacing: 0 + + Notifications.FileInfo { + id: fileInfo + url: jobItem.totalFiles === 1 && jobItem.url ? jobItem.url : "" + } + + // This item is parented to the NotificationItem iconContainer + Item { + id: jobDragIconItem + readonly property bool shown: jobDragIcon.valid + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + visible: shown + + Binding { + target: jobDragIconItem.parent + property: "visible" + value: true + when: jobDragIconItem.shown + restoreMode: Binding.RestoreBinding + } + + PlasmaCore.IconItem { + id: jobDragIcon + + anchors.fill: parent + usesPlasmaTheme: false + active: jobDragArea.containsMouse + opacity: busyIndicator.running ? 0.6 : 1 + source: !fileInfo.error ? fileInfo.iconName : "" + + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + DraggableFileArea { + id: jobDragArea + anchors.fill: parent + + hoverEnabled: true + dragParent: jobDragIcon + dragUrl: jobItem.url || "" + dragPixmap: jobDragIcon.source + + onActivated: jobItem.openUrl(jobItem.url) + onContextMenuRequested: { + // avoid menu button glowing if we didn't actually press it + otherFileActionsButton.checked = false; + + otherFileActionsMenu.visualParent = this; + otherFileActionsMenu.open(x, y); + } + } + } + + PlasmaComponents3.BusyIndicator { + id: busyIndicator + anchors.centerIn: parent + running: fileInfo.busy && !delayBusyTimer.running + visible: running + + // Avoid briefly flashing the busy indicator + Timer { + id: delayBusyTimer + interval: 500 + repeat: false + running: fileInfo.busy + } + } + } + + RowLayout { + id: progressRow + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents3.ProgressBar { + id: progressBar + Layout.fillWidth: true + from: 0 + to: 100 + // TODO do we actually need the window visible check? perhaps I do because it can be in popup or expanded plasmoid + indeterminate: visible && Window.window && Window.window.visible && percentage < 1 + && jobItem.jobState === NotificationManager.Notifications.JobStateRunning + // is this too annoying? + && (jobItem.jobDetails.processedBytes === 0 || jobItem.jobDetails.totalBytes === 0) + && jobItem.jobDetails.processedFiles === 0 + //&& jobItem.jobDetails.processedDirectories === 0 + } + + RowLayout { + spacing: 0 + + PlasmaComponents3.ToolButton { + id: suspendButton + icon.name: "media-playback-pause" + onClicked: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended ? jobItem.resumeJobClicked() + : jobItem.suspendJobClicked() + + PlasmaComponents3.ToolTip { + text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Pause running job", "Pause") + } + } + + PlasmaComponents3.ToolButton { + id: killButton + icon.name: "media-playback-stop" + onClicked: jobItem.killJobClicked() + + PlasmaComponents3.ToolTip { + text: i18ndc("plasma_applet_org.kde.plasma.notifications", "Cancel running job", "Cancel") + } + } + + PlasmaComponents3.ToolButton { + id: expandButton + icon.name: checked ? "arrow-down" : (LayoutMirroring.enabled ? "arrow-left" : "arrow-right") + checkable: true + enabled: jobItem.jobDetails && jobItem.jobDetails.hasDetails + + PlasmaComponents3.ToolTip { + text: expandButton.checked ? i18ndc("plasma_applet_org.kde.plasma.notifications", "A button tooltip; hides item details", "Hide Details") + : i18ndc("plasma_applet_org.kde.plasma.notifications", "A button tooltip; expands the item to show details", "Show Details") + } + } + } + } + + Loader { + Layout.fillWidth: true + active: expandButton.checked + // Loader doesn't reset its height when unloaded, just hide it altogether + visible: active + sourceComponent: JobDetails { + jobDetails: jobItem.jobDetails + } + } + + Row { + id: jobActionsRow + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + // We want the actions to be right-aligned but Row also reverses + // the order of items, so we put them in reverse order + layoutDirection: Qt.RightToLeft + visible: jobItem.url && jobItem.url.toString() !== "" && !fileInfo.error + + PlasmaComponents3.Button { + id: otherFileActionsButton + height: Math.max(implicitHeight, openButton.implicitHeight) + icon.name: "application-menu" + checkable: true + text: openButton.visible ? "" : Accessible.name + Accessible.name: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options…") + onPressedChanged: { + if (pressed) { + checked = Qt.binding(function() { + return otherFileActionsMenu.visible; + }); + otherFileActionsMenu.visualParent = this; + // -1 tells it to "align bottom left of visualParent (this)" + otherFileActionsMenu.open(-1, -1); + } + } + + PlasmaComponents3.ToolTip { + text: parent.Accessible.name + enabled: parent.text === "" + } + + Notifications.FileMenu { + id: otherFileActionsMenu + url: jobItem.url || "" + onActionTriggered: jobItem.fileActionInvoked(action) + } + } + + PlasmaComponents3.Button { + id: openButton + width: Math.min(implicitWidth, jobItem.width - otherFileActionsButton.width - jobActionsRow.spacing) + height: Math.max(implicitHeight, otherFileActionsButton.implicitHeight) + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open") + onClicked: jobItem.openUrl(jobItem.url) + + states: [ + State { + when: jobItem.jobDetails && jobItem.jobDetails.totalFiles !== 1 + PropertyChanges { + target: openButton + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Open Containing Folder") + icon.name: "folder-open" + } + }, + State { + when: fileInfo.openAction + PropertyChanges { + target: openButton + text: fileInfo.openAction.text + icon.name: fileInfo.openActionIconName + visible: fileInfo.openAction.enabled + onClicked: { + fileInfo.openAction.trigger(); + jobItem.fileActionInvoked(fileInfo.openAction); + } + } + } + ] + } + } + + states: [ + State { + when: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended + PropertyChanges { + target: suspendButton + checked: true + } + PropertyChanges { + target: progressBar + enabled: false + } + }, + State { + when: jobItem.jobState === NotificationManager.Notifications.JobStateStopped + PropertyChanges { + target: progressRow + visible: false + } + PropertyChanges { + target: expandButton + checked: false + } + } + ] +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/NotificationHeader.qml b/plasma/workspace/applets/notifications/package/contents/ui/NotificationHeader.qml new file mode 100644 index 0000000000..073d2ea590 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/NotificationHeader.qml @@ -0,0 +1,252 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.kcoreaddons 1.0 as KCoreAddons + +import org.kde.quickcharts 1.0 as Charts + +import "global" + +RowLayout { + id: notificationHeading + property bool inGroup + property int notificationType + + property var applicationIconSource + property string applicationName + property string originName + + property string configureActionLabel + + property alias configurable: configureButton.visible + property alias dismissable: dismissButton.visible + property bool dismissed + property alias closeButtonTooltip: closeButtonToolTip.text + property alias closable: closeButton.visible + + property var time + + property int jobState + property QtObject jobDetails + + property real timeout: 5000 + property real remainingTime: 0 + + signal configureClicked + signal dismissClicked + signal closeClicked + + // notification created/updated time changed + onTimeChanged: updateAgoText() + + function updateAgoText() { + ageLabel.agoText = ageLabel.generateAgoText(); + } + + spacing: PlasmaCore.Units.smallSpacing + Layout.preferredHeight: Math.max(applicationNameLabel.implicitHeight, PlasmaCore.Units.iconSizes.small) + + Component.onCompleted: updateAgoText() + + Connections { + target: Globals + // clock time changed + function onTimeChanged() { + notificationHeading.updateAgoText() + } + } + + PlasmaCore.IconItem { + id: applicationIconItem + Layout.preferredWidth: PlasmaCore.Units.iconSizes.small + Layout.preferredHeight: PlasmaCore.Units.iconSizes.small + source: notificationHeading.applicationIconSource + usesPlasmaTheme: false + visible: valid + } + + PlasmaExtras.Heading { + id: applicationNameLabel + Layout.fillWidth: true + level: 5 + opacity: 0.9 + textFormat: Text.PlainText + elide: Text.ElideLeft + text: notificationHeading.applicationName + (notificationHeading.originName ? " · " + notificationHeading.originName : "") + } + + Item { + id: spacer + Layout.fillWidth: true + } + + PlasmaExtras.Heading { + id: ageLabel + + // the "n minutes ago" text, for jobs we show remaining time instead + // updated periodically by a Timer hence this property with generate() function + property string agoText: "" + visible: text !== "" + level: 5 + opacity: 0.9 + text: generateRemainingText() || agoText + Layout.rightMargin: Math.round(-notificationHeading.spacing / 2) + + function generateAgoText() { + if (!time || isNaN(time.getTime()) || notificationHeading.jobState === NotificationManager.Notifications.JobStateRunning) { + return ""; + } + + var deltaMinutes = Math.floor((Date.now() - time.getTime()) / 1000 / 60); + if (deltaMinutes < 1) { + return ""; + } + + // Received less than an hour ago, show relative minutes + if (deltaMinutes < 60) { + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "Notification was added minutes ago, keep short", "%1 min ago", "%1 min ago", deltaMinutes); + } + // Received less than a day ago, show time, 22 hours so the time isn't as ambiguous between today and yesterday + if (deltaMinutes < 60 * 22) { + return Qt.formatTime(time, Qt.locale().timeFormat(Locale.ShortFormat).replace(/.ss?/i, "")); + } + + // Otherwise show relative date (Yesterday, "Last Sunday", or just date if too far in the past) + return KCoreAddons.Format.formatRelativeDate(time, Locale.ShortFormat); + } + + function generateRemainingText() { + if (notificationHeading.notificationType !== NotificationManager.Notifications.JobType + || notificationHeading.jobState !== NotificationManager.Notifications.JobStateRunning) { + return ""; + } + + var details = notificationHeading.jobDetails; + if (!details || !details.speed) { + return ""; + } + + var remaining = details.totalBytes - details.processedBytes; + if (remaining <= 0) { + return ""; + } + + var eta = remaining / details.speed; + if (eta < 0.5) { // Avoid showing "0 seconds remaining" + return ""; + } + + if (eta < 60) { // 1 minute + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "seconds remaining, keep short", + "%1 s remaining", "%1 s remaining", Math.round(eta)); + } + if (eta < 60 * 60) {// 1 hour + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "minutes remaining, keep short", + "%1 min remaining", "%1 min remaining", + Math.round(eta / 60)); + } + if (eta < 60 * 60 * 5) { // 5 hours max, if it takes even longer there's no real point in showing that + return i18ndcp("plasma_applet_org.kde.plasma.notifications", "hours remaining, keep short", + "%1 h remaining", "%1 h remaining", + Math.round(eta / 60 / 60)); + } + + return ""; + } + + PlasmaCore.ToolTipArea { + anchors.fill: parent + active: ageLabel.agoText !== "" + subText: notificationHeading.time ? notificationHeading.time.toLocaleString(Qt.locale(), Locale.LongFormat) : "" + } + } + + RowLayout { + id: headerButtonsRow + spacing: 0 + + PlasmaComponents3.ToolButton { + id: configureButton + icon.name: "configure" + visible: false + onClicked: notificationHeading.configureClicked() + + PlasmaComponents3.ToolTip { + text: notificationHeading.configureActionLabel || i18nd("plasma_applet_org.kde.plasma.notifications", "Configure") + } + } + + PlasmaComponents3.ToolButton { + id: dismissButton + icon.name: notificationHeading.dismissed ? "window-restore" : "window-minimize" + visible: false + onClicked: notificationHeading.dismissClicked() + + PlasmaComponents3.ToolTip { + text: notificationHeading.dismissed + ? i18ndc("plasma_applet_org.kde.plasma.notifications", "Opposite of minimize", "Restore") + : i18nd("plasma_applet_org.kde.plasma.notifications", "Minimize") + } + } + + PlasmaComponents3.ToolButton { + id: closeButton + visible: false + icon.name: "window-close" + onClicked: notificationHeading.closeClicked() + + PlasmaComponents3.ToolTip { + id: closeButtonToolTip + text: i18nd("plasma_applet_org.kde.plasma.notifications", "Close") + } + + Charts.PieChart { + id: chart + anchors.fill: parent + anchors.margins: PlasmaCore.Units.smallSpacing + Math.max(Math.floor(PlasmaCore.Units.devicePixelRatio), 1) + + opacity: (notificationHeading.remainingTime > 0 && notificationHeading.remainingTime < notificationHeading.timeout) ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: PlasmaCore.Units.longDuration } + } + + range { from: 0; to: notificationHeading.timeout; automatic: false } + + valueSources: Charts.SingleValueSource { value: notificationHeading.remainingTime } + colorSource: Charts.SingleValueSource { value: PlasmaCore.Theme.highlightColor } + + thickness: Math.max(Math.floor(PlasmaCore.Units.devicePixelRatio), 1) * 5 + + transform: Scale { origin.x: chart.width / 2; xScale: -1 } + } + } + } + + states: [ + State { + when: notificationHeading.inGroup + PropertyChanges { + target: applicationIconItem + source: "" + } + PropertyChanges { + target: applicationNameLabel + visible: false + } + } + + ] +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/NotificationItem.qml b/plasma/workspace/applets/notifications/package/contents/ui/NotificationItem.qml new file mode 100644 index 0000000000..2bf64c132d --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/NotificationItem.qml @@ -0,0 +1,458 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +import org.kde.notificationmanager 1.0 as NotificationManager + +ColumnLayout { + id: notificationItem + + property bool hovered: false + property int maximumLineCount: 0 + property alias bodyCursorShape: bodyLabel.cursorShape + + property int notificationType + + property bool inGroup: false + property bool inHistory: false + property ListView listViewParent: null + + property alias applicationIconSource: notificationHeading.applicationIconSource + property alias applicationName: notificationHeading.applicationName + property alias originName: notificationHeading.originName + + property string summary + property alias time: notificationHeading.time + + property alias configurable: notificationHeading.configurable + property alias dismissable: notificationHeading.dismissable + property alias dismissed: notificationHeading.dismissed + property alias closable: notificationHeading.closable + + // This isn't an alias because TextEdit RichText adds some HTML tags to it + property string body + property var icon + property var urls: [] + + property int jobState + property int percentage + property int jobError: 0 + property bool suspendable + property bool killable + + property QtObject jobDetails + + property alias configureActionLabel: notificationHeading.configureActionLabel + property var actionNames: [] + property var actionLabels: [] + + property bool hasReplyAction + property string replyActionLabel + property string replyPlaceholderText + property string replySubmitButtonText + property string replySubmitButtonIconName + + property int headingLeftPadding: 0 + property int headingRightPadding: 0 + + property int thumbnailLeftPadding: 0 + property int thumbnailRightPadding: 0 + property int thumbnailTopPadding: 0 + property int thumbnailBottomPadding: 0 + + property alias timeout: notificationHeading.timeout + property alias remainingTime: notificationHeading.remainingTime + + readonly property bool menuOpen: bodyLabel.contextMenu !== null + || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) + || (jobLoader.item && jobLoader.item.menuOpen) + + readonly property bool dragging: (thumbnailStripLoader.item && thumbnailStripLoader.item.dragging) + || (jobLoader.item && jobLoader.item.dragging) + property bool replying: false + readonly property bool hasPendingReply: replyLoader.item && replyLoader.item.text !== "" + readonly property alias headerHeight: headingElement.height + property int extraSpaceForCriticalNotificationLine: 0 + + signal bodyClicked + signal closeClicked + signal configureClicked + signal dismissClicked + signal actionInvoked(string actionName) + signal replied(string text) + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + signal suspendJobClicked + signal resumeJobClicked + signal killJobClicked + + spacing: PlasmaCore.Units.smallSpacing + + // Header + Item { + id: headingElement + Layout.fillWidth: true + Layout.preferredHeight: notificationHeading.implicitHeight + Layout.preferredWidth: notificationHeading.implicitWidth + Layout.bottomMargin: -parent.spacing + + PlasmaCore.ColorScope.colorGroup: PlasmaCore.Theme.HeaderColorGroup + PlasmaCore.ColorScope.inherit: false + + PlasmaExtras.PlasmoidHeading { + anchors.fill: parent + visible: !notificationItem.inHistory + } + + NotificationHeader { + id: notificationHeading + anchors { + fill: parent + leftMargin: notificationItem.headingLeftPadding + rightMargin: notificationItem.headingRightPadding + } + + PlasmaCore.ColorScope.colorGroup: parent.PlasmaCore.ColorScope.colorGroup + PlasmaCore.ColorScope.inherit: false + + inGroup: notificationItem.inGroup + + notificationType: notificationItem.notificationType + jobState: notificationItem.jobState + jobDetails: notificationItem.jobDetails + + onConfigureClicked: notificationItem.configureClicked() + onDismissClicked: notificationItem.dismissClicked() + onCloseClicked: notificationItem.closeClicked() + } + } + + // Everything else that goes below the header + // This is its own ColumnLayout-within-a-ColumnLayout because it lets us set + // the left margin once rather than several times, in each of its children + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: notificationItem.extraSpaceForCriticalNotificationLine + spacing: PlasmaCore.Units.smallSpacing + + // Notification body + RowLayout { + id: bodyRow + Layout.fillWidth: true + + spacing: PlasmaCore.Units.smallSpacing + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + RowLayout { + id: summaryRow + Layout.fillWidth: true + visible: summaryLabel.text !== "" + + PlasmaExtras.Heading { + id: summaryLabel + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + textFormat: Text.PlainText + maximumLineCount: 3 + wrapMode: Text.WordWrap + elide: Text.ElideRight + level: 4 + // Give it a bit more visual prominence than the app name in the header + type: PlasmaExtras.Heading.Type.Primary + text: { + if (notificationItem.notificationType === NotificationManager.Notifications.JobType) { + if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) { + if (notificationItem.summary) { + return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary); + } + } else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) { + if (notificationItem.jobError) { + if (notificationItem.summary) { + return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary); + } else { + return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Failed"); + } + } else { + if (notificationItem.summary) { + return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary); + } else { + return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Finished"); + } + } + } + } + // some apps use their app name as summary, avoid showing the same text twice + // try very hard to match the two + if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() != notificationItem.applicationName.toLocaleLowerCase().trim()) { + return notificationItem.summary; + } + return ""; + } + visible: text !== "" + } + + // inGroup headerItem is reparented here + } + + RowLayout { + id: bodyTextRow + + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + SelectableLabel { + id: bodyLabel + listViewParent: notificationItem.listViewParent + // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either + Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter + Layout.fillWidth: true + + Layout.maximumHeight: notificationItem.maximumLineCount > 0 + ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 + + // HACK RichText does not allow to specify link color and since LineEdit + // does not support StyledText, we have to inject some CSS to force the color, + // cf. QTBUG-81463 and to some extent QTBUG-80354 + text: "" + notificationItem.body + + // Cannot do text !== "" because RichText adds some HTML tags even when empty + visible: notificationItem.body !== "" + onClicked: notificationItem.bodyClicked(mouse) + onLinkActivated: Qt.openUrlExternally(link) + } + + // inGroup iconContainer is reparented here + } + } + + Item { + id: iconContainer + + Layout.preferredWidth: PlasmaCore.Units.iconSizes.large + Layout.preferredHeight: PlasmaCore.Units.iconSizes.large + Layout.topMargin: PlasmaCore.Units.smallSpacing + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + + visible: iconItem.active + + PlasmaCore.IconItem { + id: iconItem + // don't show two identical icons + readonly property bool active: valid && source != notificationItem.applicationIconSource + anchors.fill: parent + usesPlasmaTheme: false + smooth: true + // don't show a generic "info" icon since this is a notification already + source: notificationItem.icon !== "dialog-information" ? notificationItem.icon : "" + visible: active + } + + // JobItem reparents a file icon here for finished jobs with one total file + } + } + + // Job progress reporting + Loader { + id: jobLoader + Layout.fillWidth: true + active: notificationItem.notificationType === NotificationManager.Notifications.JobType + height: item ? item.implicitHeight : 0 + visible: active + sourceComponent: JobItem { + iconContainerItem: iconContainer + + jobState: notificationItem.jobState + jobError: notificationItem.jobError + percentage: notificationItem.percentage + suspendable: notificationItem.suspendable + killable: notificationItem.killable + + jobDetails: notificationItem.jobDetails + + onSuspendJobClicked: notificationItem.suspendJobClicked() + onResumeJobClicked: notificationItem.resumeJobClicked() + onKillJobClicked: notificationItem.killJobClicked() + + onOpenUrl: notificationItem.openUrl(url) + onFileActionInvoked: notificationItem.fileActionInvoked(action) + + hovered: notificationItem.hovered + } + } + + // Actions + Item { + id: actionContainer + Layout.fillWidth: true + Layout.preferredHeight: Math.max(actionFlow.implicitHeight, replyLoader.height) + visible: actionRepeater.count > 0 && actionFlow.parent === this + + // Notification actions + Flow { // it's a Flow so it can wrap if too long + id: actionFlow + // For a cleaner look, if there is a thumbnail, puts the actions next to the thumbnail strip's menu button + parent: thumbnailStripLoader.item ? thumbnailStripLoader.item.actionContainer : actionContainer + width: parent.width + spacing: PlasmaCore.Units.smallSpacing + layoutDirection: Qt.RightToLeft + enabled: !replyLoader.active + opacity: replyLoader.active ? 0 : 1 + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + Repeater { + id: actionRepeater + + model: { + var buttons = []; + var actionNames = (notificationItem.actionNames || []); + var actionLabels = (notificationItem.actionLabels || []); + // HACK We want the actions to be right-aligned but Flow also reverses + for (var i = actionNames.length - 1; i >= 0; --i) { + buttons.push({ + actionName: actionNames[i], + label: actionLabels[i] + }); + } + + if (notificationItem.hasReplyAction) { + buttons.unshift({ + actionName: "inline-reply", + label: notificationItem.replyActionLabel || i18nc("Reply to message", "Reply") + }); + } + + return buttons; + } + + PlasmaComponents3.ToolButton { + flat: false + // why does it spit "cannot assign undefined to string" when a notification becomes expired? + text: modelData.label || "" + + onClicked: { + if (modelData.actionName === "inline-reply") { + replyLoader.beginReply(); + return; + } + + notificationItem.actionInvoked(modelData.actionName); + } + } + } + } + + // inline reply field + Loader { + id: replyLoader + width: parent.width + height: active ? item.implicitHeight : 0 + // When there is only one action and it is a reply action, show text field right away + active: notificationItem.replying || (notificationItem.hasReplyAction && (notificationItem.actionNames || []).length === 0) + visible: active + opacity: active ? 1 : 0 + x: active ? 0 : parent.width + Behavior on x { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + function beginReply() { + notificationItem.replying = true; + + plasmoid.nativeInterface.forceActivateWindow(notificationItem.Window.window); + replyLoader.item.activate(); + } + + sourceComponent: NotificationReplyField { + placeholderText: notificationItem.replyPlaceholderText + buttonIconName: notificationItem.replySubmitButtonIconName + buttonText: notificationItem.replySubmitButtonText + onReplied: notificationItem.replied(text) + + replying: notificationItem.replying + onBeginReplyRequested: replyLoader.beginReply() + } + } + } + + // thumbnails + Loader { + id: thumbnailStripLoader + Layout.leftMargin: notificationItem.thumbnailLeftPadding + Layout.rightMargin: notificationItem.thumbnailRightPadding + // no change in Layout.topMargin to keep spacing to notification text consistent + Layout.topMargin: 0 + Layout.bottomMargin: notificationItem.thumbnailBottomPadding + Layout.fillWidth: true + active: notificationItem.urls.length > 0 + visible: active + sourceComponent: ThumbnailStrip { + leftPadding: -thumbnailStripLoader.Layout.leftMargin + rightPadding: -thumbnailStripLoader.Layout.rightMargin + topPadding: -notificationItem.thumbnailTopPadding + bottomPadding: -thumbnailStripLoader.Layout.bottomMargin + urls: notificationItem.urls + onOpenUrl: notificationItem.openUrl(url) + onFileActionInvoked: notificationItem.fileActionInvoked(action) + } + } + } + + states: [ + State { + when: notificationItem.inGroup + PropertyChanges { + target: headingElement + parent: summaryRow + } + + PropertyChanges { + target: summaryRow + visible: true + } + PropertyChanges { + target: summaryLabel + visible: true + } + + /*PropertyChanges { + target: bodyLabel.Label + alignment: Qt.AlignTop + }*/ + + PropertyChanges { + target: iconContainer + parent: bodyTextRow + } + } + ] +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/NotificationPopup.qml b/plasma/workspace/applets/notifications/package/contents/ui/NotificationPopup.qml new file mode 100644 index 0000000000..f9332049c9 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/NotificationPopup.qml @@ -0,0 +1,238 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 + +import org.kde.kquickcontrolsaddons 2.0 as KQuickAddons +import org.kde.plasma.core 2.0 as PlasmaCore + +import org.kde.notificationmanager 1.0 as NotificationManager + +import ".." + +PlasmaCore.Dialog { + id: notificationPopup + + property int popupWidth + + property alias notificationType: notificationItem.notificationType + + property alias applicationName: notificationItem.applicationName + property alias applicationIconSource: notificationItem.applicationIconSource + property alias originName: notificationItem.originName + + property alias time: notificationItem.time + + property alias summary: notificationItem.summary + property alias body: notificationItem.body + property alias icon: notificationItem.icon + property alias urls: notificationItem.urls + + property int urgency + property int timeout + property int dismissTimeout + + property alias jobState: notificationItem.jobState + property alias percentage: notificationItem.percentage + property alias jobError: notificationItem.jobError + property alias suspendable: notificationItem.suspendable + property alias killable: notificationItem.killable + property alias jobDetails: notificationItem.jobDetails + + property alias configureActionLabel: notificationItem.configureActionLabel + property alias configurable: notificationItem.configurable + property alias dismissable: notificationItem.dismissable + property alias closable: notificationItem.closable + + property bool hasDefaultAction + property var defaultActionFallbackWindowIdx + property alias actionNames: notificationItem.actionNames + property alias actionLabels: notificationItem.actionLabels + + property alias hasReplyAction: notificationItem.hasReplyAction + property alias replyActionLabel: notificationItem.replyActionLabel + property alias replyPlaceholderText: notificationItem.replyPlaceholderText + property alias replySubmitButtonText: notificationItem.replySubmitButtonText + property alias replySubmitButtonIconName: notificationItem.replySubmitButtonIconName + + signal configureClicked + signal dismissClicked + signal closeClicked + + signal defaultActionInvoked + signal actionInvoked(string actionName) + signal replied(string text) + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + signal expired + signal hoverEntered + signal hoverExited + + signal suspendJobClicked + signal resumeJobClicked + signal killJobClicked + + property int defaultTimeout: 5000 + readonly property int effectiveTimeout: { + if (timeout === -1) { + return defaultTimeout; + } + if (dismissTimeout) { + return dismissTimeout; + } + return timeout; + } + + location: PlasmaCore.Types.Floating + // On wayland we need focus to copy to the clipboard, we change on mouse interaction until the cursor leaves + flags: notificationItem.replying || focusListener.wantsFocus ? 0 : Qt.WindowDoesNotAcceptFocus + + visible: false + + // When notification is updated, restart hide timer + onTimeChanged: { + if (timer.running) { + timer.restart(); + } + } + + mainItem: KQuickAddons.MouseEventListener { + id: focusListener + property bool wantsFocus: false + + width: notificationPopup.popupWidth + height: notificationItem.height + notificationItem.y + + acceptedButtons: Qt.AllButtons + hoverEnabled: true + onPressed: wantsFocus = true + onContainsMouseChanged: wantsFocus = wantsFocus && containsMouse + + // Visual flourish for critical notifications to make them stand out more + Rectangle { + id: criticalNotificationLine + + anchors { + top: parent.top + // Subtract bottom margin that header sets which is not a part of + // its height, and also the PlasmoidHeading's bottom line + topMargin: notificationItem.headerHeight - notificationItem.spacing - PlasmaCore.Units.devicePixelRatio + bottom: parent.bottom + bottomMargin: -notificationPopup.margins.bottom + left: parent.left + leftMargin: -notificationPopup.margins.left + } + implicitWidth: Math.round(4 * PlasmaCore.Units.devicePixelRatio) + + visible: notificationPopup.urgency === NotificationManager.Notifications.CriticalUrgency + + color: PlasmaCore.Theme.neutralTextColor + } + + DraggableDelegate { + id: area + anchors.fill: parent + hoverEnabled: true + draggable: notificationItem.notificationType != NotificationManager.Notifications.JobType + onDismissRequested: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) + + cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: hasDefaultAction || draggable ? Qt.LeftButton : Qt.NoButton + + onClicked: { + if (hasDefaultAction) { + notificationPopup.defaultActionInvoked(); + } + } + onEntered: notificationPopup.hoverEntered() + onExited: notificationPopup.hoverExited() + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + Timer { + id: timer + interval: notificationPopup.effectiveTimeout + running: { + if (!notificationPopup.visible) { + return false; + } + if (area.containsMouse) { + return false; + } + if (interval <= 0) { + return false; + } + if (notificationItem.dragging || notificationItem.menuOpen) { + return false; + } + if (notificationItem.replying + && (notificationPopup.active || notificationItem.hasPendingReply)) { + return false; + } + return true; + } + onTriggered: { + if (notificationPopup.dismissTimeout) { + notificationPopup.dismissClicked(); + } else { + notificationPopup.expired(); + } + } + } + + NumberAnimation { + target: notificationItem + property: "remainingTime" + from: timer.interval + to: 0 + duration: timer.interval + running: timer.running && PlasmaCore.Units.longDuration > 1 + } + + NotificationItem { + id: notificationItem + // let the item bleed into the dialog margins so the close button margins cancel out + y: closable || dismissable || configurable ? -notificationPopup.margins.top : 0 + headingRightPadding: -notificationPopup.margins.right + width: parent.width + hovered: area.containsMouse + maximumLineCount: 8 + bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0 + + thumbnailLeftPadding: -notificationPopup.margins.left + thumbnailRightPadding: -notificationPopup.margins.right + thumbnailTopPadding: -notificationPopup.margins.top + thumbnailBottomPadding: -notificationPopup.margins.bottom + + extraSpaceForCriticalNotificationLine: criticalNotificationLine.visible ? criticalNotificationLine.implicitWidth : 0 + + timeout: timer.running ? timer.interval : 0 + + closable: true + + onBodyClicked: { + if (area.acceptedButtons & Qt.LeftButton) { + area.clicked(null /*mouse*/); + } + } + onCloseClicked: notificationPopup.closeClicked() + onDismissClicked: notificationPopup.dismissClicked() + onConfigureClicked: notificationPopup.configureClicked() + onActionInvoked: notificationPopup.actionInvoked(actionName) + onReplied: notificationPopup.replied(text) + onOpenUrl: notificationPopup.openUrl(url) + onFileActionInvoked: notificationPopup.fileActionInvoked(action) + + onSuspendJobClicked: notificationPopup.suspendJobClicked() + onResumeJobClicked: notificationPopup.resumeJobClicked() + onKillJobClicked: notificationPopup.killJobClicked() + } + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/NotificationReplyField.qml b/plasma/workspace/applets/notifications/package/contents/ui/NotificationReplyField.qml new file mode 100644 index 0000000000..2ac28a4edf --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/NotificationReplyField.qml @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +RowLayout { + id: replyRow + + signal beginReplyRequested + signal replied(string text) + + property bool replying: false + + property alias text: replyTextField.text + property string placeholderText + property string buttonIconName + property string buttonText + + spacing: PlasmaCore.Units.smallSpacing + + function activate() { + replyTextField.forceActiveFocus(); + } + + PlasmaComponents3.TextField { + id: replyTextField + Layout.fillWidth: true + placeholderText: replyRow.placeholderText + || i18ndc("plasma_applet_org.kde.plasma.notifications", "Text field placeholder", "Type a reply…") + onAccepted: { + if (replyButton.enabled) { + replyRow.replied(text); + } + } + + // Catches mouse click when reply field is already shown to start a reply + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + visible: !replyRow.replying + onPressed: replyRow.beginReplyRequested() + } + } + + PlasmaComponents3.Button { + id: replyButton + icon.name: replyRow.buttonIconName || "document-send" + text: replyRow.buttonText + || i18ndc("plasma_applet_org.kde.plasma.notifications", "@action:button", "Send") + enabled: replyTextField.length > 0 + onClicked: replyRow.replied(replyTextField.text) + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/SelectableLabel.qml b/plasma/workspace/applets/notifications/package/contents/ui/SelectableLabel.qml new file mode 100644 index 0000000000..bc9e04e229 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/SelectableLabel.qml @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + SPDX-FileCopyrightText: 2014, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQuick.Window 2.2 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.kirigami 2.11 as Kirigami + +import org.kde.plasma.private.notifications 2.0 as Notifications + +PlasmaComponents3.ScrollView { + id: bodyTextContainer + + property alias text: bodyText.text + + property int cursorShape + + property QtObject contextMenu: null + property ListView listViewParent: null + + signal clicked(var mouse) + signal linkActivated(string link) + + implicitHeight: Math.min(bodyText.implicitHeight, PlasmaCore.Units.gridUnit * 5) + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + contentWidth: availableWidth + + PlasmaComponents3.TextArea { + id: bodyText + enabled: !Kirigami.Settings.isMobile + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + background: Item {} + // Work around Qt bug where NativeRendering breaks for non-integer scale factors + // https://bugreports.qt.io/browse/QTBUG-67007 + renderType: Screen.devicePixelRatio % 1 !== 0 ? Text.QtRendering : Text.NativeRendering + // Selectable only when we are in desktop mode + selectByMouse: !Kirigami.Settings.tabletMode + + readOnly: true + wrapMode: Text.Wrap + textFormat: TextEdit.RichText + + onLinkActivated: bodyTextContainer.linkActivated(link) + + // Handle left-click + Notifications.TextEditClickHandler { + target: bodyText + onClicked: { + bodyTextContainer.clicked(null); + } + } + + // Handle right-click and cursorShape + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + + cursorShape: { + if (bodyText.hoveredLink) { + return Qt.PointingHandCursor; + } else if (bodyText.selectionStart !== bodyText.selectionEnd) { + return Qt.IBeamCursor; + } else { + return bodyTextContainer.cursorShape || Qt.IBeamCursor; + } + } + + onPressed: { + contextMenu = contextMenuComponent.createObject(bodyText); + contextMenu.link = bodyText.linkAt(mouse.x, mouse.y); + + contextMenu.closed.connect(function() { + contextMenu.destroy(); + contextMenu = null; + }); + contextMenu.open(mouse.x, mouse.y); + } + + // Pass wheel events to ListView to make scrolling work in FullRepresentation. + onWheel: { + if (bodyTextContainer.listViewParent + && ((wheel.angleDelta.y > 0 && !bodyTextContainer.listViewParent.atYBeginning) + || (wheel.angleDelta.y < 0 && !bodyTextContainer.listViewParent.atYEnd))) { + bodyTextContainer.listViewParent.contentY -= wheel.angleDelta.y; + wheel.accepted = true; + } else { + wheel.accepted = false; + } + } + } + } + + Component { + id: contextMenuComponent + + EditContextMenu { + target: bodyText + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/ThumbnailStrip.qml b/plasma/workspace/applets/notifications/package/contents/ui/ThumbnailStrip.qml new file mode 100644 index 0000000000..10152ddccb --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/ThumbnailStrip.qml @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +import org.kde.plasma.private.notifications 2.0 as Notifications + +DraggableFileArea { + id: thumbnailArea + + // The protocol supports multiple URLs but so far it's only used to show + // a single preview image, so this code is simplified a lot to accommodate + // this usecase and drops everything else (fallback to app icon or ListView + // for multiple files) + property var urls + + readonly property alias menuOpen: fileMenu.visible + + property int _pressX: -1 + property int _pressY: -1 + + property int leftPadding: 0 + property int rightPadding: 0 + property int topPadding: 0 + property int bottomPadding: 0 + + property alias actionContainer: thumbnailActionContainer + + signal openUrl(string url) + signal fileActionInvoked(QtObject action) + + dragParent: previewPixmap + dragUrl: thumbnailer.url + dragPixmap: thumbnailer.pixmap + + implicitHeight: Math.max(thumbnailActionRow.implicitHeight + 2 * thumbnailActionRow.anchors.topMargin, + Math.round(Math.min(width / 3, width / thumbnailer.ratio))) + + topPadding + bottomPadding + + onActivated: thumbnailArea.openUrl(thumbnailer.url) + onContextMenuRequested: { + // avoid menu button glowing if we didn't actually press it + menuButton.checked = false; + + fileMenu.visualParent = this; + fileMenu.open(x, y); + } + + Notifications.FileMenu { + id: fileMenu + url: thumbnailer.url + visualParent: menuButton + onActionTriggered: thumbnailArea.fileActionInvoked(action) + } + + Notifications.Thumbnailer { + id: thumbnailer + + readonly property real ratio: pixmapSize.height ? pixmapSize.width / pixmapSize.height : 1 + + url: urls[0] + // height is dynamic, so request a "square" size and then show it fitting to aspect ratio + size: Qt.size(thumbnailArea.width, thumbnailArea.width) + } + + KQCAddons.QPixmapItem { + id: previewBackground + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + layer.enabled: true + opacity: 0.25 + pixmap: thumbnailer.pixmap + layer.effect: FastBlur { + source: previewBackground + anchors.fill: parent + radius: 30 + } + } + + Item { + anchors { + fill: parent + leftMargin: thumbnailArea.leftPadding + rightMargin: thumbnailArea.rightPadding + topMargin: thumbnailArea.topPadding + bottomMargin: thumbnailArea.bottomPadding + } + + KQCAddons.QPixmapItem { + id: previewPixmap + anchors.fill: parent + pixmap: thumbnailer.pixmap + smooth: true + fillMode: Image.PreserveAspectFit + } + + PlasmaCore.IconItem { + anchors.centerIn: parent + width: height + height: PlasmaCore.Units.roundToIconSize(parent.height) + usesPlasmaTheme: false + source: !thumbnailer.busy && !thumbnailer.hasPreview ? thumbnailer.iconName : "" + } + + PlasmaComponents3.BusyIndicator { + anchors.centerIn: parent + running: thumbnailer.busy + visible: thumbnailer.busy + } + + RowLayout { + id: thumbnailActionRow + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: PlasmaCore.Units.smallSpacing + } + spacing: PlasmaCore.Units.smallSpacing + + Item { + id: thumbnailActionContainer + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + + // actionFlow is reparented here + } + + PlasmaComponents3.Button { + id: menuButton + Layout.alignment: Qt.AlignTop + Accessible.name: tooltip.text + icon.name: "application-menu" + checkable: true + + onPressedChanged: { + if (pressed) { + // fake "pressed" while menu is open + checked = Qt.binding(function() { + return fileMenu.visible; + }); + + fileMenu.visualParent = this; + // -1 tells it to "align bottom left of visualParent (this)" + fileMenu.open(-1, -1); + } + } + + PlasmaComponents3.ToolTip { + id: tooltip + text: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options…") + } + } + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/global/Globals.qml b/plasma/workspace/applets/notifications/package/contents/ui/global/Globals.qml new file mode 100644 index 0000000000..b0b661cd5e --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/global/Globals.qml @@ -0,0 +1,692 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +pragma Singleton +import QtQuick 2.8 +import QtQuick.Window 2.12 +import QtQuick.Layouts 1.1 +import QtQml 2.15 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.kirigami 2.11 as Kirigami + +import org.kde.notificationmanager 1.0 as NotificationManager +import org.kde.taskmanager 0.1 as TaskManager + +import org.kde.plasma.private.notifications 2.0 as Notifications + +import ".." + +// This singleton object contains stuff shared between all notification plasmoids, namely: +// - Popup creation and placement +// - Do not disturb mode +QtObject { + id: globals + + // Listened to by "ago" label in NotificationHeader to update all of them in unison + signal timeChanged + + property bool inhibited: false + + onInhibitedChanged: { + var pa = pulseAudio.item; + if (!pa) { + return; + } + + var stream = pa.notificationStream; + if (!stream) { + return; + } + + if (inhibited) { + // Only remember that we muted if previously not muted. + if (!stream.muted) { + notificationSettings.notificationSoundsInhibited = true; + stream.mute(); + } + } else { + // Only unmute if we previously muted it. + if (notificationSettings.notificationSoundsInhibited) { + stream.unmute(); + } + notificationSettings.notificationSoundsInhibited = false; + } + notificationSettings.save(); + } + + // Some parts of the code rely on plasmoid.nativeInterface and since we're in a singleton here + // this is named "plasmoid" + property QtObject plasmoid: plasmoids[0] + + // HACK When a plasmoid is destroyed, QML sets its value to "null" in the Array + // so we then remove it so we have a working "plasmoid" again + onPlasmoidChanged: { + if (!plasmoid) { + // this doesn't Q_EMIT a change, only in ratePlasmoids() it will detect the change + plasmoids.splice(0, 1); // remove first + ratePlasmoids(); + } + } + + // all notification plasmoids + property var plasmoids: [] + + property int popupLocation: { + // if we are on mobile, we can ignore the settings totally and just + // align it to top center + if (Kirigami.Settings.isMobile) { + return Qt.AlignTop | Qt.AlignHCenter; + } + switch (notificationSettings.popupPosition) { + // Auto-determine location based on plasmoid location + case NotificationManager.Settings.CloseToWidget: + if (!plasmoid) { + return Qt.AlignBottom | Qt.AlignRight; // just in case + } + + var alignment = 0; + if (plasmoid.location === PlasmaCore.Types.LeftEdge) { + alignment |= Qt.AlignLeft; + } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { + alignment |= Qt.AlignRight; + // No horizontal alignment flag has it place it left or right depending on + // which half of the *panel* the notification plasmoid is in + } + + if (plasmoid.location === PlasmaCore.Types.TopEdge) { + alignment |= Qt.AlignTop; + } else if (plasmoid.location === PlasmaCore.Types.BottomEdge) { + alignment |= Qt.AlignBottom; + // No vertical alignment flag has it place it top or bottom edge depending on + // which half of the *screen* the notification plasmoid is in + } + return alignment; + + case NotificationManager.Settings.TopLeft: + return Qt.AlignTop | Qt.AlignLeft; + case NotificationManager.Settings.TopCenter: + return Qt.AlignTop | Qt.AlignHCenter; + case NotificationManager.Settings.TopRight: + return Qt.AlignTop | Qt.AlignRight; + case NotificationManager.Settings.BottomLeft: + return Qt.AlignBottom | Qt.AlignLeft; + case NotificationManager.Settings.BottomCenter: + return Qt.AlignBottom | Qt.AlignHCenter; + case NotificationManager.Settings.BottomRight: + return Qt.AlignBottom | Qt.AlignRight; + } + } + + readonly property rect screenRect: { + if (!plasmoid) { + return Qt.rect(0, 0, -1, -1); + } + + let rect = Qt.rect(plasmoid.screenGeometry.x + plasmoid.availableScreenRect.x, + plasmoid.screenGeometry.y + plasmoid.availableScreenRect.y, + plasmoid.availableScreenRect.width, + plasmoid.availableScreenRect.height); + + // When no explicit screen corner is configured, + // restrict notification popup position by horizontal panel width + if (visualParent && notificationSettings.popupPosition === NotificationManager.Settings.CloseToWidget + && plasmoid.formFactor === PlasmaCore.Types.Horizontal) { + const visualParentWindow = visualParent.Window.window; + if (visualParentWindow) { + const left = Math.max(rect.left, visualParentWindow.x); + const right = Math.min(rect.right, visualParentWindow.x + visualParentWindow.width); + rect = Qt.rect(left, rect.y, right - left, rect.height); + } + } + + return rect; + } + onScreenRectChanged: repositionTimer.start() + + readonly property Item visualParent: { + if (!plasmoid) { + return null; + } + return plasmoid.nativeInterface.systemTrayRepresentation + || plasmoid.compactRepresentationItem + || plasmoid.fullRepresentationItem; + } + onVisualParentChanged: positionPopups() + + readonly property QtObject focusDialog: plasmoid.nativeInterface.focussedPlasmaDialog + onFocusDialogChanged: positionPopups() + + // The raw width of the popup's content item, the Dialog itself adds some margins + // Make it wider when on the top or the bottom center, since there's more horizontal + // space available without looking weird + // On mobile however we don't really want to have larger notifications + property int popupWidth: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? PlasmaCore.Units.gridUnit * 22 : PlasmaCore.Units.gridUnit * 18 + property int popupEdgeDistance: PlasmaCore.Units.largeSpacing * 2 + // Reduce spacing between popups when centered so the stack doesn't intrude into the + // view as much + property int popupSpacing: (popupLocation & Qt.AlignHCenter) && !Kirigami.Settings.isMobile ? PlasmaCore.Units.smallSpacing : PlasmaCore.Units.largeSpacing + + // How much vertical screen real estate the notification popups may consume + readonly property real popupMaximumScreenFill: 0.8 + + onPopupLocationChanged: Qt.callLater(positionPopups) + + Component.onCompleted: checkInhibition() + + function adopt(plasmoid) { + // this doesn't Q_EMIT a change, only in ratePlasmoids() it will detect the change + globals.plasmoids.push(plasmoid); + ratePlasmoids(); + } + + // Sorts plasmoids based on a heuristic to find a suitable plasmoid to follow when placing popups + function ratePlasmoids() { + var plasmoidScore = function(plasmoid) { + if (!plasmoid) { + return 0; + } + + var score = 0; + + // Prefer plasmoids in a panel, prefer horizontal panels over vertical ones + if (plasmoid.location === PlasmaCore.Types.LeftEdge + || plasmoid.location === PlasmaCore.Types.RightEdge) { + score += 1; + } else if (plasmoid.location === PlasmaCore.Types.TopEdge + || plasmoid.location === PlasmaCore.Types.BottomEdge) { + score += 2; + } + + // Prefer iconified plasmoids + if (!plasmoid.expanded) { + ++score; + } + + // Prefer plasmoids on primary screen + if (plasmoid.nativeInterface && plasmoid.nativeInterface.isPrimaryScreen(plasmoid.screenGeometry)) { + ++score; + } + + return score; + } + + var newPlasmoids = plasmoids; + newPlasmoids.sort(function (a, b) { + var scoreA = plasmoidScore(a); + var scoreB = plasmoidScore(b); + // Sort descending by score + if (scoreA < scoreB) { + return 1; + } else if (scoreA > scoreB) { + return -1; + } else { + return 0; + } + }); + globals.plasmoids = newPlasmoids; + } + + function checkInhibition() { + globals.inhibited = Qt.binding(function() { + var inhibited = false; + + if (!NotificationManager.Server.valid) { + return false; + } + + var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; + if (!isNaN(inhibitedUntil.getTime())) { + inhibited |= (Date.now() < inhibitedUntil.getTime()); + } + + if (notificationSettings.notificationsInhibitedByApplication) { + inhibited |= true; + } + + if (notificationSettings.inhibitNotificationsWhenScreensMirrored) { + inhibited |= notificationSettings.screensMirrored; + } + + return inhibited; + }); + } + + function revokeInhibitions() { + notificationSettings.notificationsInhibitedUntil = undefined; + notificationSettings.revokeApplicationInhibitions(); + // overrules current mirrored screen setup, updates again when screen configuration changes + notificationSettings.screensMirrored = false; + + notificationSettings.save(); + } + + function rectIntersect(rect1 /*dialog*/, rect2 /*popup*/) { + return rect1.x < rect2.x + rect2.width + && rect2.x < rect1.x + rect1.width + && rect1.y < rect2.y + rect2.height + && rect2.y < rect1.y + rect1.height; + } + + function positionPopups() { + if (!plasmoid) { + return; + } + + const screenRect = globals.screenRect; + if (screenRect.width <= 0 || screenRect.height <= 0) { + return; + } + + let effectivePopupLocation = popupLocation; + + const visualParent = globals.visualParent; + const visualParentWindow = visualParent.Window.window; + + // When no horizontal alignment is specified, place it depending on which half of the *panel* + // the notification plasmoid is in + if (visualParentWindow) { + if (!(effectivePopupLocation & (Qt.AlignLeft | Qt.AlignHCenter | Qt.AlignRight))) { + const iconHCenter = visualParent.mapToItem(null /*mapToScene*/, 0, 0).x + visualParent.width / 2; + + if (iconHCenter < visualParentWindow.width / 2) { + effectivePopupLocation |= Qt.AlignLeft; + } else { + effectivePopupLocation |= Qt.AlignRight; + } + } + } + + // When no vertical alignment is specified, place it depending on which half of the *screen* + // the notification plasmoid is in + if (!(effectivePopupLocation & (Qt.AlignTop | Qt.AlignBottom))) { + const screenVCenter = screenRect.y + screenRect.height / 2; + const iconVCenter = visualParent.mapToGlobal(0, visualParent.height / 2).y; + + if (iconVCenter < screenVCenter) { + effectivePopupLocation |= Qt.AlignTop; + } else { + effectivePopupLocation |= Qt.AlignBottom; + } + } + + let y = screenRect.y; + if (effectivePopupLocation & Qt.AlignBottom) { + y += screenRect.height - popupEdgeDistance; + } else { + y += popupEdgeDistance; + } + + for (var i = 0; i < popupInstantiator.count; ++i) { + let popup = popupInstantiator.objectAt(i); + if (!popup) { + continue; + } + + // Popup width is fixed, so don't rely on the actual window size + var popupEffectiveWidth = popupWidth + popup.margins.left + popup.margins.right; + + const leftMostX = screenRect.x + popupEdgeDistance; + const rightMostX = screenRect.x + screenRect.width - popupEdgeDistance - popupEffectiveWidth; + + // If available screen rect is narrower than the popup, center it in the available rect + if (screenRect.width < popupEffectiveWidth || effectivePopupLocation & Qt.AlignHCenter) { + popup.x = screenRect.x + (screenRect.width - popupEffectiveWidth) / 2 + } else if (effectivePopupLocation & Qt.AlignLeft) { + popup.x = leftMostX; + } else if (effectivePopupLocation & Qt.AlignRight) { + popup.x = rightMostX; + } + + if (effectivePopupLocation & Qt.AlignTop) { + // We want to calculate the new position based on its original target position to avoid positioning it and then + // positioning it again, hence the temporary Qt.rect with explicit "y" and not just the popup as a whole + if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup) + && rectIntersect(focusDialog, Qt.rect(popup.x, y, popup.width, popup.height))) { + y = focusDialog.y + focusDialog.height + popupEdgeDistance; + } + popup.y = y; + // If the popup isn't ready yet, ignore its occupied space for now. + // We'll reposition everything in onHeightChanged eventually. + y += popup.height + (popup.height > 0 ? popupSpacing : 0); + } else { + y -= popup.height; + if (focusDialog && focusDialog.visible && !(focusDialog instanceof NotificationPopup) + && rectIntersect(focusDialog, Qt.rect(popup.x, y, popup.width, popup.height))) { + y = focusDialog.y - popup.height - popupEdgeDistance; + } + popup.y = y; + if (popup.height > 0) { + y -= popupSpacing; + } + } + + // don't let notifications take more than popupMaximumScreenFill of the screen + var visible = true; + if (i > 0) { // however always show at least one popup + if (effectivePopupLocation & Qt.AlignTop) { + visible = (popup.y + popup.height < screenRect.y + (screenRect.height * popupMaximumScreenFill)); + } else { + visible = (popup.y > screenRect.y + (screenRect.height * (1 - popupMaximumScreenFill))); + } + } + + popup.visible = visible; + } + } + + property QtObject popupNotificationsModel: NotificationManager.Notifications { + limit: plasmoid ? (Math.ceil(globals.screenRect.height / (theme.mSize(theme.defaultFont).height * 4))) : 0 + showExpired: false + showDismissed: false + blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications + blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices + whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : [] + whitelistedNotifyRcNames: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedServices : [] + showJobs: notificationSettings.jobsInNotifications + sortMode: NotificationManager.Notifications.SortByTypeAndUrgency + sortOrder: Qt.AscendingOrder + groupMode: NotificationManager.Notifications.GroupDisabled + urgencies: { + var urgencies = 0; + + // Critical always except in do not disturb mode when disabled in settings + if (!globals.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) { + urgencies |= NotificationManager.Notifications.CriticalUrgency; + } + + // Normal only when not in do not disturb mode + if (!globals.inhibited) { + urgencies |= NotificationManager.Notifications.NormalUrgency; + } + + // Low only when enabled in settings and not in do not disturb mode + if (!globals.inhibited && notificationSettings.lowPriorityPopups) { + urgencies |=NotificationManager.Notifications.LowUrgency; + } + + return urgencies; + } + } + + property QtObject notificationSettings: NotificationManager.Settings { + onNotificationsInhibitedUntilChanged: globals.checkInhibition() + } + + property QtObject tasksModel: TaskManager.TasksModel { + groupMode: TaskManager.TasksModel.GroupApplications + groupInline: false + } + + // This periodically checks whether do not disturb mode timed out and updates the "minutes ago" labels + property QtObject timeSource: PlasmaCore.DataSource { + engine: "time" + connectedSources: ["Local"] + interval: 60000 // 1 min + intervalAlignment: PlasmaCore.Types.AlignToMinute + onDataChanged: { + checkInhibition(); + globals.timeChanged(); + } + } + + property Instantiator popupInstantiator: Instantiator { + model: popupNotificationsModel + delegate: NotificationPopup { + // so Instantiator can access that after the model row is gone + readonly property var notificationId: model.notificationId + + popupWidth: globals.popupWidth + type: model.urgency === NotificationManager.Notifications.CriticalUrgency + || (model.urgency === NotificationManager.Notifications.NormalUrgency && notificationSettings.keepNormalAlwaysOnTop) + ? PlasmaCore.Dialog.CriticalNotification : PlasmaCore.Dialog.Notification + + notificationType: model.type + + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" + + time: model.updated || model.created + + configurable: model.configurable + // For running jobs instead of offering a "close" button that might lead the user to + // think that will cancel the job, we offer a "dismiss" button that hides it in the history + dismissable: model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + // TODO would be nice to be able to "pin" jobs when they autohide + && notificationSettings.permanentJobPopups + closable: model.closable + + summary: model.summary + body: model.body || "" + icon: model.image || model.iconName + hasDefaultAction: model.hasDefaultAction || false + timeout: model.timeout + // Increase default timeout for notifications with a URL so you have enough time + // to interact with the thumbnail or bring the window to the front where you want to drag it into + defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0) + // When configured to not keep jobs open permanently, we autodismiss them after the standard timeout + dismissTimeout: !notificationSettings.permanentJobPopups + && model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + ? defaultTimeout : 0 + + urls: model.urls || [] + urgency: model.urgency || NotificationManager.Notifications.NormalUrgency + + jobState: model.jobState || 0 + percentage: model.percentage || 0 + jobError: model.jobError || 0 + suspendable: !!model.suspendable + killable: !!model.killable + jobDetails: model.jobDetails || null + + configureActionLabel: model.configureActionLabel || "" + actionNames: model.actionNames + actionLabels: model.actionLabels + + hasReplyAction: model.hasReplyAction || false + replyActionLabel: model.replyActionLabel || "" + replyPlaceholderText: model.replyPlaceholderText || "" + replySubmitButtonText: model.replySubmitButtonText || "" + replySubmitButtonIconName: model.replySubmitButtonIconName || "" + + onExpired: { + if (model.resident) { + // When resident, only mark it as expired so the popup disappears + // but don't actually invalidate the notification + model.expired = true; + } else { + popupNotificationsModel.expire(popupNotificationsModel.index(index, 0)) + } + } + onHoverEntered: model.read = true + // explicit close, even when resident + onCloseClicked: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) + onDismissClicked: model.dismissed = true + onConfigureClicked: popupNotificationsModel.configure(popupNotificationsModel.index(index, 0)) + onDefaultActionInvoked: { + if (defaultActionFallbackWindowIdx) { + if (!defaultActionFallbackWindowIdx.valid) { + console.warn("Failed fallback notification activation as window no longer exists"); + return; + } + + // When it's a group, activate the window highest in stacking order (presumably last used) + if (tasksModel.data(defaultActionFallbackWindowIdx, TaskManager.AbstractTasksModel.IsGroupParent)) { + let highestStacking = -1; + let highestIdx = undefined; + + for (let i = 0; i < tasksModel.rowCount(defaultActionFallbackWindowIdx); ++i) { + const idx = tasksModel.index(i, 0, defaultActionFallbackWindowIdx); + + const stacking = tasksModel.data(idx, TaskManager.AbstractTasksModel.StackingOrder); + + if (stacking > highestStacking) { + highestStacking = stacking; + highestIdx = tasksModel.makePersistentModelIndex(defaultActionFallbackWindowIdx.row, i); + } + } + + if (highestIdx && highestIdx.valid) { + tasksModel.requestActivate(highestIdx); + if (!model.resident) { + popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) + } + + } + return; + } + + tasksModel.requestActivate(defaultActionFallbackWindowIdx); + if (!model.resident) { + popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) + } + return; + } + + const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close; + popupNotificationsModel.invokeDefaultAction(popupNotificationsModel.index(index, 0), behavior) + } + onActionInvoked: { + const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close; + popupNotificationsModel.invokeAction(popupNotificationsModel.index(index, 0), actionName, behavior) + } + onReplied: { + const behavior = model.resident ? NotificationManager.Notifications.None : NotificationManager.Notifications.Close; + popupNotificationsModel.reply(popupNotificationsModel.index(index, 0), text, behavior); + } + onOpenUrl: { + Qt.openUrlExternally(url); + // Client isn't informed of this action, so we always hide the popup + if (model.resident) { + model.expired = true; + } else { + popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) + } + } + onFileActionInvoked: { + if (!model.resident + || (action.objectName === "movetotrash" || action.objectName === "deletefile")) { + popupNotificationsModel.close(popupNotificationsModel.index(index, 0)); + } else { + model.expired = true; + } + } + + onSuspendJobClicked: popupNotificationsModel.suspendJob(popupNotificationsModel.index(index, 0)) + onResumeJobClicked: popupNotificationsModel.resumeJob(popupNotificationsModel.index(index, 0)) + onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0)) + + // popup width is fixed + onHeightChanged: positionPopups() + + Component.onCompleted: { + if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) { + // Register apps that were seen spawning a popup so they can be configured later + // Apps with notifyrc can already be configured anyway + if (!model.notifyRcName) { + notificationSettings.registerKnownApplication(model.desktopEntry); + notificationSettings.save(); + } + + // If there is no default action, check if there is a window we could activate instead + if (!model.hasDefaultAction) { + for (let i = 0; i < tasksModel.rowCount(); ++i) { + const idx = tasksModel.index(i, 0); + + const appId = tasksModel.data(idx, TaskManager.AbstractTasksModel.AppId); + if (appId === model.desktopEntry + ".desktop") { + // Takes a row number, not a QModelIndex + defaultActionFallbackWindowIdx = tasksModel.makePersistentModelIndex(i); + hasDefaultAction = true; + break; + } + } + } + } + + // Tell the model that we're handling the timeout now + popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0)); + } + } + onObjectAdded: { + positionPopups(); + object.visible = true; + } + onObjectRemoved: { + var notificationId = object.notificationId + // Popup might have been destroyed because of a filter change, tell the model to do the timeout work for us again + // cannot use QModelIndex here as the model row is already gone + popupNotificationsModel.startTimeout(notificationId); + + positionPopups(); + } + } + + // TODO use pulseaudio-qt for this once it becomes a framework + property QtObject pulseAudio: Loader { + source: "PulseAudio.qml" + } + + // Normally popups are repositioned through Qt.callLater but in case of e.g. screen geometry changes we want to compress that + property Timer repositionTimer: Timer { + interval: 250 + onTriggered: positionPopups() + } + + // Tracks the visual parent's window since mapToItem cannot signal + // so that when user resizes panel we reposition the popups live + property Connections visualParentWindowConnections: Connections { + target: visualParent ? visualParent.Window.window : null + function onXChanged() { + repositionTimer.start(); + } + function onYChanged() { + repositionTimer.start(); + } + function onWidthChanged() { + repositionTimer.start(); + } + function onHeightChanged() { + repositionTimer.start(); + } + } + + // Keeps the Inhibited property on DBus in sync with our inhibition handling + property Binding serverInhibitedBinding: Binding { + target: NotificationManager.Server + property: "inhibited" + value: globals.inhibited + restoreMode: Binding.RestoreBinding + } + + function toggleDoNotDisturbMode() { + var oldInhibited = globals.inhibited; + if (oldInhibited) { + globals.revokeInhibitions(); + } else { + // Effectively "in a year" is "until turned off" + var d = new Date(); + d.setFullYear(d.getFullYear() + 1); + notificationSettings.notificationsInhibitedUntil = d; + notificationSettings.save(); + } + + checkInhibition(); + + if (globals.inhibited !== oldInhibited) { + shortcuts.showDoNotDisturbOsd(globals.inhibited); + } + } + + property Notifications.GlobalShortcuts shortcuts: Notifications.GlobalShortcuts { + onToggleDoNotDisturbTriggered: globals.toggleDoNotDisturbMode() + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/global/PulseAudio.qml b/plasma/workspace/applets/notifications/package/contents/ui/global/PulseAudio.qml new file mode 100644 index 0000000000..e765833c28 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/global/PulseAudio.qml @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2017, 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.2 + +import org.kde.plasma.private.volume 0.1 + +QtObject { + id: pulseAudio + + readonly property string notificationStreamId: "sink-input-by-media-role:event" + + property QtObject notificationStream + + property QtObject instantiator: Instantiator { + model: StreamRestoreModel {} + + delegate: QtObject { + readonly property string name: Name + readonly property bool muted: Muted + + function mute() { + Muted = true + } + function unmute() { + Muted = false + } + } + + onObjectAdded: { + if (object.name === notificationStreamId) { + notificationStream = object; + } + } + } +} diff --git a/plasma/workspace/applets/notifications/package/contents/ui/global/qmldir b/plasma/workspace/applets/notifications/package/contents/ui/global/qmldir new file mode 100644 index 0000000000..79c54fbb2d --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/global/qmldir @@ -0,0 +1 @@ +singleton Globals 1.0 Globals.qml diff --git a/plasma/workspace/applets/notifications/package/contents/ui/main.qml b/plasma/workspace/applets/notifications/package/contents/ui/main.qml new file mode 100644 index 0000000000..d4ceb76de4 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/contents/ui/main.qml @@ -0,0 +1,180 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.8 +import QtQml 2.15 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kquickcontrolsaddons 2.0 // For KCMShell + +import org.kde.kcoreaddons 1.0 as KCoreAddons +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +import org.kde.notificationmanager 1.0 as NotificationManager + +import "global" + +Item { + id: root + + readonly property int effectiveStatus: historyModel.activeJobsCount > 0 + || historyModel.unreadNotificationsCount > 0 + || Globals.inhibited ? PlasmaCore.Types.ActiveStatus + : PlasmaCore.Types.PassiveStatus + onEffectiveStatusChanged: { + if (effectiveStatus === PlasmaCore.Types.PassiveStatus) { + // HACK System Tray only lets applets self-hide when in Active state + // When we clear the notifications, the status is updated right away + // as a result of model signals, and when we then try to collapse + // the popup isn't hidden. + Qt.callLater(function() { + Plasmoid.status = effectiveStatus; + }); + } else { + Plasmoid.status = effectiveStatus; + } + } + + Plasmoid.status: effectiveStatus + + Plasmoid.toolTipSubText: { + var lines = []; + + if (historyModel.activeJobsCount > 0) { + lines.push(i18np("%1 running job", "%1 running jobs", historyModel.activeJobsCount)); + } + + if (!NotificationManager.Server.valid) { + lines.push(i18n("Notification service not available")); + } else { + // Any notification that is newer than "lastRead" is "unread" + // since it doesn't know the popup is on screen which makes the user see it + var actualUnread = historyModel.unreadNotificationsCount - Globals.popupNotificationsModel.activeNotificationsCount; + if (actualUnread > 0) { + lines.push(i18np("%1 unread notification", "%1 unread notifications", actualUnread)); + } + + if (Globals.inhibited) { + var inhibitedUntil = notificationSettings.notificationsInhibitedUntil + var inhibitedUntilValid = !isNaN(inhibitedUntil.getTime()); + + // Show until time if valid but not if too far in the future + // TODO check app inhibition, too + if (inhibitedUntilValid + && inhibitedUntil.getTime() - Date.now() < 100 * 24 * 60 * 60 * 1000 /* 100 days*/) { + lines.push(i18n("Do not disturb until %1", + KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat))); + } else { + lines.push(i18n("Do not disturb")); + } + } else if (lines.length === 0) { + lines.push(i18n("No unread notifications")); + } + } + + return lines.join("\n"); + } + + Plasmoid.switchWidth: PlasmaCore.Units.gridUnit * 14 + // This is to let the plasmoid expand in a vertical panel for a "sidebar" notification panel + // The CompactRepresentation size is limited to not have the notification icon grow gigantic + // but it should still switch over to full rep once there's enough width (disregarding the limited height) + Plasmoid.switchHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? 1 : PlasmaCore.Units.gridUnit * 10 + + Plasmoid.onExpandedChanged: { + if (!plasmoid.expanded) { + historyModel.lastRead = undefined; // reset to now + historyModel.collapseAllGroups(); + } + } + + Plasmoid.compactRepresentation: CompactRepresentation { + activeCount: Globals.popupNotificationsModel.activeNotificationsCount + unreadCount: Math.min(99, historyModel.unreadNotificationsCount) + + jobsCount: historyModel.activeJobsCount + jobsPercentage: historyModel.jobsPercentage + + inhibited: Globals.inhibited || !NotificationManager.Server.valid + } + + Plasmoid.fullRepresentation: FullRepresentation { + + } + + NotificationManager.Settings { + id: notificationSettings + } + + NotificationManager.Notifications { + id: historyModel + showExpired: true + showDismissed: true + showJobs: notificationSettings.jobsInNotifications + sortMode: NotificationManager.Notifications.SortByTypeAndUrgency + groupMode: NotificationManager.Notifications.GroupApplicationsFlat + groupLimit: 2 + expandUnread: true + blacklistedDesktopEntries: notificationSettings.historyBlacklistedApplications + blacklistedNotifyRcNames: notificationSettings.historyBlacklistedServices + urgencies: { + var urgencies = NotificationManager.Notifications.CriticalUrgency + | NotificationManager.Notifications.NormalUrgency; + if (notificationSettings.lowPriorityHistory) { + urgencies |= NotificationManager.Notifications.LowUrgency; + } + return urgencies; + } + + onCountChanged: { + if (count === 0) { + closePlasmoid(); + } + } + } + + Binding { + target: plasmoid.nativeInterface + property: "dragPixmapSize" + value: PlasmaCore.Units.iconSizes.large + restoreMode: Binding.RestoreBinding + } + + function closePlasmoid() { + if (plasmoid.hideOnWindowDeactivate) { + plasmoid.expanded = false; + } + } + + function action_clearHistory() { + historyModel.clear(NotificationManager.Notifications.ClearExpired); + if (historyModel.count === 0) { + closePlasmoid(); + } + } + + function action_configure() { + KQCAddons.KCMShell.openSystemSettings("kcm_notifications"); + } + + Component.onCompleted: { + Globals.adopt(plasmoid); + + plasmoid.setAction("clearHistory", i18n("Clear All Notifications"), "edit-clear-history"); + var clearAction = plasmoid.action("clearHistory"); + clearAction.visible = Qt.binding(function() { + return historyModel.expiredNotificationsCount > 0; + }); + + // The applet's config window has nothing in it, so let's make the header's + // "Configure" button open the KCM instead, like we do in the Bluetooth + // and Networks applets + plasmoid.removeAction("configure"); + plasmoid.setAction("configure", i18n("&Configure Event Notifications and Actions…"), "configure"); + plasmoid.action("configure").visible = (KQCAddons.KCMShell.authorize("kcm_notifications.desktop").length > 0); + } +} diff --git a/plasma/workspace/applets/notifications/package/metadata.json b/plasma/workspace/applets/notifications/package/metadata.json new file mode 100644 index 0000000000..7528db5717 --- /dev/null +++ b/plasma/workspace/applets/notifications/package/metadata.json @@ -0,0 +1,182 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Category": "Tasks", + "Description": "Display notifications and jobs", + "Description[ar]": "اعرض الإخطارات والمهام", + "Description[az]": "Bildirişləri və əməlləri göstərmək", + "Description[ca]": "Mostra les notificacions i els treballs", + "Description[cs]": "Upozornění a úlohy", + "Description[de]": "Benachrichtigungen und Aktionen anzeigen", + "Description[en_GB]": "Display notifications and jobs", + "Description[es]": "Mostrar notificaciones y tareas", + "Description[eu]": "Azaldu jakinarazpenak eta atazak", + "Description[fi]": "Näytä ilmoituksia ja töitä", + "Description[fr]": "Afficher les notifications et les tâches", + "Description[hu]": "Értesítések és feladatok megjelenítése", + "Description[ia]": "Monstra notificationes e labores", + "Description[it]": "Visualizza le notifiche ed i processi", + "Description[ko]": "알림과 작업 표시", + "Description[lt]": "Rodyti pranešimus ir darbus", + "Description[nl]": "Meldingen en taken tonen", + "Description[nn]": "Vis varslingar og jobbar", + "Description[pa]": "ਨੋਟੀਫਿਕੇਸ਼ਨ ਤੇ ਜਾਬ ਵੇਖੋ", + "Description[pl]": "Wyświetla powiadomienia i zadania", + "Description[pt_BR]": "Exibe notificações e tarefas", + "Description[ro]": "Afișează notificări și sarcini", + "Description[ru]": "Уведомления и задания", + "Description[sk]": "Zobrazenie upozornení a úloh", + "Description[sl]": "Prikaz obvestil in poslov", + "Description[sv]": "Visa underrättelser och jobb", + "Description[ta]": "அறிவிப்புகள் மற்றும் பணிகளை காட்டும்", + "Description[tr]": "Görevleri ve bildirimleri görüntüle", + "Description[uk]": "Показ сповіщень і завдань", + "Description[vi]": "Hiển thị thông báo và công việc", + "Description[x-test]": "xxDisplay notifications and jobsxx", + "Description[zh_CN]": "显示通知和任务", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "preferences-desktop-notification-bell", + "Id": "org.kde.plasma.notifications", + "License": "GPL-2.0+", + "Name": "Notifications", + "Name[ar]": "إخطارات", + "Name[ast]": "Avisos", + "Name[az]": "Bildirilşlər", + "Name[be@latin]": "Infarmavańnie", + "Name[be]": "Абвяшчэнні", + "Name[bg]": "Уведомяване", + "Name[bn]": "বিজ্ঞপ্তি", + "Name[bn_IN]": "সূচনাবার্তা", + "Name[br]": "Kemenn", + "Name[bs]": "Obavještenja", + "Name[ca@valencia]": "Notificacions", + "Name[ca]": "Notificacions", + "Name[cs]": "Upozornění", + "Name[csb]": "Dôwanié wiédzë", + "Name[da]": "Bekendtgørelser", + "Name[de]": "Benachrichtigungen", + "Name[el]": "Ειδοποιήσεις", + "Name[en_GB]": "Notifications", + "Name[eo]": "Atentigoj", + "Name[es]": "Notificaciones", + "Name[et]": "Märguanded", + "Name[eu]": "Jakinarazpenak", + "Name[fa]": "اخطارها", + "Name[fi]": "Ilmoitukset", + "Name[fr]": "Notifications", + "Name[fy]": "Notifikaasjes", + "Name[ga]": "Fógairt", + "Name[gl]": "Notificacións", + "Name[gu]": "નોંધણીઓ", + "Name[he]": "הודעות", + "Name[hi]": "सूचनाएँ", + "Name[hne]": "सूचना मन ल", + "Name[hr]": "Obavijesti", + "Name[hsb]": "Zdźělenki", + "Name[hu]": "Rendszerüzenetek", + "Name[ia]": "Notificationes", + "Name[id]": "Notifikasi", + "Name[is]": "Kerfistilkynningar", + "Name[it]": "Notifiche", + "Name[ja]": "通知", + "Name[kk]": "Құлақтандыру", + "Name[km]": "សេចក្តី​ជូន​ដំណឹង​", + "Name[kn]": "ಸೂಚನೆಗಳು", + "Name[ko]": "알림", + "Name[ku]": "Agahdarî", + "Name[lt]": "Pranešimai", + "Name[lv]": "Paziņojumi", + "Name[mai]": "सूचनासभ", + "Name[mk]": "Известувања", + "Name[ml]": "അറിയിപ്പുകള്‍", + "Name[mr]": "सूचना", + "Name[ms]": "Pemberitahuan", + "Name[nb]": "Varslinger", + "Name[nds]": "Bescheden", + "Name[ne]": "सूचना", + "Name[nl]": "Meldingen", + "Name[nn]": "Varslingar", + "Name[oc]": "Notificacions", + "Name[or]": "ବିଜ୍ଞପ୍ତି", + "Name[pa]": "ਨੋਟੀਫਿਕੇਸ਼ਨ", + "Name[pl]": "Powiadomienia", + "Name[pt]": "Notificações", + "Name[pt_BR]": "Notificações", + "Name[ro]": "Notificări", + "Name[ru]": "Уведомления", + "Name[se]": "Dieđáhusat", + "Name[si]": "දැනුම් දීම්", + "Name[sk]": "Upozornenia", + "Name[sl]": "Obvestila", + "Name[sr@ijekavian]": "обавјештења", + "Name[sr@ijekavianlatin]": "obavještenja", + "Name[sr@latin]": "obaveštenja", + "Name[sr]": "обавештења", + "Name[sv]": "Underrättelser", + "Name[ta]": "அறிவிப்புகள்", + "Name[te]": "నోటీసులు", + "Name[tg]": "Огоҳиҳо", + "Name[th]": "การแจ้งให้ทราบต่าง ๆ", + "Name[tr]": "Bildirimler", + "Name[ug]": "ئۇقتۇرۇشلار", + "Name[uk]": "Сповіщення", + "Name[uz@cyrillic]": "Хабарномалар", + "Name[uz]": "Xabarnomalar", + "Name[vi]": "Thông báo", + "Name[wa]": "Notifiaedjes", + "Name[x-test]": "xxNotificationsxx", + "Name[zh_CN]": "通知", + "Name[zh_TW]": "通知", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "4.0", + "Website": "https://kde.org/plasma-desktop/" + }, + "X-KDE-ParentApp": "org.kde.plasmashell", + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "ApplicationStatus", + "X-Plasma-Provides": [ + "org.kde.plasma.notifications" + ] +} diff --git a/plasma/workspace/applets/notifications/texteditclickhandler.cpp b/plasma/workspace/applets/notifications/texteditclickhandler.cpp new file mode 100644 index 0000000000..6eb8da29e6 --- /dev/null +++ b/plasma/workspace/applets/notifications/texteditclickhandler.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2021 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "texteditclickhandler.h" + +#include +#include +#include +#include + +TextEditClickHandler::TextEditClickHandler(QObject *parent) + : QObject(parent) +{ +} + +TextEditClickHandler::~TextEditClickHandler() = default; + +QQuickItem *TextEditClickHandler::target() const +{ + return m_target.data(); +} + +void TextEditClickHandler::setTarget(QQuickItem *target) +{ + if (m_target.data() == target) { + return; + } + + if (m_target) { + m_target->removeEventFilter(this); + } + + m_target = target; + m_target->installEventFilter(this); + Q_EMIT targetChanged(target); +} + +bool TextEditClickHandler::eventFilter(QObject *watched, QEvent *event) +{ + Q_ASSERT(watched == m_target.data()); + + if (event->type() == QEvent::MouseButtonPress) { + const auto *e = static_cast(event); + m_pressPos = e->pos(); + } else if (event->type() == QEvent::MouseButtonRelease) { + const auto *e = static_cast(event); + + if (m_pressPos.x() > -1 && m_pressPos.y() > -1 // + && (m_pressPos - e->pos()).manhattanLength() < qGuiApp->styleHints()->startDragDistance()) { + Q_EMIT clicked(); + } + } + + return false; +} diff --git a/plasma/workspace/applets/notifications/texteditclickhandler.h b/plasma/workspace/applets/notifications/texteditclickhandler.h new file mode 100644 index 0000000000..bffa1ed1dd --- /dev/null +++ b/plasma/workspace/applets/notifications/texteditclickhandler.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2021 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include + +class QQuickItem; + +/** + * Simple event filter that emits a clicked() signal when clicking + * on a TextEdit while still letting the user select text like normal. + * + * It's just for this very specific use case, which is also why it has + * no acceptedButtons, MouseEvent argument on clicked signal, etc. + */ +class TextEditClickHandler : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged) + +public: + explicit TextEditClickHandler(QObject *parent = nullptr); + ~TextEditClickHandler() override; + + QQuickItem *target() const; + void setTarget(QQuickItem *target); + Q_SIGNAL void targetChanged(QQuickItem *target); + + bool eventFilter(QObject *watched, QEvent *event) override; + +Q_SIGNALS: + void clicked(); + +private: + QPointer m_target; + QPointF m_pressPos{-1, -1}; +}; diff --git a/plasma/workspace/applets/notifications/thumbnailer.cpp b/plasma/workspace/applets/notifications/thumbnailer.cpp new file mode 100644 index 0000000000..131f7ab86f --- /dev/null +++ b/plasma/workspace/applets/notifications/thumbnailer.cpp @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "thumbnailer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +Thumbnailer::Thumbnailer(QObject *parent) + : QObject(parent) +{ +} + +Thumbnailer::~Thumbnailer() = default; + +void Thumbnailer::classBegin() +{ +} + +void Thumbnailer::componentComplete() +{ + m_inited = true; + generatePreview(); +} + +QUrl Thumbnailer::url() const +{ + return m_url; +} + +void Thumbnailer::setUrl(const QUrl &url) +{ + if (m_url != url) { + m_url = url; + Q_EMIT urlChanged(); + + generatePreview(); + } +} + +QSize Thumbnailer::size() const +{ + return m_size; +} + +void Thumbnailer::setSize(const QSize &size) +{ + if (m_size != size) { + m_size = size; + Q_EMIT sizeChanged(); + + generatePreview(); + } +} + +bool Thumbnailer::busy() const +{ + return m_busy; +} + +bool Thumbnailer::hasPreview() const +{ + return !m_pixmap.isNull(); +} + +QPixmap Thumbnailer::pixmap() const +{ + return m_pixmap; +} + +QSize Thumbnailer::pixmapSize() const +{ + return m_pixmap.size(); +} + +QString Thumbnailer::iconName() const +{ + return m_iconName; +} + +bool Thumbnailer::menuVisible() const +{ + return m_menuVisible; +} + +void Thumbnailer::generatePreview() +{ + if (!m_inited) { + return; + } + + if (!m_url.isValid() || !m_url.isLocalFile() || !m_size.isValid() || m_size.isEmpty()) { + return; + } + + auto maxSize = qMax(m_size.width(), m_size.height()); + + KConfigGroup previewSettings(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), "PreviewSettings"); + const QStringList enabledPlugins = previewSettings.readEntry("Plugins", KIO::PreviewJob::defaultPlugins()); + + KIO::PreviewJob *job = KIO::filePreview(KFileItemList({KFileItem(m_url)}), QSize(maxSize, maxSize), &enabledPlugins); + job->setScaleType(KIO::PreviewJob::Scaled); + job->setIgnoreMaximumSize(true); + + connect(job, &KIO::PreviewJob::gotPreview, this, [this](const KFileItem &item, const QPixmap &preview) { + Q_UNUSED(item); + m_pixmap = preview; + Q_EMIT pixmapChanged(); + + if (!m_iconName.isEmpty()) { + m_iconName.clear(); + Q_EMIT iconNameChanged(); + } + }); + + connect(job, &KIO::PreviewJob::failed, this, [this](const KFileItem &item) { + m_pixmap = QPixmap(); + Q_EMIT pixmapChanged(); + + const QString &iconName = item.determineMimeType().iconName(); + if (m_iconName != iconName) { + m_iconName = iconName; + Q_EMIT iconNameChanged(); + } + }); + + connect(job, &KJob::result, this, [this] { + m_busy = false; + Q_EMIT busyChanged(); + }); + + m_busy = true; + Q_EMIT busyChanged(); + + job->start(); +} diff --git a/plasma/workspace/applets/notifications/thumbnailer.h b/plasma/workspace/applets/notifications/thumbnailer.h new file mode 100644 index 0000000000..ff68ba0535 --- /dev/null +++ b/plasma/workspace/applets/notifications/thumbnailer.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +class Thumbnailer : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(QSize size READ size WRITE setSize NOTIFY sizeChanged) + + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(bool hasPreview READ hasPreview NOTIFY pixmapChanged) + Q_PROPERTY(QPixmap pixmap READ pixmap NOTIFY pixmapChanged) + Q_PROPERTY(QSize pixmapSize READ pixmapSize NOTIFY pixmapChanged) + + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged) + + Q_PROPERTY(bool menuVisible READ menuVisible NOTIFY menuVisibleChanged) + +public: + explicit Thumbnailer(QObject *parent = nullptr); + ~Thumbnailer() override; + + QUrl url() const; + void setUrl(const QUrl &url); + + QSize size() const; + void setSize(const QSize &size); + + bool busy() const; + bool hasPreview() const; + QPixmap pixmap() const; + QSize pixmapSize() const; + + QString iconName() const; + + bool menuVisible() const; + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void menuVisibleChanged(); + + void urlChanged(); + void sizeChanged(); + void busyChanged(); + void pixmapChanged(); + void iconNameChanged(); + +private: + void generatePreview(); + + bool m_inited = false; + + bool m_menuVisible = false; + + QUrl m_url; + QSize m_size; + + bool m_busy = false; + + QPixmap m_pixmap; + + QString m_iconName; +}; diff --git a/plasma/workspace/applets/panelspacer/CMakeLists.txt b/plasma/workspace/applets/panelspacer/CMakeLists.txt new file mode 100644 index 0000000000..41592cbcf3 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/CMakeLists.txt @@ -0,0 +1,5 @@ + +plasma_install_package(package org.kde.plasma.panelspacer) + +add_subdirectory(plugin) + diff --git a/plasma/workspace/applets/panelspacer/Messages.sh b/plasma/workspace/applets/panelspacer/Messages.sh new file mode 100644 index 0000000000..79d6c23162 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.panelspacer.pot diff --git a/plasma/workspace/applets/panelspacer/package/contents/config/main.xml b/plasma/workspace/applets/panelspacer/package/contents/config/main.xml new file mode 100644 index 0000000000..81feb80daa --- /dev/null +++ b/plasma/workspace/applets/panelspacer/package/contents/config/main.xml @@ -0,0 +1,19 @@ + + + + + + + + true + + + + -1 + + + + diff --git a/plasma/workspace/applets/panelspacer/package/contents/ui/main.qml b/plasma/workspace/applets/panelspacer/package/contents/ui/main.qml new file mode 100644 index 0000000000..d0d46d55f7 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/package/contents/ui/main.qml @@ -0,0 +1,144 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 +import org.kde.kirigami 2.10 as Kirigami + +Item { + id: root + + property bool horizontal: plasmoid.formFactor !== PlasmaCore.Types.Vertical + + Layout.fillWidth: plasmoid.configuration.expanding + Layout.fillHeight: plasmoid.configuration.expanding + + Layout.minimumWidth: plasmoid.nativeInterface.containment.editMode ? PlasmaCore.Units.gridUnit * 2 : 1 + Layout.minimumHeight: plasmoid.nativeInterface.containment.editMode ? PlasmaCore.Units.gridUnit * 2 : 1 + Layout.preferredWidth: horizontal + ? (plasmoid.configuration.expanding ? optimalSize : plasmoid.configuration.length) + : 0 + Layout.preferredHeight: horizontal + ? 0 + : (plasmoid.configuration.expanding ? optimalSize : plasmoid.configuration.length) + + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + + property int optimalSize: PlasmaCore.Units.largeSpacing + + function action_expanding() { + plasmoid.configuration.expanding = plasmoid.action("expanding").checked; + } + + // Search the actual gridLayout of the panel + property GridLayout panelLayout: { + var candidate = root.parent; + while (candidate) { + if (candidate instanceof GridLayout) { + return candidate; + } + candidate = candidate.parent; + } + } + + Component.onCompleted: { + plasmoid.setAction("expanding", i18n("Set flexible size")); + var action = plasmoid.action("expanding"); + action.checkable = true; + action.checked = Qt.binding(function() {return plasmoid.configuration.expanding}); + + plasmoid.removeAction("configure"); + } + + property real middleItemsSizeHint: { + if (!twinSpacer || !panelLayout || !leftTwin || !rightTwin) { + optimalSize = horizontal ? plasmoid.nativeInterface.containment.width : plasmoid.nativeInterface.containment.height; + return 0; + } + + var leftTwinParent = leftTwin.parent; + var rightTwinParent = rightTwin.parent; + if (!leftTwinParent || !rightTwinParent) { + return 0; + } + var firstSpacerFound = false; + var secondSpacerFound = false; + var leftItemsHint = 0; + var middleItemsHint = 0; + var rightItemsHint = 0; + + // Children order is guaranteed to be the same as the visual order of items in the layout + for (var i in panelLayout.children) { + var child = panelLayout.children[i]; + if (!child.visible) { + continue; + } else if (child == leftTwinParent) { + firstSpacerFound = true; + } else if (child == rightTwinParent) { + secondSpacerFound = true; + } else if (secondSpacerFound) { + if (root.horizontal) { + rightItemsHint += Math.min(child.Layout.maximumWidth, Math.max(child.Layout.minimumWidth, child.Layout.preferredWidth)) + panelLayout.rowSpacing; + } else { + rightItemsHint += Math.min(child.Layout.maximumWidth, Math.max(child.Layout.minimumHeight, child.Layout.preferredHeight)) + panelLayout.columnSpacing; + } + } else if (firstSpacerFound) { + if (root.horizontal) { + middleItemsHint += Math.min(child.Layout.maximumWidth, Math.max(child.Layout.minimumWidth, child.Layout.preferredWidth)) + panelLayout.rowSpacing; + } else { + middleItemsHint += Math.min(child.Layout.maximumWidth, Math.max(child.Layout.minimumHeight, child.Layout.preferredHeight)) + panelLayout.columnSpacing; + } + } else { + if (root.horizontal) { + leftItemsHint += Math.min(child.Layout.maximumWidth, Math.max(child.Layout.minimumWidth, child.Layout.preferredWidth)) + panelLayout.rowSpacing; + } else { + leftItemsHint += Math.min(child.Layout.maximumHeight, Math.max(child.Layout.minimumHeight, child.Layout.preferredHeight)) + panelLayout.columnSpacing; + } + } + } + + var halfContainment = root.horizontal ?plasmoid.nativeInterface.containment.width/2 : plasmoid.nativeInterface.containment.height/2; + + if (leftTwin == plasmoid) { + optimalSize = Math.max(PlasmaCore.Units.smallSpacing, halfContainment - middleItemsHint/2 - leftItemsHint) + } else { + optimalSize = Math.max(PlasmaCore.Units.smallSpacing, halfContainment - middleItemsHint/2 - rightItemsHint) + } + return middleItemsHint; + } + + readonly property Item twinSpacer: plasmoid.configuration.expanding && plasmoid.nativeInterface.twinSpacer && plasmoid.nativeInterface.twinSpacer.configuration.expanding ? plasmoid.nativeInterface.twinSpacer : null + readonly property Item leftTwin: { + if (!twinSpacer) { + return null; + } + + if (root.horizontal) { + return root.Kirigami.ScenePosition.x < twinSpacer.Kirigami.ScenePosition.x ? plasmoid : twinSpacer; + } else { + return root.Kirigami.ScenePosition.y < twinSpacer.Kirigami.ScenePosition.y ? plasmoid : twinSpacer; + } + } + readonly property Item rightTwin: { + if (!twinSpacer) { + return null; + } + + if (root.horizontal) { + return root.Kirigami.ScenePosition.x >= twinSpacer.Kirigami.ScenePosition.x ? plasmoid : twinSpacer; + } else { + return root.Kirigami.ScenePosition.y >= twinSpacer.Kirigami.ScenePosition.y ? plasmoid : twinSpacer; + } + } + + Rectangle { + anchors.fill: parent + color: PlasmaCore.Theme.highlightColor + visible: plasmoid.nativeInterface.containment.editMode + } +} diff --git a/plasma/workspace/applets/panelspacer/package/metadata.json b/plasma/workspace/applets/panelspacer/package/metadata.json new file mode 100644 index 0000000000..5d5c2cab69 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/package/metadata.json @@ -0,0 +1,154 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "plasma-devel@kde.org", + "Name": "The Plasma Team", + "Name[ar]": "فريق بلازما", + "Name[az]": "Plasma komandası", + "Name[ca]": "L'equip del Plasma", + "Name[cs]": "Team Plasma", + "Name[de]": "Das Plasma-Team", + "Name[en_GB]": "The Plasma Team", + "Name[es]": "El equipo de Plasma", + "Name[eu]": "Plasma taldea", + "Name[fi]": "Plasma-työryhmä", + "Name[fr]": "L'équipe de Plasma", + "Name[hu]": "A Plasma fejlesztői", + "Name[ia]": "Le equipa de Plasma", + "Name[it]": "La squadra di Plasma", + "Name[ko]": "Plasma 팀", + "Name[lt]": "Plasma komanda", + "Name[nl]": "Het team van Plasma", + "Name[nn]": "Utviklingslaget for Plasma", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਟੀਮ", + "Name[pl]": "Zespół Plazmy", + "Name[pt_BR]": "Temas do Plasma", + "Name[ro]": "Echipa Plasma", + "Name[ru]": "Команда разработчиков Plasma", + "Name[sk]": "Plasma Tím", + "Name[sl]": "Ekipa Plasme", + "Name[sv]": "Plasma-gruppen", + "Name[ta]": "பிளாஸ்மா குழு", + "Name[tr]": "Plazma Takımı", + "Name[uk]": "Команда розробників Плазми", + "Name[vi]": "Đội Plasma", + "Name[x-test]": "xxThe Plasma Teamxx", + "Name[zh_CN]": "Plasma 开发团队" + } + ], + "Category": "System Information", + "Description": "Reserve empty spaces within the panel.", + "Description[ar]": "يحجز مساحات فارغة في اللوحة.", + "Description[az]": "Paneldə boş yeri tutun.", + "Description[ca]": "Reserva espais buits en el plafó.", + "Description[cs]": "Rezervovat prázdné místo v panelu.", + "Description[de]": "Freien Platz in der Kontrollleiste reservieren.", + "Description[en_GB]": "Reserve empty spaces within the panel.", + "Description[es]": "Reservar espacios vacíos en el panel.", + "Description[eu]": "Gorde panel barruan leku hutsak.", + "Description[fi]": "Varaa paneeliin tyhjää tilaa.", + "Description[fr]": "Réserver des espaces vides dans le panneau.", + "Description[hu]": "Üres helyet választ le egy panelen belül.", + "Description[ia]": "Reserva spatios vacue intra le pannello.", + "Description[it]": "Lascia spazi vuoti nel pannello.", + "Description[ko]": "패널의 빈 공간을 채웁니다.", + "Description[lt]": "Rezervuoti skydelyje tuščią vietą.", + "Description[nl]": "Reserveer lege ruimte in het paneel.", + "Description[nn]": "Reserver tomme plassar i panelet.", + "Description[pa]": "ਪੈਨਲ ਵਿੱਚ ਖਾਲੀ ਥਾਂ ਬਦਲੋ।", + "Description[pl]": "Rezerwuje wolną przestrzeń na panelu.", + "Description[pt_BR]": "Reserva espaços vazios no painel.", + "Description[ro]": "Rezervează spații goale în cadrul panoului.", + "Description[ru]": "Занимает свободное место на панели", + "Description[sk]": "Rezervuje prázdne miesto v paneli.", + "Description[sl]": "Rezerviraj prazne prostorčke na plošči.", + "Description[sv]": "Reservera tomt utrymme inne i panelen.", + "Description[ta]": "பலகையில் காலியான இடைவெளிகளை இடும்", + "Description[tr]": "Panelde boş alanlar ayırın.", + "Description[uk]": "Створює прогалини на панелі.", + "Description[vi]": "Giữ các khoảng trống trong bảng.", + "Description[x-test]": "xxReserve empty spaces within the panel.xx", + "Description[zh_CN]": "在面板中预留空白。", + "EnabledByDefault": true, + "Id": "org.kde.plasma.panelspacer", + "License": "GPL-2.0+", + "Name": "Panel Spacer", + "Name[ar]": "مُباعِد لوحة", + "Name[az]": "Panel Ayırıcısı", + "Name[bg]": "Разделител за панела", + "Name[bs]": "Panelska razmaknica", + "Name[ca@valencia]": "Espaiador del plafó", + "Name[ca]": "Espaiador del plafó", + "Name[cs]": "Mezera v panelu", + "Name[da]": "Panel-afstandsstykke", + "Name[de]": "Abstandhalter", + "Name[el]": "Κενό πίνακα", + "Name[en_GB]": "Panel Spacer", + "Name[eo]": "Spacigilo de Panelo", + "Name[es]": "Espaciador", + "Name[et]": "Paneeliruumi korraldaja", + "Name[eu]": "Paneleko bereizlea", + "Name[fi]": "Paneelivälilevy", + "Name[fr]": "Espaceur de tableau de bord", + "Name[fy]": "Paniel spaasje", + "Name[ga]": "Scarthóir Painéil", + "Name[gl]": "Espazador do panel", + "Name[he]": "מרווח לוח", + "Name[hi]": "पैनल परिवर्तक", + "Name[hr]": "Razmak na traci", + "Name[hsb]": "Mjezota mjez panelemi", + "Name[hu]": "Panelelválasztó", + "Name[ia]": "Spatiator de pannello", + "Name[id]": "Panel Spacer", + "Name[is]": "Spjaldabil", + "Name[it]": "Spaziatore del pannello", + "Name[ja]": "パネルのスペーサー", + "Name[kk]": "Панель бос орын бөлгіші", + "Name[km]": "ចន្លោះ​បន្ទះ", + "Name[kn]": "ಫಲಕ ಸ್ಪೇಸರ್", + "Name[ko]": "패널 공백", + "Name[lt]": "Skydelio skirtukas", + "Name[lv]": "Paneļa atdalītājs", + "Name[mai]": "पटल स्पेसर", + "Name[mk]": "Разграничувач на панели", + "Name[ml]": "പാളിയില്‍ ഒഴിഞ്ഞസ്ഥലമിടാന്‍", + "Name[mr]": "पटल स्पेसर", + "Name[nb]": "Avstandsholder for panel", + "Name[nds]": "Paneel-Platzmaker", + "Name[nl]": "Paneelscheider", + "Name[nn]": "Panelmellomrom", + "Name[pa]": "ਪੈਨਲ ਸਪੇਸਰ", + "Name[pl]": "Odstęp w panelu", + "Name[pt]": "Espaço do Painel", + "Name[pt_BR]": "Espaçador do painel", + "Name[ro]": "Spațiator panou", + "Name[ru]": "Разделитель", + "Name[si]": "පැනල ඉඩතැබීම", + "Name[sk]": "Medzera v paneli", + "Name[sl]": "Praznina za na pult", + "Name[sr@ijekavian]": "панелска размакница", + "Name[sr@ijekavianlatin]": "panelska razmaknica", + "Name[sr@latin]": "panelska razmaknica", + "Name[sr]": "панелска размакница", + "Name[sv]": "Panelavskiljare", + "Name[ta]": "பலகைக்கான இடைவெளி", + "Name[th]": "ตัวสร้างพื้นที่ว่าง", + "Name[tr]": "Panel Ayırıcı", + "Name[ug]": "تاختا ئارىلىقى", + "Name[uk]": "Розпірка панелі", + "Name[vi]": "Khoảng trống bảng", + "Name[wa]": "Separateu e scriftôr", + "Name[x-test]": "xxPanel Spacerxx", + "Name[zh_CN]": "面板间隙", + "Name[zh_TW]": "面板間隔器", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "NoDisplay": true, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml" +} diff --git a/plasma/workspace/applets/panelspacer/plugin/CMakeLists.txt b/plasma/workspace/applets/panelspacer/plugin/CMakeLists.txt new file mode 100644 index 0000000000..77dce35511 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/plugin/CMakeLists.txt @@ -0,0 +1,7 @@ +kde_enable_exceptions() + +add_definitions(-DTRANSLATION_DOMAIN=\"panelspacer\") + +kcoreaddons_add_plugin(org.kde.plasma.panelspacer SOURCES panelspacer.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(org.kde.plasma.panelspacer Qt::Gui Qt::Core Qt::Qml Qt::Quick KF5::Plasma KF5::PlasmaQuick KF5::I18n) diff --git a/plasma/workspace/applets/panelspacer/plugin/panelspacer.cpp b/plasma/workspace/applets/panelspacer/plugin/panelspacer.cpp new file mode 100644 index 0000000000..0753815f40 --- /dev/null +++ b/plasma/workspace/applets/panelspacer/plugin/panelspacer.cpp @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2020 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "panelspacer.h" + +#include +#include +#include + +#include +#include +#include + +class SpacersTrackerSingleton +{ +public: + SpacersTracker self; +}; + +Q_GLOBAL_STATIC(SpacersTrackerSingleton, privateSpacersTrackerSelf) + +SpacersTracker::SpacersTracker(QObject *parent) + : QObject(parent) +{ +} + +SpacersTracker::~SpacersTracker() +{ +} + +SpacersTracker *SpacersTracker::self() +{ + return &privateSpacersTrackerSelf()->self; +} + +void SpacersTracker::insertSpacer(Plasma::Containment *containment, PanelSpacer *spacer) +{ + const bool wasTwin = m_spacers[containment].count() == 2; + m_spacers[containment] << (spacer); + const bool isTwin = m_spacers[containment].count() == 2; + + if (isTwin) { + auto *lay1 = m_spacers[containment].first(); + auto *lay2 = m_spacers[containment].last(); + lay1->setTwinSpacer(lay2->property("_plasma_graphicObject").value()); + lay2->setTwinSpacer(lay1->property("_plasma_graphicObject").value()); + } else if (wasTwin) { + for (auto *lay : m_spacers[containment]) { + lay->setTwinSpacer(nullptr); + } + } +} + +void SpacersTracker::removeSpacer(Plasma::Containment *containment, PanelSpacer *spacer) +{ + const bool wasTwin = m_spacers[containment].count() == 2; + m_spacers[containment].removeAll(spacer); + const bool isTwin = m_spacers[containment].count() == 2; + + if (isTwin) { + auto *lay1 = m_spacers[containment].first(); + auto *lay2 = m_spacers[containment].last(); + lay1->setTwinSpacer(lay2->property("_plasma_graphicObject").value()); + lay2->setTwinSpacer(lay1->property("_plasma_graphicObject").value()); + + } else if (wasTwin) { + for (auto *lay : m_spacers[containment]) { + lay->setTwinSpacer(nullptr); + } + } + + if (m_spacers[containment].isEmpty()) { + m_spacers.remove(containment); + } +} + +///////////////////////////////////////////////////////////////////// + +PanelSpacer::PanelSpacer(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ +} + +PanelSpacer::~PanelSpacer() +{ + SpacersTracker::self()->removeSpacer(containment(), this); +} + +void PanelSpacer::init() +{ +} + +void PanelSpacer::constraintsEvent(Plasma::Types::Constraints constraints) +{ + // At this point we're sure the AppletQuickItem has been created already + if (constraints & Plasma::Types::UiReadyConstraint) { + Q_ASSERT(containment()); + Q_ASSERT(containment()->corona()); + + SpacersTracker::self()->insertSpacer(containment(), this); + } + + Plasma::Applet::constraintsEvent(constraints); +} + +void PanelSpacer::setTwinSpacer(PlasmaQuick::AppletQuickItem *spacer) +{ + if (m_twinSpacer == spacer) { + return; + } + + m_twinSpacer = spacer; + Q_EMIT twinSpacerChanged(); +} + +PlasmaQuick::AppletQuickItem *PanelSpacer::twinSpacer() const +{ + return m_twinSpacer; +} + +PlasmaQuick::AppletQuickItem *PanelSpacer::containmentGraphicObject() const +{ + return containment()->property("_plasma_graphicObject").value(); +} + +K_PLUGIN_CLASS_WITH_JSON(PanelSpacer, "../package/metadata.json") + +#include "panelspacer.moc" diff --git a/plasma/workspace/applets/panelspacer/plugin/panelspacer.h b/plasma/workspace/applets/panelspacer/plugin/panelspacer.h new file mode 100644 index 0000000000..aff3aa0f9e --- /dev/null +++ b/plasma/workspace/applets/panelspacer/plugin/panelspacer.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2020 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace Plasma +{ +class Containment; +} + +class PanelSpacer; + +class SpacersTracker : public QObject +{ + Q_OBJECT + +public: + SpacersTracker(QObject *parent = nullptr); + ~SpacersTracker() override; + + static SpacersTracker *self(); + + void insertSpacer(Plasma::Containment *containment, PanelSpacer *spacer); + void removeSpacer(Plasma::Containment *containment, PanelSpacer *spacer); + +private: + QHash> m_spacers; +}; + +class PanelSpacer : public Plasma::Applet +{ + Q_OBJECT + Q_PROPERTY(PlasmaQuick::AppletQuickItem *twinSpacer READ twinSpacer NOTIFY twinSpacerChanged) + Q_PROPERTY(PlasmaQuick::AppletQuickItem *containment READ containmentGraphicObject CONSTANT) + +public: + PanelSpacer(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~PanelSpacer() override; + + void init() override; + void constraintsEvent(Plasma::Types::Constraints constraints) override; + + void setTwinSpacer(PlasmaQuick::AppletQuickItem *spacer); + PlasmaQuick::AppletQuickItem *twinSpacer() const; + + PlasmaQuick::AppletQuickItem *containmentGraphicObject() const; + +Q_SIGNALS: + void twinSpacerChanged(); + +private: + PlasmaQuick::AppletQuickItem *m_twinSpacer = nullptr; +}; diff --git a/plasma/workspace/applets/systemmonitor/CMakeLists.txt b/plasma/workspace/applets/systemmonitor/CMakeLists.txt new file mode 100644 index 0000000000..fdc4ae6498 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/CMakeLists.txt @@ -0,0 +1,10 @@ + +add_subdirectory(systemmonitor) + +# Systemmonitor presets +plasma_install_package(coreusage org.kde.plasma.systemmonitor.cpucore) +plasma_install_package(cpu org.kde.plasma.systemmonitor.cpu) +plasma_install_package(memory org.kde.plasma.systemmonitor.memory) +plasma_install_package(diskusage org.kde.plasma.systemmonitor.diskusage) +plasma_install_package(diskactivity org.kde.plasma.systemmonitor.diskactivity) +plasma_install_package(net org.kde.plasma.systemmonitor.net) diff --git a/plasma/workspace/applets/systemmonitor/coreusage/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/coreusage/contents/config/faceproperties new file mode 100644 index 0000000000..7961e52a36 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/coreusage/contents/config/faceproperties @@ -0,0 +1,10 @@ +[Config] +chartFace=org.kde.ksysguard.barchart +highPrioritySensorIds=["cpu/cpu.*/usage"] +totalSensors=["cpu/all/usage"] + +[FaceConfig] +rangeAuto=false +rangeFrom=0 +rangeTo=100 + diff --git a/plasma/workspace/applets/systemmonitor/coreusage/metadata.json b/plasma/workspace/applets/systemmonitor/coreusage/metadata.json new file mode 100644 index 0000000000..85390dca17 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/coreusage/metadata.json @@ -0,0 +1,127 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Category": "System Information", + "Description": "System monitor Widget that shows usage of individual CPU cores", + "Description[ar]": "أداة مراقبة النظام التي تعرض استخدام كل نواة من المعالج", + "Description[az]": "Sistem monitoru vidjeti hər CPU nüvəsinin istifadəsini göstərir", + "Description[ca]": "Giny del monitor del sistema que mostra l'ús individual dels nuclis de la CPU", + "Description[en_GB]": "System monitor Widget that shows usage of individual CPU cores", + "Description[es]": "Elemento gráfico del monitor del sistema que muestra el uso de los núcleos individuales de la CPU", + "Description[eu]": "PUZeko nukleoen banakako erabilera erakusten duen sistema gainbegiratzeko trepeta", + "Description[fi]": "Yksittäisten järjestelmäydinten käytön näyttävä järjestelmänvalvontasovelma", + "Description[fr]": "Composant graphique de surveillance du système, affichant l'utilisation de chaque cœur du processeur.", + "Description[hu]": "Rendszermonitor elem, amely az egyes logikai processzorok használatát mutatja", + "Description[ia]": "Widget de supervision de systema que monstra le uso del nucleos individual", + "Description[it]": "Oggetto di monitoraggio del sistema che mostra l'utilizzo dei singoli core del processore", + "Description[ko]": "개별 CPU 코어 사용량을 표시하는 시스템 모니터 위젯", + "Description[lt]": "Sistemos prižiūryklės valdiklis, kuris rodo atskirų procesoriaus branduolių naudojimą", + "Description[nl]": "Systeemmonitorwidget die gebruik toont van individuele CPU-kernen", + "Description[nn]": "System­overvaking som viser bruk av einskilde prosessor­kjernar", + "Description[pa]": "ਵੱਖ-ਵੱਖ CPU ਕੋਰਾਂ ਦੀ ਵਰਤੋਂ ਵਿਖਾਉਣ ਵਾਲਾ ਸਿਸਟਮ ਮਾਨੀਟਰ ਵਿਜੈੱਟ", + "Description[pl]": "Element interfejsu, który pokazuje wykorzystanie poszczególnych rdzeni procesora", + "Description[pt_BR]": "Widget monitor do sistema que mostra o uso individual dos núcleos da CPU", + "Description[ro]": "Control grafic de monitorizare a sistemului ce arată utilizarea nucleelor individuale ale procesorului", + "Description[ru]": "Мониторинг загрузки процессора, предоставляющий сведения об индивидуальном использовании ядер", + "Description[sk]": "Miniaplikácia monitorovania systému, ktorá zobrazuje využitie jednotlivých jadier", + "Description[sl]": "Gradnik sistemskega monitorja, ki prikazuje rabo posameznega jedra CPE", + "Description[sv]": "Grafisk systemövervakningskomponent som visar användning av individuella processorkärnor", + "Description[ta]": "தனிப்பட்ட CPU core-களின் பயன்பாட்டைக் காட்டும் பிளாஸ்மாய்ட்", + "Description[tr]": "Bireysel CPU çekirdeklerini gösteren sistem izleyicisi araç takımı", + "Description[uk]": "Віджет нагляду за системою, який показує використання окремих ядер процесора", + "Description[vi]": "Phụ kiện giám sát hệ thống cho biết lượng dùng các lõi CPU riêng lẻ", + "Description[x-test]": "xxSystem monitor Widget that shows usage of individual CPU coresxx", + "Description[zh_CN]": "显示单个 CPU 核心使用情况的系统监视部件", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "ksysguardd", + "Id": "org.kde.plasma.systemmonitor.cpucore", + "License": "GPL-2.0+", + "Name": "Individual Core Usage", + "Name[ar]": "استخدام النوى الفردية", + "Name[az]": "MP nüvəsinin yükü", + "Name[ca]": "Ús individual del nucli", + "Name[da]": "Brug af hver kerne", + "Name[de]": "Verwendung einzelner Kerne", + "Name[en_GB]": "Individual Core Usage", + "Name[es]": "Uso de núcleos individuales", + "Name[et]": "Iga tuuma kasutus", + "Name[eu]": "Nukleoen banakako erabilera", + "Name[fi]": "Yksittäisten ydinten käyttö", + "Name[fr]": "Utilisation de chaque cœur ", + "Name[hi]": "व्यक्तिगत कोर उपयोग ", + "Name[hu]": "Logikai processzorok használata", + "Name[ia]": "Uso de nucleo (core) individual", + "Name[id]": "Penggunaan Core Individual", + "Name[it]": "Utilizzo del singolo core", + "Name[ko]": "개별 코어 사용량", + "Name[lt]": "Atskirų branduolių naudojimas", + "Name[ml]": "വ്യക്തിഗത കോർ ഉപയോഗം", + "Name[nl]": "Individueel gebruik van kernen", + "Name[nn]": "Einskildkjerne-bruk", + "Name[pa]": "ਵੱਖੋ-ਵੱਖ ਕੋਰ ਵਰਤੋਂ", + "Name[pl]": "Wykorzystanie na rdzeń", + "Name[pt]": "Carga dos Núcleos Individuais", + "Name[pt_BR]": "Uso individual do núcleo", + "Name[ro]": "Utilizare individuală nuclee", + "Name[ru]": "Загрузка ядер ЦП", + "Name[sk]": "Využitie jednotlivých jadier", + "Name[sl]": "Poraba posameznega jedra", + "Name[sv]": "Användning av individuella kärnor", + "Name[ta]": "தனிப்பட்ட கணிப்பி பயன்பாட்டு", + "Name[tr]": "Bireysel Çekirdek Kullanımı", + "Name[uk]": "Використання окремих ядер", + "Name[vi]": "Lượng dùng mỗi lõi riêng lẻ", + "Name[x-test]": "xxIndividual Core Usagexx", + "Name[zh_CN]": "单个核心使用率", + "Name[zh_TW]": "獨立核心用量", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor" +} diff --git a/plasma/workspace/applets/systemmonitor/cpu/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/cpu/contents/config/faceproperties new file mode 100644 index 0000000000..47581ef31c --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/cpu/contents/config/faceproperties @@ -0,0 +1,11 @@ +[Config] +chartFace=org.kde.ksysguard.piechart +highPrioritySensorIds=["cpu/all/usage"] +totalSensors=["cpu/all/usage"] +lowPrioritySensorIds=["cpu/all/cpuCount","cpu/all/coreCount"] + +[FaceConfig] +rangeAuto=false +rangeFrom=0 +rangeTo=100 + diff --git a/plasma/workspace/applets/systemmonitor/cpu/metadata.json b/plasma/workspace/applets/systemmonitor/cpu/metadata.json new file mode 100644 index 0000000000..3dd1b931e5 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/cpu/metadata.json @@ -0,0 +1,127 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Category": "System Information", + "Description": "System monitor Widget that shows the total CPU usage", + "Description[ar]": "أداة مراقبة النظام التي تعرض مجموع استخدام المعالج", + "Description[az]": "Sistem monitoru vidjeti ümumi CPU istifadəsini göstərir", + "Description[ca]": "Giny del monitor del sistema que mostra l'ús total de la CPU", + "Description[en_GB]": "System monitor Widget that shows the total CPU usage", + "Description[es]": "Elemento gráfico del monitor del sistema que muestra el uso total de CPU", + "Description[eu]": "PUZ erabileraren guztizkoa erakusten duen sistema gainbegiratzeko trepeta", + "Description[fi]": "Suorittimen kokonaiskäytön näyttävä järjestelmänvalvontasovelma", + "Description[fr]": "Composant graphique de surveillance du système, affichant l'utilisation globale du processeur.", + "Description[hu]": "Rendszermonitor elem, amely a teljes processzorhasználatot mutatja", + "Description[ia]": "Widget de supervision de systema que monstra le uso total de CPU", + "Description[it]": "Oggetto di monitoraggio del sistema che mostra l'utilizzo totale del processore", + "Description[ko]": "총 CPU 사용량을 표시하는 시스템 모니터 위젯", + "Description[lt]": "Sistemos prižiūryklės valdiklis, kuris rodo bendrą procesoriaus naudojimą", + "Description[nl]": "Systeemmonitorwidget die het totale gebruik toont de CPU toont", + "Description[nn]": "System­overvaking som viser total prosessorlast", + "Description[pa]": "ਕੁੱਲ CPU ਵਰਤੋਂ ਵਿਖਾਉਣ ਵਾਲਾ ਸਿਸਟਮ ਮਾਨੀਟਰ ਵਿਜੈੱਟ", + "Description[pl]": "Element interfejsu, który pokazuje całkowite wykorzystanie procesora", + "Description[pt_BR]": "Widget monitor do sistema que mostra o uso total da CPU", + "Description[ro]": "Control grafic de monitorizare a sistemului ce arată utilizarea totală a procesorului", + "Description[ru]": "Мониторинг общей загрузки процессора", + "Description[sk]": "Miniaplikácia monitorovania systému, ktorá zobrazuje celkové využitie CPU", + "Description[sl]": "Gradnik sistemskega monitorja, ki prikazuje celotno rabo CPE", + "Description[sv]": "Grafisk systemövervakningskomponent som visar total processoranvändning", + "Description[ta]": "மொத்த CPU பயன்பாட்டைக் காட்டும் பிளாஸ்மாய்ட்", + "Description[tr]": "Toplam CPU kullanımını gösteren sistem izleyicisi araç takımı", + "Description[uk]": "Віджет нагляду за системою, який показує загальне використання процесора", + "Description[vi]": "Phụ kiện giám sát hệ thống cho biết tổng lượng dùng CPU", + "Description[x-test]": "xxSystem monitor Widget that shows the total CPU usagexx", + "Description[zh_CN]": "显示总 CPU 使用情况的系统监视部件", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "cpu", + "Id": "org.kde.plasma.systemmonitor.cpu", + "License": "GPL-2.0+", + "Name": "Total CPU Use", + "Name[ar]": "مجموع استخدام المعالج", + "Name[az]": "Ümumi MP istifadəsi", + "Name[ca]": "Ús total de CPU", + "Name[cs]": "Celkové použití CPU", + "Name[da]": "Samlet CPU-brug", + "Name[de]": "Verwendung gesamter Prozessor", + "Name[en_GB]": "Total CPU Use", + "Name[es]": "Uso total de CPU", + "Name[et]": "Protsessori kogukasutus", + "Name[eu]": "PUZ erabileraren guztizkoa", + "Name[fi]": "Suorittimen kokonaiskäyttö", + "Name[fr]": "Utilisation globale du processeur", + "Name[hi]": "कुल सीपीयू उपयोग", + "Name[hu]": "Teljes processzorhasználat", + "Name[ia]": "Uso total de CPU", + "Name[it]": "Utilizzo totale CPU", + "Name[ko]": "총 CPU 사용량", + "Name[lt]": "Bendras procesoriaus naudojimas", + "Name[ml]": "ആകെ സിപിയു ഉപയോഗം", + "Name[nl]": "Totaal CPU-gebruik", + "Name[nn]": "Total prosessorlast", + "Name[pa]": "ਕੁੱਲ CPU ਵਰਤੋ", + "Name[pl]": "Całkowite użycie procesora", + "Name[pt]": "Carga Total do CPU", + "Name[pt_BR]": "Uso total da CPU", + "Name[ro]": "Utilizare totală procesor", + "Name[ru]": "Общая загрузка ЦП", + "Name[sk]": "Celkové využitie CPU", + "Name[sl]": "Celotna raba CPE", + "Name[sv]": "Total processoranvändning", + "Name[ta]": "மொத்த CPU பயன்பாட்டு", + "Name[tr]": "Toplam CPU kullanımı", + "Name[uk]": "Загальне використання процесора", + "Name[vi]": "Tổng lượng dùng CPU", + "Name[x-test]": "xxTotal CPU Usexx", + "Name[zh_CN]": "总 CPU 使用情况", + "Name[zh_TW]": "總 CPU 用量", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor" +} diff --git a/plasma/workspace/applets/systemmonitor/diskactivity/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/diskactivity/contents/config/faceproperties new file mode 100644 index 0000000000..694dc4f4c6 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/diskactivity/contents/config/faceproperties @@ -0,0 +1,8 @@ +[Config] +chartFace=org.kde.ksysguard.linechart +highPrioritySensorIds=["disk/all/write","disk/all/read"] + +[FaceConfig] +rangeAuto=true +lineChartStacked=true + diff --git a/plasma/workspace/applets/systemmonitor/diskactivity/metadata.json b/plasma/workspace/applets/systemmonitor/diskactivity/metadata.json new file mode 100644 index 0000000000..b0fb02f3d7 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/diskactivity/metadata.json @@ -0,0 +1,134 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "System Information", + "Description": "An applet that monitors hard disk throughput and input/output", + "Description[ar]": "بُريْمج يراقب استخدام إنتاجيّة القرص الصلب ودَخْله/خَرْجه", + "Description[az]": "Sərt Diskdə verilənlərin ötürmələsini və giriş/çıxışını izləyən tətbiq", + "Description[ca]": "Una miniaplicació que controla la velocitat de transferència, així com l'entrada/sortida de les dades al disc dur", + "Description[cs]": "Aplet, jenž monitoruje propustnost disku a vstup/výstup", + "Description[de]": "Ein Miniprogramm, das den Festplattendurchsatz und die Festplattenein- und -ausgabe überwacht", + "Description[en_GB]": "An applet that monitors hard disk throughput and input/output", + "Description[es]": "Una miniaplicación que monitoriza el rendimiento y la entrada/salida del disco duro", + "Description[eu]": "Disko zurrunaren transferentzia tasa eraginkorra («throughput») eta sarrera/irteera gainbegiratzen dituen aplikaziotxo bat", + "Description[fi]": "Tarkkailee levyjen suoritustehoa ja siirtomäärää", + "Description[fr]": "Une applet surveillant le débit des disques durs et leurs entrées / sorties.", + "Description[hu]": "Egy kisalkalmazás, amely figyeli a merevlemez átvitelét és a bemenetet/kimenetet", + "Description[ia]": "Un applet que que monitora le prestation (throughput) e ingresso/egresso de disco dur", + "Description[it]": "Un'applet che controlla le prestazioni e l'uso del disco fisso", + "Description[ko]": "하드디스크 대역폭 및 I/O 상태를 보여 주는 애플릿", + "Description[lt]": "Programėlė, kuri stebi disko apkrovą ir įvedimą/išvedimą", + "Description[nl]": "Een applet die de activiteit van de harde schijf volgt", + "Description[nn]": "Skjermelement som overvaker dataflyt til og frå harddisk", + "Description[pa]": "ਹਾਰਡ ਡਿਸਕ ਥਰੂਪੁੱਟ ਅਤੇ ਇੰਪੁੱਟ/ਆਉਟਪੁੱਟ ਦੀ ਨਿਗਰਾਨੀ ਲਈ ਐਪਲਿਟ", + "Description[pl]": "Monitoruje przepustowość WE/WY dysku twardego", + "Description[pt_BR]": "Monitora a taxa de transferência e entrada/saída do disco rígido", + "Description[ro]": "Miniaplicație ce monitorizează traficul de intrare și cel de ieșire pentru discul dur", + "Description[ru]": "Мониторинг пропускной способности и процессов ввода/вывода жёстких дисков", + "Description[sk]": "Applet, ktorý monitoruje priepustnosť pevného disku a vstup/výstup", + "Description[sl]": "Programček, ki nadzoruje prepustnost in vhod/izhod trdega diska", + "Description[sv]": "Ett miniprogram som övervakar hårddiskprestanda samt in- och utmatning", + "Description[ta]": "வட்டின் உள்ளீடு/வெளியீடு மற்றும் செயல் வீதம் ஆகியவற்றை கண்காணிக்கும் பிளாஸ்மாய்ட்", + "Description[tr]": "Sabit disk girdisini/çıktısını izleyen bir uygulamacık", + "Description[uk]": "Аплет, який стежить за даними, які записуються на жорсткий диск та читаються з жорсткого диска", + "Description[vi]": "Một tiểu ứng dụng giám sát vào/ra và thông lượng của đĩa cứng", + "Description[x-test]": "xxAn applet that monitors hard disk throughput and input/outputxx", + "Description[zh_CN]": "监视硬盘吞吐量和输入输出情况的小程序", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "drive-harddisk", + "Id": "org.kde.plasma.systemmonitor.diskactivity", + "License": "GPL-2.0+", + "Name": "Hard Disk Activity", + "Name[ar]": "نشاط القرص الصلب", + "Name[az]": "Sərt Disk istifadəsi", + "Name[ca]": "Activitat del disc dur", + "Name[cs]": "Aktivita pevného disku", + "Name[da]": "Harddisk-aktivitet", + "Name[de]": "Festplattenaktivität", + "Name[en_GB]": "Hard Disk Activity", + "Name[es]": "Actividad del disco duro", + "Name[et]": "Kõvaketta tegevus", + "Name[eu]": "Disko zurruneko jarduera", + "Name[fi]": "Kiintolevyn toiminta", + "Name[fr]": "Activité des disques durs", + "Name[hi]": "हार्ड डिस्क गतिविधि", + "Name[hsb]": "Monitor, kiž pokazuje aktiwitu kruteje tačele", + "Name[hu]": "Merevlemez-aktivitás", + "Name[ia]": "Activitate del disco dur", + "Name[id]": "Aktivitas Hard Disk", + "Name[it]": "Attività disco fisso", + "Name[ko]": "하드디스크 활동", + "Name[lt]": "Standžiojo disko veikla", + "Name[ml]": "ഹാർഡ് ഡിസ്ക് പ്രവർത്തനം", + "Name[nl]": "Activiteit van de vaste schijf", + "Name[nn]": "Harddiskaktivitet", + "Name[pa]": "ਹਾਰਡ ਡਿਸਕ ਸਰਗਰਮੀ", + "Name[pl]": "Ruch na dysku twardym", + "Name[pt]": "Actividade do Disco Rígido", + "Name[pt_BR]": "Atividade do disco rígido", + "Name[ro]": "Activitate disc dur", + "Name[ru]": "Загрузка дисковой подсистемы", + "Name[sk]": "Aktivita pevného disku", + "Name[sl]": "Nadzornik aktivnosti trdega diska", + "Name[sv]": "Hårddiskaktivitet", + "Name[ta]": "வட்டு செயல்பாட்டு", + "Name[tg]": "Фаъолияти диски компютерӣ", + "Name[tr]": "Sabit Disk Etkinliği", + "Name[uk]": "Робота із жорстким диском", + "Name[vi]": "Hoạt động của đĩa cứng", + "Name[x-test]": "xxHard Disk Activityxx", + "Name[zh_CN]": "磁盘活动", + "Name[zh_TW]": "硬碟活動", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor", + "X-Plasma-StandAloneApp": true +} diff --git a/plasma/workspace/applets/systemmonitor/diskusage/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/diskusage/contents/config/faceproperties new file mode 100644 index 0000000000..2c60a809bf --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/diskusage/contents/config/faceproperties @@ -0,0 +1,11 @@ +[Config] +chartFace=org.kde.ksysguard.horizontalbars +highPrioritySensorIds=["disk/.*/usedPercent"] +totalSensors=["disk/all/usedPercent"] +lowPrioritySensorIds=["disk/all/total"] + +[FaceConfig] +rangeAuto=true +rangeFrom=0 +rangeTo=100 + diff --git a/plasma/workspace/applets/systemmonitor/diskusage/metadata.json b/plasma/workspace/applets/systemmonitor/diskusage/metadata.json new file mode 100644 index 0000000000..12e180ab4e --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/diskusage/metadata.json @@ -0,0 +1,130 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "System Information", + "Description": "System monitor Widget that shows the usage of the root partition", + "Description[ar]": "أداة مراقبة النظام التي تعرض استخدام قسم الجذر", + "Description[az]": "Sistem monitoru vidjeti kök bölməsinin istifadəsini göstərir", + "Description[ca]": "Giny del monitor del sistema que mostra l'ús de la partició arrel", + "Description[en_GB]": "System monitor Widget that shows the usage of the root partition", + "Description[es]": "Elemento gráfico del monitor del sistema que muestra el uso de la partición raíz", + "Description[eu]": "Erro partizioaren erabilera erakusten duen sistema gainbegiratzeko trepeta", + "Description[fi]": "Juuriosion käyttöasteen näyttävä järjestelmänvalvontasovelma", + "Description[fr]": "Composant graphique de surveillance du système, affichant l'utilisation de la partition « Système ».", + "Description[hu]": "Rendszermonitor elem, amely a root partíció használatát mutatja", + "Description[ia]": "Widget de supervision de systema que monstra le uso del partition de root (radice)", + "Description[it]": "Oggetto di monitoraggio del sistema che mostra l'utilizzo della partizione radice", + "Description[ko]": "루트 파티션 사용량을 표시하는 시스템 모니터 위젯", + "Description[lt]": "Sistemos prižiūryklės valdiklis, kuris rodo šaknies skaidinio naudojimą", + "Description[nl]": "Systeemmonitorwidget die het totale gebruik toont van de root-partitie", + "Description[nn]": "System­overvaking som viser bruk av rotpartisjonen", + "Description[pl]": "Element interfejsu, który pokazuje wykorzystanie głównej partycji", + "Description[pt_BR]": "Widget monitor do sistema que mostra o uso da partição raiz", + "Description[ro]": "Control grafic de monitorizare a sistemului ce arată utilizarea partiției root", + "Description[ru]": "Мониторинг использования дискового пространства корневым разделом", + "Description[sk]": "Miniaplikácia monitorovania systému, ktorá zobrazuje využitie koreňovej partície", + "Description[sl]": "Gradnik sistemskega monitorja, ki prikazuje rabo korenske particije", + "Description[sv]": "Grafisk systemövervakningskomponent som visar användning av rotpartitionen", + "Description[ta]": "ரூட் வகிர்வின் பயன்பாட்டைக் காட்டும் பிளாஸ்மாய்ட்", + "Description[tr]": "Kök bölüm kullanımını gösteren sistem izleyicisi araç takımı", + "Description[uk]": "Віджет нагляду за системою, який показує використання кореневого розділу", + "Description[vi]": "Phụ kiện giám sát hệ thống cho biết lượng dùng của phân vùng gốc", + "Description[x-test]": "xxSystem monitor Widget that shows the usage of the root partitionxx", + "Description[zh_CN]": "显示根分区使用情况的系统监视部件", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "cpu", + "Id": "org.kde.plasma.systemmonitor.diskusage", + "License": "GPL-2.0+", + "Name": "Disk Usage", + "Name[ar]": "استخدام القرص", + "Name[az]": "Disk sahəsnin İstifadəsi", + "Name[ca]": "Ús del disc", + "Name[cs]": "Zaplnění Disku", + "Name[da]": "Diskforbrug", + "Name[de]": "Datenträgerbelegung", + "Name[en_GB]": "Disk Usage", + "Name[es]": "Uso del disco duro", + "Name[et]": "Kettakasutus", + "Name[eu]": "Diskoaren erabilera", + "Name[fi]": "Levyn käyttö", + "Name[fr]": "Utilisation des disques", + "Name[hi]": "डिस्क उपयोग", + "Name[hsb]": "Wužiwanje tačele", + "Name[hu]": "Lemezhasználat", + "Name[ia]": "Uso de disco", + "Name[id]": "Penggunaan Disk", + "Name[it]": "Utilizzo disco", + "Name[ko]": "디스크 사용량", + "Name[lt]": "Disko naudojimas", + "Name[ml]": "ഡ‍ിസ്ക് ഉപയോഗം", + "Name[nl]": "Schijfgebruik", + "Name[nn]": "Diskbruk", + "Name[pa]": "ਡਿਸਕ ਥਾਂ ਦੀ ਵਰਤੋਂ", + "Name[pl]": "Wykorzystanie dysku", + "Name[pt]": "Utilização do Disco", + "Name[pt_BR]": "Uso do disco", + "Name[ro]": "Utilizare disc", + "Name[ru]": "Использование дискового пространства", + "Name[sk]": "Využitie disku", + "Name[sl]": "Uporaba diska", + "Name[sv]": "Diskanvändning", + "Name[ta]": "வட்டு பயன்பாட்டு", + "Name[tg]": "Истифодабарии диск", + "Name[tr]": "Disk Kullanımı", + "Name[uk]": "Використання диска", + "Name[vi]": "Lượng dùng đĩa", + "Name[x-test]": "xxDisk Usagexx", + "Name[zh_CN]": "磁盘使用情况", + "Name[zh_TW]": "磁碟用量", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor" +} diff --git a/plasma/workspace/applets/systemmonitor/memory/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/memory/contents/config/faceproperties new file mode 100644 index 0000000000..3bf41bb8ce --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/memory/contents/config/faceproperties @@ -0,0 +1,11 @@ +[Config] +chartFace=org.kde.ksysguard.piechart +highPrioritySensorIds=["memory/physical/used"] +totalSensors=["memory/physical/usedPercent"] +lowPrioritySensorIds=["memory/physical/total"] + +[FaceConfig] +rangeAuto=true +rangeFrom=0 +rangeTo=100 + diff --git a/plasma/workspace/applets/systemmonitor/memory/metadata.json b/plasma/workspace/applets/systemmonitor/memory/metadata.json new file mode 100644 index 0000000000..b8bdcdd837 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/memory/metadata.json @@ -0,0 +1,130 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Category": "System Information", + "Description": "System monitor Widget that shows physical memory usage", + "Description[ar]": "أداة مراقبة النظام التي تعرض استخدام الذاكرة الفعلية", + "Description[az]": "Sistem monitoru vidjeti fiziki yaddaşın istifadəsini göstərir", + "Description[ca]": "Giny del monitor del sistema que mostra l'ús de la memòria física", + "Description[en_GB]": "System monitor Widget that shows physical memory usage", + "Description[es]": "Elemento gráfico del monitor del sistema que muestra el uso de la memoria física", + "Description[eu]": "Memoria fisikoaren erabilera erakusten duen sistema gainbegiratzeko trepeta", + "Description[fi]": "Fyysisen muistin käytön näyttävä järjestelmänvalvontasovelma", + "Description[fr]": "Composant graphique de surveillance du système, affichant l'utilisation de la mémoire physique.", + "Description[hu]": "A fizikai memória használati statisztikáit megjelenítő elem", + "Description[ia]": "Widget de supervision de systema que monstra le uso de memoria physic", + "Description[it]": "Oggetto di monitoraggio del sistema che mostra l'utilizzo della memoria fisica", + "Description[ko]": "물리적 메모리 사용량을 표시하는 시스템 모니터 위젯", + "Description[lt]": "Sistemos prižiūryklės valdiklis, kuris rodo fizinės atminties naudojimą", + "Description[nl]": "Systeemmonitorwidget die het gebruik van fysiek geheugen toont", + "Description[nn]": "System­overvaking som viser bruk av fysisk minne", + "Description[pl]": "Element interfejsu, który pokazuje wykorzystanie pamięci fizycznej", + "Description[pt_BR]": "Widget monitor do sistema que mostra o uso da memória física", + "Description[ro]": "Control grafic de monitorizare a sistemului ce arată utilizarea memoriei fizice", + "Description[ru]": "Мониторинг использования физической памяти", + "Description[sk]": "Miniaplikácia monitorovania systému, ktorá zobrazuje využitie fyzickej pamäte", + "Description[sl]": "Gradnik sistemskega monitorja, ki prikazuje rabo fizičnega pomnilnika", + "Description[sv]": "Grafisk systemövervakningskomponent som fysisk minnesanvändning", + "Description[ta]": "நினைவின் பயன்பாட்டைக் காட்டும் பிளாஸ்மாய்ட்", + "Description[tr]": "Fiziksel bellek kullanımını gösteren sistem izleyicisi araç takımı", + "Description[uk]": "Віджет нагляду за системою, який показує використання фізичної пам'яті", + "Description[vi]": "Phụ kiện giám sát hệ thống cho biết lượng dùng bộ nhớ vật lí", + "Description[x-test]": "xxSystem monitor Widget that shows physical memory usagexx", + "Description[zh_CN]": "显示物理内存使用情况的系统监视部件", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "ksysguardd", + "Id": "org.kde.plasma.systemmonitor.memory", + "License": "GPL-2.0+", + "Name": "Memory Usage", + "Name[ar]": "استخدام الذاكرة", + "Name[ast]": "Usu de la memoria", + "Name[az]": "Yaddaş İstifadəsi", + "Name[ca]": "Ús de memòria", + "Name[cs]": "Spotřeba paměti", + "Name[da]": "Hukommelsesforbrug", + "Name[de]": "Speicherbelegung", + "Name[en_GB]": "Memory Usage", + "Name[es]": "Uso de memoria", + "Name[et]": "Mälukasutus", + "Name[eu]": "Memoria erabilera", + "Name[fi]": "Muistin käyttö", + "Name[fr]": "Utilisation de la mémoire", + "Name[hi]": "मेमोरी उपयोग", + "Name[hsb]": "Wužiwanje pomjatka", + "Name[hu]": "Memóriahasználat", + "Name[ia]": "Uso de Memoria", + "Name[id]": "Penggunaan Memori", + "Name[it]": "Utilizzo memoria", + "Name[ko]": "메모리 사용량", + "Name[lt]": "Atminties naudojimas", + "Name[ml]": "മെമ്മറി ഉപയോഗം", + "Name[nl]": "Geheugengebruik", + "Name[nn]": "Minnebruk", + "Name[pa]": "ਮੈਮੋਰੀ ਦੀ ਵਰਤੋਂ", + "Name[pl]": "Wykorzystanie pamięci", + "Name[pt]": "Utilização da Memória", + "Name[pt_BR]": "Uso da memória", + "Name[ro]": "Utilizare memorie", + "Name[ru]": "Использование памяти", + "Name[sk]": "Využitie pamäte", + "Name[sl]": "Raba pomnilnika", + "Name[sv]": "Minnesanvändning", + "Name[ta]": "நினைவு பயன்பாட்டு", + "Name[tg]": "Истифодабарии ҳофиза", + "Name[tr]": "Bellek Kullanımı", + "Name[uk]": "Використання пам'яті", + "Name[vi]": "Lượng dùng bộ nhớ", + "Name[x-test]": "xxMemory Usagexx", + "Name[zh_CN]": "内存使用情况", + "Name[zh_TW]": "記憶體用量", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor" +} diff --git a/plasma/workspace/applets/systemmonitor/net/contents/config/faceproperties b/plasma/workspace/applets/systemmonitor/net/contents/config/faceproperties new file mode 100644 index 0000000000..363130e8e5 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/net/contents/config/faceproperties @@ -0,0 +1,7 @@ +[Config] +chartFace=org.kde.ksysguard.linechart +highPrioritySensorIds=["network/all/download","network/all/upload"] + +[FaceConfig] +lineChartSmooth=true + diff --git a/plasma/workspace/applets/systemmonitor/net/metadata.json b/plasma/workspace/applets/systemmonitor/net/metadata.json new file mode 100644 index 0000000000..95b8901d13 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/net/metadata.json @@ -0,0 +1,130 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "System Information", + "Description": "System monitor Widget that shows the download and upload data rate", + "Description[ar]": "أداة مراقبة النظام التي تعرض سرعة تنزيل ورفع البيانات", + "Description[az]": "Sistem monitporu vidjeti verilənlərin endirilməsi və yüklənməsi tezliyini göstərir", + "Description[ca]": "Giny del monitor del sistema que mostra la velocitat de baixada i pujada de les dades", + "Description[en_GB]": "System monitor Widget that shows the download and upload data rate", + "Description[es]": "Elemento gráfico del monitor del sistema que muestra las velocidades de subida y bajada de datos", + "Description[eu]": "Zama-jaisteko eta -igotzeko datuen emaria erakusten duen sistema gainbegiratzeko trepeta", + "Description[fi]": "Lataus- ja lähetysnopeuden näyttävä järjestelmänvalvontasovelma", + "Description[fr]": "Composant graphique de surveillance du système, affichant le débit pour les échanges montant /descendant de données.", + "Description[hu]": "A le- és feltöltési sebességet megjelenítő elem", + "Description[ia]": "Widget de supervision de systema que monstra le velocitate de discargar e incargar datos", + "Description[it]": "Oggetto di monitoraggio del sistema che mostra la velocità di trasferimento e ricezione dei dati", + "Description[ko]": "데이터 다운로드 및 업로드 전송률을 표시하는 시스템 모니터 위젯", + "Description[lt]": "Sistemos prižiūryklės valdiklis, kuris rodo duomenų atsiuntimo ir išsiuntimo spartą", + "Description[nl]": "Systeemmonitorwidget die gegevenssnelheid van down- en uploaden toont", + "Description[nn]": "System­overvaking som viser ned- og opplastingsratar", + "Description[pl]": "Element interfejsu, który pokazuje szybkość wysyłania i pobierania danych", + "Description[pt_BR]": "Widget monitor do sistema que mostra a taxa de download e upload", + "Description[ro]": "Control grafic de monitorizare a sistemului ce arată rata de încărcare și descărcare a datelor", + "Description[ru]": "Мониторинг скорости получения и передачи данных по сети", + "Description[sk]": "Miniaplikácia monitorovania systému, ktorá zobrazuje množstvo sťahovaných a odosielaných dát", + "Description[sl]": "Gradnik sistemskega monitorja, ki prikazuje podatke prenosa podatkov navznoter in navzven", + "Description[sv]": "Grafisk systemövervakningskomponent som visar nerladdnings- och uppladdningshastighet för data", + "Description[ta]": "பதிவிறக்க மற்றும் பதிவேற்ற வீதத்தைக் காட்டும் பிளாஸ்மாய்ட்", + "Description[tr]": "İndirme ve yükleme veri hızını gösteren sistem izleyicisi araç takımı", + "Description[uk]": "Віджет нагляду за системою, який показує швидкість отримання та вивантаження даних", + "Description[vi]": "Phụ kiện giám sát hệ thống cho biết tốc độ tải dữ liệu về và lên", + "Description[x-test]": "xxSystem monitor Widget that shows the download and upload data ratexx", + "Description[zh_CN]": "显示下载和上传速率的系统监视部件", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "network-workgroup", + "Id": "org.kde.plasma.systemmonitor.net", + "License": "GPL-2.0+", + "Name": "Network speed", + "Name[ar]": "سرعة الشبكة", + "Name[az]": "Şəbəkə sürəti", + "Name[ca]": "Velocitat de xarxa", + "Name[cs]": "Rychlost sítě", + "Name[da]": "Netværkshastighed", + "Name[de]": "Netzwerkgeschwindigkeit", + "Name[en_GB]": "Network speed", + "Name[es]": "Velocidad de la red", + "Name[et]": "Võrgukiirus", + "Name[eu]": "Sareko abiadura", + "Name[fi]": "Verkon nopeus", + "Name[fr]": "Débit du réseau", + "Name[hi]": "नेटवर्क की गती", + "Name[hsb]": "Spěšnosć syće", + "Name[hu]": "Hálózati sebesség", + "Name[ia]": "Velocitate de Rete", + "Name[id]": "Kecepatan Jaringan", + "Name[it]": "Velocità di rete", + "Name[ko]": "네트워크 속도", + "Name[lt]": "Tinklo greitis", + "Name[ml]": "നെറ്റ്‍വർക്ക് വേഗത", + "Name[nl]": "Netwerksnelheid", + "Name[nn]": "Nettverksfart", + "Name[pa]": "ਨੈੱਟਵਰਕ ਸਪੀਡ", + "Name[pl]": "Szybkość sieci", + "Name[pt]": "Velocidade da rede", + "Name[pt_BR]": "Velocidade de rede", + "Name[ro]": "Viteza rețelei", + "Name[ru]": "Скорость передачи данных по сети", + "Name[sk]": "Rýchlosť siete", + "Name[sl]": "Hitrost omrežja", + "Name[sv]": "Nätverkshastighet", + "Name[ta]": "பிணைய வேகம்", + "Name[tg]": "Суръати шабака", + "Name[tr]": "Ağ hızı", + "Name[uk]": "Швидкість мережі", + "Name[vi]": "Tốc độ mạng", + "Name[x-test]": "xxNetwork speedxx", + "Name[zh_CN]": "网络速度", + "Name[zh_TW]": "網路速度", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ], + "X-Plasma-RootPath": "org.kde.plasma.systemmonitor" +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/CMakeLists.txt b/plasma/workspace/applets/systemmonitor/systemmonitor/CMakeLists.txt new file mode 100644 index 0000000000..a02311b936 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/CMakeLists.txt @@ -0,0 +1,20 @@ +kcoreaddons_add_plugin(plasma_applet_systemmonitor SOURCES systemmonitor.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(plasma_applet_systemmonitor + Qt::Gui + Qt::Qml + Qt::Quick + Qt::DBus + KF5::Plasma + KF5::I18n + KF5::ConfigCore + KF5::ConfigGui + KF5::Declarative + KF5::KIOGui + KF5::Notifications + KSysGuard::SysGuard + KSysGuard::Sensors + KSysGuard::SensorFaces + ) + +plasma_install_package(package org.kde.plasma.systemmonitor) diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/Messages.sh b/plasma/workspace/applets/systemmonitor/systemmonitor/Messages.sh new file mode 100644 index 0000000000..40eb1f9e1e --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_org.kde.plasma.systemmonitor.pot diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/config.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/config.qml new file mode 100644 index 0000000000..8dde98428e --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/config.qml @@ -0,0 +1,25 @@ +import QtQuick 2.0 +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("Appearance") + icon: "preferences-desktop-color" + source: "config/ConfigAppearance.qml" + } + ConfigCategory { + name: i18n("%1 Details", plasmoid.nativeInterface.faceController.name) + icon: plasmoid.nativeInterface.faceController.icon + visible: plasmoid.nativeInterface.faceController.faceConfigUi !== null + source: "config/FaceDetails.qml" + } + ConfigCategory { + name: i18n("Sensors Details") + icon: "ksysguardd" + source: "config/ConfigSensors.qml" + } +} + diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/main.xml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/main.xml new file mode 100644 index 0000000000..a7d3191ceb --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/config/main.xml @@ -0,0 +1,25 @@ + + + + + + + + org.kde.ksysguard.piechart + + + + + + + + + + + + + + diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/CompactRepresentation.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/CompactRepresentation.qml new file mode 100644 index 0000000000..5e5dcfb458 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/CompactRepresentation.qml @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 +import QtQml 2.15 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.quickcharts 1.0 as Charts + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.kirigami 2.8 as Kirigami + +Control { + id: chartFace + Layout.fillWidth: contentItem ? contentItem.Layout.fillWidth : false + Layout.fillHeight: contentItem ? contentItem.Layout.fillHeight : false + + Layout.minimumWidth: (contentItem ? contentItem.Layout.minimumWidth : 0) + leftPadding + rightPadding + Layout.minimumHeight: (contentItem ? contentItem.Layout.minimumHeight : 0) + leftPadding + rightPadding + + Layout.preferredWidth: (contentItem ? contentItem.Layout.preferredWidth : 0) + leftPadding + rightPadding + Layout.preferredHeight: (contentItem ? contentItem.Layout.preferredHeight : 0) + leftPadding + rightPadding + + Layout.maximumWidth: (contentItem ? contentItem.Layout.maximumWidth : 0) + leftPadding + rightPadding + Layout.maximumHeight: (contentItem ? contentItem.Layout.maximumHeight : 0) + leftPadding + rightPadding + + Kirigami.Theme.inherit: false + Kirigami.Theme.textColor: PlasmaCore.ColorScope.textColor + + leftPadding: 0 + topPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + anchors.fill: parent + contentItem: plasmoid.nativeInterface.faceController.compactRepresentation + + Binding { + target: plasmoid.nativeInterface.faceController.compactRepresentation + property: "formFactor" + value: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Horizontal: + return Faces.SensorFace.Horizontal; + case PlasmaCore.Types.Vertical: + return Faces.SensorFace.Vertical; + default: + return Faces.SensorFace.Planar; + } + } + restoreMode: Binding.RestoreBinding + } + + MouseArea { + parent: chartFace + anchors.fill: parent + onClicked: plasmoid.expanded = !plasmoid.expanded + } +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/FullRepresentation.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/FullRepresentation.qml new file mode 100644 index 0000000000..cd32659d48 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/FullRepresentation.qml @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 +import QtQuick.Window 2.12 +import QtGraphicalEffects 1.0 +import QtQml 2.15 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts + +Control { + id: chartFace + + Layout.minimumWidth: (contentItem ? contentItem.Layout.minimumWidth : 0) + leftPadding + rightPadding + Layout.minimumHeight: (contentItem ? contentItem.Layout.minimumHeight : 0) + leftPadding + rightPadding + Layout.preferredWidth: (contentItem + ? (contentItem.Layout.preferredWidth > 0 ? contentItem.Layout.preferredWidth : contentItem.implicitWidth) + : 0) + leftPadding + rightPadding + Layout.preferredHeight: (contentItem + ? (contentItem.Layout.preferredHeight > 0 ? contentItem.Layout.preferredHeight: contentItem.implicitHeight) + : 0) + leftPadding + rightPadding + Layout.maximumWidth: (contentItem ? contentItem.Layout.maximumWidth : 0) + leftPadding + rightPadding + Layout.maximumHeight: (contentItem ? contentItem.Layout.maximumHeight : 0) + leftPadding + rightPadding + + Kirigami.Theme.inherit: false + Kirigami.Theme.textColor: PlasmaCore.ColorScope.textColor + Kirigami.Theme.backgroundColor: PlasmaCore.ColorScope.backgroundColor + Kirigami.Theme.disabledTextColor: PlasmaCore.ColorScope.disabledTextColor + + + contentItem: plasmoid.nativeInterface.faceController.fullRepresentation + + // This empty mousearea serves for the sole purpose of refusing touch events + // which otherwise are eaten by Control stealing the event from any of its parents + // TODO KF6: Check if this is still needed as Qt6 doesn't accept touch by default on Control + MouseArea { + parent: chartFace + anchors.fill:parent + } + + Binding { + target: plasmoid.nativeInterface.faceController.fullRepresentation + property: "formFactor" + value: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Horizontal: + return Faces.SensorFace.Horizontal; + case PlasmaCore.Types.Vertical: + return Faces.SensorFace.Verical; + default: + return Faces.SensorFace.Planar; + } + } + restoreMode: Binding.RestoreBinding + } +} + diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigAppearance.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigAppearance.qml new file mode 100644 index 0000000000..e6a58bc3cf --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigAppearance.qml @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kquickcontrols 2.0 +import org.kde.kconfig 1.0 // for KAuthorized +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.newstuff 1.62 as NewStuff + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +QQC2.Control { + id: root + + signal configurationChanged + + function saveConfig() { + contentItem.saveConfig(); + plasmoid.nativeInterface.faceController.reloadConfig() + } + + // Workaround for Bug 424458, when reusing the controller/item things break + contentItem: plasmoid.nativeInterface.workaroundController(root).appearanceConfigUi + + Connections { + target: contentItem + function onConfigurationChanged() { + root.configurationChanged() + } + } +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigSensors.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigSensors.qml new file mode 100644 index 0000000000..c8853b94ca --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/ConfigSensors.qml @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 +import QtQml.Models 2.12 + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.kquickcontrols 2.0 + +import org.kde.kitemmodels 1.0 as KItemModels + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.plasma.core 2.1 as PlasmaCore + +QQC2.Control { + id: root + + signal configurationChanged + + function saveConfig() { + contentItem.saveConfig(); + } + + contentItem: plasmoid.nativeInterface.faceController.sensorsConfigUi + + Connections { + target: contentItem + function onConfigurationChanged() { + root.configurationChanged() + } + } +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/FaceDetails.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/FaceDetails.qml new file mode 100644 index 0000000000..5b62061ed1 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/config/FaceDetails.qml @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 as QQC2 + +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kquickcontrols 2.0 + + +QQC2.Control { + id: root + + signal configurationChanged + + function saveConfig() { + contentItem.saveConfig(); + } + + contentItem: plasmoid.nativeInterface.faceController.faceConfigUi + + Connections { + target: contentItem + function onConfigurationChanged() { + root.configurationChanged() + } + } +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/main.qml b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/main.qml new file mode 100644 index 0000000000..f094a597e3 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/contents/ui/main.qml @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Arjen Hiemstra + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + +import org.kde.ksysguard.sensors 1.0 as Sensors +import org.kde.ksysguard.faces 1.0 as Faces + +import org.kde.quickcharts 1.0 as Charts + +Item { + Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground + + Plasmoid.switchWidth: Plasmoid.formFactor === PlasmaCore.Types.Planar + ? -1 + : (Plasmoid.fullRepresentationItem ? Plasmoid.fullRepresentationItem.Layout.minimumWidth : PlasmaCore.Units.gridUnit * 8) + Plasmoid.switchHeight: Plasmoid.formFactor === PlasmaCore.Types.Planar + ? -1 + : (Plasmoid.fullRepresentationItem ? Plasmoid.fullRepresentationItem.Layout.minimumHeight : PlasmaCore.Units.gridUnit * 12) + + Plasmoid.preferredRepresentation: Plasmoid.formFactor === PlasmaCore.Types.Planar ? Plasmoid.fullRepresentation : null + + Plasmoid.title: plasmoid.nativeInterface.faceController.title || i18n("System Monitor") + Plasmoid.toolTipSubText: "" + + Plasmoid.compactRepresentation: CompactRepresentation { + } + Plasmoid.fullRepresentation: FullRepresentation { + } + + Plasmoid.configurationRequired: plasmoid.nativeInterface.faceController.highPrioritySensorIds.length == 0 && plasmoid.nativeInterface.faceController.lowPrioritySensorIds.length == 0 && plasmoid.nativeInterface.faceController.totalSensor.length == 0 + + MouseArea { + parent: plasmoid + anchors.fill: plasmoid + acceptedButtons: Qt.MiddleButton + onClicked: action_openSystemMonitor() + } + + function action_openSystemMonitor() { + Plasmoid.nativeInterface.openSystemMonitor() + } + + Component.onCompleted: { + Plasmoid.setAction("openSystemMonitor", i18nc("@action", "Open System Monitor…"), "utilities-system-monitor") + } +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/package/metadata.json b/plasma/workspace/applets/systemmonitor/systemmonitor/package/metadata.json new file mode 100644 index 0000000000..903049a46d --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/package/metadata.json @@ -0,0 +1,128 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "System Information", + "Description": "Displays a configurable chart of a system monitor sensor", + "Description[ar]": "يعرض مخططًا قابلًا للتكوين لمتحسسات مراقبة النظام", + "Description[az]": "Sistem monitoru sensorunun ayarlana bilən zolaqlarını göstərir", + "Description[ca]": "Mostra un diagrama configurable d'un sensor del monitor del sistema", + "Description[en_GB]": "Displays a configurable chart of a system monitor sensor", + "Description[es]": "Muestra un gráfico configurable de un sensor del monitor del sistema", + "Description[eu]": "Sistema gainbegiratzeko sentsore baten diagrama konfiguragarri bat azaltzen du", + "Description[fi]": "Näyttää mukautettavissa olevan kaavion järjestelmänvalvonnan anturista", + "Description[fr]": "Affiche un graphique configurable du senseur de surveillance du système.", + "Description[hu]": "Testre szabható szenzorokat megjelenítő elem", + "Description[ia]": "Monstra un graphico configurabile de un sensor de supervision de systema", + "Description[it]": "Visualizza un grafico configurabile di un sensore di monitoraggio del sistema", + "Description[ko]": "설정 가능한 시스템 모니터 센서 차트 표시", + "Description[lt]": "Rodo konfigūruojamą sistemos prižiūryklės jutiklio diagramą", + "Description[nl]": "Toont een te configureren grafiek van een systeemmonitorsensor", + "Description[nn]": "Viser eit tilpassbart diagram for ein systemovervakingssensor", + "Description[pa]": "ਸਿਸਟਮ ਮਾਨੀਟਰ ਸੈਂਸਰ ਦੀ ਸੰਰਚਨਾ-ਯੋਗ ਚਾਰਟ ਦਿਖਾਉਂਦਾ ਹੈ", + "Description[pl]": "Wyświetla wykres miernika monitora systemowego", + "Description[pt_BR]": "Mostra um gráfico configurável do sensor do monitor do sistema", + "Description[ro]": "Afișează un grafic configurabil al unui senzor de monitorizare a sistemului", + "Description[ru]": "Настраиваемый график датчика системного монитора", + "Description[sk]": "Zobrazuje konfigurovateľný graf senzoru monitorovania systému", + "Description[sl]": "Prikaže nastavljiv grafikon sistemskega monitorja senzorja", + "Description[sv]": "Visar ett anpassningsbart diagram av en systemövervakningssensor", + "Description[ta]": "ஏதாவதொரு கணினி கண்காணிப்பு உணரியைக் காட்டும்", + "Description[tr]": "Sistem izleyicisi sensörünün yapılandırılabilir bir grafiğini görüntüler", + "Description[uk]": "Показує придатну до налаштовування діаграму на основі даних датчика нагляду за системою", + "Description[vi]": "Hiển thị một biểu đồ cấu hình được của một cảm biến giám sát hệ thống", + "Description[x-test]": "xxDisplays a configurable chart of a system monitor sensorxx", + "Description[zh_CN]": "显示系统监视器的可配置图表", + "FormFactors": [ + "desktop" + ], + "Icon": "ksysguardd", + "Id": "org.kde.plasma.systemmonitor", + "License": "GPL", + "Name": "System monitor Sensor", + "Name[ar]": "متحسس مراقبة النظام", + "Name[az]": "Sistem İzləməsi Sensoru", + "Name[ca]": "Sensor del monitor del sistema", + "Name[cs]": "Senzor monitoru systému", + "Name[da]": "Systemovervågning-sensor", + "Name[en_GB]": "System monitor Sensor", + "Name[es]": "Sensor del monitor del sistema", + "Name[et]": "Süsteemi jälgija sensor", + "Name[eu]": "Sistema gainbegiratzeko sentsorea", + "Name[fi]": "Järjestelmävalvonnan anturi", + "Name[fr]": "Senseur de surveillance du système", + "Name[hi]": "तंत्र परिवीक्षक सेंसर", + "Name[hsb]": "Systemowy monitor - sensor", + "Name[hu]": "Rendszermonitor-szenzor", + "Name[ia]": "Sensor de Monitor (supervisor) de systema", + "Name[id]": "Sensor Pemantau Sistem", + "Name[it]": "Sensore Monitor di sistema", + "Name[ko]": "시스템 모니터 센서", + "Name[lt]": "Sistemos prižiūryklės jutiklis", + "Name[ml]": "സിസ്റ്റം നിരീക്ഷക സെൻസർ", + "Name[nl]": "Systeemmonitorsensor", + "Name[nn]": "Systemovervakingssensor", + "Name[pa]": "ਸਿਸਟਮ ਮਾਨੀਟਰ ਸੈਂਸਰ", + "Name[pl]": "Miernik monitora systemowego", + "Name[pt]": "Sensor de monitorização do sistema", + "Name[pt_BR]": "Sensor do monitor do sistema ", + "Name[ro]": "Senzor de monitorizare a sistemului", + "Name[ru]": "Датчик системного монитора", + "Name[sk]": "Senzor monitorovania systému", + "Name[sl]": "Sistemski nadzornik senzorja", + "Name[sv]": "Systemövervakningssensor", + "Name[ta]": "கணினி கண்காணிப்பு உணரி", + "Name[tg]": "Назорати низом - Эҳсосгар", + "Name[tr]": "Sistem izleme Sensörü", + "Name[uk]": "Датчик нагляду за системою", + "Name[vi]": "Cảm biến giám sát hệ thống", + "Name[x-test]": "xxSystem monitor Sensorxx", + "Name[zh_CN]": "系统监视传感器", + "Name[zh_TW]": "系統監視器感測器", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-Provides": [ + "org.kde.plasma.systemmonitor" + ] +} diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.cpp b/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.cpp new file mode 100644 index 0000000000..29e60c847e --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.cpp @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemmonitor.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +SystemMonitor::SystemMonitor(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ + setHasConfigurationInterface(true); + + // Don't set the preset right now as we can't write on the config here because we don't have a Corona yet + if (args.count() > 2 && args.mid(3).length() > 0) { + const QString preset = args.mid(3).constFirst().toString(); + if (preset.length() > 0) { + m_pendingStartupPreset = preset; + } + } +} + +SystemMonitor::~SystemMonitor() = default; + +void SystemMonitor::init() +{ + configChanged(); + + // NOTE: taking the pluginId this way, we take it from the child applet (cpu monitor, memory, whatever) rather than the parent fallback applet + // (systemmonitor) + const QString pluginId = kPackage().metadata().pluginId(); + + // FIXME HACK: better way to get the engine At least AppletQuickItem should have an engine() getter + KDeclarative::QmlObjectSharedEngine *qmlObject = new KDeclarative::QmlObjectSharedEngine(); + KConfigGroup cg = config(); + m_sensorFaceController = new KSysGuard::SensorFaceController(cg, qmlObject->engine()); + qmlObject->deleteLater(); + + if (!m_pendingStartupPreset.isNull()) { + m_sensorFaceController->loadPreset(m_pendingStartupPreset); + } else { + // Take it from the config, which is *not* accessible from plasmoid.config as is not in config.xml + const QString preset = config().readEntry("CurrentPreset", pluginId); + m_sensorFaceController->loadPreset(preset); + } +} + +KSysGuard::SensorFaceController *SystemMonitor::faceController() const +{ + return m_sensorFaceController; +} + +KSysGuard::SensorFaceController *SystemMonitor::workaroundController(QQuickItem *context) const +{ + KConfigGroup cg = config(); + return new KSysGuard::SensorFaceController(cg, qmlEngine(context)); +} + +void SystemMonitor::configChanged() +{ + if (m_sensorFaceController) { + m_sensorFaceController->reloadConfig(); + } +} + +void SystemMonitor::openSystemMonitor() +{ + auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName("org.kde.plasma-systemmonitor")); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); +} + +K_PLUGIN_CLASS_WITH_JSON(SystemMonitor, "package/metadata.json") + +#include "systemmonitor.moc" diff --git a/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.h b/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.h new file mode 100644 index 0000000000..671c096ca5 --- /dev/null +++ b/plasma/workspace/applets/systemmonitor/systemmonitor/systemmonitor.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +#include + +class ApplicationListModel; +class QQuickItem; + +namespace KSysGuard +{ +class SensorFace; +} + +class KConfigLoader; + +class SystemMonitor : public Plasma::Applet +{ + Q_OBJECT + + Q_PROPERTY(KSysGuard::SensorFaceController *faceController READ faceController CONSTANT) + +public: + SystemMonitor(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~SystemMonitor() override; + + void init() override; + Q_INVOKABLE void openSystemMonitor(); + + KSysGuard::SensorFaceController *faceController() const; + + // Workaround for Bug 424458, when reusing the controller/item things break in ConfigAppearance + Q_INVOKABLE KSysGuard::SensorFaceController *workaroundController(QQuickItem *context) const; + +public Q_SLOTS: + void configChanged() override; + +private: + KSysGuard::SensorFaceController *m_sensorFaceController = nullptr; + QString m_pendingStartupPreset; +}; diff --git a/plasma/workspace/applets/systemtray/CMakeLists.txt b/plasma/workspace/applets/systemtray/CMakeLists.txt new file mode 100644 index 0000000000..d286a79e68 --- /dev/null +++ b/plasma/workspace/applets/systemtray/CMakeLists.txt @@ -0,0 +1,56 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.private.systemtray\") + +plasma_install_package(package org.kde.plasma.private.systemtray) + +include_directories(${plasma-workspace_SOURCE_DIR}/statusnotifierwatcher) + +set(systemtray_SRCS + dbusserviceobserver.cpp + plasmoidregistry.cpp + sortedsystemtraymodel.cpp + statusnotifieritemjob.cpp + statusnotifieritemhost.cpp + statusnotifieritemservice.cpp + statusnotifieritemsource.cpp + systemtraymodel.cpp + systemtraysettings.cpp + systemtraytypes.cpp +) + +qt_add_dbus_interface(systemtray_SRCS ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierWatcher.xml statusnotifierwatcher_interface) +qt_add_dbus_interface(systemtray_SRCS ${plasma-workspace_SOURCE_DIR}/dataengines/mpris2/org.freedesktop.DBus.Properties.xml dbusproperties) + +set(statusnotifieritem_xml ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierItem.xml) +set_source_files_properties(${statusnotifieritem_xml} PROPERTIES + NO_NAMESPACE false + INCLUDE "systemtraytypes.h" + CLASSNAME OrgKdeStatusNotifierItem +) +qt_add_dbus_interface(systemtray_SRCS ${statusnotifieritem_xml} statusnotifieritem_interface) + +ecm_qt_declare_logging_category(systemtray_SRCS HEADER debug.h + IDENTIFIER SYSTEM_TRAY + CATEGORY_NAME kde.systemtray + DEFAULT_SEVERITY Info) +add_library(systemtraymodel_static STATIC ${systemtray_SRCS}) +target_link_libraries(systemtraymodel_static + Qt::Gui + Qt::Quick + Qt::DBus + KF5::XmlGui + KF5::I18n + KF5::ItemModels + KF5::Plasma + KF5::IconThemes + KF5::WindowSystem + dbusmenuqt) + +kcoreaddons_add_plugin(org.kde.plasma.private.systemtray SOURCES systemtray.cpp INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(org.kde.plasma.private.systemtray systemtraymodel_static) + +add_subdirectory(container) +if(BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() diff --git a/plasma/workspace/applets/systemtray/Messages.sh b/plasma/workspace/applets/systemtray/Messages.sh new file mode 100644 index 0000000000..b47489a91a --- /dev/null +++ b/plasma/workspace/applets/systemtray/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp | grep -v '/tests/'` -o $podir/plasma_applet_org.kde.plasma.private.systemtray.pot diff --git a/plasma/workspace/applets/systemtray/autotests/CMakeLists.txt b/plasma/workspace/applets/systemtray/autotests/CMakeLists.txt new file mode 100644 index 0000000000..94b7d6f488 --- /dev/null +++ b/plasma/workspace/applets/systemtray/autotests/CMakeLists.txt @@ -0,0 +1,6 @@ +include(ECMAddTests) + +ecm_add_tests(systemtraymodeltest.cpp + LINK_LIBRARIES systemtraymodel_static + Qt::Test +) diff --git a/plasma/workspace/applets/systemtray/autotests/data/devicenotifier/metadata.json b/plasma/workspace/applets/systemtray/autotests/data/devicenotifier/metadata.json new file mode 100644 index 0000000000..a6ff45bb29 --- /dev/null +++ b/plasma/workspace/applets/systemtray/autotests/data/devicenotifier/metadata.json @@ -0,0 +1,31 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "wilderkde@gmail.com", + "Name": "Viranch Mehta, Jacopo De Simoi" + } + ], + "Category": "System Information", + "Description": "Notifications and access for new devices", + "Description[pl]": "Powiadamia i daje dostęp do nowych urządzeń", + "EnabledByDefault": true, + "Icon": "device-notifier", + "Id": "org.kde.plasma.devicenotifier.test", + "License": "GPL-2.0+", + "Name": "Device Notifier", + "Name[pl]": "Powiadomienia o urządzeniach", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://userbase.kde.org/Plasma/DeviceNotifier" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-MainScript": "ui/devicenotifier.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "Hardware", + "X-Plasma-Provides": [ + "org.kde.plasma.removabledevices" + ] +} diff --git a/plasma/workspace/applets/systemtray/autotests/data/mediacontroller/metadata.json b/plasma/workspace/applets/systemtray/autotests/data/mediacontroller/metadata.json new file mode 100644 index 0000000000..ac3b7f9663 --- /dev/null +++ b/plasma/workspace/applets/systemtray/autotests/data/mediacontroller/metadata.json @@ -0,0 +1,33 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "sebas@kde.org", + "Name": "Sebastian Kügler" + } + ], + "Category": "Multimedia", + "Description": "Media Player Controls", + "Description[pl]": "Obsługa odtwarzacza multimedialnego", + "EnabledByDefault": true, + "Icon": "applications-multimedia", + "Id": "org.kde.plasma.mediacontroller.test", + "License": "GPL-2.0+", + "Name": "Media Player", + "Name[pl]": "Odtwarzacz multimedialny", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-DBusActivationService": "org.mpris.MediaPlayer2.*", + "X-Plasma-MainScript": "ui/main.qml", + "X-Plasma-NotificationArea": "true", + "X-Plasma-NotificationAreaCategory": "ApplicationStatus", + "X-Plasma-Provides": [ + "org.kde.plasma.multimediacontrols" + ], + "X-Plasma-StandAloneApp": true +} diff --git a/plasma/workspace/applets/systemtray/autotests/systemtraymodeltest.cpp b/plasma/workspace/applets/systemtray/autotests/systemtraymodeltest.cpp new file mode 100644 index 0000000000..aab607e7df --- /dev/null +++ b/plasma/workspace/applets/systemtray/autotests/systemtraymodeltest.cpp @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include +#include + +#include +#include +#include + +#include "../plasmoidregistry.h" +#include "../systemtraymodel.h" +#include "../systemtraysettings.h" + +static const QString DEVICENOTIFIER_ID = QStringLiteral("org.kde.plasma.devicenotifier.test"); +static const QString MEDIACONROLLER_ID = QStringLiteral("org.kde.plasma.mediacontroller.test"); + +class SystemTrayModelTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void testPlasmoidModel(); +}; + +void SystemTrayModelTest::init() +{ + QLocale::setDefault(QLocale("en_US")); + qunsetenv("LANGUAGE"); + qunsetenv("LC_ALL"); + qunsetenv("LC_MESSAGES"); + qunsetenv("LANG"); +} + +class MockedPlasmoidRegistry : public PlasmoidRegistry +{ +public: + MockedPlasmoidRegistry(QPointer settings) + : PlasmoidRegistry(settings) + { + } + QMap systemTrayApplets() override + { + return m_systemTrayApplets; + } + + QMap m_systemTrayApplets; +}; + +class MockedSystemTraySettings : public SystemTraySettings +{ +public: + MockedSystemTraySettings() + : SystemTraySettings(nullptr) + { + } + + bool isKnownPlugin(const QString &pluginId) override + { + return m_knownPlugins.contains(pluginId); + } + const QStringList knownPlugins() const override + { + return m_knownPlugins; + } + void addKnownPlugin(const QString &pluginId) override + { + m_knownPlugins << pluginId; + } + void removeKnownPlugin(const QString &pluginId) override + { + m_knownPlugins.removeAll(pluginId); + } + bool isEnabledPlugin(const QString &pluginId) const override + { + return m_enabledPlugins.contains(pluginId); + } + const QStringList enabledPlugins() const override + { + return m_enabledPlugins; + } + void addEnabledPlugin(const QString &pluginId) override + { + m_enabledPlugins << pluginId; + } + void removeEnabledPlugin(const QString &pluginId) override + { + m_enabledPlugins.removeAll(pluginId); + } + bool isShowAllItems() const override + { + return m_showAllItems; + } + const QStringList shownItems() const override + { + return m_shownItems; + } + const QStringList hiddenItems() const override + { + return m_hiddenItems; + }; + + QStringList m_knownPlugins; + QStringList m_enabledPlugins; + QStringList m_shownItems; + QStringList m_hiddenItems; + bool m_showAllItems = false; +}; + +void SystemTrayModelTest::testPlasmoidModel() +{ + // given: mocked PlasmoidRegistry with sample plugin meta data + MockedSystemTraySettings *settings = new MockedSystemTraySettings(); + MockedPlasmoidRegistry *plasmoidRegistry = new MockedPlasmoidRegistry(settings); + plasmoidRegistry->m_systemTrayApplets.insert(DEVICENOTIFIER_ID, KPluginMetaData(QFINDTESTDATA("data/devicenotifier/metadata.desktop"))); + plasmoidRegistry->m_systemTrayApplets.insert(MEDIACONROLLER_ID, KPluginMetaData(QFINDTESTDATA("data/mediacontroller/metadata.desktop"))); + + // when: model is initialized + PlasmoidModel *model = new PlasmoidModel(settings, plasmoidRegistry); + + // expect: passes consistency tests + new QAbstractItemModelTester(model, QAbstractItemModelTester::FailureReportingMode::Fatal); + + // and expect: correct model size + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->roleNames().size(), 10); + // and expect: correct data returned + QModelIndex idx = model->index(0, 0); + QCOMPARE(model->data(idx, Qt::DisplayRole).toString(), "Device Notifier"); + QVERIFY(model->data(idx, Qt::DecorationRole).isValid()); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).toString(), "Plasmoid"); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::ItemId)).toString(), DEVICENOTIFIER_ID); + QVERIFY(!model->data(idx, static_cast(BaseModel::BaseRole::CanRender)).toBool()); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::Category)).toString(), "Hardware"); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::Status)), QVariant(Plasma::Types::ItemStatus::UnknownStatus)); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::EffectiveStatus)), QVariant(Plasma::Types::ItemStatus::HiddenStatus)); + QVERIFY(!model->data(idx, static_cast(PlasmoidModel::Role::HasApplet)).toBool()); + idx = model->index(1, 0); + QCOMPARE(model->data(idx, Qt::DisplayRole).toString(), "Media Player (Automatic load)"); + QVERIFY(model->data(idx, Qt::DecorationRole).isValid()); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).toString(), "Plasmoid"); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::ItemId)).toString(), MEDIACONROLLER_ID); + QVERIFY(!model->data(idx, static_cast(BaseModel::BaseRole::CanRender)).toBool()); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::Category)).toString(), "ApplicationStatus"); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::Status)), QVariant(Plasma::Types::ItemStatus::UnknownStatus)); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::EffectiveStatus)), QVariant(Plasma::Types::ItemStatus::HiddenStatus)); + QVERIFY(!model->data(idx, static_cast(PlasmoidModel::Role::HasApplet)).toBool()); + + // when: language is changed + QLocale::setDefault(QLocale("pl_PL")); + qputenv("LANG", "pl_PL.UTF-8"); + qputenv("LC_MESSAGES", "pl_PL.UTF-8"); + // then expect: translated data returned + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), "Powiadomienia o urz\u0105dzeniach"); + + // when: applet added + model->addApplet(new Plasma::Applet(nullptr, plasmoidRegistry->m_systemTrayApplets.value(MEDIACONROLLER_ID), QVariantList{})); + // then: applet can be rendered + QVERIFY(model->data(idx, static_cast(BaseModel::BaseRole::CanRender)).toBool()); + QVERIFY(model->data(idx, static_cast(PlasmoidModel::Role::HasApplet)).toBool()); + QCOMPARE(model->data(idx, static_cast(BaseModel::BaseRole::EffectiveStatus)), QVariant(Plasma::Types::ItemStatus::ActiveStatus)); + + // and when: applet removed + model->removeApplet(new Plasma::Applet(nullptr, plasmoidRegistry->m_systemTrayApplets.value(MEDIACONROLLER_ID), QVariantList{})); + // then: applet cannot be rendered anymore + QVERIFY(!model->data(idx, static_cast(BaseModel::BaseRole::CanRender)).toBool()); + QVERIFY(!model->data(idx, static_cast(PlasmoidModel::Role::HasApplet)).toBool()); + + // and when: invalid index + idx = model->index(4, 0); + // then: empty value + QVERIFY(model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).isNull()); + QVERIFY(!model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).isValid()); + idx = model->index(1, 1); + QVERIFY(model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).isNull()); + QVERIFY(!model->data(idx, static_cast(BaseModel::BaseRole::ItemType)).isValid()); + + delete model; +} + +QTEST_MAIN(SystemTrayModelTest) + +#include "systemtraymodeltest.moc" diff --git a/plasma/workspace/applets/systemtray/container/CMakeLists.txt b/plasma/workspace/applets/systemtray/container/CMakeLists.txt new file mode 100644 index 0000000000..1adf18638a --- /dev/null +++ b/plasma/workspace/applets/systemtray/container/CMakeLists.txt @@ -0,0 +1,20 @@ + +plasma_install_package(package org.kde.plasma.systemtray) + +set(systemtraycontainer_SRCS + systemtraycontainer.cpp +) + +ecm_qt_declare_logging_category(systemtraycontainer_SRCS HEADER debug.h + IDENTIFIER SYSTEM_TRAY_CONTAINER + CATEGORY_NAME kde.systemtraycontainer + DEFAULT_SEVERITY Info) + +kcoreaddons_add_plugin(org.kde.plasma.systemtray SOURCES ${systemtraycontainer_SRCS} INSTALL_NAMESPACE "plasma/applets") + +target_link_libraries(org.kde.plasma.systemtray + Qt::Gui + Qt::Quick + KF5::Plasma + KF5::XmlGui + KF5::I18n) diff --git a/plasma/workspace/applets/systemtray/container/package/contents/ui/main.qml b/plasma/workspace/applets/systemtray/container/package/contents/ui/main.qml new file mode 100644 index 0000000000..3bf7b41144 --- /dev/null +++ b/plasma/workspace/applets/systemtray/container/package/contents/ui/main.qml @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + +//SystemTray is a Containment. To have it presented as a widget in Plasma we need thin wrapping applet +Item { + id: root + + Layout.minimumWidth: internalSystray ? internalSystray.Layout.minimumWidth : 0 + Layout.minimumHeight: internalSystray ? internalSystray.Layout.minimumHeight : 0 + Layout.preferredWidth: Layout.minimumWidth + Layout.preferredHeight: Layout.minimumHeight + + Plasmoid.preferredRepresentation: Plasmoid.fullRepresentation + Plasmoid.status: internalSystray ? internalSystray.status : PlasmaCore.Types.UnknownStatus + + //synchronize state between SystemTray and wrapping Applet + Plasmoid.onExpandedChanged: { + if (internalSystray) { + internalSystray.expanded = plasmoid.expanded + } + } + Connections { + target: internalSystray + function onExpandedChanged() { + plasmoid.expanded = internalSystray.expanded + } + } + + property Item internalSystray + + Component.onCompleted: { + root.internalSystray = plasmoid.nativeInterface.internalSystray; + + if (root.internalSystray == null) { + return; + } + root.internalSystray.parent = root; + root.internalSystray.anchors.fill = root; + } + + Connections { + target: plasmoid.nativeInterface + function onInternalSystrayChanged() { + root.internalSystray = plasmoid.nativeInterface.internalSystray; + root.internalSystray.parent = root; + root.internalSystray.anchors.fill = root; + } + } +} diff --git a/plasma/workspace/applets/systemtray/container/package/metadata.json b/plasma/workspace/applets/systemtray/container/package/metadata.json new file mode 100644 index 0000000000..5963bbcb7b --- /dev/null +++ b/plasma/workspace/applets/systemtray/container/package/metadata.json @@ -0,0 +1,178 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "Windows and Tasks", + "Description": "Access hidden applications minimized in the system tray", + "Description[ar]": "الوصول إلى التطبيقات المصغّرة في صينية النظام", + "Description[az]": "Sistem treyinə yığılmış gizli tətbiqlərə giriş", + "Description[ca]": "Accés a les aplicacions minimitzades ocultes a la safata del sistema", + "Description[cs]": "Přístup ke skrytým aplikacím, které jsou minimalizované v systémové oblasti", + "Description[de]": "Ermöglicht den Zugriff auf Programme, die im Systemabschnitt der Kontrollleiste laufen", + "Description[en_GB]": "Access hidden applications minimised in the system tray", + "Description[es]": "Acceder a aplicaciones ocultas minimizadas en la bandeja del sistema", + "Description[eu]": "Atzitu sistema-erretiluan ikonotuta dauden ezkutuko aplikazioak", + "Description[fi]": "Hae ilmoitusalueelle pienennettyjä piilotettuja sovelluksia", + "Description[fr]": "Accéder aux applications cachées et réduites dans la boîte à miniatures.", + "Description[hu]": "Minimalizált alkalmazások elérését teszi lehetővé a paneltálcáról", + "Description[ia]": "Accede a applicationes celate minimisate in le tabuliero de systema", + "Description[it]": "Accedi alle applicazioni minimizzate nel vassoio di sistema", + "Description[ko]": "시스템 트레이에 숨어 있는 프로그램에 접근합니다", + "Description[lt]": "Gauti prieigą prie į sistemos dėklą suskleistų paslėptų programų", + "Description[nl]": "Biedt toegang tot programma's die in het systeemvak draaien", + "Description[nn]": "Tilgang til program minimerte i systemtrauet", + "Description[pa]": "ਸਿਸਟਮ ਟਰੇ ਵਿੱਚ ਲੁਕਵੀਆਂ ਘੱਟੋ-ਘੱਟ ਕੀਤੀਆਂ ਆਈਟਮਾਂ ਲਈ ਅਸੈੱਸ", + "Description[pl]": "Zapewnia dostęp do programów zminimalizowanych na tacce systemowej", + "Description[pt_BR]": "Acessa aplicativos ocultos minimizados na área de notificação", + "Description[ro]": "Accesați aplicațiile ascunse minimizate în tava de sistem", + "Description[ru]": "Показ значков приложений, свёрнутых в системный лоток", + "Description[sk]": "Prístup k skrytým aplikáciam minimalizovaným do systémovej lišty", + "Description[sl]": "Dostop do skritih programov, skrčenih v sistemski pladenj", + "Description[sv]": "Kom åt dolda program minimerade i systembrickan", + "Description[ta]": "கணினித்தட்டுக்கு ஒதுக்கப்பட்ட மறைந்துள்ள செயலிகளை அணுகுங்கள்", + "Description[tr]": "Sistem çekmecesine küçültülen uygulamalara erişin", + "Description[uk]": "Доступ до прихованих програм, мінімізованих до системного лотка", + "Description[vi]": "Truy cập các ứng dụng ẩn được thu nhỏ trong khay hệ thống", + "Description[x-test]": "xxAccess hidden applications minimized in the system trayxx", + "Description[zh_CN]": "访问在系统托盘中最小化隐藏的应用程序", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "preferences-desktop-notification", + "Id": "org.kde.plasma.systemtray", + "License": "GPL-2.0+", + "Name": "System Tray", + "Name[af]": "Stelsellaai", + "Name[ar]": "صينية النظام", + "Name[ast]": "Bandexa del sistema", + "Name[az]": "Sistem Çəkməcəsi", + "Name[be@latin]": "Systemny trej", + "Name[be]": "Сістэмны трэй", + "Name[bg]": "Системен панел", + "Name[bn]": "সিস্টেম ট্রে", + "Name[bn_IN]": "সিস্টেম ট্রে", + "Name[br]": "Barlenn ar reizhiad", + "Name[bs]": "Sistemska kaseta", + "Name[ca@valencia]": "Safata del sistema", + "Name[ca]": "Safata del sistema", + "Name[cs]": "Systémová část panelu", + "Name[csb]": "Systemòwi zabiérnik", + "Name[cy]": "Bar Tasgau", + "Name[da]": "Statusområde", + "Name[de]": "Systemabschnitt der Kontrollleiste", + "Name[el]": "Πλαίσιο συστήματος", + "Name[en_GB]": "System Tray", + "Name[eo]": "Sistempleto", + "Name[es]": "Bandeja del sistema", + "Name[et]": "Süsteemne dokk", + "Name[eu]": "Sistema-erretilua", + "Name[fa]": "سینی سیستم", + "Name[fi]": "Ilmoitusalue", + "Name[fr]": "Boîte à miniatures", + "Name[fy]": "Systeemfak", + "Name[ga]": "Tráidire an Chórais", + "Name[gl]": "Bandexa do sistema", + "Name[gu]": "સિસ્ટમ ટ્રે", + "Name[he]": "מגש המערכת", + "Name[hi]": "तंत्र तश्तरी", + "Name[hne]": "तंत्र तस्तरी", + "Name[hr]": "Sistemski blok", + "Name[hsb]": "Systemowa wotkładka", + "Name[hu]": "Paneltálca", + "Name[ia]": "Tabuliero de systema", + "Name[id]": "System Tray", + "Name[is]": "Kerfisbakki", + "Name[it]": "Vassoio di sistema", + "Name[ja]": "システムトレイ", + "Name[ka]": "სისტემური პანელი", + "Name[kk]": "Жүйелік сөре", + "Name[km]": "ថាស​ប្រព័ន្ធ", + "Name[kn]": "ವ್ಯವಸ್ಥಾ ಖಾನೆ (ಟ್ರೇ)", + "Name[ko]": "시스템 트레이", + "Name[lt]": "Sistemos dėklas", + "Name[lv]": "Sistēmas ikonu josla", + "Name[mai]": "तंत्र तश्तरी", + "Name[mk]": "Системска лента", + "Name[ml]": "സിസ്റ്റം ട്രേ", + "Name[mr]": "प्रणाली ट्रे", + "Name[ms]": "Dulang Sistem", + "Name[nb]": "Systemkurv", + "Name[nds]": "Paneel-Systeemafsnitt", + "Name[ne]": "प्रणाली ट्रे", + "Name[nl]": "Systeemvak", + "Name[nn]": "Systemtrau", + "Name[or]": "ତନ୍ତ୍ର ଧାରକ", + "Name[pa]": "ਸਿਸਟਮ ਟਰੇ", + "Name[pl]": "Tacka systemowa", + "Name[pt]": "Bandeja do Sistema", + "Name[pt_BR]": "Área de notificação", + "Name[ro]": "Tavă de sistem", + "Name[ru]": "Системный лоток", + "Name[se]": "Vuogádatgárcu", + "Name[si]": "පද්ධතිය තැටිය", + "Name[sk]": "Systémová lišta", + "Name[sl]": "Sistemska vrstica", + "Name[sr@ijekavian]": "системска касета", + "Name[sr@ijekavianlatin]": "sistemska kaseta", + "Name[sr@latin]": "sistemska kaseta", + "Name[sr]": "системска касета", + "Name[sv]": "Systembricka", + "Name[ta]": "சாதனத் தட்டு", + "Name[te]": "వ్యవస్థ ట్రె", + "Name[tg]": "Лавҳачаи низомӣ", + "Name[th]": "ถาดระบบ", + "Name[tr]": "Sistem Çekmecesi", + "Name[ug]": "سىستېما قوندىقى", + "Name[uk]": "Системний лоток", + "Name[vi]": "Khay hệ thống", + "Name[wa]": "Boesse ås imådjetes sistinme", + "Name[x-test]": "xxSystem Trayxx", + "Name[xh]": "Itreyi Yendlela yokusebenza", + "Name[zh_CN]": "系统托盘", + "Name[zh_TW]": "系統匣", + "ServiceTypes": [ + "Plasma/Applet" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-ContainmentType": "Panel", + "X-Plasma-MainScript": "ui/main.qml" +} diff --git a/plasma/workspace/applets/systemtray/container/systemtraycontainer.cpp b/plasma/workspace/applets/systemtray/container/systemtraycontainer.cpp new file mode 100644 index 0000000000..614d33f4ca --- /dev/null +++ b/plasma/workspace/applets/systemtray/container/systemtraycontainer.cpp @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtraycontainer.h" +#include "debug.h" + +#include + +#include +#include +#include + +SystemTrayContainer::SystemTrayContainer(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Applet(parent, data, args) +{ +} + +SystemTrayContainer::~SystemTrayContainer() +{ + if (destroyed()) { + m_innerContainment->destroy(); + } +} + +void SystemTrayContainer::init() +{ + Applet::init(); + + // in the first creation we immediately create the systray: so it's accessible during desktop scripting + uint id = config().readEntry("SystrayContainmentId", 0); + + if (id == 0) { + ensureSystrayExists(); + } +} + +void SystemTrayContainer::ensureSystrayExists() +{ + if (m_innerContainment) { + return; + } + + Plasma::Containment *cont = containment(); + if (!cont) { + return; + } + + Plasma::Corona *c = cont->corona(); + if (!c) { + return; + } + + uint id = config().readEntry("SystrayContainmentId", 0); + if (id > 0) { + foreach (Plasma::Containment *candidate, c->containments()) { + if (candidate->id() == id) { + m_innerContainment = candidate; + break; + } + } + qCDebug(SYSTEM_TRAY_CONTAINER) << "Containment id" << id << "that used to be a system tray was deleted"; + // id = 0; + } + + if (!m_innerContainment) { + m_innerContainment = c->createContainment(QStringLiteral("org.kde.plasma.private.systemtray"), QVariantList() << "org.kde.plasma:force-create"); + config().writeEntry("SystrayContainmentId", m_innerContainment->id()); + } + + if (!m_innerContainment) { + return; + } + + m_innerContainment->setParent(this); + connect(containment(), &Plasma::Containment::screenChanged, m_innerContainment.data(), &Plasma::Containment::reactToScreenChange); + + if (formFactor() == Plasma::Types::Horizontal || formFactor() == Plasma::Types::Vertical) { + m_innerContainment->setFormFactor(formFactor()); + } else { + m_innerContainment->setFormFactor(Plasma::Types::Horizontal); + } + + if (m_innerContainment) { + m_innerContainment->setLocation(location()); + } + + m_internalSystray = m_innerContainment->property("_plasma_graphicObject").value(); + Q_EMIT internalSystrayChanged(); + + actions()->addAction("configure", m_innerContainment->actions()->action("configure")); + connect(m_innerContainment.data(), &Plasma::Containment::configureRequested, this, [this](Plasma::Applet *applet) { + Q_EMIT containment()->configureRequested(applet); + }); + + if (m_internalSystray) { + // don't let internal systray manage context menus + m_internalSystray->setAcceptedMouseButtons(Qt::NoButton); + } + + // replace internal remove action with ours + m_innerContainment->actions()->addAction("remove", actions()->action("remove")); + + // Sync the display hints + m_innerContainment->setContainmentDisplayHints(containmentDisplayHints() | Plasma::Types::ContainmentDrawsPlasmoidHeading + | Plasma::Types::ContainmentForcesSquarePlasmoids); + connect(cont, &Plasma::Containment::containmentDisplayHintsChanged, this, [this]() { + m_innerContainment->setContainmentDisplayHints(containmentDisplayHints() | Plasma::Types::ContainmentDrawsPlasmoidHeading + | Plasma::Types::ContainmentForcesSquarePlasmoids); + }); +} + +void SystemTrayContainer::constraintsEvent(Plasma::Types::Constraints constraints) +{ + if (constraints & Plasma::Types::LocationConstraint) { + if (m_innerContainment) { + m_innerContainment->setLocation(location()); + } + } + + if (constraints & Plasma::Types::FormFactorConstraint) { + if (m_innerContainment) { + if (formFactor() == Plasma::Types::Horizontal || formFactor() == Plasma::Types::Vertical) { + m_innerContainment->setFormFactor(formFactor()); + } else { + m_innerContainment->setFormFactor(Plasma::Types::Horizontal); + } + } + } + + if (constraints & Plasma::Types::UiReadyConstraint) { + ensureSystrayExists(); + } +} + +QQuickItem *SystemTrayContainer::internalSystray() +{ + return m_internalSystray; +} + +K_PLUGIN_CLASS_WITH_JSON(SystemTrayContainer, "package/metadata.json") + +#include "systemtraycontainer.moc" diff --git a/plasma/workspace/applets/systemtray/container/systemtraycontainer.h b/plasma/workspace/applets/systemtray/container/systemtraycontainer.h new file mode 100644 index 0000000000..f19b5999f0 --- /dev/null +++ b/plasma/workspace/applets/systemtray/container/systemtraycontainer.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +/** + * @brief Thin wrapping 'Plasma::Applet' for SystemTray. + * + * SystemTray is of 'Plasma::Containment' type. To have it presented as a widget in Plasma we need a wrapping applet. + */ +class SystemTrayContainer : public Plasma::Applet +{ + Q_OBJECT + Q_PROPERTY(QQuickItem *internalSystray READ internalSystray NOTIFY internalSystrayChanged) + +public: + SystemTrayContainer(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~SystemTrayContainer() override; + + void init() override; + + QQuickItem *internalSystray(); + +protected: + void constraintsEvent(Plasma::Types::Constraints constraints) override; + void ensureSystrayExists(); + +Q_SIGNALS: + void internalSystrayChanged(); + +private: + QPointer m_innerContainment; + QPointer m_internalSystray; +}; diff --git a/plasma/workspace/applets/systemtray/dbusserviceobserver.cpp b/plasma/workspace/applets/systemtray/dbusserviceobserver.cpp new file mode 100644 index 0000000000..3ae2ad5a80 --- /dev/null +++ b/plasma/workspace/applets/systemtray/dbusserviceobserver.cpp @@ -0,0 +1,175 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "dbusserviceobserver.h" +#include "debug.h" + +#include "systemtraysettings.h" + +#include + +#include +#include +#include +#include + +DBusServiceObserver::DBusServiceObserver(QPointer settings, QObject *parent) + : QObject(parent) + , m_settings(settings) + , m_sessionServiceWatcher(new QDBusServiceWatcher(this)) + , m_systemServiceWatcher(new QDBusServiceWatcher(this)) +{ + m_sessionServiceWatcher->setConnection(QDBusConnection::sessionBus()); + m_systemServiceWatcher->setConnection(QDBusConnection::systemBus()); + + connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &DBusServiceObserver::initDBusActivatables); + + // Watch for new services + connect(m_sessionServiceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &serviceName) { + if (!m_dbusSessionServiceNamesFetched) { + return; + } + serviceRegistered(serviceName); + }); + connect(m_sessionServiceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) { + if (!m_dbusSessionServiceNamesFetched) { + return; + } + serviceUnregistered(serviceName); + }); + connect(m_systemServiceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &serviceName) { + if (!m_dbusSystemServiceNamesFetched) { + return; + } + serviceRegistered(serviceName); + }); + connect(m_systemServiceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) { + if (!m_dbusSystemServiceNamesFetched) { + return; + } + serviceUnregistered(serviceName); + }); +} + +void DBusServiceObserver::registerPlugin(const KPluginMetaData &pluginMetaData) +{ + const QString dbusactivation = pluginMetaData.value(QStringLiteral("X-Plasma-DBusActivationService")); + if (!dbusactivation.isEmpty()) { + qCDebug(SYSTEM_TRAY) << "Found DBus-able Applet: " << pluginMetaData.pluginId() << dbusactivation; + QRegExp rx(dbusactivation); + rx.setPatternSyntax(QRegExp::Wildcard); + m_dbusActivatableTasks[pluginMetaData.pluginId()] = rx; + + const QString watchedService = QString(dbusactivation).replace(".*", "*"); + m_sessionServiceWatcher->addWatchedService(watchedService); + m_systemServiceWatcher->addWatchedService(watchedService); + } +} + +void DBusServiceObserver::unregisterPlugin(const QString &pluginId) +{ + if (m_dbusActivatableTasks.contains(pluginId)) { + QRegExp rx = m_dbusActivatableTasks.take(pluginId); + const QString watchedService = rx.pattern().replace(".*", "*"); + m_sessionServiceWatcher->removeWatchedService(watchedService); + m_systemServiceWatcher->removeWatchedService(watchedService); + } +} + +bool DBusServiceObserver::isDBusActivable(const QString &pluginId) +{ + return m_dbusActivatableTasks.contains(pluginId); +} + +/* Loading and unloading Plasmoids when dbus services come and go + * + * This works as follows: + * - we collect a list of plugins and related services in m_dbusActivatableTasks + * - we query DBus for the list of services, async (initDBusActivatables()) + * - we go over that list, adding tasks when a service and plugin match (serviceNameFetchFinished()) + * - we start watching for new services, and do the same (serviceNameFetchFinished()) + * - whenever a service is gone, we check whether to unload a Plasmoid (serviceUnregistered()) + * + * Order of events has to be: + * - create a match rule for new service on DBus daemon + * - start fetching a list of names + * - ignore all changes that happen in the meantime + * - handle the list of all names + */ +void DBusServiceObserver::initDBusActivatables() +{ + // fetch list of existing services + QDBusPendingCall async = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, [=](QDBusPendingCallWatcher *callWatcher) { + serviceNameFetchFinished(callWatcher); + m_dbusSessionServiceNamesFetched = true; + }); + + QDBusPendingCall systemAsync = QDBusConnection::systemBus().interface()->asyncCall(QStringLiteral("ListNames")); + QDBusPendingCallWatcher *systemCallWatcher = new QDBusPendingCallWatcher(systemAsync, this); + connect(systemCallWatcher, &QDBusPendingCallWatcher::finished, [=](QDBusPendingCallWatcher *callWatcher) { + serviceNameFetchFinished(callWatcher); + m_dbusSystemServiceNamesFetched = true; + }); +} + +void DBusServiceObserver::serviceNameFetchFinished(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply propsReply = *watcher; + watcher->deleteLater(); + + if (propsReply.isError()) { + qCWarning(SYSTEM_TRAY) << "Could not get list of available D-Bus services"; + } else { + const auto propsReplyValue = propsReply.value(); + for (const QString &serviceName : propsReplyValue) { + serviceRegistered(serviceName); + } + } +} + +void DBusServiceObserver::serviceRegistered(const QString &service) +{ + if (service.startsWith(QLatin1Char(':'))) { + return; + } + + for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) { + const QString &plugin = it.key(); + if (!m_settings->isEnabledPlugin(plugin)) { + continue; + } + + const auto &rx = it.value(); + if (rx.exactMatch(service)) { + qCDebug(SYSTEM_TRAY) << "DBus service" << service << "matching" << m_dbusActivatableTasks[plugin] << "appeared. Loading" << plugin; + Q_EMIT serviceStarted(plugin); + m_dbusServiceCounts[plugin]++; + } + } +} + +void DBusServiceObserver::serviceUnregistered(const QString &service) +{ + for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) { + const QString &plugin = it.key(); + if (!m_settings->isEnabledPlugin(plugin)) { + continue; + } + + const auto &rx = it.value(); + if (rx.exactMatch(service)) { + m_dbusServiceCounts[plugin]--; + Q_ASSERT(m_dbusServiceCounts[plugin] >= 0); + if (m_dbusServiceCounts[plugin] == 0) { + qCDebug(SYSTEM_TRAY) << "DBus service" << service << "matching" << m_dbusActivatableTasks[plugin] << "disappeared. Unloading" << plugin; + Q_EMIT serviceStopped(plugin); + } + } + } +} diff --git a/plasma/workspace/applets/systemtray/dbusserviceobserver.h b/plasma/workspace/applets/systemtray/dbusserviceobserver.h new file mode 100644 index 0000000000..cbe404154d --- /dev/null +++ b/plasma/workspace/applets/systemtray/dbusserviceobserver.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class KPluginMetaData; +class SystemTraySettings; +class QDBusPendingCallWatcher; +class QDBusServiceWatcher; + +/** + * @brief Loading and unloading Plasmoids when DBus services come and go. + */ +class DBusServiceObserver : public QObject +{ + Q_OBJECT +public: + explicit DBusServiceObserver(QPointer settings, QObject *parent = nullptr); + + void registerPlugin(const KPluginMetaData &pluginMetaData); + void unregisterPlugin(const QString &pluginId); + bool isDBusActivable(const QString &pluginId); + +Q_SIGNALS: + void serviceStarted(const QString &pluginId); + void serviceStopped(const QString &pluginId); + +public Q_SLOTS: + void initDBusActivatables(); + +private: + void serviceNameFetchFinished(QDBusPendingCallWatcher *watcher); + void serviceRegistered(const QString &service); + void serviceUnregistered(const QString &service); + + QPointer m_settings; + + QDBusServiceWatcher *m_sessionServiceWatcher; + QDBusServiceWatcher *m_systemServiceWatcher; + + QHash m_dbusActivatableTasks; + QHash m_dbusServiceCounts; + bool m_dbusSessionServiceNamesFetched = false; + bool m_dbusSystemServiceNamesFetched = false; +}; diff --git a/plasma/workspace/applets/systemtray/package/contents/applet/CompactApplet.qml b/plasma/workspace/applets/systemtray/package/contents/applet/CompactApplet.qml new file mode 100644 index 0000000000..afb1634970 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/applet/CompactApplet.qml @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + + +PlasmaCore.ToolTipArea { + id: appletRoot + objectName: "org.kde.desktop-CompactApplet" + anchors.fill: parent + + mainText: plasmoid.toolTipMainText + subText: plasmoid.toolTipSubText + location: if (plasmoid.parent && plasmoid.parent.inHiddenLayout && plasmoid.location !== PlasmaCore.Types.LeftEdge) { + return PlasmaCore.Types.RightEdge; + } else { + return plasmoid.location; + } + active: !plasmoid.expanded + textFormat: plasmoid.toolTipTextFormat + mainItem: plasmoid.toolTipItem ? plasmoid.toolTipItem : null + + property Item fullRepresentation + property Item compactRepresentation + + Connections { + target: plasmoid + function onContextualActionsAboutToShow() { + appletRoot.hideImmediately() + } + } + + Layout.minimumWidth: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Vertical: + return 0; + case PlasmaCore.Types.Horizontal: + return height; + default: + return PlasmaCore.Units.gridUnit * 3; + } + } + + Layout.minimumHeight: { + switch (plasmoid.formFactor) { + case PlasmaCore.Types.Vertical: + return width; + case PlasmaCore.Types.Horizontal: + return 0; + default: + return PlasmaCore.Units.gridUnit * 3; + } + } + + onCompactRepresentationChanged: { + if (compactRepresentation) { + compactRepresentation.parent = appletRoot; + compactRepresentation.anchors.fill = appletRoot; + compactRepresentation.visible = true; + } + appletRoot.visible = true; + } +} + diff --git a/plasma/workspace/applets/systemtray/package/contents/config/config.qml b/plasma/workspace/applets/systemtray/package/contents/config/config.qml new file mode 100644 index 0000000000..3b62d0b5fb --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/config/config.qml @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("General") + icon: "plasma" + source: "ConfigGeneral.qml" + } + ConfigCategory { + name: i18n("Entries") + icon: "preferences-desktop-notification" + source: "ConfigEntries.qml" + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/config/main.xml b/plasma/workspace/applets/systemtray/package/contents/config/main.xml new file mode 100644 index 0000000000..2f8c52fddd --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/config/main.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + false + + + + + false + + + + 2 + + + + false + + + + + diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/ConfigEntries.qml b/plasma/workspace/applets/systemtray/package/contents/ui/ConfigEntries.qml new file mode 100644 index 0000000000..38f18331bd --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/ConfigEntries.qml @@ -0,0 +1,284 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2014 Marco Martin + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QQC2 +import QtQuick.Layouts 1.3 + +import org.kde.kquickcontrols 2.0 as KQC +import org.kde.kirigami 2.10 as Kirigami + +ColumnLayout { + id: iconsPage + + signal configurationChanged + + property var cfg_shownItems: [] + property var cfg_hiddenItems: [] + property var cfg_extraItems: [] + property alias cfg_showAllItems: showAllCheckBox.checked + + QQC2.CheckBox { + id: showAllCheckBox + text: i18n("Always show all entries") + } + + function categoryName(category) { + switch (category) { + case "ApplicationStatus": + return i18n("Application Status") + case "Communications": + return i18n("Communications") + case "SystemServices": + return i18n("System Services") + case "Hardware": + return i18n("Hardware Control") + case "UnknownCategory": + default: + return i18n("Miscellaneous") + } + } + + QQC2.ScrollView { + id: scrollView + + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: itemsList.implicitHeight + + Component.onCompleted: scrollView.background.visible = true + + property bool scrollBarVisible: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible + property var scrollBarWidth: scrollBarVisible ? QQC2.ScrollBar.vertical.width : 0 + + ListView { + id: itemsList + + property var visibilityColumnWidth: Kirigami.Units.gridUnit + property var keySequenceColumnWidth: Kirigami.Units.gridUnit + + clip: true + + model: plasmoid.nativeInterface.configSystemTrayModel + + header: Kirigami.AbstractListItem { + + hoverEnabled: false + + RowLayout { + Kirigami.Heading { + text: i18nc("Name of the system tray entry", "Entry") + level: 2 + Layout.fillWidth: true + } + Kirigami.Heading { + text: i18n("Visibility") + level: 2 + Layout.preferredWidth: itemsList.visibilityColumnWidth + Component.onCompleted: itemsList.visibilityColumnWidth = Math.max(implicitWidth, itemsList.visibilityColumnWidth) + } + Kirigami.Heading { + text: i18n("Keyboard Shortcut") + level: 2 + Layout.preferredWidth: itemsList.keySequenceColumnWidth + Component.onCompleted: itemsList.keySequenceColumnWidth = Math.max(implicitWidth, itemsList.keySequenceColumnWidth) + } + QQC2.Button { // Configure button column + icon.name: "configure" + enabled: false + opacity: 0 + } + } + } + + section { + property: "category" + delegate: Kirigami.ListSectionHeader { + label: categoryName(section) + } + } + + delegate: Kirigami.AbstractListItem { + highlighted: false + hoverEnabled: false + + property bool isPlasmoid: model.itemType === "Plasmoid" + + contentItem: RowLayout { + RowLayout { + Layout.fillWidth: true + + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + source: model.decoration + } + QQC2.Label { + Layout.fillWidth: true + text: model.display + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + QQC2.ComboBox { + id: visibilityComboBox + + property var contentWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitWidth: Math.max(contentWidth, itemsList.visibilityColumnWidth) + Component.onCompleted: itemsList.visibilityColumnWidth = Math.max(implicitWidth, itemsList.visibilityColumnWidth) + + enabled: (!showAllCheckBox.checked || isPlasmoid) && itemId + textRole: "text" + model: comboBoxModel() + + currentIndex: { + var value + + if (cfg_shownItems.indexOf(itemId) !== -1) { + value = "shown" + } else if (cfg_hiddenItems.indexOf(itemId) !== -1) { + value = "hidden" + } else if (isPlasmoid && cfg_extraItems.indexOf(itemId) === -1) { + value = "disabled" + } else { + value = "auto" + } + + for (var i = 0; i < model.length; i++) { + if (model[i].value === value) { + return i + } + } + + return 0 + } + + property var myCurrentValue: model[currentIndex].value + + onActivated: { + var shownIndex = cfg_shownItems.indexOf(itemId) + var hiddenIndex = cfg_hiddenItems.indexOf(itemId) + var extraIndex = cfg_extraItems.indexOf(itemId) + + switch (myCurrentValue) { + case "auto": + if (shownIndex > -1) { + cfg_shownItems.splice(shownIndex, 1) + } + if (hiddenIndex > -1) { + cfg_hiddenItems.splice(hiddenIndex, 1) + } + if (extraIndex === -1) { + cfg_extraItems.push(itemId) + } + break + case "shown": + if (shownIndex === -1) { + cfg_shownItems.push(itemId) + } + if (hiddenIndex > -1) { + cfg_hiddenItems.splice(hiddenIndex, 1) + } + if (extraIndex === -1) { + cfg_extraItems.push(itemId) + } + break + case "hidden": + if (shownIndex > -1) { + cfg_shownItems.splice(shownIndex, 1) + } + if (hiddenIndex === -1) { + cfg_hiddenItems.push(itemId) + } + if (extraIndex === -1) { + cfg_extraItems.push(itemId) + } + break + case "disabled": + if (shownIndex > -1) { + cfg_shownItems.splice(shownIndex, 1) + } + if (hiddenIndex > -1) { + cfg_hiddenItems.splice(hiddenIndex, 1) + } + if (extraIndex > -1) { + cfg_extraItems.splice(extraIndex, 1) + } + break + } + iconsPage.configurationChanged() + } + + function comboBoxModel() { + var autoElement = {"value": "auto", "text": i18n("Shown when relevant")} + var shownElement = {"value": "shown", "text": i18n("Always shown")} + var hiddenElement = {"value": "hidden", "text": i18n("Always hidden")} + var disabledElement = {"value": "disabled", "text": i18n("Disabled")} + + if (showAllCheckBox.checked) { + if (isPlasmoid) { + return [autoElement, disabledElement] + } else { + return [shownElement] + } + } else { + if (isPlasmoid) { + return [autoElement, shownElement, hiddenElement, disabledElement] + } else { + return [autoElement, shownElement, hiddenElement] + } + } + } + } + KQC.KeySequenceItem { + id: keySequenceItem + Layout.minimumWidth: itemsList.keySequenceColumnWidth + Layout.preferredWidth: itemsList.keySequenceColumnWidth + Component.onCompleted: itemsList.keySequenceColumnWidth = Math.max(implicitWidth, itemsList.keySequenceColumnWidth) + + visible: isPlasmoid + enabled: visibilityComboBox.myCurrentValue !== "disabled" + keySequence: model.applet ? model.applet.globalShortcut : "" + onKeySequenceChanged: { + if (model.applet && keySequence !== model.applet.globalShortcut) { + model.applet.globalShortcut = keySequence + + itemsList.keySequenceColumnWidth = Math.max(implicitWidth, itemsList.keySequenceColumnWidth) + } + } + } + // Placeholder for when KeySequenceItem is not visible + Item { + Layout.minimumWidth: itemsList.keySequenceColumnWidth + Layout.maximumWidth: itemsList.keySequenceColumnWidth + visible: !keySequenceItem.visible + } + + QQC2.Button { + readonly property QtObject configureAction: (model.applet && model.applet.action("configure")) || null + + Accessible.name: configureAction ? configureAction.text : "" + icon.name: "configure" + enabled: configureAction && configureAction.visible && configureAction.enabled + // Still reserve layout space, so not setting visible to false + opacity: enabled ? 1 : 0 + onClicked: configureAction.trigger() + + QQC2.ToolTip { + // Strip out ampersands right before non-whitespace characters, i.e. + // those used to determine the alt key shortcut + text: parent.Accessible.name.replace(/&(?=\S)/g, "") + } + } + } + } + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/ConfigGeneral.qml b/plasma/workspace/applets/systemtray/package/contents/ui/ConfigGeneral.qml new file mode 100644 index 0000000000..c703572c83 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/ConfigGeneral.qml @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as QQC2 +import QtQuick.Layouts 1.13 + +import org.kde.plasma.core 2.1 as PlasmaCore + +import org.kde.kirigami 2.13 as Kirigami + +ColumnLayout { + property bool cfg_scaleIconsToFit + property int cfg_iconSpacing + + Kirigami.FormLayout { + Layout.fillHeight: true + + QQC2.RadioButton { + Kirigami.FormData.label: i18nc("The arrangement of system tray icons in the Panel", "Panel icon size:") + enabled: !Kirigami.Settings.tabletMode + text: i18n("Small") + checked: cfg_scaleIconsToFit == false && !Kirigami.Settings.tabletMode + onToggled: cfg_scaleIconsToFit = !checked + } + QQC2.RadioButton { + id: automaticRadioButton + enabled: !Kirigami.Settings.tabletMode + text: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? i18n("Scale with Panel height") + : i18n("Scale with Panel width") + checked: cfg_scaleIconsToFit == true || Kirigami.Settings.tabletMode + onToggled: cfg_scaleIconsToFit = checked + } + QQC2.Label { + visible: Kirigami.Settings.tabletMode + text: i18n("Automatically enabled when in tablet mode") + font: PlasmaCore.Theme.smallestFont + } + + Item { + Kirigami.FormData.isSection: true + } + + QQC2.ComboBox { + Kirigami.FormData.label: i18nc("@label:listbox The spacing between system tray icons in the Panel", "Panel icon spacing:") + model: [ + { + "label": i18nc("@item:inlistbox Icon spacing", "Small"), + "spacing": 1 + }, + { + "label": i18nc("@item:inlistbox Icon spacing", "Normal"), + "spacing": 2 + }, + { + "label": i18nc("@item:inlistbox Icon spacing", "Large"), + "spacing": 4 + } + ] + textRole: "label" + enabled: !Kirigami.Settings.tabletMode + + currentIndex: { + if (Kirigami.Settings.tabletMode) { + return 2; // Large + } + + switch (cfg_iconSpacing) { + case 1: return 0; // Small + case 2: return 1; // Normal + case 4: return 2; // Large + } + } + + onActivated: cfg_iconSpacing = model[currentIndex]["spacing"]; + } + QQC2.Label { + visible: Kirigami.Settings.tabletMode + text: i18nc("@info:usagetip under a combobox when tablet mode is on", "Automatically set to Large when in tablet mode") + font: PlasmaCore.Theme.smallestFont + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/CurrentItemHighLight.qml b/plasma/workspace/applets/systemtray/package/contents/ui/CurrentItemHighLight.qml new file mode 100644 index 0000000000..7e24d47dec --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/CurrentItemHighLight.qml @@ -0,0 +1,175 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +PlasmaCore.FrameSvgItem { + id: currentItemHighLight + + property int location + + property bool animationEnabled: true + property var highlightedItem: null + + property var containerMargins: { + let item = currentItemHighLight; + while (item.parent) { + item = item.parent; + if (item.isAppletContainer) { + return item.getMargins; + } + } + return undefined; + } + + z: -1 // always draw behind icons + opacity: systemTrayState.expanded ? 1 : 0 + + imagePath: "widgets/tabbar" + prefix: { + var prefix = "" + switch (location) { + case PlasmaCore.Types.LeftEdge: + prefix = "west-active-tab"; + break; + case PlasmaCore.Types.TopEdge: + prefix = "north-active-tab"; + break; + case PlasmaCore.Types.RightEdge: + prefix = "east-active-tab"; + break; + default: + prefix = "south-active-tab"; + } + if (!hasElementPrefix(prefix)) { + prefix = "active-tab"; + } + return prefix; + } + + // update when System Tray is expanded - applet activated or hidden icons shown + Connections { + target: systemTrayState + + function onActiveAppletChanged() { + Qt.callLater(updateHighlightedItem); + } + + function onExpandedChanged() { + Qt.callLater(updateHighlightedItem); + } + } + + // update when applet changes parent (e.g. moves from active to hidden icons) + Connections { + target: systemTrayState.activeApplet + + function onParentChanged() { + Qt.callLater(updateHighlightedItem); + } + } + + // update when System Tray size changes + Connections { + target: parent + + function onWidthChanged() { + Qt.callLater(updateHighlightedItem); + } + + function onHeightChanged() { + Qt.callLater(updateHighlightedItem); + } + } + + // update when scale of newly added tray item changes (check 'add' animation in GridView in main.qml) + Connections { + target: !!highlightedItem && highlightedItem.parent ? highlightedItem.parent : null + + function onScaleChanged() { + Qt.callLater(updateHighlightedItem); + } + } + + function updateHighlightedItem() { + var forceEdgeHighlight; + if (systemTrayState.expanded) { + if (systemTrayState.activeApplet && systemTrayState.activeApplet.parent && systemTrayState.activeApplet.parent.inVisibleLayout) { + changeHighlightedItem(systemTrayState.activeApplet.parent.container, forceEdgeHighlight=false); + } else { // 'Show hiden items' popup + changeHighlightedItem(parent, forceEdgeHighlight=true); + } + } else { + highlightedItem = null; + } + } + + function changeHighlightedItem(nextItem, forceEdgeHighlight) { + // do not animate the first appearance + // or when the property value of a highlighted item changes + if (!highlightedItem || (highlightedItem === nextItem)) { + animationEnabled = false; + } + var returnAllMargins; + + const p = parent.mapFromItem(nextItem, 0, 0); + if (containerMargins && (parent.oneRowOrColumn || forceEdgeHighlight)) { + x = p.x - containerMargins('left', returnAllMargins=true); + y = p.y - containerMargins('top', returnAllMargins=true); + width = nextItem.width + containerMargins('left', returnAllMargins=true) + containerMargins('right', true); + height = nextItem.height + containerMargins('bottom', returnAllMargins=true) + containerMargins('top', true); + } else { + x = p.x; + y = p.y; + width = nextItem.width + height = nextItem.height + } + + highlightedItem = nextItem; + animationEnabled = true; + } + + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: systemTrayState.expanded ? Easing.OutCubic : Easing.InCubic + } + } + Behavior on x { + id: xAnim + enabled: animationEnabled + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutCubic + } + } + Behavior on y { + id: yAnim + enabled: animationEnabled + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutCubic + } + } + Behavior on width { + id: widthAnim + enabled: animationEnabled + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutCubic + } + } + Behavior on height { + id: heightAnim + enabled: animationEnabled + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutCubic + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/ExpandedRepresentation.qml b/plasma/workspace/applets/systemtray/package/contents/ui/ExpandedRepresentation.qml new file mode 100644 index 0000000000..6d55475d93 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/ExpandedRepresentation.qml @@ -0,0 +1,211 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + SPDX-FileCopyrightText: 2020 Nate Graham + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +// We still need PC2 here for that version of Menu, as PC2 Menu is still very problematic with QActions +// Not being a proper popup window, makes it a showstopper to be used in Plasma +import org.kde.plasma.components 2.0 as PC2 +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +Item { + id: popup + //set width/height to avoid useless Dialog resize + readonly property int defaultWidth: PlasmaCore.Units.gridUnit * 24 + readonly property int defaultHeight: PlasmaCore.Units.gridUnit * 24 + + width: defaultWidth + Layout.minimumWidth: defaultWidth + Layout.preferredWidth: defaultWidth + Layout.maximumWidth: defaultWidth + + height: defaultHeight + Layout.minimumHeight: defaultHeight + Layout.preferredHeight: defaultHeight + Layout.maximumHeight: defaultHeight + + property alias hiddenLayout: hiddenItemsView.layout + property alias plasmoidContainer: container + + // Header + PlasmaExtras.PlasmoidHeading { + id: plasmoidHeading + anchors { + top: parent.top + left: parent.left + right: parent.right + } + height: trayHeading.height + bottomPadding + container.headingHeight + Behavior on height { + NumberAnimation { duration: PlasmaCore.Units.shortDuration/2; easing.type: Easing.InOutQuad } + } + } + + // Main content layout + ColumnLayout { + id: expandedRepresentation + anchors.fill: parent + // TODO: remove this so the scrollview fully touches the header; + // add top padding internally + spacing: plasmoidHeading.bottomPadding + + // Header content layout + RowLayout { + id: trayHeading + + PlasmaComponents.ToolButton { + id: backButton + visible: systemTrayState.activeApplet && systemTrayState.activeApplet.expanded && (hiddenLayout.itemCount > 0) + icon.name: LayoutMirroring.enabled ? "go-previous-symbolic-rtl" : "go-previous-symbolic" + onClicked: systemTrayState.setActiveApplet(null) + } + + PlasmaExtras.Heading { + Layout.fillWidth: true + leftPadding: systemTrayState.activeApplet ? 0 : PlasmaCore.Units.smallSpacing * 2 + + level: 1 + text: systemTrayState.activeApplet ? systemTrayState.activeApplet.title : i18n("Status and Notifications") + } + + PlasmaComponents.ToolButton { + id: actionsButton + visible: visibleActions > 0 + checked: visibleActions > 1 ? configMenu.status !== PC2.DialogStatus.Closed : singleAction && singleAction.checked + property QtObject applet: systemTrayState.activeApplet || plasmoid + property int visibleActions: menuItemFactory.count + property QtObject singleAction: visibleActions === 1 && menuItemFactory.object ? menuItemFactory.object.action : null + + icon.name: "application-menu" + checkable: visibleActions > 1 || (singleAction && singleAction.checkable) + contentItem.opacity: visibleActions > 1 + // NOTE: it needs an IconItem because QtQuickControls2 buttons cannot load QIcons as their icon + PlasmaCore.IconItem { + parent: actionsButton + anchors.centerIn: parent + active: actionsButton.hovered + implicitWidth: PlasmaCore.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + source: actionsButton.singleAction !== null ? actionsButton.singleAction.icon : "" + visible: actionsButton.singleAction + } + onToggled: { + if (visibleActions > 1) { + if (checked) { + configMenu.openRelative(); + } else { + configMenu.close(); + } + } + } + onClicked: { + if (singleAction) { + singleAction.trigger(); + } + } + PlasmaComponents.ToolTip { + text: actionsButton.singleAction ? actionsButton.singleAction.text : i18n("More actions") + } + PC2.Menu { + id: configMenu + visualParent: actionsButton + placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup + } + + Instantiator { + id: menuItemFactory + model: { + configMenu.clearMenuItems(); + let actions = []; + for (let i in actionsButton.applet.contextualActions) { + const action = actionsButton.applet.contextualActions[i]; + if (action.visible && action.priority > 0 && action !== actionsButton.applet.action("configure")) { + actions.push(action); + } + } + return actions; + } + delegate: PC2.MenuItem { + id: menuItem + action: modelData + } + onObjectAdded: { + configMenu.addMenuItem(object); + } + } + } + PlasmaComponents.ToolButton { + icon.name: "configure" + visible: actionsButton.applet && actionsButton.applet.action("configure") + PlasmaComponents.ToolTip { + text: parent.visible ? actionsButton.applet.action("configure").text : "" + } + onClicked: actionsButton.applet.action("configure").trigger(); + } + + PlasmaComponents.ToolButton { + id: pinButton + checkable: true + checked: plasmoid.configuration.pin + onToggled: plasmoid.configuration.pin = checked + icon.name: "window-pin" + PlasmaComponents.ToolTip { + text: i18n("Keep Open") + } + } + } + + // Grid view of all available items + HiddenItemsView { + id: hiddenItemsView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + visible: !systemTrayState.activeApplet + onVisibleChanged: { + if (visible) { + layout.forceActiveFocus(); + } + } + } + + // Container for currently visible item + PlasmoidPopupsContainer { + id: container + Layout.fillWidth: true + Layout.fillHeight: true + visible: systemTrayState.activeApplet + + // We need to add margin on the top so it matches the dialog's own margin + Layout.topMargin: mergeHeadings ? 0 : dialog.margins.top + onVisibleChanged: { + if (visible) { + forceActiveFocus(); + } + } + } + } + + // Footer + PlasmaExtras.PlasmoidHeading { + id: plasmoidFooter + location: PlasmaExtras.PlasmoidHeading.Location.Footer + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + visible: container.appletHasFooter + height: container.footerHeight + // So that it doesn't appear over the content view, which results in + // the footer controls being inaccessible + z: -9999 + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/ExpanderArrow.qml b/plasma/workspace/applets/systemtray/package/contents/ui/ExpanderArrow.qml new file mode 100644 index 0000000000..6a1f021893 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/ExpanderArrow.qml @@ -0,0 +1,118 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore + +PlasmaCore.ToolTipArea { + id: tooltip + + property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical + property int iconSize: PlasmaCore.Units.iconSizes.smallMedium + implicitWidth: iconSize + implicitHeight: iconSize + activeFocusOnTab: true + + Accessible.name: i18n("Expand System Tray") + Accessible.description: i18n("Show all the items in the system tray in a popup") + Accessible.role: Accessible.Button + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + systemTrayState.expanded = !systemTrayState.expanded; + } + } + + subText: systemTrayState.expanded ? i18n("Close popup") : i18n("Show hidden icons") + + MouseArea { + id: arrowMouseArea + anchors.fill: parent + onClicked: { + systemTrayState.expanded = !systemTrayState.expanded; + expandedRepresentation.hiddenLayout.currentIndex = -1; + } + + readonly property int arrowAnimationDuration: PlasmaCore.Units.shortDuration + + PlasmaCore.Svg { + id: arrowSvg + imagePath: "widgets/arrows" + } + + PlasmaCore.SvgItem { + id: arrow + + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + + rotation: systemTrayState.expanded ? 180 : 0 + Behavior on rotation { + RotationAnimation { + duration: arrowMouseArea.arrowAnimationDuration + } + } + opacity: systemTrayState.expanded ? 0 : 1 + Behavior on opacity { + NumberAnimation { + duration: arrowMouseArea.arrowAnimationDuration + } + } + + svg: arrowSvg + elementId: { + if (plasmoid.location === PlasmaCore.Types.TopEdge) { + return "down-arrow"; + } else if (plasmoid.location === PlasmaCore.Types.LeftEdge) { + return "right-arrow"; + } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { + return "left-arrow"; + } else { + return "up-arrow"; + } + } + } + + PlasmaCore.SvgItem { + anchors.centerIn: parent + width: arrow.width + height: arrow.height + + rotation: systemTrayState.expanded ? 0 : -180 + Behavior on rotation { + RotationAnimation { + duration: arrowMouseArea.arrowAnimationDuration + } + } + opacity: systemTrayState.expanded ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: arrowMouseArea.arrowAnimationDuration + } + } + + svg: arrowSvg + elementId: { + if (plasmoid.location === PlasmaCore.Types.TopEdge) { + return "up-arrow"; + } else if (plasmoid.location === PlasmaCore.Types.LeftEdge) { + return "left-arrow"; + } else if (plasmoid.location === PlasmaCore.Types.RightEdge) { + return "right-arrow"; + } else { + return "down-arrow"; + } + } + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/HiddenItemsView.qml b/plasma/workspace/applets/systemtray/package/contents/ui/HiddenItemsView.qml new file mode 100644 index 0000000000..a5c322daf7 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/HiddenItemsView.qml @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + SPDX-FileCopyrightText: 2020 Nate Graham + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents // For Highlight +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +import "items" + +PlasmaComponents3.ScrollView { + id: hiddenTasksView + + property alias layout: hiddenTasks + + hoverEnabled: true + onHoveredChanged: if (!hovered) { + hiddenTasks.currentIndex = -1; + } + background: null + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + PlasmaComponents3.ScrollBar.vertical.policy: systemTrayState.activeApplet ? PlasmaComponents3.ScrollBar.AlwaysOff : PlasmaComponents3.ScrollBar.AsNeeded + + GridView { + id: hiddenTasks + + readonly property int rows: 4 + readonly property int columns: 4 + + cellWidth: Math.floor(hiddenTasks.width / hiddenTasks.columns) + cellHeight: Math.floor(hiddenTasks.height / hiddenTasks.rows) + + currentIndex: -1 + highlight: PlasmaComponents.Highlight {} + highlightMoveDuration: 0 + + pixelAligned: true + + readonly property int itemCount: model.count + + //! This is used in order to identify the minimum required label height in order for all + //! labels to be aligned properly at all items. At the same time this approach does not + //! enforce labels with 3 lines at all cases so translations that require only one or two + //! lines will always look consistent with no too much padding + readonly property int minLabelHeight: { + var minHeight = 0; + + for(let i in contentItem.children){ + var gridItem = contentItem.children[i]; + if (!gridItem || !gridItem.hasOwnProperty("item") || !gridItem.item.hasOwnProperty("labelHeight")) { + continue; + } + + if (gridItem.item.labelHeight > minHeight) { + minHeight = gridItem.item.labelHeight; + } + } + + return minHeight; + } + + model: PlasmaCore.SortFilterModel { + sourceModel: plasmoid.nativeInterface.systemTrayModel + filterRole: "effectiveStatus" + filterCallback: function(source_row, value) { + return value === PlasmaCore.Types.PassiveStatus + } + } + delegate: ItemLoader {} + + keyNavigationEnabled: true + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus && currentIndex === -1) { + currentIndex = 0 + } else if (!activeFocus && currentIndex >= 0) { + currentIndex = -1 + } + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/PlasmoidPopupsContainer.qml b/plasma/workspace/applets/systemtray/package/contents/ui/PlasmoidPopupsContainer.qml new file mode 100644 index 0000000000..80c1e88058 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/PlasmoidPopupsContainer.qml @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.4 +//needed for units +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 +import org.kde.plasma.extras 2.0 as PlasmaExtras + +StackView { + id: mainStack + focus: true + + Layout.minimumWidth: PlasmaCore.Units.gridUnit * 12 + Layout.minimumHeight: PlasmaCore.Units.gridUnit * 12 + + readonly property Item activeApplet: systemTrayState.activeApplet + + /* Heading */ + property bool appletHasHeading: false + property bool mergeHeadings: appletHasHeading && activeApplet.fullRepresentationItem.header.visible + property int headingHeight: mergeHeadings ? activeApplet.fullRepresentationItem.header.height : 0 + /* Footer */ + property bool appletHasFooter: false + property bool mergeFooters: appletHasFooter && activeApplet.fullRepresentationItem.footer.visible + property int footerHeight: mergeFooters ? activeApplet.fullRepresentationItem.footer.height : 0 + + onActiveAppletChanged: { + mainStack.appletHasHeading = false + mainStack.appletHasFooter = false + if (activeApplet != null) { + //reset any potential anchor + activeApplet.fullRepresentationItem.anchors.left = undefined; + activeApplet.fullRepresentationItem.anchors.top = undefined; + activeApplet.fullRepresentationItem.anchors.right = undefined; + activeApplet.fullRepresentationItem.anchors.bottom = undefined; + activeApplet.fullRepresentationItem.anchors.centerIn = undefined; + activeApplet.fullRepresentationItem.anchors.fill = undefined; + + if (activeApplet.fullRepresentationItem instanceof PlasmaComponents3.Page || + activeApplet.fullRepresentationItem instanceof PlasmaExtras.Representation) { + if (activeApplet.fullRepresentationItem.header && activeApplet.fullRepresentationItem.header instanceof PlasmaExtras.PlasmoidHeading) { + mainStack.appletHasHeading = true + activeApplet.fullRepresentationItem.header.background.visible = false + } + if (activeApplet.fullRepresentationItem.footer && activeApplet.fullRepresentationItem.footer instanceof PlasmaExtras.PlasmoidHeading) { + mainStack.appletHasFooter = true + activeApplet.fullRepresentationItem.footer.background.visible = false + } + } + + mainStack.replace({item: activeApplet.fullRepresentationItem, immediate: !systemTrayState.expanded, properties: {focus: true}}); + } else { + mainStack.replace(emptyPage); + } + } + + onCurrentItemChanged: { + if (currentItem !== null && plasmoid.expanded) { + currentItem.forceActiveFocus(); + } + } + + Connections { + target: plasmoid + function onAppletRemoved(applet) { + if (applet === systemTrayState.activeApplet) { + mainStack.clear() + } + } + } + //used to animate away to nothing + Item { + id: emptyPage + } + + delegate: StackViewDelegate { + id: transitioner + function transitionFinished(properties) { + properties.exitItem.opacity = 1 + } + property bool goingLeft: { + const unFlipped = systemTrayState.oldVisualIndex < systemTrayState.newVisualIndex + + if (Qt.application.layoutDirection == Qt.LeftToRight) { + return unFlipped + } else { + return !unFlipped + } + } + replaceTransition: StackViewTransition { + ParallelAnimation { + PropertyAnimation { + target: enterItem + property: "x" + from: root.vertical ? 0 : (transitioner.goingLeft ? enterItem.width : -enterItem.width) + to: 0 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.shortDuration + } + SequentialAnimation { + PropertyAction { + target: enterItem + property: "opacity" + value: 0 + } + PauseAnimation { + duration: root.vertical ? (PlasmaCore.Units.shortDuration/2) : 0 + } + PropertyAnimation { + target: enterItem + property: "opacity" + from: 0 + to: 1 + easing.type: Easing.InOutQuad + duration: (PlasmaCore.Units.shortDuration/2) + } + } + } + ParallelAnimation { + PropertyAnimation { + target: exitItem + property: "x" + from: 0 + to: root.vertical ? 0 : (transitioner.goingLeft ? -exitItem.width : exitItem.width) + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.shortDuration + } + PropertyAnimation { + target: exitItem + property: "opacity" + from: 1 + to: 0 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.shortDuration/2 + } + } + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/SystemTrayState.qml b/plasma/workspace/applets/systemtray/package/contents/ui/SystemTrayState.qml new file mode 100644 index 0000000000..adb745ed9e --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/SystemTrayState.qml @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 + +//This object contains state of the SystemTray, mainly related to the 'expanded' state +QtObject { + //true if System Tray is 'expanded'. It may be when: + // - there is an active applet or + // - 'Status and Notification' with hidden items is shown + property bool expanded: false + //set when there is an applet selected + property Item activeApplet + + //allow expanded change only when activated at least once + //this is to suppress expanded state change during Plasma startup + property bool acceptExpandedChange: false + + // These properties allow us to keep track of where the expanded applet + // was and is on the panel, allowing PlasmoidPopupContainer.qml to animate + // depending on their locations. + property int oldVisualIndex: -1 + property int newVisualIndex: -1 + + function setActiveApplet(applet, visualIndex) { + if (visualIndex === undefined) { + oldVisualIndex = -1 + newVisualIndex = -1 + } else { + oldVisualIndex = newVisualIndex + newVisualIndex = visualIndex + } + + const oldApplet = activeApplet + activeApplet = applet + if (oldApplet && oldApplet !== applet) { + oldApplet.expanded = false + } + expanded = true + } + + onExpandedChanged: { + if (expanded) { + plasmoid.status = PlasmaCore.Types.RequiresAttentionStatus + } else { + plasmoid.status = PlasmaCore.Types.PassiveStatus; + if (activeApplet) { + // if not expanded we don't have an active applet anymore + activeApplet.expanded = false + activeApplet = null + } + } + acceptExpandedChange = false + plasmoid.expanded = expanded + } + + //listen on SystemTray AppletInterface signals + property Connections plasmoidConnections: Connections { + target: plasmoid + //emitted when activation is requested, for example by using a global keyboard shortcut + function onActivated() { + acceptExpandedChange = true + } + function onExpandedChanged() { + if (acceptExpandedChange) { + expanded = plasmoid.expanded + } else { + plasmoid.expanded = expanded + } + } + } + + property Connections activeAppletConnections: Connections { + target: activeApplet + + function onExpandedChanged() { + if (!activeApplet.expanded) { + expanded = false + } + } + } + +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/items/AbstractItem.qml b/plasma/workspace/applets/systemtray/package/contents/ui/items/AbstractItem.qml new file mode 100644 index 0000000000..f6e9805086 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/items/AbstractItem.qml @@ -0,0 +1,202 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + SPDX-FileCopyrightText: 2020 Nate Graham + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents3 + +PlasmaCore.ToolTipArea { + id: abstractItem + + height: inVisibleLayout ? visibleLayout.cellHeight : hiddenTasks.cellHeight + width: inVisibleLayout ? visibleLayout.cellWidth : hiddenTasks.cellWidth + + property var model: itemModel + + property string itemId + property alias text: label.text + property alias labelHeight: label.implicitHeight + property alias iconContainer: iconContainer + property int /*PlasmaCore.Types.ItemStatus*/ status: model.status || PlasmaCore.Types.UnknownStatus + property int /*PlasmaCore.Types.ItemStatus*/ effectiveStatus: model.effectiveStatus || PlasmaCore.Types.UnknownStatus + readonly property bool inHiddenLayout: effectiveStatus === PlasmaCore.Types.PassiveStatus + readonly property bool inVisibleLayout: effectiveStatus === PlasmaCore.Types.ActiveStatus + + // input agnostic way to trigger the main action + signal activated(var pos) + + // proxy signals for MouseArea + signal clicked(var mouse) + signal pressed(var mouse) + signal wheel(var wheel) + signal contextMenu(var mouse) + + // Make sure the proper item manages the keyboard + onActiveFocusChanged: { + if (activeFocus) { + iconContainer.forceActiveFocus(); + } + } + + /* subclasses need to assign to this tooltip properties + mainText: + subText: + */ + + location: { + if (inHiddenLayout) { + if (LayoutMirroring.enabled && plasmoid.location !== PlasmaCore.Types.RightEdge) { + return PlasmaCore.Types.LeftEdge; + } else if (plasmoid.location !== PlasmaCore.Types.LeftEdge) { + return PlasmaCore.Types.RightEdge; + } + } + + return plasmoid.location; + } + + PulseAnimation { + targetItem: iconContainer + running: (abstractItem.status === PlasmaCore.Types.NeedsAttentionStatus || + abstractItem.status === PlasmaCore.Types.RequiresAttentionStatus ) && + PlasmaCore.Units.longDuration > 0 + } + + function startActivatedAnimation() { + activatedAnimation.start() + } + + SequentialAnimation { + id: activatedAnimation + loops: 1 + + ScaleAnimator { + target: iconContainer + from: 1 + to: 0.5 + duration: PlasmaCore.Units.shortDuration + easing.type: Easing.InQuad + } + + ScaleAnimator { + target: iconContainer + from: 0.5 + to: 1 + duration: PlasmaCore.Units.shortDuration + easing.type: Easing.OutQuad + } + } + + MouseArea { + propagateComposedEvents: true + // This needs to be above applets when it's in the grid hidden area + // so that it can receive hover events while the mouse is over an applet, + // but below them on regular systray, so collapsing works + z: inHiddenLayout ? 1 : 0 + anchors.fill: abstractItem + hoverEnabled: true + drag.filterChildren: true + // Necessary to make the whole delegate area forward all mouse events + acceptedButtons: Qt.AllButtons + // Using onPositionChanged instead of onEntered because changing the + // index in a scrollable view also changes the view position. + // onEntered will change the index while the items are scrolling, + // making it harder to scroll. + onPositionChanged: if (inHiddenLayout) { + root.hiddenLayout.currentIndex = index + } + onClicked: abstractItem.clicked(mouse) + onPressed: { + if (inHiddenLayout) { + root.hiddenLayout.currentIndex = index + } + abstractItem.hideImmediately() + abstractItem.pressed(mouse) + } + onPressAndHold: if (mouse.button === Qt.LeftButton) { + abstractItem.contextMenu(mouse) + } + onWheel: { + abstractItem.wheel(wheel); + //Don't accept the event in order to make the scrolling by mouse wheel working + //for the parent scrollview this icon is in. + wheel.accepted = false; + } + } + + ColumnLayout { + anchors.fill: abstractItem + spacing: 0 + + FocusScope { + id: iconContainer + activeFocusOnTab: true + Accessible.name: abstractItem.text + Accessible.description: abstractItem.subText + Accessible.role: Accessible.Button + Accessible.onPressAction: abstractItem.activated(Qt.point(iconContainer.width/2, iconContainer.height/2)); + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Space: + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Select: + abstractItem.activated(Qt.point(width/2, height/2)); + break; + case Qt.Key_Menu: + abstractItem.contextMenu(null); + event.accepted = true; + break; + } + } + + property alias container: abstractItem + property alias inVisibleLayout: abstractItem.inVisibleLayout + readonly property int size: abstractItem.inVisibleLayout ? root.itemSize : PlasmaCore.Units.iconSizes.medium + + Layout.alignment: Qt.Bottom | Qt.AlignHCenter + Layout.fillHeight: abstractItem.inHiddenLayout ? true : false + implicitWidth: root.vertical && abstractItem.inVisibleLayout ? abstractItem.width : size + implicitHeight: !root.vertical && abstractItem.inVisibleLayout ? abstractItem.height : size + Layout.topMargin: abstractItem.inHiddenLayout ? Math.round(PlasmaCore.Units.smallSpacing * 1.5): 0 + } + PlasmaComponents3.Label { + id: label + + Layout.fillWidth: true + Layout.fillHeight: abstractItem.inHiddenLayout ? true : false + //! Minimum required height for all labels is used in order for all + //! labels to be aligned properly at all items. At the same time this approach does not + //! enforce labels with 3 lines at all cases so translations that require only one or two + //! lines will always look consistent with no too much padding + Layout.minimumHeight: abstractItem.inHiddenLayout ? hiddenTasks.minLabelHeight : 0 + Layout.leftMargin: abstractItem.inHiddenLayout ? PlasmaCore.Units.smallSpacing : 0 + Layout.rightMargin: abstractItem.inHiddenLayout ? PlasmaCore.Units.smallSpacing : 0 + Layout.bottomMargin: abstractItem.inHiddenLayout ? PlasmaCore.Units.smallSpacing : 0 + + visible: abstractItem.inHiddenLayout + + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 3 + + opacity: visible ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + } + } +} + diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/items/ItemLoader.qml b/plasma/workspace/applets/systemtray/package/contents/ui/items/ItemLoader.qml new file mode 100644 index 0000000000..0135ff7058 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/items/ItemLoader.qml @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 + +Loader { + id: itemLoader + + z: x + property var itemModel: model + onActiveFocusChanged: { + if (activeFocus && item) { + item.forceActiveFocus(); + } + } + + source: { + if (model.itemType === "Plasmoid" && model.hasApplet) { + return "PlasmoidItem.qml" + } else if (model.itemType === "StatusNotifier") { + return "StatusNotifierItem.qml" + } + console.warn("SystemTray ItemLoader: Invalid state, cannot determine source!") + return "" + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/items/PlasmoidItem.qml b/plasma/workspace/applets/systemtray/package/contents/ui/items/PlasmoidItem.qml new file mode 100644 index 0000000000..d0c224b7a0 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/items/PlasmoidItem.qml @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 +import QtQml 2.15 + +import org.kde.plasma.core 2.0 as PlasmaCore + +AbstractItem { + id: plasmoidContainer + + property Item applet: model.applet || null + text: applet ? applet.title : "" + + itemId: applet ? applet.pluginName : "" + mainText: applet ? applet.toolTipMainText : "" + subText: applet ? applet.toolTipSubText : "" + mainItem: applet && applet.toolTipItem ? applet.toolTipItem : null + textFormat: applet ? applet.toolTipTextFormat : "" + active: systemTrayState.activeApplet !== applet + + // FIXME: Use an input type agnostic way to activate whatever the primary + // action of a plasmoid is supposed to be, even if it's just expanding the + // plasmoid. Not all plasmoids are supposed to expand and not all plasmoids + // do anything with onActivated. + onActivated: { + if (applet) { + applet.nativeInterface.activated() + } + } + + onClicked: { + if (!applet) { + return + } + //forward click event to the applet + const mouseArea = findMouseArea(applet.compactRepresentationItem) + if (mouseArea) { + mouseArea.clicked(mouse) + } else if (mouse.button === Qt.LeftButton) {//falback + plasmoidContainer.activated(null) + } + } + onPressed: { + // Only Plasmoids can show context menu on the mouse pressed event. + // SNI has few problems, for example legacy applications that still use XEmbed require mouse to be released. + if (mouse.button === Qt.RightButton) { + plasmoidContainer.contextMenu(mouse); + } + } + onContextMenu: if (applet) { + plasmoid.nativeInterface.showPlasmoidMenu(applet, 0, + plasmoidContainer.inHiddenLayout ? applet.height : 0); + } + onWheel: { + if (!applet) { + return + } + //forward wheel event to the applet + const mouseArea = findMouseArea(applet.compactRepresentationItem) + if (mouseArea) { + mouseArea.wheel(wheel) + } + } + + //some heuristics to find MouseArea + function findMouseArea(item) { + if (!item) { + return null + } + + if (item instanceof MouseArea) { + return item + } + for (var i = 0; i < item.children.length; i++) { + const child = item.children[i] + if (child instanceof MouseArea && child.enabled) { + //check if MouseArea covers the entire item + if (child.anchors.fill === item || (child.x === 0 && child.y === 0 && child.height === item.height && child.width === item.width)) { + return child + } + } + } + + return null + } + + //This is to make preloading effective, minimizes the scene changes + function preloadFullRepresentationItem(fullRepresentationItem) { + if (fullRepresentationItem && fullRepresentationItem.parent === null) { + fullRepresentationItem.width = expandedRepresentation.width + fullRepresentationItem.width = expandedRepresentation.height + fullRepresentationItem.parent = preloadedStorage; + } + } + + onAppletChanged: { + if (applet) { + applet.parent = plasmoidContainer.iconContainer + applet.anchors.fill = applet.parent + applet.visible = true + + preloadFullRepresentationItem(applet.fullRepresentationItem) + } + } + + Connections { + target: applet + + //activation using global keyboard shortcut + function onActivated() { + plasmoidContainer.startActivatedAnimation() + } + + function onExpandedChanged(expanded) { + if (expanded) { + systemTrayState.setActiveApplet(applet, model.row) + plasmoidContainer.startActivatedAnimation() + } + } + + function onFullRepresentationItemChanged(fullRepresentationItem) { + preloadFullRepresentationItem(fullRepresentationItem) + } + } + + Binding { + property: "hideOnWindowDeactivate" + value: !plasmoid.configuration.pin + target: plasmoidContainer.applet + when: null !== plasmoidContainer.applet + restoreMode: Binding.RestoreBinding + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/items/PulseAnimation.qml b/plasma/workspace/applets/systemtray/package/contents/ui/items/PulseAnimation.qml new file mode 100644 index 0000000000..b3c1ae7321 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/items/PulseAnimation.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.2 +import org.kde.plasma.core 2.0 as PlasmaCore + +SequentialAnimation { + id: pulseAnimation + objectName: "pulseAnimation" + + property Item targetItem + readonly property int duration: PlasmaCore.Units.veryLongDuration * 5 + + loops: Animation.Infinite + alwaysRunToEnd: true + + ScaleAnimator { + target: targetItem + from: 1 + to: 1.2 + duration: pulseAnimation.duration * 0.15 + easing.type: Easing.InQuad + } + + ScaleAnimator { + target: targetItem + from: 1.2 + to: 1 + duration: pulseAnimation.duration * 0.15 + easing.type: Easing.InQuad + } + + PauseAnimation { + duration: pulseAnimation.duration * 0.7 + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml b/plasma/workspace/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml new file mode 100644 index 0000000000..cc7bbc3c78 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 +import org.kde.plasma.core 2.0 as PlasmaCore + +AbstractItem { + id: taskIcon + + itemId: model.Id + text: model.Title || model.ToolTipTitle + mainText: model.ToolTipTitle !== "" ? model.ToolTipTitle : model.Title + subText: model.ToolTipSubTitle + textFormat: Text.AutoText + + PlasmaCore.IconItem { + id: iconItem + parent: taskIcon.iconContainer + anchors.fill: iconItem.parent + + source: { + if (model.status === PlasmaCore.Types.NeedsAttentionStatus) { + if (model.AttentionIcon) { + return model.AttentionIcon + } + if (model.AttentionIconName) { + return model.AttentionIconName + } + } + return model.Icon ? model.Icon : model.IconName + } + active: taskIcon.containsMouse + } + + onActivated: { + let service = model.Service; + let operation = service.operationDescription("Activate"); + operation.x = pos.x; //mouseX + operation.y = pos.y; //mouseY + let job = service.startOperationCall(operation); + job.finished.connect(() => { + if (!job.result) { + // On error try to invoke the context menu. + // Workaround primarily for apps using libappindicator. + openContextMenu(pos); + } + }) + taskIcon.startActivatedAnimation(); + } + + onContextMenu: { + openContextMenu(plasmoid.nativeInterface.popupPosition(taskIcon, mouse.x, mouse.y)) + } + + onClicked: { + var pos = plasmoid.nativeInterface.popupPosition(taskIcon, mouse.x, mouse.y); + switch (mouse.button) { + case Qt.LeftButton: + taskIcon.activated(pos) + break; + case Qt.RightButton: + openContextMenu(pos); + break; + case Qt.MiddleButton: + var operation = service.operationDescription("SecondaryActivate"); + let service = model.Service; + operation.x = pos.x; + + operation.y = pos.y; + service.startOperationCall(operation); + taskIcon.startActivatedAnimation() + break; + } + } + + function openContextMenu(pos = Qt.point(width/2, height/2)) { + var service = model.Service; + var operation = service.operationDescription("ContextMenu"); + operation.x = pos.x; + operation.y = pos.y; + + var job = service.startOperationCall(operation); + job.finished.connect(function () { + plasmoid.nativeInterface.showStatusNotifierContextMenu(job, taskIcon); + }); + } + + onWheel: { + //don't send activateVertScroll with a delta of 0, some clients seem to break (kmix) + if (wheel.angleDelta.y !== 0) { + var service = model.Service; + var operation = service.operationDescription("Scroll"); + operation.delta =wheel.angleDelta.y; + operation.direction = "Vertical"; + service.startOperationCall(operation); + } + if (wheel.angleDelta.x !== 0) { + var service = model.Service; + var operation = service.operationDescription("Scroll"); + operation.delta =wheel.angleDelta.x; + operation.direction = "Horizontal"; + service.startOperationCall(operation); + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/contents/ui/main.qml b/plasma/workspace/applets/systemtray/package/contents/ui/main.qml new file mode 100644 index 0000000000..7ef26defb4 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/contents/ui/main.qml @@ -0,0 +1,279 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 +import org.kde.draganddrop 2.0 as DnD +import org.kde.kirigami 2.5 as Kirigami // For Settings.tabletMode + +import "items" + +MouseArea { + id: root + + readonly property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical + + Layout.minimumWidth: vertical ? PlasmaCore.Units.iconSizes.small : mainLayout.implicitWidth + PlasmaCore.Units.smallSpacing + Layout.minimumHeight: vertical ? mainLayout.implicitHeight + PlasmaCore.Units.smallSpacing : PlasmaCore.Units.iconSizes.small + + LayoutMirroring.enabled: !vertical && Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + readonly property alias systemTrayState: systemTrayState + readonly property alias itemSize: tasksGrid.itemSize + readonly property alias visibleLayout: tasksGrid + readonly property alias hiddenLayout: expandedRepresentation.hiddenLayout + readonly property bool oneRowOrColumn: tasksGrid.rowsOrColumns == 1 + + onWheel: { + // Don't propagate unhandled wheel events + wheel.accepted = true; + } + + SystemTrayState { + id: systemTrayState + } + + //being there forces the items to fully load, and they will be reparented in the popup one by one, this item is *never* visible + Item { + id: preloadedStorage + visible: false + } + + CurrentItemHighLight { + location: plasmoid.location + parent: root + } + + DnD.DropArea { + anchors.fill: parent + + preventStealing: true; + + /** Extracts the name of the system tray applet in the drag data if present + * otherwise returns null*/ + function systemTrayAppletName(event) { + if (event.mimeData.formats.indexOf("text/x-plasmoidservicename") < 0) { + return null; + } + var plasmoidId = event.mimeData.getDataAsByteArray("text/x-plasmoidservicename"); + + if (!plasmoid.nativeInterface.isSystemTrayApplet(plasmoidId)) { + return null; + } + return plasmoidId; + } + + onDragEnter: { + if (!systemTrayAppletName(event)) { + event.ignore(); + } + } + + onDrop: { + var plasmoidId = systemTrayAppletName(event); + if (!plasmoidId) { + event.ignore(); + return; + } + + if (plasmoid.configuration.extraItems.indexOf(plasmoidId) < 0) { + var extraItems = plasmoid.configuration.extraItems; + extraItems.push(plasmoidId); + plasmoid.configuration.extraItems = extraItems; + } + } + } + + //Main Layout + GridLayout { + id: mainLayout + + rowSpacing: 0 + columnSpacing: 0 + anchors.fill: parent + + flow: vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight + + GridView { + id: tasksGrid + + Layout.alignment: Qt.AlignCenter + + interactive: false //disable features we don't need + flow: vertical ? GridView.LeftToRight : GridView.TopToBottom + + // The icon size to display when not using the auto-scaling setting + readonly property int smallIconSize: PlasmaCore.Units.iconSizes.smallMedium + + // Automatically use autoSize setting when in tablet mode, if it's + // not already being used + readonly property bool autoSize: plasmoid.configuration.scaleIconsToFit || Kirigami.Settings.tabletMode + + readonly property int gridThickness: root.vertical ? root.width : root.height + // Should change to 2 rows/columns on a 56px panel (in standard DPI) + readonly property int rowsOrColumns: autoSize ? 1 : Math.max(1, Math.min(count, Math.floor(gridThickness / (smallIconSize + PlasmaCore.Units.smallSpacing)))) + + // Add margins only if the panel is larger than a small icon (to avoid large gaps between tiny icons) + readonly property int cellSpacing: PlasmaCore.Units.smallSpacing * (Kirigami.Settings.tabletMode ? 4 : plasmoid.configuration.iconSpacing) + readonly property int smallSizeCellLength: gridThickness < smallIconSize ? smallIconSize : smallIconSize + cellSpacing + + cellHeight: { + if (root.vertical) { + return autoSize ? itemSize + (gridThickness < itemSize ? 0 : cellSpacing) : smallSizeCellLength + } else { + return autoSize ? root.height : Math.floor(root.height / rowsOrColumns) + } + } + cellWidth: { + if (root.vertical) { + return autoSize ? root.width : Math.floor(root.width / rowsOrColumns) + } else { + return autoSize ? itemSize + (gridThickness < itemSize ? 0 : cellSpacing) : smallSizeCellLength + } + } + + //depending on the form factor, we are calculating only one dimension, second is always the same as root/parent + implicitHeight: root.vertical ? cellHeight * Math.ceil(count / rowsOrColumns) : root.height + implicitWidth: !root.vertical ? cellWidth * Math.ceil(count / rowsOrColumns) : root.width + + readonly property int itemSize: { + if (autoSize) { + return PlasmaCore.Units.roundToIconSize(Math.min(Math.min(root.width, root.height) / rowsOrColumns, PlasmaCore.Units.iconSizes.enormous)) + } else { + return smallIconSize + } + } + + model: PlasmaCore.SortFilterModel { + sourceModel: plasmoid.nativeInterface.systemTrayModel + filterRole: "effectiveStatus" + filterCallback: function(source_row, value) { + return value === PlasmaCore.Types.ActiveStatus + } + } + + delegate: ItemLoader { + id: delegate + // We need to recalculate the stacking order of the z values due to how keyboard navigation works + // the tab order depends exclusively from this, so we redo it as the position in the list + // ensuring tab navigation focuses the expected items + Component.onCompleted: { + let item = tasksGrid.itemAtIndex(index - 1); + if (item) { + plasmoid.nativeInterface.stackItemBefore(delegate, item) + } else { + item = tasksGrid.itemAtIndex(index + 1); + } + if (item) { + plasmoid.nativeInterface.stackItemAfter(delegate, item) + } + } + } + + add: Transition { + enabled: itemSize > 0 + + NumberAnimation { + property: "scale" + from: 0 + to: 1 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration + } + } + + displaced: Transition { + //ensure scale value returns to 1.0 + //https://doc.qt.io/qt-5/qml-qtquick-viewtransition.html#handling-interrupted-animations + NumberAnimation { + property: "scale" + to: 1 + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration + } + } + + move: Transition { + NumberAnimation { + properties: "x,y" + easing.type: Easing.InOutQuad + duration: PlasmaCore.Units.longDuration + } + } + } + + ExpanderArrow { + id: expander + Layout.fillWidth: vertical + Layout.fillHeight: !vertical + Layout.alignment: vertical ? Qt.AlignVCenter : Qt.AlignHCenter + iconSize: tasksGrid.itemSize + visible: root.hiddenLayout.itemCount > 0 + } + } + + //Main popup + PlasmaCore.Dialog { + id: dialog + visualParent: root + flags: Qt.WindowStaysOnTopHint + location: plasmoid.location + hideOnWindowDeactivate: !plasmoid.configuration.pin + visible: systemTrayState.expanded + backgroundHints: (plasmoid.containmentDisplayHints & PlasmaCore.Types.DesktopFullyCovered) ? PlasmaCore.Dialog.SolidBackground : PlasmaCore.Dialog.StandardBackground + + onVisibleChanged: { + systemTrayState.expanded = visible; + if (!systemTrayState.expanded) { + return; + } + + if (expandedRepresentation.plasmoidContainer.visible) { + expandedRepresentation.plasmoidContainer.forceActiveFocus(); + } else if (expandedRepresentation.hiddenLayout.visible) { + expandedRepresentation.hiddenLayout.forceActiveFocus(); + } + } + mainItem: ExpandedRepresentation { + id: expandedRepresentation + + Keys.onEscapePressed: { + systemTrayState.expanded = false + } + + // Draws a line between the applet dialog and the panel + PlasmaCore.SvgItem { + // Only draw for popups of panel applets, not desktop applets + visible: [PlasmaCore.Types.TopEdge, PlasmaCore.Types.LeftEdge, PlasmaCore.Types.RightEdge, PlasmaCore.Types.BottomEdge] + .includes(plasmoid.location) + anchors { + top: plasmoid.location == PlasmaCore.Types.BottomEdge ? undefined : parent.top + left: plasmoid.location == PlasmaCore.Types.RightEdge ? undefined : parent.left + right: plasmoid.location == PlasmaCore.Types.LeftEdge ? undefined : parent.right + bottom: plasmoid.location == PlasmaCore.Types.TopEdge ? undefined : parent.bottom + topMargin: plasmoid.location == PlasmaCore.Types.BottomEdge ? undefined : -dialog.margins.top + leftMargin: plasmoid.location == PlasmaCore.Types.RightEdge ? undefined : -dialog.margins.left + rightMargin: plasmoid.location == PlasmaCore.Types.LeftEdge ? undefined : -dialog.margins.right + bottomMargin: plasmoid.location == PlasmaCore.Types.TopEdge ? undefined : -dialog.margins.bottom + } + height: (plasmoid.location == PlasmaCore.Types.TopEdge || plasmoid.location == PlasmaCore.Types.BottomEdge) ? 1 : undefined + width: (plasmoid.location == PlasmaCore.Types.LeftEdge || plasmoid.location == PlasmaCore.Types.RightEdge) ? 1 : undefined + z: 999 /* Draw the line on top of the applet */ + elementId: (plasmoid.location == PlasmaCore.Types.TopEdge || plasmoid.location == PlasmaCore.Types.BottomEdge) ? "horizontal-line" : "vertical-line" + svg: PlasmaCore.Svg { + imagePath: "widgets/line" + } + } + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + } + } +} diff --git a/plasma/workspace/applets/systemtray/package/metadata.json b/plasma/workspace/applets/systemtray/package/metadata.json new file mode 100644 index 0000000000..57d87c99e6 --- /dev/null +++ b/plasma/workspace/applets/systemtray/package/metadata.json @@ -0,0 +1,148 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "Windows and Tasks", + "EnabledByDefault": true, + "FormFactors": [ + "desktop" + ], + "Icon": "preferences-desktop-notification", + "Id": "org.kde.plasma.private.systemtray", + "License": "GPL-2.0+", + "Name": "System Tray", + "Name[af]": "Stelsellaai", + "Name[ar]": "صينية النظام", + "Name[ast]": "Bandexa del sistema", + "Name[az]": "Sistem Çəkməcəsi", + "Name[be@latin]": "Systemny trej", + "Name[be]": "Сістэмны трэй", + "Name[bg]": "Системен панел", + "Name[bn]": "সিস্টেম ট্রে", + "Name[bn_IN]": "সিস্টেম ট্রে", + "Name[br]": "Barlenn ar reizhiad", + "Name[bs]": "Sistemska kaseta", + "Name[ca@valencia]": "Safata del sistema", + "Name[ca]": "Safata del sistema", + "Name[cs]": "Systémová část panelu", + "Name[csb]": "Systemòwi zabiérnik", + "Name[cy]": "Bar Tasgau", + "Name[da]": "Statusområde", + "Name[de]": "Systemabschnitt der Kontrollleiste", + "Name[el]": "Πλαίσιο συστήματος", + "Name[en_GB]": "System Tray", + "Name[eo]": "Sistempleto", + "Name[es]": "Bandeja del sistema", + "Name[et]": "Süsteemne dokk", + "Name[eu]": "Sistema-erretilua", + "Name[fa]": "سینی سیستم", + "Name[fi]": "Ilmoitusalue", + "Name[fr]": "Boîte à miniatures", + "Name[fy]": "Systeemfak", + "Name[ga]": "Tráidire an Chórais", + "Name[gl]": "Bandexa do sistema", + "Name[gu]": "સિસ્ટમ ટ્રે", + "Name[he]": "מגש המערכת", + "Name[hi]": "तंत्र तश्तरी", + "Name[hne]": "तंत्र तस्तरी", + "Name[hr]": "Sistemski blok", + "Name[hsb]": "Systemowa wotkładka", + "Name[hu]": "Paneltálca", + "Name[ia]": "Tabuliero de systema", + "Name[id]": "System Tray", + "Name[is]": "Kerfisbakki", + "Name[it]": "Vassoio di sistema", + "Name[ja]": "システムトレイ", + "Name[ka]": "სისტემური პანელი", + "Name[kk]": "Жүйелік сөре", + "Name[km]": "ថាស​ប្រព័ន្ធ", + "Name[kn]": "ವ್ಯವಸ್ಥಾ ಖಾನೆ (ಟ್ರೇ)", + "Name[ko]": "시스템 트레이", + "Name[lt]": "Sistemos dėklas", + "Name[lv]": "Sistēmas ikonu josla", + "Name[mai]": "तंत्र तश्तरी", + "Name[mk]": "Системска лента", + "Name[ml]": "സിസ്റ്റം ട്രേ", + "Name[mr]": "प्रणाली ट्रे", + "Name[ms]": "Dulang Sistem", + "Name[nb]": "Systemkurv", + "Name[nds]": "Paneel-Systeemafsnitt", + "Name[ne]": "प्रणाली ट्रे", + "Name[nl]": "Systeemvak", + "Name[nn]": "Systemtrau", + "Name[or]": "ତନ୍ତ୍ର ଧାରକ", + "Name[pa]": "ਸਿਸਟਮ ਟਰੇ", + "Name[pl]": "Tacka systemowa", + "Name[pt]": "Bandeja do Sistema", + "Name[pt_BR]": "Área de notificação", + "Name[ro]": "Tavă de sistem", + "Name[ru]": "Системный лоток", + "Name[se]": "Vuogádatgárcu", + "Name[si]": "පද්ධතිය තැටිය", + "Name[sk]": "Systémová lišta", + "Name[sl]": "Sistemska vrstica", + "Name[sr@ijekavian]": "системска касета", + "Name[sr@ijekavianlatin]": "sistemska kaseta", + "Name[sr@latin]": "sistemska kaseta", + "Name[sr]": "системска касета", + "Name[sv]": "Systembricka", + "Name[ta]": "சாதனத் தட்டு", + "Name[te]": "వ్యవస్థ ట్రె", + "Name[tg]": "Лавҳачаи низомӣ", + "Name[th]": "ถาดระบบ", + "Name[tr]": "Sistem Çekmecesi", + "Name[ug]": "سىستېما قوندىقى", + "Name[uk]": "Системний лоток", + "Name[vi]": "Khay hệ thống", + "Name[wa]": "Boesse ås imådjetes sistinme", + "Name[x-test]": "xxSystem Trayxx", + "Name[xh]": "Itreyi Yendlela yokusebenza", + "Name[zh_CN]": "系统托盘", + "Name[zh_TW]": "系統匣", + "ServiceTypes": [ + "Plasma/Applet", + "Plasma/Containment" + ], + "Version": "1.0", + "Website": "https://www.kde.org/plasma-desktop" + }, + "NoDisplay": true, + "X-Plasma-API": "declarativeappletscript", + "X-Plasma-ContainmentType": "Panel", + "X-Plasma-MainScript": "ui/main.qml" +} diff --git a/plasma/workspace/applets/systemtray/plasmoidregistry.cpp b/plasma/workspace/applets/systemtray/plasmoidregistry.cpp new file mode 100644 index 0000000000..b509d7ca6d --- /dev/null +++ b/plasma/workspace/applets/systemtray/plasmoidregistry.cpp @@ -0,0 +1,167 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plasmoidregistry.h" +#include "debug.h" + +#include "dbusserviceobserver.h" +#include "systemtraysettings.h" + +#include +#include + +#include + +PlasmoidRegistry::PlasmoidRegistry(QPointer settings, QObject *parent) + : QObject(parent) + , m_settings(settings) + , m_dbusObserver(new DBusServiceObserver(settings, this)) +{ + connect(m_dbusObserver, &DBusServiceObserver::serviceStarted, this, &PlasmoidRegistry::plasmoidEnabled); + connect(m_dbusObserver, &DBusServiceObserver::serviceStopped, this, &PlasmoidRegistry::plasmoidStopped); +} + +void PlasmoidRegistry::init() +{ + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Applet"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageInstalled"), + this, + SLOT(packageInstalled(QString))); + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Applet"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageUpdated"), + this, + SLOT(packageInstalled(QString))); + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Applet"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageUninstalled"), + this, + SLOT(packageUninstalled(QString))); + + connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &PlasmoidRegistry::onEnabledPluginsChanged); + + for (const auto &info : Plasma::PluginLoader::self()->listAppletMetaData(QString())) { + registerPlugin(info); + } + + m_dbusObserver->initDBusActivatables(); + + sanitizeSettings(); +} + +QMap PlasmoidRegistry::systemTrayApplets() +{ + return m_systrayApplets; +} + +bool PlasmoidRegistry::isSystemTrayApplet(const QString &pluginId) +{ + return m_systrayApplets.contains(pluginId); +} + +void PlasmoidRegistry::onEnabledPluginsChanged(const QStringList &enabledPlugins, const QStringList &disabledPlugins) +{ + for (const QString &pluginId : enabledPlugins) { + if (m_systrayApplets.contains(pluginId) && !m_dbusObserver->isDBusActivable(pluginId)) { + Q_EMIT plasmoidEnabled(pluginId); + } + } + for (const QString &pluginId : disabledPlugins) { + if (m_systrayApplets.contains(pluginId)) { + Q_EMIT plasmoidDisabled(pluginId); + } + } +} + +void PlasmoidRegistry::packageInstalled(const QString &pluginId) +{ + qCDebug(SYSTEM_TRAY) << "New package installed" << pluginId; + + if (m_systrayApplets.contains(pluginId)) { + if (m_settings->isEnabledPlugin(pluginId) && !m_dbusObserver->isDBusActivable(pluginId)) { + // restart plasmoid + Q_EMIT plasmoidStopped(pluginId); + Q_EMIT plasmoidEnabled(pluginId); + } + return; + } + + for (const auto &info : Plasma::PluginLoader::self()->listAppletMetaData(QString())) { + if (info.pluginId() == pluginId) { + registerPlugin(info); + } + } +} + +void PlasmoidRegistry::packageUninstalled(const QString &pluginId) +{ + qCDebug(SYSTEM_TRAY) << "Package uninstalled" << pluginId; + if (m_systrayApplets.contains(pluginId)) { + unregisterPlugin(pluginId); + } +} + +void PlasmoidRegistry::registerPlugin(const KPluginMetaData &pluginMetaData) +{ + if (!pluginMetaData.isValid() || pluginMetaData.value(QStringLiteral("X-Plasma-NotificationArea")) != "true") { + return; + } + + const QString &pluginId = pluginMetaData.pluginId(); + + m_systrayApplets[pluginId] = pluginMetaData; + m_dbusObserver->registerPlugin(pluginMetaData); + + Q_EMIT pluginRegistered(pluginMetaData); + + // add plasmoid if is both not enabled explicitly and not already known + if (pluginMetaData.isEnabledByDefault()) { + const QString &candidate = pluginMetaData.pluginId(); + if (!m_settings->isKnownPlugin(candidate)) { + m_settings->addKnownPlugin(candidate); + if (!m_settings->isEnabledPlugin(candidate)) { + m_settings->addEnabledPlugin(candidate); + } + } + } + + if (m_settings->isEnabledPlugin(pluginId) && !m_dbusObserver->isDBusActivable(pluginId)) { + Q_EMIT plasmoidEnabled(pluginId); + } +} + +void PlasmoidRegistry::unregisterPlugin(const QString &pluginId) +{ + Q_EMIT pluginUnregistered(pluginId); + + m_dbusObserver->unregisterPlugin(pluginId); + m_systrayApplets.remove(pluginId); + + m_settings->cleanupPlugin(pluginId); +} + +void PlasmoidRegistry::sanitizeSettings() +{ + // remove all no longer available in the system (e.g. uninstalled) + const QStringList knownPlugins = m_settings->knownPlugins(); + for (const QString &pluginId : knownPlugins) { + if (!m_systrayApplets.contains(pluginId)) { + m_settings->removeKnownPlugin(pluginId); + } + } + + const QStringList enabledPlugins = m_settings->enabledPlugins(); + for (const QString &pluginId : enabledPlugins) { + if (!m_systrayApplets.contains(pluginId)) { + m_settings->removeEnabledPlugin(pluginId); + } + } +} diff --git a/plasma/workspace/applets/systemtray/plasmoidregistry.h b/plasma/workspace/applets/systemtray/plasmoidregistry.h new file mode 100644 index 0000000000..b81fcbb8ef --- /dev/null +++ b/plasma/workspace/applets/systemtray/plasmoidregistry.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class DBusServiceObserver; +class KPluginMetaData; +class SystemTraySettings; + +class PlasmoidRegistry : public QObject +{ + Q_OBJECT +public: + explicit PlasmoidRegistry(QPointer settings, QObject *parent = nullptr); + + void init(); + + virtual QMap systemTrayApplets(); + bool isSystemTrayApplet(const QString &pluginId); + +Q_SIGNALS: + /** + * @brief Emitted when new plasmoid configuration is registered. + * Emitted on initial run or when new plasmoid is installed in the system. + * + * @param pluginMetaData @see KPluginMetaData + */ + void pluginRegistered(const KPluginMetaData &pluginMetaData); + /** + * @brief Emitten when new plasmoid configuration is unregistered. + * As of now, the only case is when plasmoid is uninstalled for the system. + * + * @param pluginId also known as applet Id + */ + void pluginUnregistered(const QString &pluginId); + + /** + * @brief Emittend when plasmoid id enabled. + * Note: this signal can be emitted for already enabled plasmoid. + * + * @param pluginId also known as applet Id + */ + void plasmoidEnabled(const QString &pluginId); + /** + * @brief Emitted when plasmoid should be stopped, for example when DBus service is no longer available. + * Note: this signal can be emitted for already stopped plasmoid. + * + * @param pluginId also known as applet Id + */ + void plasmoidStopped(const QString &pluginId); + /** + * @brief Emitted when plasmoid is disabled entirely by user. + * + * @param pluginId also known as applet Id + */ + void plasmoidDisabled(const QString &pluginId); + +private Q_SLOTS: + void onEnabledPluginsChanged(const QStringList &enabledPlugins, const QStringList &disabledPlugins); + void packageInstalled(const QString &pluginId); + void packageUninstalled(const QString &pluginId); + +private: + void registerPlugin(const KPluginMetaData &pluginMetaData); + void unregisterPlugin(const QString &pluginId); + void sanitizeSettings(); + + QPointer m_settings; + QPointer m_dbusObserver; + + QMap m_systrayApplets; +}; diff --git a/plasma/workspace/applets/systemtray/sortedsystemtraymodel.cpp b/plasma/workspace/applets/systemtray/sortedsystemtraymodel.cpp new file mode 100644 index 0000000000..68e98cf59f --- /dev/null +++ b/plasma/workspace/applets/systemtray/sortedsystemtraymodel.cpp @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "sortedsystemtraymodel.h" +#include "debug.h" +#include "systemtraymodel.h" + +#include + +static const QList s_categoryOrder = { + QStringLiteral("UnknownCategory"), + QStringLiteral("ApplicationStatus"), + QStringLiteral("Communications"), + QStringLiteral("SystemServices"), + QStringLiteral("Hardware"), +}; + +SortedSystemTrayModel::SortedSystemTrayModel(SortingType sorting, QObject *parent) + : QSortFilterProxyModel(parent) + , m_sorting(sorting) +{ + setSortLocaleAware(true); + sort(0); +} + +bool SortedSystemTrayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + switch (m_sorting) { + case SortedSystemTrayModel::SortingType::ConfigurationPage: + return lessThanConfigurationPage(left, right); + case SortedSystemTrayModel::SortingType::SystemTray: + return lessThanSystemTray(left, right); + } + + return QSortFilterProxyModel::lessThan(left, right); +} + +bool SortedSystemTrayModel::lessThanConfigurationPage(const QModelIndex &left, const QModelIndex &right) const +{ + const int categoriesComparison = compareCategoriesAlphabetically(left, right); + if (categoriesComparison == 0) { + return QSortFilterProxyModel::lessThan(left, right); + } else { + return categoriesComparison < 0; + } +} + +bool SortedSystemTrayModel::lessThanSystemTray(const QModelIndex &left, const QModelIndex &right) const +{ + QVariant itemIdLeft = sourceModel()->data(left, static_cast(BaseModel::BaseRole::ItemId)); + QVariant itemIdRight = sourceModel()->data(right, static_cast(BaseModel::BaseRole::ItemId)); + if (itemIdRight.toString() == QLatin1String("org.kde.plasma.notifications")) { + // return false when at least right is "org.kde.plasma.notifications" + return false; + } else if (itemIdLeft.toString() == QLatin1String("org.kde.plasma.notifications")) { + // return true when only left is "org.kde.plasma.notifications" + return true; + } + + const int categoriesComparison = compareCategoriesOrderly(left, right); + if (categoriesComparison == 0) { + return QSortFilterProxyModel::lessThan(left, right); + } else { + return categoriesComparison < 0; + } +} + +int SortedSystemTrayModel::compareCategoriesAlphabetically(const QModelIndex &left, const QModelIndex &right) const +{ + QVariant leftData = sourceModel()->data(left, static_cast(BaseModel::BaseRole::Category)); + QString leftCategory = leftData.isNull() ? QStringLiteral("UnknownCategory") : leftData.toString(); + + QVariant rightData = sourceModel()->data(right, static_cast(BaseModel::BaseRole::Category)); + QString rightCategory = rightData.isNull() ? QStringLiteral("UnknownCategory") : rightData.toString(); + + return QString::localeAwareCompare(leftCategory, rightCategory); +} + +int SortedSystemTrayModel::compareCategoriesOrderly(const QModelIndex &left, const QModelIndex &right) const +{ + QVariant leftData = sourceModel()->data(left, static_cast(BaseModel::BaseRole::Category)); + QString leftCategory = leftData.isNull() ? QStringLiteral("UnknownCategory") : leftData.toString(); + + QVariant rightData = sourceModel()->data(right, static_cast(BaseModel::BaseRole::Category)); + QString rightCategory = rightData.isNull() ? QStringLiteral("UnknownCategory") : rightData.toString(); + + int leftIndex = s_categoryOrder.indexOf(leftCategory); + if (leftIndex == -1) { + leftIndex = s_categoryOrder.indexOf(QStringLiteral("UnknownCategory")); + } + + int rightIndex = s_categoryOrder.indexOf(rightCategory); + if (rightIndex == -1) { + rightIndex = s_categoryOrder.indexOf(QStringLiteral("UnknownCategory")); + } + + return leftIndex - rightIndex; +} diff --git a/plasma/workspace/applets/systemtray/sortedsystemtraymodel.h b/plasma/workspace/applets/systemtray/sortedsystemtraymodel.h new file mode 100644 index 0000000000..1cca2c78f0 --- /dev/null +++ b/plasma/workspace/applets/systemtray/sortedsystemtraymodel.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class SortedSystemTrayModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + enum class SortingType { + ConfigurationPage, + SystemTray, + }; + + explicit SortedSystemTrayModel(SortingType sorting, QObject *parent = nullptr); + +protected: + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + +private: + bool lessThanConfigurationPage(const QModelIndex &left, const QModelIndex &right) const; + bool lessThanSystemTray(const QModelIndex &left, const QModelIndex &right) const; + + int compareCategoriesAlphabetically(const QModelIndex &left, const QModelIndex &right) const; + int compareCategoriesOrderly(const QModelIndex &left, const QModelIndex &right) const; + + SortingType m_sorting; +}; diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemhost.cpp b/plasma/workspace/applets/systemtray/statusnotifieritemhost.cpp new file mode 100644 index 0000000000..117c29f179 --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemhost.cpp @@ -0,0 +1,189 @@ +/* + + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusnotifieritemhost.h" +#include "statusnotifieritemsource.h" +#include + +#include "dbusproperties.h" + +#include "debug.h" +#include + +class StatusNotifierItemHostSingleton +{ +public: + StatusNotifierItemHost self; +}; + +Q_GLOBAL_STATIC(StatusNotifierItemHostSingleton, privateStatusNotifierItemHostSelf) + +static const QString s_watcherServiceName(QStringLiteral("org.kde.StatusNotifierWatcher")); + +StatusNotifierItemHost::StatusNotifierItemHost() + : QObject() + , m_statusNotifierWatcher(nullptr) +{ + init(); +} + +StatusNotifierItemHost::~StatusNotifierItemHost() +{ + QDBusConnection::sessionBus().unregisterService(m_serviceName); +} + +StatusNotifierItemHost *StatusNotifierItemHost::self() +{ + return &privateStatusNotifierItemHostSelf()->self; +} + +const QList StatusNotifierItemHost::services() const +{ + return m_sniServices.keys(); +} + +StatusNotifierItemSource *StatusNotifierItemHost::itemForService(const QString service) +{ + return m_sniServices.value(service); +} + +void StatusNotifierItemHost::init() +{ + if (QDBusConnection::sessionBus().isConnected()) { + m_serviceName = "org.kde.StatusNotifierHost-" + QString::number(QCoreApplication::applicationPid()); + QDBusConnection::sessionBus().registerService(m_serviceName); + + QDBusServiceWatcher *watcher = + new QDBusServiceWatcher(s_watcherServiceName, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &StatusNotifierItemHost::serviceChange); + + registerWatcher(s_watcherServiceName); + } +} + +void StatusNotifierItemHost::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + qCDebug(SYSTEM_TRAY) << "Service" << name << "status change, old owner:" << oldOwner << "new:" << newOwner; + + if (newOwner.isEmpty()) { + // unregistered + unregisterWatcher(name); + } else if (oldOwner.isEmpty()) { + // registered + registerWatcher(name); + } +} + +void StatusNotifierItemHost::registerWatcher(const QString &service) +{ + if (service == s_watcherServiceName) { + delete m_statusNotifierWatcher; + + m_statusNotifierWatcher = + new org::kde::StatusNotifierWatcher(s_watcherServiceName, QStringLiteral("/StatusNotifierWatcher"), QDBusConnection::sessionBus()); + if (m_statusNotifierWatcher->isValid()) { + m_statusNotifierWatcher->call(QDBus::NoBlock, QStringLiteral("RegisterStatusNotifierHost"), m_serviceName); + + OrgFreedesktopDBusPropertiesInterface propetriesIface(m_statusNotifierWatcher->service(), + m_statusNotifierWatcher->path(), + m_statusNotifierWatcher->connection()); + + connect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemRegistered, + this, + &StatusNotifierItemHost::serviceRegistered); + connect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemUnregistered, + this, + &StatusNotifierItemHost::serviceUnregistered); + + QDBusPendingReply pendingItems = propetriesIface.Get(m_statusNotifierWatcher->interface(), "RegisteredStatusNotifierItems"); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingItems, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() { + watcher->deleteLater(); + QDBusReply reply = *watcher; + QStringList registeredItems = reply.value().variant().toStringList(); + foreach (const QString &service, registeredItems) { + if (!m_sniServices.contains(service)) { // due to async nature of this call, service may be already there + addSNIService(service); + } + } + }); + } else { + delete m_statusNotifierWatcher; + m_statusNotifierWatcher = nullptr; + qCDebug(SYSTEM_TRAY) << "System tray daemon not reachable"; + } + } +} + +void StatusNotifierItemHost::unregisterWatcher(const QString &service) +{ + if (service == s_watcherServiceName) { + qCDebug(SYSTEM_TRAY) << s_watcherServiceName << "disappeared"; + + disconnect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemRegistered, + this, + &StatusNotifierItemHost::serviceRegistered); + disconnect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemUnregistered, + this, + &StatusNotifierItemHost::serviceUnregistered); + + removeAllSNIServices(); + + delete m_statusNotifierWatcher; + m_statusNotifierWatcher = nullptr; + } +} + +void StatusNotifierItemHost::serviceRegistered(const QString &service) +{ + qCDebug(SYSTEM_TRAY) << "Registering" << service; + addSNIService(service); +} + +void StatusNotifierItemHost::serviceUnregistered(const QString &service) +{ + removeSNIService(service); +} + +void StatusNotifierItemHost::removeAllSNIServices() +{ + QHashIterator it(m_sniServices); + while (it.hasNext()) { + it.next(); + + StatusNotifierItemSource *item = it.value(); + item->disconnect(); + item->deleteLater(); + Q_EMIT itemRemoved(it.key()); + } + m_sniServices.clear(); +} + +void StatusNotifierItemHost::addSNIService(const QString &service) +{ + StatusNotifierItemSource *item = new StatusNotifierItemSource(service, this); + m_sniServices.insert(service, item); + Q_EMIT itemAdded(service); +} + +void StatusNotifierItemHost::removeSNIService(const QString &service) +{ + if (m_sniServices.contains(service)) { + auto item = m_sniServices.value(service); + item->disconnect(); + item->deleteLater(); + m_sniServices.remove(service); + Q_EMIT itemRemoved(service); + } +} + diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemhost.h b/plasma/workspace/applets/systemtray/statusnotifieritemhost.h new file mode 100644 index 0000000000..65d521491e --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemhost.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "statusnotifierwatcher_interface.h" +#include + +class StatusNotifierItemSource; + +// Define our plasma Runner +class StatusNotifierItemHost : public QObject +{ + Q_OBJECT + +public: + StatusNotifierItemHost(); + virtual ~StatusNotifierItemHost(); + + static StatusNotifierItemHost *self(); + + const QList services() const; + StatusNotifierItemSource *itemForService(const QString service); + +Q_SIGNALS: + void itemAdded(const QString &service); + void itemRemoved(const QString &service); + +private Q_SLOTS: + void serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner); + void registerWatcher(const QString &service); + void unregisterWatcher(const QString &service); + void serviceRegistered(const QString &service); + void serviceUnregistered(const QString &service); + +private: + void init(); + void removeAllSNIServices(); + void addSNIService(const QString &service); + void removeSNIService(const QString &service); + int indexOfItem(const QString &service) const; + + org::kde::StatusNotifierWatcher *m_statusNotifierWatcher; + QString m_serviceName; + static const int s_protocolVersion = 0; + QHash m_sniServices; +}; diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemjob.cpp b/plasma/workspace/applets/systemtray/statusnotifieritemjob.cpp new file mode 100644 index 0000000000..2fa6138279 --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemjob.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "statusnotifieritemjob.h" +#include +#include + +StatusNotifierItemJob::StatusNotifierItemJob(StatusNotifierItemSource *source, const QString &operation, QMap ¶meters, QObject *parent) + : ServiceJob(source->objectName(), operation, parameters, parent) + , m_source(source) +{ + // Queue connection, so that all 'deleteLater' are performed before we use updated menu. + connect(source, SIGNAL(contextMenuReady(QMenu *)), this, SLOT(contextMenuReady(QMenu *)), Qt::QueuedConnection); + connect(source, &StatusNotifierItemSource::activateResult, this, &StatusNotifierItemJob::activateCallback); +} + +StatusNotifierItemJob::~StatusNotifierItemJob() +{ +} + +void StatusNotifierItemJob::start() +{ + if (operationName() == QLatin1String("Scroll")) { + performJob(); + return; + } + + QWindow *window = nullptr; + const quint32 launchedSerial = KWindowSystem::lastInputSerial(window); + auto conn = QSharedPointer::create(); + *conn = connect(KWindowSystem::self(), &KWindowSystem::xdgActivationTokenArrived, this, [this, launchedSerial, conn](quint32 serial, const QString &token) { + if (serial == launchedSerial) { + disconnect(*conn); + m_source->provideXdgActivationToken(token); + performJob(); + } + }); + KWindowSystem::requestXdgActivationToken(window, launchedSerial, {}); +} + +void StatusNotifierItemJob::performJob() +{ + if (operationName() == QString::fromLatin1("Activate")) { + m_source->activate(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + } else if (operationName() == QString::fromLatin1("SecondaryActivate")) { + m_source->secondaryActivate(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + setResult(0); + } else if (operationName() == QString::fromLatin1("ContextMenu")) { + m_source->contextMenu(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + } else if (operationName() == QString::fromLatin1("Scroll")) { + m_source->scroll(parameters()[QStringLiteral("delta")].toInt(), parameters()[QStringLiteral("direction")].toString()); + setResult(0); + } +} + +void StatusNotifierItemJob::activateCallback(bool success) +{ + if (operationName() == QString::fromLatin1("Activate")) { + setResult(QVariant(success)); + } +} + +void StatusNotifierItemJob::contextMenuReady(QMenu *menu) +{ + if (operationName() == QString::fromLatin1("ContextMenu")) { + setResult(QVariant::fromValue((QObject *)menu)); + } +} diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemjob.h b/plasma/workspace/applets/systemtray/statusnotifieritemjob.h new file mode 100644 index 0000000000..142d881e7a --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemjob.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// Qt +#include + +// own +#include "statusnotifieritemsource.h" + +// plasma +#include + +/** + * Task Job + */ +class StatusNotifierItemJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + StatusNotifierItemJob(StatusNotifierItemSource *source, const QString &operation, QMap ¶meters, QObject *parent = nullptr); + ~StatusNotifierItemJob() override; + +protected: + void start() override; + +private Q_SLOTS: + void activateCallback(bool success); + void contextMenuReady(QMenu *menu); + +private: + void performJob(); + StatusNotifierItemSource *m_source; +}; diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemservice.cpp b/plasma/workspace/applets/systemtray/statusnotifieritemservice.cpp new file mode 100644 index 0000000000..0665c05b9a --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemservice.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "statusnotifieritemservice.h" + +// own +#include "statusnotifieritemjob.h" + +StatusNotifierItemService::StatusNotifierItemService(StatusNotifierItemSource *source) + : Plasma::Service(source) + , m_source(source) +{ + setName(QStringLiteral("statusnotifieritem")); +} + +StatusNotifierItemService::~StatusNotifierItemService() +{ +} + +Plasma::ServiceJob *StatusNotifierItemService::createJob(const QString &operation, QMap ¶meters) +{ + return new StatusNotifierItemJob(m_source, operation, parameters, this); +} diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemservice.h b/plasma/workspace/applets/systemtray/statusnotifieritemservice.h new file mode 100644 index 0000000000..59bcf24088 --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemservice.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// own +#include "statusnotifieritemsource.h" + +// plasma +#include +#include + +/** + * StatusNotifierItem Service + */ +class StatusNotifierItemService : public Plasma::Service +{ + Q_OBJECT + +public: + explicit StatusNotifierItemService(StatusNotifierItemSource *source); + ~StatusNotifierItemService() override; + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + StatusNotifierItemSource *m_source; +}; diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemsource.cpp b/plasma/workspace/applets/systemtray/statusnotifieritemsource.cpp new file mode 100644 index 0000000000..31e60392d4 --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemsource.cpp @@ -0,0 +1,552 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusnotifieritemsource.h" +#include "statusnotifieritem_interface.h" +#include "statusnotifieritemservice.h" +#include "systemtraytypes.h" + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +class PlasmaDBusMenuImporter : public DBusMenuImporter +{ +public: + PlasmaDBusMenuImporter(const QString &service, const QString &path, KIconLoader *iconLoader, QObject *parent) + : DBusMenuImporter(service, path, parent) + , m_iconLoader(iconLoader) + { + } + +protected: + QIcon iconForName(const QString &name) override + { + return QIcon(new KIconEngine(name, m_iconLoader)); + } + +private: + KIconLoader *m_iconLoader; +}; + +StatusNotifierItemSource::StatusNotifierItemSource(const QString ¬ifierItemId, QObject *parent) + : QObject(parent) + , m_customIconLoader(nullptr) + , m_menuImporter(nullptr) + , m_refreshing(false) + , m_needsReRefreshing(false) +{ + setObjectName(notifierItemId); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + m_servicename = notifierItemId; + + int slash = notifierItemId.indexOf('/'); + if (slash == -1) { + qCWarning(SYSTEM_TRAY) << "Invalid notifierItemId:" << notifierItemId; + m_valid = false; + m_statusNotifierItemInterface = nullptr; + return; + } + QString service = notifierItemId.left(slash); + QString path = notifierItemId.mid(slash); + + m_statusNotifierItemInterface = new org::kde::StatusNotifierItem(service, path, QDBusConnection::sessionBus(), this); + + m_refreshTimer.setSingleShot(true); + m_refreshTimer.setInterval(10); + connect(&m_refreshTimer, &QTimer::timeout, this, &StatusNotifierItemSource::performRefresh); + + m_valid = !service.isEmpty() && m_statusNotifierItemInterface->isValid(); + if (m_valid) { + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewTitle, this, &StatusNotifierItemSource::refresh); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewIcon, this, &StatusNotifierItemSource::refresh); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewAttentionIcon, this, &StatusNotifierItemSource::refresh); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewOverlayIcon, this, &StatusNotifierItemSource::refresh); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewToolTip, this, &StatusNotifierItemSource::refresh); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewStatus, this, &StatusNotifierItemSource::syncStatus); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewMenu, this, &StatusNotifierItemSource::refreshMenu); + refresh(); + } +} + +StatusNotifierItemSource::~StatusNotifierItemSource() +{ + delete m_statusNotifierItemInterface; +} + +KIconLoader *StatusNotifierItemSource::iconLoader() const +{ + return m_customIconLoader ? m_customIconLoader : KIconLoader::global(); +} + +QIcon StatusNotifierItemSource::attentionIcon() const +{ + return m_attentionIcon; +} + +QString StatusNotifierItemSource::attentionIconName() const +{ + return m_attentionIconName; +} + +QString StatusNotifierItemSource::attentionMovieName() const +{ + return m_attentionMovieName; +} + +QString StatusNotifierItemSource::category() const +{ + return m_category; +} + +QIcon StatusNotifierItemSource::icon() const +{ + return m_icon; +} + +QString StatusNotifierItemSource::iconName() const +{ + return m_iconName; +} + +QString StatusNotifierItemSource::iconThemePath() const +{ + return m_iconThemePath; +} + +QString StatusNotifierItemSource::id() const +{ + return m_id; +} + +bool StatusNotifierItemSource::itemIsMenu() const +{ + return m_itemIsMenu; +} + +QString StatusNotifierItemSource::overlayIconName() const +{ + return m_overlayIconName; +} + +QString StatusNotifierItemSource::status() const +{ + return m_status; +} + +QString StatusNotifierItemSource::title() const +{ + return m_title; +} + +QVariant StatusNotifierItemSource::toolTipIcon() const +{ + return m_toolTipIcon; +} + +QString StatusNotifierItemSource::toolTipSubTitle() const +{ + return m_toolTipSubTitle; +} + +QString StatusNotifierItemSource::toolTipTitle() const +{ + return m_toolTipTitle; +} + +QString StatusNotifierItemSource::windowId() const +{ + return m_windowId; +} + +Plasma::Service *StatusNotifierItemSource::createService() +{ + return new StatusNotifierItemService(this); +} + +void StatusNotifierItemSource::syncStatus(const QString &status) +{ + m_status = status; + Q_EMIT dataUpdated(); +} + +void StatusNotifierItemSource::refreshMenu() +{ + if (m_menuImporter) { + delete m_menuImporter; + m_menuImporter = nullptr; + } + refresh(); +} + +void StatusNotifierItemSource::refresh() +{ + if (!m_refreshTimer.isActive()) { + m_refreshTimer.start(); + } +} + +void StatusNotifierItemSource::performRefresh() +{ + if (m_refreshing) { + m_needsReRefreshing = true; + return; + } + + m_refreshing = true; + QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(), + m_statusNotifierItemInterface->path(), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("GetAll")); + + message << m_statusNotifierItemInterface->interface(); + QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::refreshCallback); +} + +/** + \todo add a smart pointer to guard call and to automatically delete it at the end of the function + */ +void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call) +{ + m_refreshing = false; + if (m_needsReRefreshing) { + m_needsReRefreshing = false; + performRefresh(); + call->deleteLater(); + return; + } + + QDBusPendingReply reply = *call; + if (reply.isError()) { + m_valid = false; + } else { + // IconThemePath (handle this one first, because it has an impact on + // others) + QVariantMap properties = reply.argumentAt<0>(); + QString path = properties[QStringLiteral("IconThemePath")].toString(); + + if (!path.isEmpty() && path != m_iconThemePath) { + if (!m_customIconLoader) { + m_customIconLoader = new KIconLoader(QString(), QStringList(), this); + } + // FIXME: If last part of path is not "icons", this won't work! + QString appName; + auto tokens = path.splitRef('/', Qt::SkipEmptyParts); + if (tokens.length() >= 3 && tokens.takeLast() == QLatin1String("icons")) + appName = tokens.takeLast().toString(); + + // icons may be either in the root directory of the passed path or in a appdir format + // i.e hicolor/32x32/iconname.png + + m_customIconLoader->reconfigure(appName, QStringList(path)); + + // add app dir requires an app name, though this is completely unused in this context + m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path); + + connect(m_customIconLoader, &KIconLoader::iconChanged, this, [=] { + m_customIconLoader->reconfigure(appName, QStringList(path)); + m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path); + }); + } + m_iconThemePath = path; + + m_category = properties[QStringLiteral("Category")].toString(); + m_status = properties[QStringLiteral("Status")].toString(); + m_title = properties[QStringLiteral("Title")].toString(); + m_id = properties[QStringLiteral("Id")].toString(); + m_windowId = properties[QStringLiteral("WindowId")].toString(); + m_itemIsMenu = properties[QStringLiteral("ItemIsMenu")].toBool(); + + // Attention Movie + m_attentionMovieName = properties[QStringLiteral("AttentionMovieName")].toString(); + + QIcon overlay; + QStringList overlayNames; + + // Icon + { + KDbusImageVector image; + QIcon icon; + QString iconName; + + properties[QStringLiteral("OverlayIconPixmap")].value() >> image; + if (image.isEmpty()) { + QString iconName = properties[QStringLiteral("OverlayIconName")].toString(); + m_overlayIconName = iconName; + if (!iconName.isEmpty()) { + overlayNames << iconName; + overlay = QIcon(new KIconEngine(iconName, iconLoader())); + } + } else { + overlay = imageVectorToPixmap(image); + } + + properties[QStringLiteral("IconPixmap")].value() >> image; + if (image.isEmpty()) { + iconName = properties[QStringLiteral("IconName")].toString(); + if (!iconName.isEmpty()) { + icon = QIcon(new KIconEngine(iconName, iconLoader(), overlayNames)); + + if (overlayNames.isEmpty() && !overlay.isNull()) { + overlayIcon(&icon, &overlay); + } + } + } else { + icon = imageVectorToPixmap(image); + if (!icon.isNull() && !overlay.isNull()) { + overlayIcon(&icon, &overlay); + } + } + m_icon = icon; + m_iconName = iconName; + } + + // Attention icon + { + KDbusImageVector image; + QIcon attentionIcon; + + properties[QStringLiteral("AttentionIconPixmap")].value() >> image; + if (image.isEmpty()) { + QString iconName = properties[QStringLiteral("AttentionIconName")].toString(); + m_attentionIconName = iconName; + if (!iconName.isEmpty()) { + attentionIcon = QIcon(new KIconEngine(iconName, iconLoader(), overlayNames)); + + if (overlayNames.isEmpty() && !overlay.isNull()) { + overlayIcon(&attentionIcon, &overlay); + } + } + } else { + attentionIcon = imageVectorToPixmap(image); + if (!attentionIcon.isNull() && !overlay.isNull()) { + overlayIcon(&attentionIcon, &overlay); + } + } + m_attentionIcon = attentionIcon; + } + + // ToolTip + { + KDbusToolTipStruct toolTip; + properties[QStringLiteral("ToolTip")].value() >> toolTip; + if (toolTip.title.isEmpty()) { + m_toolTipTitle = QString(); + m_toolTipSubTitle = QString(); + m_toolTipIcon = QString(); + } else { + QIcon toolTipIcon; + if (toolTip.image.size() == 0) { + toolTipIcon = QIcon(new KIconEngine(toolTip.icon, iconLoader())); + } else { + toolTipIcon = imageVectorToPixmap(toolTip.image); + } + m_toolTipTitle = toolTip.title; + m_toolTipSubTitle = toolTip.subTitle; + if (toolTipIcon.isNull() || toolTipIcon.availableSizes().isEmpty()) { + m_toolTipIcon = QString(); + } else { + m_toolTipIcon = toolTipIcon; + } + } + } + + // Menu + if (!m_menuImporter) { + QString menuObjectPath = properties[QStringLiteral("Menu")].value().path(); + if (!menuObjectPath.isEmpty()) { + if (menuObjectPath == QLatin1String("/NO_DBUSMENU")) { + // This is a hack to make it possible to disable DBusMenu in an + // application. The string "/NO_DBUSMENU" must be the same as in + // KStatusNotifierItem::setContextMenu(). + qCWarning(SYSTEM_TRAY) << "DBusMenu disabled for this application"; + } else { + m_menuImporter = new PlasmaDBusMenuImporter(m_statusNotifierItemInterface->service(), menuObjectPath, iconLoader(), this); + connect(m_menuImporter, &PlasmaDBusMenuImporter::menuUpdated, this, [this](QMenu *menu) { + if (menu == m_menuImporter->menu()) { + contextMenuReady(); + } + }); + } + } + } + } + + Q_EMIT dataUpdated(); + call->deleteLater(); +} + +void StatusNotifierItemSource::contextMenuReady() +{ + Q_EMIT contextMenuReady(m_menuImporter->menu()); +} + +QPixmap StatusNotifierItemSource::KDbusImageStructToPixmap(const KDbusImageStruct &image) const +{ + // swap from network byte order if we are little endian + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { + uint *uintBuf = (uint *)image.data.data(); + for (uint i = 0; i < image.data.size() / sizeof(uint); ++i) { + *uintBuf = ntohl(*uintBuf); + ++uintBuf; + } + } + if (image.width == 0 || image.height == 0) { + return QPixmap(); + } + + // avoid a deep copy of the image data + // we need to keep a reference to the image.data alive for the lifespan of the image, even if the image is copied + // we create a new QByteArray with a shallow copy of the original data on the heap, then delete this in the QImage cleanup + auto dataRef = new QByteArray(image.data); + + QImage iconImage( + reinterpret_cast(dataRef->data()), + image.width, + image.height, + QImage::Format_ARGB32, + [](void *ptr) { + delete static_cast(ptr); + }, + dataRef); + return QPixmap::fromImage(iconImage); +} + +QIcon StatusNotifierItemSource::imageVectorToPixmap(const KDbusImageVector &vector) const +{ + QIcon icon; + + for (int i = 0; i < vector.size(); ++i) { + icon.addPixmap(KDbusImageStructToPixmap(vector[i])); + } + + return icon; +} + +void StatusNotifierItemSource::overlayIcon(QIcon *icon, QIcon *overlay) +{ + QIcon tmp; + QPixmap m_iconPixmap = icon->pixmap(KIconLoader::SizeSmall, KIconLoader::SizeSmall); + + QPainter p(&m_iconPixmap); + + const int size = KIconLoader::SizeSmall / 2; + p.drawPixmap(QRect(size, size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + + // if an m_icon exactly that size wasn't found don't add it to the vector + m_iconPixmap = icon->pixmap(KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium); + if (m_iconPixmap.width() == KIconLoader::SizeSmallMedium) { + const int size = KIconLoader::SizeSmall / 2; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + m_iconPixmap = icon->pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium); + if (m_iconPixmap.width() == KIconLoader::SizeMedium) { + const int size = KIconLoader::SizeSmall / 2; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + m_iconPixmap = icon->pixmap(KIconLoader::SizeLarge, KIconLoader::SizeLarge); + if (m_iconPixmap.width() == KIconLoader::SizeLarge) { + const int size = KIconLoader::SizeSmall; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + // We can't do 'm_icon->addPixmap()' because if 'm_icon' uses KIconEngine, + // it will ignore the added pixmaps. This is not a bug in KIconEngine, + // QIcon::addPixmap() doc says: "Custom m_icon engines are free to ignore + // additionally added pixmaps". + *icon = tmp; + // hopefully huge and enormous not necessary right now, since it's quite costly +} + +void StatusNotifierItemSource::activate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(), + m_statusNotifierItemInterface->path(), + m_statusNotifierItemInterface->interface(), + QStringLiteral("Activate")); + + message << x << y; + QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::activateCallback); + } +} + +void StatusNotifierItemSource::activateCallback(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + Q_EMIT activateResult(!reply.isError()); + call->deleteLater(); +} + +void StatusNotifierItemSource::secondaryActivate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("SecondaryActivate"), x, y); + } +} + +void StatusNotifierItemSource::scroll(int delta, const QString &direction) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("Scroll"), delta, direction); + } +} + +void StatusNotifierItemSource::contextMenu(int x, int y) +{ + if (m_menuImporter) { + m_menuImporter->updateMenu(); + } else { + qCWarning(SYSTEM_TRAY) << "Could not find DBusMenu interface, falling back to calling ContextMenu()"; + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("ContextMenu"), x, y); + } + } +} + +void StatusNotifierItemSource::provideXdgActivationToken(const QString &token) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->ProvideXdgActivationToken(token); + } +} diff --git a/plasma/workspace/applets/systemtray/statusnotifieritemsource.h b/plasma/workspace/applets/systemtray/statusnotifieritemsource.h new file mode 100644 index 0000000000..98c7c883df --- /dev/null +++ b/plasma/workspace/applets/systemtray/statusnotifieritemsource.h @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include "statusnotifieritem_interface.h" + +class DBusMenuImporter; +class KIconLoader; + +class StatusNotifierItemSource : public QObject +{ + Q_OBJECT + +public: + StatusNotifierItemSource(const QString &service, QObject *parent); + ~StatusNotifierItemSource() override; + Plasma::Service *createService(); + + void activate(int x, int y); + void secondaryActivate(int x, int y); + void scroll(int delta, const QString &direction); + void contextMenu(int x, int y); + void provideXdgActivationToken(const QString &token); + + QIcon attentionIcon() const; + QString attentionIconName() const; + QString attentionMovieName() const; + QString category() const; + QIcon icon() const; + QString iconName() const; + QString iconThemePath() const; + QString id() const; + bool itemIsMenu() const; + QString overlayIconName() const; + QString status() const; + QString title() const; + QVariant toolTipIcon() const; + QString toolTipSubTitle() const; + QString toolTipTitle() const; + QString windowId() const; + +Q_SIGNALS: + void contextMenuReady(QMenu *menu); + void activateResult(bool success); + void dataUpdated(); + +private Q_SLOTS: + void contextMenuReady(); + void refreshMenu(); + void refresh(); + void performRefresh(); + void syncStatus(const QString &); + void refreshCallback(QDBusPendingCallWatcher *); + void activateCallback(QDBusPendingCallWatcher *); + +private: + QPixmap KDbusImageStructToPixmap(const KDbusImageStruct &image) const; + QIcon imageVectorToPixmap(const KDbusImageVector &vector) const; + void overlayIcon(QIcon *icon, QIcon *overlay); + KIconLoader *iconLoader() const; + + bool m_valid; + QString m_servicename; + QTimer m_refreshTimer; + KIconLoader *m_customIconLoader; + DBusMenuImporter *m_menuImporter; + org::kde::StatusNotifierItem *m_statusNotifierItemInterface; + bool m_refreshing : 1; + bool m_needsReRefreshing : 1; + + QIcon m_attentionIcon; + QString m_attentionIconName; + QString m_attentionMovieName; + QString m_category; + QIcon m_icon; + QString m_iconName; + QString m_iconThemePath; + QString m_id; + bool m_itemIsMenu; + QString m_overlayIconName; + QString m_status; + QString m_title; + QVariant m_toolTipIcon; + QString m_toolTipSubTitle; + QString m_toolTipTitle; + QString m_windowId; +}; diff --git a/plasma/workspace/applets/systemtray/systemtray.cpp b/plasma/workspace/applets/systemtray/systemtray.cpp new file mode 100644 index 0000000000..f1af1e60cc --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtray.cpp @@ -0,0 +1,387 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtray.h" +#include "debug.h" + +#include "plasmoidregistry.h" +#include "sortedsystemtraymodel.h" +#include "systemtraymodel.h" +#include "systemtraysettings.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +SystemTray::SystemTray(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : Plasma::Containment(parent, data, args) + , m_plasmoidModel(nullptr) + , m_statusNotifierModel(nullptr) + , m_systemTrayModel(nullptr) + , m_sortedSystemTrayModel(nullptr) + , m_configSystemTrayModel(nullptr) +{ + setHasConfigurationInterface(true); + setContainmentType(Plasma::Types::CustomEmbeddedContainment); + setContainmentDisplayHints(Plasma::Types::ContainmentDrawsPlasmoidHeading | Plasma::Types::ContainmentForcesSquarePlasmoids); +} + +SystemTray::~SystemTray() +{ +} + +void SystemTray::init() +{ + Containment::init(); + + m_settings = new SystemTraySettings(configScheme(), this); + connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &SystemTray::onEnabledAppletsChanged); + + m_plasmoidRegistry = new PlasmoidRegistry(m_settings, this); + connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidEnabled, this, &SystemTray::startApplet); + connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidStopped, this, &SystemTray::stopApplet); + + // we don't want to automatically propagate the activated signal from the Applet to the Containment + // even if SystemTray is of type Containment, it is de facto Applet and should act like one + connect(this, &Containment::appletAdded, this, [this](Plasma::Applet *applet) { + disconnect(applet, &Applet::activated, this, &Applet::activated); + }); +} + +void SystemTray::restoreContents(KConfigGroup &group) +{ + if (!isContainment()) { + qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened"; + return; + } + + KConfigGroup shortcutConfig(&group, "Shortcuts"); + QString shortcutText = shortcutConfig.readEntryUntranslated("global", QString()); + if (!shortcutText.isEmpty()) { + setGlobalShortcut(QKeySequence(shortcutText)); + } + + // cache known config group ids for applets + KConfigGroup cg = group.group("Applets"); + for (const QString &group : cg.groupList()) { + KConfigGroup appletConfig(&cg, group); + QString plugin = appletConfig.readEntry("plugin"); + if (!plugin.isEmpty()) { + m_configGroupIds[plugin] = group.toInt(); + } + } + + m_plasmoidRegistry->init(); +} + +void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y) +{ + if (!appletInterface) { + return; + } + + Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); + + QPointF pos = appletInterface->mapToScene(QPointF(x, y)); + + if (appletInterface->window() && appletInterface->window()->screen()) { + pos = appletInterface->window()->mapToGlobal(pos.toPoint()); + } else { + pos = QPoint(); + } + + QMenu *desktopMenu = new QMenu; + connect(this, &QObject::destroyed, desktopMenu, &QMenu::close); + desktopMenu->setAttribute(Qt::WA_DeleteOnClose); + + // 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 = [appletInterface]() { + if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) { + appletInterface->window()->mouseGrabberItem()->ungrabMouse(); + } + }; + + QTimer::singleShot(0, appletInterface, ungrabMouseHack); + // end workaround + + Q_EMIT applet->contextualActionsAboutToShow(); + const auto contextActions = applet->contextualActions(); + for (QAction *action : contextActions) { + if (action) { + desktopMenu->addAction(action); + } + } + + QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application")); + if (runAssociatedApplication && runAssociatedApplication->isEnabled()) { + desktopMenu->addAction(runAssociatedApplication); + } + + if (applet->actions()->action(QStringLiteral("configure"))) { + desktopMenu->addAction(applet->actions()->action(QStringLiteral("configure"))); + } + + if (desktopMenu->isEmpty()) { + delete desktopMenu; + return; + } + + desktopMenu->adjustSize(); + + if (QScreen *screen = appletInterface->window()->screen()) { + const QRect geo = screen->availableGeometry(); + + pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()), // + qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height())); + } + + KAcceleratorManager::manage(desktopMenu); + desktopMenu->winId(); + desktopMenu->windowHandle()->setTransientParent(appletInterface->window()); + desktopMenu->popup(pos.toPoint()); +} + +void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon) +{ + if (QCoreApplication::closingDown() || !statusNotifierIcon) { + // apparently an edge case can be triggered due to the async nature of all this + // see: https://bugs.kde.org/show_bug.cgi?id=251977 + return; + } + + Plasma::ServiceJob *sjob = qobject_cast(job); + if (!sjob) { + return; + } + + QMenu *menu = qobject_cast(sjob->result().value()); + + if (menu && !menu->isEmpty()) { + menu->adjustSize(); + const auto parameters = sjob->parameters(); + int x = parameters[QStringLiteral("x")].toInt(); + int y = parameters[QStringLiteral("y")].toInt(); + + // try tofind the icon screen coordinates, and adjust the position as a poor + // man's popupPosition + + QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height())); + + if (statusNotifierIcon->window()) { + screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft())); + } + + switch (location()) { + case Plasma::Types::LeftEdge: + x = screenItemRect.right(); + y = screenItemRect.top(); + break; + case Plasma::Types::RightEdge: + x = screenItemRect.left() - menu->width(); + y = screenItemRect.top(); + break; + case Plasma::Types::TopEdge: + x = screenItemRect.left(); + y = screenItemRect.bottom(); + break; + case Plasma::Types::BottomEdge: + x = screenItemRect.left(); + y = screenItemRect.top() - menu->height(); + break; + default: + x = screenItemRect.left(); + if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) { + y = screenItemRect.top() - menu->height(); + } else { + y = screenItemRect.bottom(); + } + } + + KAcceleratorManager::manage(menu); + menu->winId(); + menu->windowHandle()->setTransientParent(statusNotifierIcon->window()); + menu->popup(QPoint(x, y)); + } +} + +QPointF SystemTray::popupPosition(QQuickItem *visualParent, int x, int y) +{ + if (!visualParent) { + return QPointF(0, 0); + } + + QPointF pos = visualParent->mapToScene(QPointF(x, y)); + + if (visualParent->window() && visualParent->window()->screen()) { + pos = visualParent->window()->mapToGlobal(pos.toPoint()); + } else { + return QPoint(); + } + return pos; +} + +bool SystemTray::isSystemTrayApplet(const QString &appletId) +{ + if (m_plasmoidRegistry) { + return m_plasmoidRegistry->isSystemTrayApplet(appletId); + } + return false; +} + +SystemTrayModel *SystemTray::systemTrayModel() +{ + if (!m_systemTrayModel) { + m_systemTrayModel = new SystemTrayModel(this); + + m_plasmoidModel = new PlasmoidModel(m_settings, m_plasmoidRegistry, m_systemTrayModel); + connect(this, &SystemTray::appletAdded, m_plasmoidModel, &PlasmoidModel::addApplet); + connect(this, &SystemTray::appletRemoved, m_plasmoidModel, &PlasmoidModel::removeApplet); + for (auto applet : applets()) { + m_plasmoidModel->addApplet(applet); + } + + m_statusNotifierModel = new StatusNotifierModel(m_settings, m_systemTrayModel); + + m_systemTrayModel->addSourceModel(m_plasmoidModel); + m_systemTrayModel->addSourceModel(m_statusNotifierModel); + } + + return m_systemTrayModel; +} + +QAbstractItemModel *SystemTray::sortedSystemTrayModel() +{ + if (!m_sortedSystemTrayModel) { + m_sortedSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::SystemTray, this); + m_sortedSystemTrayModel->setSourceModel(systemTrayModel()); + } + return m_sortedSystemTrayModel; +} + +QAbstractItemModel *SystemTray::configSystemTrayModel() +{ + if (!m_configSystemTrayModel) { + m_configSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::ConfigurationPage, this); + m_configSystemTrayModel->setSourceModel(systemTrayModel()); + } + return m_configSystemTrayModel; +} + +void SystemTray::onEnabledAppletsChanged() +{ + // remove all that are not allowed anymore + const auto appletsList = applets(); + for (Plasma::Applet *applet : appletsList) { + // Here it should always be valid. + // for some reason it not always is. + if (!applet->pluginMetaData().isValid()) { + applet->config().parent().deleteGroup(); + applet->deleteLater(); + } else { + const QString task = applet->pluginMetaData().pluginId(); + if (!m_settings->isEnabledPlugin(task)) { + // in those cases we do delete the applet config completely + // as they were explicitly disabled by the user + applet->config().parent().deleteGroup(); + applet->deleteLater(); + m_configGroupIds.remove(task); + } + } + } +} + +void SystemTray::startApplet(const QString &pluginId) +{ + const auto appletsList = applets(); + for (Plasma::Applet *applet : appletsList) { + if (!applet->pluginMetaData().isValid()) { + continue; + } + + // only allow one instance per applet + if (pluginId == applet->pluginMetaData().pluginId()) { + // Applet::destroy doesn't delete the applet from Containment::applets in the same event + // potentially a dbus activated service being restarted can be added in this time. + if (!applet->destroyed()) { + return; + } + } + } + + qCDebug(SYSTEM_TRAY) << "Adding applet:" << pluginId; + + // known one, recycle the id to reuse old config + if (m_configGroupIds.contains(pluginId)) { + Applet *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, m_configGroupIds.value(pluginId), QVariantList()); + // this should never happen unless explicitly wrong config is hand-written or + //(more likely) a previously added applet is uninstalled + if (!applet) { + qCWarning(SYSTEM_TRAY) << "Unable to find applet" << pluginId; + return; + } + applet->setProperty("org.kde.plasma:force-create", true); + addApplet(applet); + // create a new one automatic id, new config group + } else { + Applet *applet = createApplet(pluginId, QVariantList() << "org.kde.plasma:force-create"); + if (applet) { + m_configGroupIds[pluginId] = applet->id(); + } + } +} + +void SystemTray::stopApplet(const QString &pluginId) +{ + const auto appletsList = applets(); + for (Plasma::Applet *applet : appletsList) { + if (applet->pluginMetaData().isValid() && pluginId == applet->pluginMetaData().pluginId()) { + // we are *not* cleaning the config here, because since is one + // of those automatically loaded/unloaded by dbus, we want to recycle + // the config the next time it's loaded, in case the user configured something here + applet->deleteLater(); + // HACK: we need to remove the applet from Containment::applets() as soon as possible + // otherwise we may have disappearing applets for restarting dbus services + // this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called + Q_EMIT appletDeleted(applet); + } + } +} + +void SystemTray::stackItemBefore(QQuickItem *newItem, QQuickItem *beforeItem) +{ + if (!newItem || !beforeItem) { + return; + } + newItem->stackBefore(beforeItem); +} + +void SystemTray::stackItemAfter(QQuickItem *newItem, QQuickItem *afterItem) +{ + if (!newItem || !afterItem) { + return; + } + newItem->stackAfter(afterItem); +} + +K_PLUGIN_CLASS_WITH_JSON(SystemTray, "package/metadata.json") + +#include "systemtray.moc" diff --git a/plasma/workspace/applets/systemtray/systemtray.h b/plasma/workspace/applets/systemtray/systemtray.h new file mode 100644 index 0000000000..9fe70e2881 --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtray.h @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +class QDBusPendingCallWatcher; +class QDBusServiceWatcher; +class QQuickItem; +namespace Plasma +{ +class Service; +} +class PlasmoidRegistry; +class PlasmoidModel; +class SystemTraySettings; +class StatusNotifierModel; +class SystemTrayModel; +class SortedSystemTrayModel; + +class SystemTray : public Plasma::Containment +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *systemTrayModel READ sortedSystemTrayModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *configSystemTrayModel READ configSystemTrayModel CONSTANT) + +public: + SystemTray(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~SystemTray() override; + + void init() override; + + void restoreContents(KConfigGroup &group) override; + + QAbstractItemModel *sortedSystemTrayModel(); + + QAbstractItemModel *configSystemTrayModel(); + + // Invokable utilities + /** + * Given an AppletInterface pointer, shows a proper context menu for it + */ + Q_INVOKABLE void showPlasmoidMenu(QQuickItem *appletInterface, int x, int y); + + /** + * Shows the context menu for a statusnotifieritem + */ + Q_INVOKABLE void showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon); + + /** + * Find out global coordinates for a popup given local MouseArea + * coordinates + */ + Q_INVOKABLE QPointF popupPosition(QQuickItem *visualParent, int x, int y); + + /** + * @brief isSystemTrayApplet checks if applet is allowed in the System Tray + * @param appletId also known as plugin Id + * @return true if it is a system tray applet, otherwise false + */ + Q_INVOKABLE bool isSystemTrayApplet(const QString &appletId); + + /** + * Needed to preserve keyboard navigation + */ + Q_INVOKABLE void stackItemBefore(QQuickItem *newItem, QQuickItem *beforeItem); + + Q_INVOKABLE void stackItemAfter(QQuickItem *newItem, QQuickItem *afterItem); + +private Q_SLOTS: + // synchronizes with configuration and deletes not allowed applets + void onEnabledAppletsChanged(); + // creates an applet *if not already existing* + void startApplet(const QString &pluginId); + // deletes/stops all instances of a given applet + void stopApplet(const QString &pluginId); + +private: + SystemTrayModel *systemTrayModel(); + + QPointer m_settings; + QPointer m_plasmoidRegistry; + + PlasmoidModel *m_plasmoidModel; + StatusNotifierModel *m_statusNotifierModel; + SystemTrayModel *m_systemTrayModel; + SortedSystemTrayModel *m_sortedSystemTrayModel; + SortedSystemTrayModel *m_configSystemTrayModel; + + QHash m_configGroupIds; +}; diff --git a/plasma/workspace/applets/systemtray/systemtraymodel.cpp b/plasma/workspace/applets/systemtray/systemtraymodel.cpp new file mode 100644 index 0000000000..4869f53577 --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraymodel.cpp @@ -0,0 +1,476 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtraymodel.h" +#include "debug.h" + +#include "plasmoidregistry.h" +#include "statusnotifieritemhost.h" +#include "statusnotifieritemsource.h" +#include "systemtraysettings.h" + +#include +#include +#include +#include +#include + +#include +#include + +BaseModel::BaseModel(QPointer settings, QObject *parent) + : QAbstractListModel(parent) + , m_settings(settings) + , m_showAllItems(m_settings->isShowAllItems()) + , m_shownItems(m_settings->shownItems()) + , m_hiddenItems(m_settings->hiddenItems()) +{ + connect(m_settings, &SystemTraySettings::configurationChanged, this, &BaseModel::onConfigurationChanged); +} + +QHash BaseModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::DecorationRole, QByteArrayLiteral("decoration")}, + {static_cast(BaseRole::ItemType), QByteArrayLiteral("itemType")}, + {static_cast(BaseRole::ItemId), QByteArrayLiteral("itemId")}, + {static_cast(BaseRole::CanRender), QByteArrayLiteral("canRender")}, + {static_cast(BaseRole::Category), QByteArrayLiteral("category")}, + {static_cast(BaseRole::Status), QByteArrayLiteral("status")}, + {static_cast(BaseRole::EffectiveStatus), QByteArrayLiteral("effectiveStatus")}, + }; +} + +void BaseModel::onConfigurationChanged() +{ + m_showAllItems = m_settings->isShowAllItems(); + m_shownItems = m_settings->shownItems(); + m_hiddenItems = m_settings->hiddenItems(); + + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {static_cast(BaseModel::BaseRole::EffectiveStatus)}); +} + +Plasma::Types::ItemStatus BaseModel::calculateEffectiveStatus(bool canRender, Plasma::Types::ItemStatus status, QString itemId) const +{ + if (!canRender) { + return Plasma::Types::ItemStatus::HiddenStatus; + } + + if (status == Plasma::Types::ItemStatus::HiddenStatus) { + return Plasma::Types::ItemStatus::HiddenStatus; + } + + bool forcedShown = m_showAllItems || m_shownItems.contains(itemId); + bool forcedHidden = m_hiddenItems.contains(itemId); + + if (forcedShown || (!forcedHidden && status != Plasma::Types::ItemStatus::PassiveStatus)) { + return Plasma::Types::ItemStatus::ActiveStatus; + } else { + return Plasma::Types::ItemStatus::PassiveStatus; + } +} + +static QString plasmoidCategoryForMetadata(const KPluginMetaData &metadata) +{ + QString category = QStringLiteral("UnknownCategory"); + + if (metadata.isValid()) { + const QString notificationAreaCategory = metadata.value(QStringLiteral("X-Plasma-NotificationAreaCategory")); + if (!notificationAreaCategory.isEmpty()) { + category = notificationAreaCategory; + } + } + + return category; +} + +PlasmoidModel::PlasmoidModel(QPointer settings, QPointer plasmoidRegistry, QObject *parent) + : BaseModel(settings, parent) + , m_plasmoidRegistry(plasmoidRegistry) +{ + connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginRegistered, this, &PlasmoidModel::appendRow); + connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginUnregistered, this, &PlasmoidModel::removeRow); + + const auto appletMetaDataList = m_plasmoidRegistry->systemTrayApplets(); + for (const auto &info : appletMetaDataList) { + if (!info.isValid() || info.value(QStringLiteral("X-Plasma-NotificationArea")) != "true") { + continue; + } + appendRow(info); + } +} + +QVariant PlasmoidModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { + return QVariant(); + } + + const PlasmoidModel::Item &item = m_items[index.row()]; + const KPluginMetaData &pluginMetaData = item.pluginMetaData; + const Plasma::Applet *applet = item.applet; + + if (role <= Qt::UserRole) { + switch (role) { + case Qt::DisplayRole: { + const QString dbusactivation = pluginMetaData.value(QStringLiteral("X-Plasma-DBusActivationService")); + if (dbusactivation.isEmpty()) { + return pluginMetaData.name(); + } else { + return i18nc("Suffix added to the applet name if the applet is autoloaded via DBus activation", "%1 (Automatic load)", pluginMetaData.name()); + } + } + case Qt::DecorationRole: { + QIcon icon = QIcon::fromTheme(pluginMetaData.iconName()); + return icon.isNull() ? QVariant() : icon; + } + default: + return QVariant(); + } + } + + if (role < static_cast(Role::Applet)) { + Plasma::Types::ItemStatus status = Plasma::Types::ItemStatus::UnknownStatus; + if (applet) { + status = applet->status(); + } + + switch (static_cast(role)) { + case BaseRole::ItemType: + return QStringLiteral("Plasmoid"); + case BaseRole::ItemId: + return pluginMetaData.pluginId(); + case BaseRole::CanRender: + return applet != nullptr; + case BaseRole::Category: + return plasmoidCategoryForMetadata(pluginMetaData); + case BaseRole::Status: + return status; + case BaseRole::EffectiveStatus: + return calculateEffectiveStatus(applet != nullptr, status, pluginMetaData.pluginId()); + default: + return QVariant(); + } + } + + switch (static_cast(role)) { + case Role::Applet: + return applet ? applet->property("_plasma_graphicObject") : QVariant(); + case Role::HasApplet: + return applet != nullptr; + default: + return QVariant(); + } +} + +int PlasmoidModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_items.size(); +} + +QHash PlasmoidModel::roleNames() const +{ + QHash roles = BaseModel::roleNames(); + + roles.insert(static_cast(Role::Applet), QByteArrayLiteral("applet")); + roles.insert(static_cast(Role::HasApplet), QByteArrayLiteral("hasApplet")); + + return roles; +} + +void PlasmoidModel::addApplet(Plasma::Applet *applet) +{ + auto pluginMetaData = applet->pluginMetaData(); + + int idx = indexOfPluginId(pluginMetaData.pluginId()); + + if (idx < 0) { + idx = rowCount(); + appendRow(pluginMetaData); + } + + m_items[idx].applet = applet; + connect(applet, &Plasma::Applet::statusChanged, this, [this, applet](Plasma::Types::ItemStatus status) { + Q_UNUSED(status) + int idx = indexOfPluginId(applet->pluginMetaData().pluginId()); + Q_EMIT dataChanged(index(idx, 0), index(idx, 0), {static_cast(BaseRole::Status)}); + }); + + Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); +} + +void PlasmoidModel::removeApplet(Plasma::Applet *applet) +{ + int idx = indexOfPluginId(applet->pluginMetaData().pluginId()); + if (idx >= 0) { + m_items[idx].applet = nullptr; + Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); + applet->disconnect(this); + } +} + +void PlasmoidModel::appendRow(const KPluginMetaData &pluginMetaData) +{ + int idx = rowCount(); + beginInsertRows(QModelIndex(), idx, idx); + + PlasmoidModel::Item item; + item.pluginMetaData = pluginMetaData; + m_items.append(item); + + endInsertRows(); +} + +void PlasmoidModel::removeRow(const QString &pluginId) +{ + int idx = indexOfPluginId(pluginId); + beginRemoveRows(QModelIndex(), idx, idx); + m_items.removeAt(idx); + endRemoveRows(); +} + +int PlasmoidModel::indexOfPluginId(const QString &pluginId) const +{ + for (int i = 0; i < rowCount(); i++) { + if (m_items[i].pluginMetaData.pluginId() == pluginId) { + return i; + } + } + return -1; +} + +StatusNotifierModel::StatusNotifierModel(QPointer settings, QObject *parent) + : BaseModel(settings, parent) +{ + m_sniHost = StatusNotifierItemHost::self(); + + connect(m_sniHost, &StatusNotifierItemHost::itemAdded, this, &StatusNotifierModel::addSource); + connect(m_sniHost, &StatusNotifierItemHost::itemRemoved, this, &StatusNotifierModel::removeSource); + + for (auto service : m_sniHost->services()) { + addSource(service); + } +} + +static Plasma::Types::ItemStatus extractStatus(const StatusNotifierItemSource *sniData) +{ + QString status = sniData->status(); + if (status == QLatin1String("Active")) { + return Plasma::Types::ItemStatus::ActiveStatus; + } else if (status == QLatin1String("NeedsAttention")) { + return Plasma::Types::ItemStatus::NeedsAttentionStatus; + } else if (status == QLatin1String("Passive")) { + return Plasma::Types::ItemStatus::PassiveStatus; + } else { + return Plasma::Types::ItemStatus::UnknownStatus; + } +} + +static QVariant extractIcon(const QIcon &icon, const QVariant &defaultValue = QVariant()) +{ + if (!icon.isNull()) { + return icon; + } else { + return defaultValue; + } +} + +static QString extractItemId(const StatusNotifierItemSource *sniData) +{ + const QString itemId = sniData->id(); + // Bug 378910: workaround for Dropbox not following the SNI specification + if (itemId.startsWith(QLatin1String("dropbox-client-"))) { + return QLatin1String("dropbox-client-PID"); + } else { + return itemId; + } +} + +QVariant StatusNotifierModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid)) { + return QVariant(); + } + + StatusNotifierModel::Item item = m_items[index.row()]; + StatusNotifierItemSource *sniData = m_sniHost->itemForService(item.source); + + const QString itemId = extractItemId(sniData); + + if (role <= Qt::UserRole) { + switch (role) { + case Qt::DisplayRole: + return sniData->title(); + case Qt::DecorationRole: + return extractIcon(sniData->icon(), sniData->iconName()); + default: + return QVariant(); + } + } + + if (role < static_cast(Role::DataEngineSource)) { + switch (static_cast(role)) { + case BaseRole::ItemType: + return QStringLiteral("StatusNotifier"); + case BaseRole::ItemId: + return itemId; + case BaseRole::CanRender: + return true; + case BaseRole::Category: { + QVariant category = sniData->category(); + return category.isNull() ? QStringLiteral("UnknownCategory") : sniData->category(); + } + case BaseRole::Status: + return extractStatus(sniData); + case BaseRole::EffectiveStatus: + return calculateEffectiveStatus(true, extractStatus(sniData), itemId); + default: + return QVariant(); + } + } + + switch (static_cast(role)) { + case Role::DataEngineSource: + return item.source; + case Role::Service: + return QVariant::fromValue(item.service); + case Role::AttentionIcon: + return extractIcon(sniData->attentionIcon()); + case Role::AttentionIconName: + return sniData->attentionIconName(); + case Role::AttentionMovieName: + return sniData->attentionMovieName(); + case Role::Category: + return sniData->category(); + case Role::Icon: + return extractIcon(sniData->icon()); + case Role::IconName: + return sniData->iconName(); + case Role::IconThemePath: + return sniData->iconThemePath(); + case Role::Id: + return itemId; + case Role::ItemIsMenu: + return sniData->itemIsMenu(); + case Role::OverlayIconName: + return sniData->overlayIconName(); + case Role::Status: + return extractStatus(sniData); + case Role::Title: + return sniData->title(); + case Role::ToolTipSubTitle: + return sniData->toolTipSubTitle(); + case Role::ToolTipTitle: + return sniData->toolTipTitle(); + case Role::WindowId: + return sniData->windowId(); + default: + return QVariant(); + } +} + +int StatusNotifierModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_items.size(); +} + +QHash StatusNotifierModel::roleNames() const +{ + QHash roles = BaseModel::roleNames(); + + roles.insert(static_cast(Role::DataEngineSource), QByteArrayLiteral("DataEngineSource")); + roles.insert(static_cast(Role::Service), QByteArrayLiteral("Service")); + roles.insert(static_cast(Role::AttentionIcon), QByteArrayLiteral("AttentionIcon")); + roles.insert(static_cast(Role::AttentionIconName), QByteArrayLiteral("AttentionIconName")); + roles.insert(static_cast(Role::AttentionMovieName), QByteArrayLiteral("AttentionMovieName")); + roles.insert(static_cast(Role::Category), QByteArrayLiteral("Category")); + roles.insert(static_cast(Role::Icon), QByteArrayLiteral("Icon")); + roles.insert(static_cast(Role::IconName), QByteArrayLiteral("IconName")); + roles.insert(static_cast(Role::IconThemePath), QByteArrayLiteral("IconThemePath")); + roles.insert(static_cast(Role::Id), QByteArrayLiteral("Id")); + roles.insert(static_cast(Role::ItemIsMenu), QByteArrayLiteral("ItemIsMenu")); + roles.insert(static_cast(Role::OverlayIconName), QByteArrayLiteral("OverlayIconName")); + roles.insert(static_cast(Role::Status), QByteArrayLiteral("Status")); + roles.insert(static_cast(Role::Title), QByteArrayLiteral("Title")); + roles.insert(static_cast(Role::ToolTipSubTitle), QByteArrayLiteral("ToolTipSubTitle")); + roles.insert(static_cast(Role::ToolTipTitle), QByteArrayLiteral("ToolTipTitle")); + roles.insert(static_cast(Role::WindowId), QByteArrayLiteral("WindowId")); + + return roles; +} + +void StatusNotifierModel::addSource(const QString &source) +{ + int count = rowCount(); + beginInsertRows(QModelIndex(), count, count); + + StatusNotifierModel::Item item; + item.source = source; + + StatusNotifierItemSource *sni = m_sniHost->itemForService(source); + connect(sni, &StatusNotifierItemSource::dataUpdated, this, [=]() { + dataUpdated(source); + }); + item.service = sni->createService(); + m_items.append(item); + endInsertRows(); +} + +void StatusNotifierModel::removeSource(const QString &source) +{ + int idx = indexOfSource(source); + if (idx >= 0) { + beginRemoveRows(QModelIndex(), idx, idx); + delete m_items[idx].service; + m_items.removeAt(idx); + endRemoveRows(); + } +} + +void StatusNotifierModel::dataUpdated(const QString &sourceName) +{ + int idx = indexOfSource(sourceName); + + if (idx >= 0) { + Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); + } +} + +int StatusNotifierModel::indexOfSource(const QString &source) const +{ + for (int i = 0; i < rowCount(); i++) { + if (m_items[i].source == source) { + return i; + } + } + return -1; +} + +SystemTrayModel::SystemTrayModel(QObject *parent) + : QConcatenateTablesProxyModel(parent) +{ + m_roleNames = QConcatenateTablesProxyModel::roleNames(); +} + +QHash SystemTrayModel::roleNames() const +{ + return m_roleNames; +} + +void SystemTrayModel::addSourceModel(QAbstractItemModel *sourceModel) +{ + QHashIterator it(sourceModel->roleNames()); + while (it.hasNext()) { + it.next(); + + if (!m_roleNames.contains(it.key())) { + m_roleNames.insert(it.key(), it.value()); + } + } + + QConcatenateTablesProxyModel::addSourceModel(sourceModel); +} diff --git a/plasma/workspace/applets/systemtray/systemtraymodel.h b/plasma/workspace/applets/systemtray/systemtraymodel.h new file mode 100644 index 0000000000..a3054a4aff --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraymodel.h @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace Plasma +{ +class Applet; +class PluginLoader; +} + +class PlasmoidRegistry; +class SystemTraySettings; +class StatusNotifierItemHost; + +/** + * @brief Base class for models used in System Tray. + * + */ +class BaseModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum class BaseRole { + ItemType = Qt::UserRole + 1, + ItemId, + CanRender, + Category, + Status, + EffectiveStatus, + LastBaseRole, + }; + + explicit BaseModel(QPointer settings, QObject *parent = nullptr); + + QHash roleNames() const override; + +private Q_SLOTS: + void onConfigurationChanged(); + +protected: + Plasma::Types::ItemStatus calculateEffectiveStatus(bool canRender, Plasma::Types::ItemStatus status, QString itemId) const; + +private: + QPointer m_settings; + + bool m_showAllItems; + QStringList m_shownItems; + QStringList m_hiddenItems; +}; + +/** + * @brief Data model for plasmoids/applets. + */ +class PlasmoidModel : public BaseModel +{ + Q_OBJECT +public: + enum class Role { + Applet = static_cast(BaseModel::BaseRole::LastBaseRole) + 1, + HasApplet, + }; + + explicit PlasmoidModel(QPointer settings, QPointer plasmoidRegistry, QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +public Q_SLOTS: + void addApplet(Plasma::Applet *applet); + void removeApplet(Plasma::Applet *applet); + +private Q_SLOTS: + void appendRow(const KPluginMetaData &pluginMetaData); + void removeRow(const QString &pluginId); + +private: + struct Item { + KPluginMetaData pluginMetaData; + Plasma::Applet *applet = nullptr; + }; + + int indexOfPluginId(const QString &pluginId) const; + + QPointer m_plasmoidRegistry; + + QVector m_items; +}; + +/** + * @brief Data model for Status Notifier Items (SNI). + */ +class StatusNotifierModel : public BaseModel +{ + Q_OBJECT +public: + enum class Role { + DataEngineSource = static_cast(BaseModel::BaseRole::LastBaseRole) + 100, + Service, + AttentionIcon, + AttentionIconName, + AttentionMovieName, + Category, + Icon, + IconName, + IconThemePath, + Id, + ItemIsMenu, + OverlayIconName, + Status, + Title, + ToolTipSubTitle, + ToolTipTitle, + WindowId, + }; + + StatusNotifierModel(QPointer settings, QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +public Q_SLOTS: + void addSource(const QString &source); + void removeSource(const QString &source); + void dataUpdated(const QString &sourceName); + +public: + struct Item { + QString source; + Plasma::Service *service = nullptr; + }; + int indexOfSource(const QString &source) const; + + StatusNotifierItemHost *m_sniHost = nullptr; + QVector m_items; +}; +Q_DECLARE_TYPEINFO(StatusNotifierModel::Item, Q_MOVABLE_TYPE); + +/** + * @brief Cantenating model for system tray, that can expose multiple data models as one. + */ +class SystemTrayModel : public QConcatenateTablesProxyModel +{ + Q_OBJECT +public: + explicit SystemTrayModel(QObject *parent = nullptr); + + QHash roleNames() const override; + + void addSourceModel(QAbstractItemModel *sourceModel); + +private: + QHash m_roleNames; +}; diff --git a/plasma/workspace/applets/systemtray/systemtraysettings.cpp b/plasma/workspace/applets/systemtray/systemtraysettings.cpp new file mode 100644 index 0000000000..a47c183c0f --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraysettings.cpp @@ -0,0 +1,164 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtraysettings.h" + +#include "debug.h" + +#include + +static const QString KNOWN_ITEMS_KEY = QStringLiteral("knownItems"); +static const QString EXTRA_ITEMS_KEY = QStringLiteral("extraItems"); +static const QString SHOW_ALL_ITEMS_KEY = QStringLiteral("showAllItems"); +static const QString SHOWN_ITEMS_KEY = QStringLiteral("shownItems"); +static const QString HIDDEN_ITEMS_KEY = QStringLiteral("hiddenItems"); + +SystemTraySettings::SystemTraySettings(KConfigLoader *config, QObject *parent) + : QObject(parent) + , config(config) +{ + connect(config, &KConfigLoader::configChanged, this, [this]() { + if (!updatingConfigValue) { + loadConfig(); + } + }); + + loadConfig(); +} + +bool SystemTraySettings::isKnownPlugin(const QString &pluginId) +{ + return m_knownItems.contains(pluginId); +} + +const QStringList SystemTraySettings::knownPlugins() const +{ + return m_knownItems; +} + +void SystemTraySettings::addKnownPlugin(const QString &pluginId) +{ + m_knownItems << pluginId; + writeConfigValue(KNOWN_ITEMS_KEY, m_knownItems); +} + +void SystemTraySettings::removeKnownPlugin(const QString &pluginId) +{ + m_knownItems.removeAll(pluginId); + writeConfigValue(KNOWN_ITEMS_KEY, m_knownItems); +} + +bool SystemTraySettings::isEnabledPlugin(const QString &pluginId) const +{ + return m_extraItems.contains(pluginId); +} + +const QStringList SystemTraySettings::enabledPlugins() const +{ + return m_extraItems; +} + +void SystemTraySettings::addEnabledPlugin(const QString &pluginId) +{ + m_extraItems << pluginId; + writeConfigValue(EXTRA_ITEMS_KEY, m_extraItems); + Q_EMIT enabledPluginsChanged({pluginId}, {}); +} + +void SystemTraySettings::removeEnabledPlugin(const QString &pluginId) +{ + m_extraItems.removeAll(pluginId); + writeConfigValue(EXTRA_ITEMS_KEY, m_extraItems); + Q_EMIT enabledPluginsChanged({}, {pluginId}); +} + +bool SystemTraySettings::isShowAllItems() const +{ + return config->property(SHOW_ALL_ITEMS_KEY).toBool(); +} + +const QStringList SystemTraySettings::shownItems() const +{ + return config->property(SHOWN_ITEMS_KEY).toStringList(); +} + +const QStringList SystemTraySettings::hiddenItems() const +{ + return config->property(HIDDEN_ITEMS_KEY).toStringList(); +} + +void SystemTraySettings::cleanupPlugin(const QString &pluginId) +{ + removeKnownPlugin(pluginId); + removeEnabledPlugin(pluginId); + + QStringList shown = shownItems(); + shown.removeAll(pluginId); + writeConfigValue(SHOWN_ITEMS_KEY, shown); + + QStringList hidden = hiddenItems(); + hidden.removeAll(pluginId); + writeConfigValue(HIDDEN_ITEMS_KEY, hidden); +} + +void SystemTraySettings::loadConfig() +{ + if (!config) { + return; + } + config->load(); + + m_knownItems = config->property(KNOWN_ITEMS_KEY).toStringList(); + + QStringList extraItems = config->property(EXTRA_ITEMS_KEY).toStringList(); + if (extraItems != m_extraItems) { + QStringList extraItemsOld = m_extraItems; + m_extraItems = extraItems; + notifyAboutChangedEnabledPlugins(extraItemsOld, m_extraItems); + } + + Q_EMIT configurationChanged(); +} + +void SystemTraySettings::writeConfigValue(const QString &key, const QVariant &value) +{ + if (!config) { + return; + } + + KConfigSkeletonItem *item = config->findItemByName(key); + if (item) { + updatingConfigValue = true; + item->setWriteFlags(KConfigBase::Notify); + item->setProperty(value); + config->save(); + // refresh state of config scheme, if not, above writes are ignored + config->read(); + updatingConfigValue = false; + } + + Q_EMIT configurationChanged(); +} + +void SystemTraySettings::notifyAboutChangedEnabledPlugins(const QStringList &enabledPluginsOld, const QStringList &enabledPluginsNew) +{ + QStringList newlyEnabled; + QStringList newlyDisabled; + + for (const QString &pluginId : enabledPluginsOld) { + if (!enabledPluginsNew.contains(pluginId)) { + newlyDisabled << pluginId; + } + } + + for (const QString &pluginId : enabledPluginsNew) { + if (!enabledPluginsOld.contains(pluginId)) { + newlyEnabled << pluginId; + } + } + + Q_EMIT enabledPluginsChanged(newlyEnabled, newlyDisabled); +} diff --git a/plasma/workspace/applets/systemtray/systemtraysettings.h b/plasma/workspace/applets/systemtray/systemtraysettings.h new file mode 100644 index 0000000000..656f94403d --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraysettings.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2020 Konrad Materka + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class KConfigLoader; + +/** + * @brief The SystemTraySettings class + */ +class SystemTraySettings : public QObject +{ + Q_OBJECT +public: + explicit SystemTraySettings(KConfigLoader *config, QObject *parent = nullptr); + + virtual bool isKnownPlugin(const QString &pluginId); + virtual const QStringList knownPlugins() const; + virtual void addKnownPlugin(const QString &pluginId); + virtual void removeKnownPlugin(const QString &pluginId); + + virtual bool isEnabledPlugin(const QString &pluginId) const; + virtual const QStringList enabledPlugins() const; + virtual void addEnabledPlugin(const QString &pluginId); + virtual void removeEnabledPlugin(const QString &pluginId); + + virtual bool isShowAllItems() const; + virtual const QStringList shownItems() const; + virtual const QStringList hiddenItems() const; + + virtual void cleanupPlugin(const QString &pluginId); + +Q_SIGNALS: + void configurationChanged(); + void enabledPluginsChanged(const QStringList &enabledPlugins, const QStringList &disabledPlugins); + +private: + void loadConfig(); + void writeConfigValue(const QString &key, const QVariant &value); + void notifyAboutChangedEnabledPlugins(const QStringList &enabledPluginsOld, const QStringList &enabledPluginsNew); + + QPointer config; + + bool updatingConfigValue = false; + QStringList m_extraItems; + QStringList m_knownItems; +}; diff --git a/plasma/workspace/applets/systemtray/systemtraytypes.cpp b/plasma/workspace/applets/systemtray/systemtraytypes.cpp new file mode 100644 index 0000000000..2cc1d8304e --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraytypes.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtraytypes.h" + +// Marshall the ImageStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.data; + argument.endStructure(); + return argument; +} +#include + +// Retrieve the ImageStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon) +{ + qint32 width = 0; + qint32 height = 0; + QByteArray data; + + if (argument.currentType() == QDBusArgument::StructureType) { + argument.beginStructure(); + // qCDebug(DATAENGINE_SNI)() << "begun structure"; + argument >> width; + // qCDebug(DATAENGINE_SNI)() << width; + argument >> height; + // qCDebug(DATAENGINE_SNI)() << height; + argument >> data; + // qCDebug(DATAENGINE_SNI)() << data.size(); + argument.endStructure(); + } + + icon.width = width; + icon.height = height; + icon.data = data; + + return argument; +} + +// Marshall the ImageVector data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector) +{ + argument.beginArray(qMetaTypeId()); + for (int i = 0; i < iconVector.size(); ++i) { + argument << iconVector[i]; + } + argument.endArray(); + return argument; +} + +// Retrieve the ImageVector data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector) +{ + iconVector.clear(); + + if (argument.currentType() == QDBusArgument::ArrayType) { + argument.beginArray(); + + while (!argument.atEnd()) { + KDbusImageStruct element; + argument >> element; + iconVector.append(element); + } + + argument.endArray(); + } + + return argument; +} + +// Marshall the ToolTipStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip) +{ + argument.beginStructure(); + argument << toolTip.icon; + argument << toolTip.image; + argument << toolTip.title; + argument << toolTip.subTitle; + argument.endStructure(); + + return argument; +} + +// Retrieve the ToolTipStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip) +{ + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; + + if (argument.currentType() == QDBusArgument::StructureType) { + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + } + + toolTip.icon = icon; + toolTip.image = image; + toolTip.title = title; + toolTip.subTitle = subTitle; + + return argument; +} diff --git a/plasma/workspace/applets/systemtray/systemtraytypes.h b/plasma/workspace/applets/systemtray/systemtraytypes.h new file mode 100644 index 0000000000..aec1f7974c --- /dev/null +++ b/plasma/workspace/applets/systemtray/systemtraytypes.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "systemtraytypedefs.h" + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip); diff --git a/plasma/workspace/applets/systemtray/tests/CMakeLists.txt b/plasma/workspace/applets/systemtray/tests/CMakeLists.txt new file mode 100644 index 0000000000..e7e87bc68a --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(statusnotifier) diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/CMakeLists.txt b/plasma/workspace/applets/systemtray/tests/statusnotifier/CMakeLists.txt new file mode 100644 index 0000000000..59d7912f03 --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/CMakeLists.txt @@ -0,0 +1,23 @@ +set(statusnotifiertest_SRCS + main.cpp + statusnotifiertest.cpp + pumpjob.cpp +) + +ki18n_wrap_ui(statusnotifiertest_SRCS statusnotifiertest.ui) + +add_executable(statusnotifiertest ${statusnotifiertest_SRCS}) + +target_link_libraries(statusnotifiertest + Qt::Widgets + Qt::Core + KF5::CoreAddons + KF5::KIOCore + KF5::Service + KF5::Notifications + KF5::I18n + Qt::DBus +) + +include(ECMMarkAsTest) +ecm_mark_as_test(statusnotifiertest) diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/main.cpp b/plasma/workspace/applets/systemtray/tests/statusnotifier/main.cpp new file mode 100644 index 0000000000..0f9a8b5141 --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/main.cpp @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include + +#include "statusnotifiertest.h" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + QCommandLineParser parser; + + const QString description = QStringLiteral("Statusnotifier test app"); + const char version[] = "1.0"; + + app.setApplicationVersion(version); + parser.addVersionOption(); + parser.addHelpOption(); + parser.setApplicationDescription(description); + + StatusNotifierTest test; + int ex = test.runMain(); + app.exec(); + return ex; +} diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.cpp b/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.cpp new file mode 100644 index 0000000000..09d2d4f352 --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.cpp @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "pumpjob.h" + +#include + +#include +#include +#include +#include + +static QTextStream cout(stdout); + +class PumpJobPrivate +{ +public: + QString name; + QString error; + + QTimer *timer; + int interval = 200; + + int counter = 0; + + bool suspended = false; +}; + +PumpJob::PumpJob(int interval) + : KIO::Job() +{ + d = new PumpJobPrivate; + + if (interval) { + d->interval = d->interval * interval; + } + KIO::getJobTracker()->registerJob(this); + + d->timer = new QTimer(this); + d->timer->setInterval(d->interval); + qDebug() << "Starting job with interval: " << d->interval; + + connect(d->timer, &QTimer::timeout, this, &PumpJob::timeout); + + init(); +} + +void PumpJob::init() +{ + Q_EMIT description(this, + i18n("Pump Job"), + qMakePair(i18n("Source"), QStringLiteral("this is the source")), + qMakePair(i18n("Destination"), QStringLiteral("destination, baby"))); + d->timer->start(); +} + +PumpJob::~PumpJob() +{ + KIO::getJobTracker()->unregisterJob(this); + qDebug() << "Bye bye"; + delete d; +} + +void PumpJob::start() +{ + qDebug() << "Starting job / timer"; + d->timer->start(); +} + +bool PumpJob::doKill() +{ + qDebug() << "kill"; + emitResult(); + d->timer->stop(); + setError(KIO::ERR_USER_CANCELED); + setErrorText(QStringLiteral("You killed the job.")); + return KJob::doKill(); +} + +bool PumpJob::doResume() +{ + d->suspended = false; + qDebug() << "resume"; + d->timer->start(); + Q_EMIT resumed(this); + return KJob::doResume(); +} + +bool PumpJob::isSuspended() const +{ + return d->suspended; +} + +bool PumpJob::doSuspend() +{ + d->suspended = true; + qDebug() << "suspend"; + d->timer->stop(); + Q_EMIT suspended(this); + return KJob::doSuspend(); +} + +void PumpJob::timeout() +{ + d->counter++; + setPercent(d->counter); + emitSpeed(1024 * 1024 * d->counter / 70); // just something randomly changing + int seconds = (int)((d->interval * 100) - (d->interval * percent())) / 1000; + Q_EMIT infoMessage(this, i18n("Testing kuiserver (%1 seconds remaining)", seconds), i18n("Testing kuiserver (%1 seconds remaining)", seconds)); + + qDebug() << "percent: " << percent() << " Seconds: " << seconds; + if (d->counter % 20 == 0) { + // qDebug() << "percent: " << percent() << " Seconds: " << seconds; + } + + if (d->counter >= 100) { + qDebug() << "Job done"; + emitResult(); + } +} + +#include "moc_pumpjob.cpp" diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.h b/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.h new file mode 100644 index 0000000000..b93a125cb4 --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/pumpjob.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class PumpJobPrivate; + +class PumpJob : public KIO::Job +{ + Q_OBJECT + +public: + PumpJob(int interval = 0); + ~PumpJob() override; + + void start() override; + bool doKill() override; + bool doSuspend() override; + bool doResume() override; + + virtual bool isSuspended() const; + + void init(); +Q_SIGNALS: + void suspended(KJob *job); + void resumed(KJob *job); + +public Q_SLOTS: + void timeout(); + +private: + PumpJobPrivate *d; +}; diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.cpp b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.cpp new file mode 100644 index 0000000000..9f7d32f0ff --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.cpp @@ -0,0 +1,240 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "statusnotifiertest.h" +#include "pumpjob.h" + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include + +static QTextStream cout(stdout); + +class StatusNotifierTestPrivate +{ +public: + QString pluginName; + QTimer *timer; + int interval = 1500; + QStringList loglines; + + KStatusNotifierItem *systemNotifier; + PumpJob *job; +}; + +StatusNotifierTest::StatusNotifierTest(QWidget *parent) + : QDialog(parent) +{ + d = new StatusNotifierTestPrivate; + d->job = nullptr; + + init(); + + setupUi(this); + connect(updateButton, &QPushButton::clicked, this, &StatusNotifierTest::updateNotifier); + connect(jobEnabledCheck, &QCheckBox::toggled, this, &StatusNotifierTest::enableJob); + updateUi(); + iconName->setText(QStringLiteral("plasma")); + show(); + raise(); + log(QStringLiteral("started")); +} + +void StatusNotifierTest::init() +{ + d->systemNotifier = new KStatusNotifierItem(this); + // d->systemNotifier->setCategory(KStatusNotifierItem::SystemServices); + // d->systemNotifier->setCategory(KStatusNotifierItem::Hardware); + d->systemNotifier->setCategory(KStatusNotifierItem::Communications); + d->systemNotifier->setIconByName(QStringLiteral("plasma")); + d->systemNotifier->setStatus(KStatusNotifierItem::Active); + d->systemNotifier->setToolTipTitle(i18nc("tooltip title", "System Service Item")); + d->systemNotifier->setTitle(i18nc("title", "StatusNotifierTest")); + d->systemNotifier->setToolTipSubTitle(i18nc("tooltip subtitle", "Some explanation from the beach.")); + + connect(d->systemNotifier, &KStatusNotifierItem::activateRequested, this, &StatusNotifierTest::activateRequested); + connect(d->systemNotifier, &KStatusNotifierItem::secondaryActivateRequested, this, &StatusNotifierTest::secondaryActivateRequested); + connect(d->systemNotifier, &KStatusNotifierItem::scrollRequested, this, &StatusNotifierTest::scrollRequested); + + auto menu = new QMenu(this); + menu->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), QStringLiteral("action 1")); + menu->addAction(QIcon::fromTheme(QStringLiteral("mail-send")), QStringLiteral("action 2")); + auto subMenu = new QMenu(this); + subMenu->setTitle(QStringLiteral("Sub Menu")); + subMenu->addAction(QStringLiteral("subaction1")); + subMenu->addAction(QStringLiteral("subaction2")); + menu->addMenu(subMenu); + + d->systemNotifier->setContextMenu(menu); +} + +StatusNotifierTest::~StatusNotifierTest() +{ + delete d; +} + +void StatusNotifierTest::log(const QString &msg) +{ + qDebug() << "msg: " << msg; + d->loglines.prepend(msg); + + logEdit->setText(d->loglines.join('\n')); +} + +void StatusNotifierTest::updateUi() +{ + if (!d->systemNotifier) { + return; + } + statusActive->setChecked(d->systemNotifier->status() == KStatusNotifierItem::Active); + statusPassive->setChecked(d->systemNotifier->status() == KStatusNotifierItem::Passive); + statusNeedsAttention->setChecked(d->systemNotifier->status() == KStatusNotifierItem::NeedsAttention); + + statusActive->setEnabled(!statusAuto->isChecked()); + statusPassive->setEnabled(!statusAuto->isChecked()); + statusNeedsAttention->setEnabled(!statusAuto->isChecked()); + + tooltipText->setText(d->systemNotifier->toolTipTitle()); + tooltipSubtext->setText(d->systemNotifier->toolTipSubTitle()); +} + +void StatusNotifierTest::updateNotifier() +{ + // log("update"); + if (!enabledCheck->isChecked()) { + delete d->systemNotifier; + d->systemNotifier = nullptr; + return; + } else { + if (!d->systemNotifier) { + init(); + } + } + + if (!d->systemNotifier) { + return; + } + if (statusAuto->isChecked()) { + d->timer->start(); + } else { + d->timer->stop(); + } + + KStatusNotifierItem::ItemStatus s = KStatusNotifierItem::Passive; + if (statusActive->isChecked()) { + s = KStatusNotifierItem::Active; + } else if (statusNeedsAttention->isChecked()) { + s = KStatusNotifierItem::NeedsAttention; + } + d->systemNotifier->setStatus(s); + + iconPixmapCheckbox->isChecked() ? d->systemNotifier->setIconByPixmap(QIcon::fromTheme(iconName->text())) + : d->systemNotifier->setIconByName(iconName->text()); + overlayIconPixmapCheckbox->isChecked() ? d->systemNotifier->setOverlayIconByPixmap(QIcon::fromTheme(overlayIconName->text())) + : d->systemNotifier->setOverlayIconByName(overlayIconName->text()); + attentionIconPixmapCheckbox->isChecked() ? d->systemNotifier->setAttentionIconByPixmap(QIcon::fromTheme(attentionIconName->text())) + : d->systemNotifier->setAttentionIconByName(attentionIconName->text()); + + d->systemNotifier->setToolTip(iconName->text(), tooltipText->text(), tooltipSubtext->text()); + + updateUi(); +} + +int StatusNotifierTest::runMain() +{ + d->timer = new QTimer(this); + connect(d->timer, &QTimer::timeout, this, &StatusNotifierTest::timeout); + d->timer->setInterval(d->interval); + // d->timer->start(); + return 0; +} + +void StatusNotifierTest::timeout() +{ + if (!d->systemNotifier) { + return; + } + + if (d->systemNotifier->status() == KStatusNotifierItem::Passive) { + d->systemNotifier->setStatus(KStatusNotifierItem::Active); + qDebug() << " Now Active"; + } else if (d->systemNotifier->status() == KStatusNotifierItem::Active) { + d->systemNotifier->setStatus(KStatusNotifierItem::NeedsAttention); + qDebug() << " Now NeedsAttention"; + } else if (d->systemNotifier->status() == KStatusNotifierItem::NeedsAttention) { + d->systemNotifier->setStatus(KStatusNotifierItem::Passive); + qDebug() << " Now passive"; + } + updateUi(); +} + +void StatusNotifierTest::activateRequested(bool active, const QPoint &pos) +{ + Q_UNUSED(active); + Q_UNUSED(pos); + log(QStringLiteral("Activated")); +} + +void StatusNotifierTest::secondaryActivateRequested(const QPoint &pos) +{ + Q_UNUSED(pos); + log(QStringLiteral("secondaryActivateRequested")); +} + +void StatusNotifierTest::scrollRequested(int delta, Qt::Orientation orientation) +{ + QString msg(QStringLiteral("Scrolled by ")); + msg.append(QString::number(delta)); + msg.append((orientation == Qt::Horizontal) ? " Horizontally" : " Vertically"); + log(msg); +} + +// Jobs + +void StatusNotifierTest::enableJob(bool enable) +{ + qDebug() << "Job enabled." << enable; + if (enable) { + d->job = new PumpJob(speedSlider->value()); + QObject::connect(d->job, SIGNAL(percent(KJob *, unsigned long)), this, SLOT(setJobProgress(KJob *, unsigned long))); + QObject::connect(d->job, &KJob::result, this, &StatusNotifierTest::result); + } else { + if (d->job) { + d->timer->stop(); + jobEnabledCheck->setChecked(Qt::Unchecked); + d->job->kill(); + } + } +} + +void StatusNotifierTest::setJobProgress(KJob *j, unsigned long v) +{ + Q_UNUSED(j) + jobProgressBar->setValue(v); +} + +void StatusNotifierTest::result(KJob *job) +{ + if (job->error()) { + qDebug() << "Job Error:" << job->errorText() << job->errorString(); + } else { + qDebug() << "Job finished successfully."; + } + jobEnabledCheck->setCheckState(Qt::Unchecked); +} + +#include "moc_statusnotifiertest.cpp" diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.h b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.h new file mode 100644 index 0000000000..31ec23c579 --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +#include "ui_statusnotifiertest.h" + +class StatusNotifierTestPrivate; + +class StatusNotifierTest : public QDialog, public Ui_StatusNotifierTest +{ + Q_OBJECT + +public: + StatusNotifierTest(QWidget *parent = nullptr); + ~StatusNotifierTest() override; + + void init(); + void log(const QString &msg); + +public Q_SLOTS: + int runMain(); + void timeout(); + void updateUi(); + void updateNotifier(); + + void activateRequested(bool active, const QPoint &pos); + void scrollRequested(int delta, Qt::Orientation orientation); + void secondaryActivateRequested(const QPoint &pos); + + void enableJob(bool enable = true); + void setJobProgress(KJob *j, unsigned long v); + void result(KJob *job); + +private: + StatusNotifierTestPrivate *d; +}; diff --git a/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.ui b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.ui new file mode 100644 index 0000000000..6fac8c56ac --- /dev/null +++ b/plasma/workspace/applets/systemtray/tests/statusnotifier/statusnotifiertest.ui @@ -0,0 +1,283 @@ + + + StatusNotifierTest + + + + 0 + 0 + 471 + 692 + + + + + 0 + 100 + + + + Stat&usNotifier Testap + + + + .. + + + + + + 0 + + + + Stat&us Notifier + + + + 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">Log</span></p></body></html> + + + + + + + + 0 + 120 + + + + true + + + + + + + <b>Status</b> + + + + + + + Enabled + + + true + + + + + + + Automatic + + + + + + + Passive + + + + + + + Ac&tive + + + + + + + &NeedsAttention + + + + + + + <b>Icon</b> + + + + + + + <b>ToolTip</b> + + + + + + + + + + + + + Update + + + + + + + + + Icon + + + + + + + AttentionIcon + + + + + + + OverlayIcon + + + + + + + + + + + + Use Pixmap + + + + + + + + + + + + + + Use Pixmap + + + + + + + + + + + + + + Use Pixmap + + + + + + + + + + + + &Jobs + + + + 0 + + + + + <b>Job Control</b> + + + + + + + <b>Progress</b> + + + + + + + 0 + + + + + + + <b>Naming</b> + + + + + + + + + + + + + 1 + + + 10 + + + 10 + + + 5 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + Job Started + + + + + + + + + + + + diff --git a/plasma/workspace/appmenu/CMakeLists.txt b/plasma/workspace/appmenu/CMakeLists.txt new file mode 100644 index 0000000000..738f6e6d9d --- /dev/null +++ b/plasma/workspace/appmenu/CMakeLists.txt @@ -0,0 +1,45 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_KEYWORDS) + +set(kded_appmenu_SRCS + appmenu.cpp + menuimporter.cpp + appmenu_dbus.cpp + verticalmenu.cpp + ) + +qt_add_dbus_adaptor(kded_appmenu_SRCS com.canonical.AppMenu.Registrar.xml + menuimporter.h MenuImporter menuimporteradaptor MenuImporterAdaptor) + +qt_add_dbus_adaptor(kded_appmenu_SRCS org.kde.kappmenu.xml + appmenu_dbus.h AppmenuDBus appmenuadaptor AppmenuAdaptor) + +kcoreaddons_add_plugin(appmenu SOURCES ${kded_appmenu_SRCS} INSTALL_NAMESPACE "kf5/kded") + +pkg_check_modules(XKBCommon REQUIRED IMPORTED_TARGET xkbcommon) + +target_link_libraries(appmenu + Qt::DBus + Qt::WaylandClientPrivate + Qt::XkbCommonSupportPrivate + KF5::DBusAddons + KF5::KIOCore + KF5::WaylandClient + KF5::WindowSystem + Wayland::Client + PkgConfig::XKBCommon + dbusmenuqt +) + +if (HAVE_X11) + target_link_libraries(appmenu Qt::X11Extras XCB::XCB) +endif() + +ecm_qt_declare_logging_category(appmenu + HEADER appmenu_debug.h + IDENTIFIER APPMENU_DEBUG + CATEGORY_NAME org.kde.plasma.appmenu) + +########### install files ############### + +install( FILES com.canonical.AppMenu.Registrar.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} ) +install( FILES org.kde.kappmenu.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} ) diff --git a/plasma/workspace/appmenu/appmenu.cpp b/plasma/workspace/appmenu/appmenu.cpp new file mode 100644 index 0000000000..01937ad161 --- /dev/null +++ b/plasma/workspace/appmenu/appmenu.cpp @@ -0,0 +1,263 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#include + +#include "appmenu.h" +#include "appmenu_dbus.h" +#include "appmenu_debug.h" +#include "appmenuadaptor.h" +#include "kdbusimporter.h" +#include "menuimporteradaptor.h" +#include "verticalmenu.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if HAVE_X11 +#include +#include +#endif + +static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); +static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); + +K_PLUGIN_FACTORY_WITH_JSON(AppMenuFactory, "appmenu.json", registerPlugin();) + +AppMenuModule::AppMenuModule(QObject *parent, const QList &) + : KDEDModule(parent) + , m_appmenuDBus(new AppmenuDBus(this)) +{ + reconfigure(); + + m_appmenuDBus->connectToBus(); + + connect(m_appmenuDBus, &AppmenuDBus::appShowMenu, this, &AppMenuModule::slotShowMenu); + connect(m_appmenuDBus, &AppmenuDBus::reconfigured, this, &AppMenuModule::reconfigure); + + // transfer our signals to dbus + connect(this, &AppMenuModule::showRequest, m_appmenuDBus, &AppmenuDBus::showRequest); + connect(this, &AppMenuModule::menuHidden, m_appmenuDBus, &AppmenuDBus::menuHidden); + connect(this, &AppMenuModule::menuShown, m_appmenuDBus, &AppmenuDBus::menuShown); + + m_menuViewWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.kappmenuview"), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, + this); + + auto setupMenuImporter = [this]() { + QDBusConnection::sessionBus().connect({}, + {}, + QStringLiteral("com.canonical.dbusmenu"), + QStringLiteral("ItemActivationRequested"), + this, + SLOT(itemActivationRequested(int, uint))); + + // Setup a menu importer if needed + if (!m_menuImporter) { + m_menuImporter = new MenuImporter(this); + connect(m_menuImporter, &MenuImporter::WindowRegistered, this, &AppMenuModule::slotWindowRegistered); + m_menuImporter->connectToBus(); + } + }; + connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceRegistered, this, setupMenuImporter); + connect(m_menuViewWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + Q_UNUSED(service) + QDBusConnection::sessionBus().disconnect({}, + {}, + QStringLiteral("com.canonical.dbusmenu"), + QStringLiteral("ItemActivationRequested"), + this, + SLOT(itemActivationRequested(int, uint))); + delete m_menuImporter; + m_menuImporter = nullptr; + }); + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kappmenuview"))) { + setupMenuImporter(); + } + +#if HAVE_X11 + if (!QX11Info::connection()) { + m_xcbConn = xcb_connect(nullptr, nullptr); + } +#endif + if (qGuiApp->platformName() == QLatin1String("wayland")) { + auto connection = KWayland::Client::ConnectionThread::fromApplication(); + KWayland::Client::Registry registry; + registry.create(connection); + connect(®istry, &KWayland::Client::Registry::plasmaShellAnnounced, this, [this, ®istry](quint32 name, quint32 version) { + m_plasmashell = registry.createPlasmaShell(name, version, this); + }); + registry.setup(); + connection->roundtrip(); + } +} + +AppMenuModule::~AppMenuModule() +{ +#if HAVE_X11 + if (m_xcbConn) { + xcb_disconnect(m_xcbConn); + } +#endif +} + +void AppMenuModule::slotWindowRegistered(WId id, const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ +#if HAVE_X11 + auto *c = QX11Info::connection(); + if (!c) { + c = m_xcbConn; + } + + if (c) { + static xcb_atom_t s_serviceNameAtom = XCB_ATOM_NONE; + static xcb_atom_t s_objectPathAtom = XCB_ATOM_NONE; + + auto setWindowProperty = [c](WId id, xcb_atom_t &atom, const QByteArray &name, const QByteArray &value) { + if (atom == XCB_ATOM_NONE) { + const xcb_intern_atom_cookie_t cookie = xcb_intern_atom(c, false, name.length(), name.constData()); + QScopedPointer reply(xcb_intern_atom_reply(c, cookie, nullptr)); + if (reply.isNull()) { + return; + } + atom = reply->atom; + if (atom == XCB_ATOM_NONE) { + return; + } + } + + auto cookie = xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData()); + xcb_generic_error_t *error; + if ((error = xcb_request_check(c, cookie))) { + qCWarning(APPMENU_DEBUG) << "Got an error"; + free(error); + return; + } + }; + + // TODO only set the property if it doesn't already exist + + setWindowProperty(id, s_serviceNameAtom, s_x11AppMenuServiceNamePropertyName, serviceName.toUtf8()); + setWindowProperty(id, s_objectPathAtom, s_x11AppMenuObjectPathPropertyName, menuObjectPath.path().toUtf8()); + } +#endif +} + +void AppMenuModule::slotShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId) +{ + if (!m_menuImporter) { + return; + } + + // If menu visible, hide it + if (m_menu && m_menu.data()->isVisible()) { + m_menu.data()->hide(); + return; + } + + // dbus call by user (for khotkey shortcut) + if (x == -1 || y == -1) { + // We do not know kwin button position, so tell kwin to show menu + Q_EMIT showRequest(serviceName, menuObjectPath, actionId); + return; + } + + auto *importer = new KDBusMenuImporter(serviceName, menuObjectPath.path(), this); + QMetaObject::invokeMethod(importer, "updateMenu", Qt::QueuedConnection); + disconnect(importer, nullptr, this, nullptr); // ensure we don't popup multiple times in case the menu updates again later + + connect(importer, &KDBusMenuImporter::menuUpdated, this, [=](QMenu *m) { + QMenu *menu = importer->menu(); + if (!menu || menu != m) { + return; + } + m_menu = qobject_cast(menu); + + m_menu.data()->setServiceName(serviceName); + m_menu.data()->setMenuObjectPath(menuObjectPath); + + connect(m_menu.data(), &QMenu::aboutToHide, this, [this, importer] { + hideMenu(); + importer->deleteLater(); + }); + + if (m_plasmashell) { + connect(m_menu.data(), &QMenu::aboutToShow, this, &AppMenuModule::initMenuWayland, Qt::UniqueConnection); + m_menu.data()->popup(QPoint(x, y)); + } else { + m_menu.data()->popup(QPoint(x, y) / qApp->devicePixelRatio()); + } + + QAction *actiontoActivate = importer->actionForId(actionId); + + Q_EMIT menuShown(serviceName, menuObjectPath); + + if (actiontoActivate) { + m_menu.data()->setActiveAction(actiontoActivate); + } + }); +} + +void AppMenuModule::hideMenu() +{ + if (m_menu) { + Q_EMIT menuHidden(m_menu.data()->serviceName(), m_menu->menuObjectPath()); + } +} + +void AppMenuModule::itemActivationRequested(int actionId, uint timeStamp) +{ + Q_UNUSED(timeStamp); + Q_EMIT showRequest(message().service(), QDBusObjectPath(message().path()), actionId); +} + +// this method is not really used anymore but has to be kept for DBus compatibility +void AppMenuModule::reconfigure() +{ +} + +void AppMenuModule::initMenuWayland() +{ + auto window = m_menu->windowHandle(); + if (window && m_plasmashell) { + window->setFlag(Qt::FramelessWindowHint); + window->requestActivate(); + auto plasmaSurface = m_plasmashell->createSurface(KWayland::Client::Surface::fromWindow(window), m_menu.data()); + plasmaSurface->setPosition(window->position()); + plasmaSurface->setSkipSwitcher(true); + plasmaSurface->setSkipTaskbar(true); + m_menu->installEventFilter(this); + } +} + +bool AppMenuModule::eventFilter(QObject *object, QEvent *event) +{ + // HACK we need an input serial to create popups but Qt only sets them on click + if (object == m_menu && event->type() == QEvent::Enter && m_plasmashell) { + auto waylandWindow = dynamic_cast(m_menu->windowHandle()->handle()); + if (waylandWindow) { + const auto device = waylandWindow->display()->currentInputDevice(); + waylandWindow->display()->setLastInputDevice(device, device->pointer()->mEnterSerial, waylandWindow); + } + } + return KDEDModule::eventFilter(object, event); +} + +#include "appmenu.moc" diff --git a/plasma/workspace/appmenu/appmenu.desktop b/plasma/workspace/appmenu/appmenu.desktop new file mode 100644 index 0000000000..6f98ee73cb --- /dev/null +++ b/plasma/workspace/appmenu/appmenu.desktop @@ -0,0 +1,117 @@ +[Desktop Entry] +Type=Service +Name=Application menus daemon +Name[ar]=عفريت قوائم التطبيقات +Name[az]=Tətbiqlər menyusu fon xidməti +Name[bs]=Demon za aplikacijske menije +Name[ca]=Dimoni de menús d'aplicació +Name[ca@valencia]=Dimoni de menús d'aplicació +Name[cs]=Démon nabídky aplikací +Name[da]=Dæmon til programmenuer +Name[de]=Dienst für Anwendungsmenüs +Name[el]=Δαίμων για τα μενού των εφαρμογών +Name[en_GB]=Application menus daemon +Name[es]=Demonio de menús de aplicación +Name[et]=Rakenduste menüü deemon +Name[eu]=Aplikazio-menuen daimona +Name[fi]=Sovellusvalikkopalvelu +Name[fr]=Démon de menus des applications +Name[gl]=Servizo de menús da aplicación +Name[he]=תהליך רקע של תפריט היישומים +Name[hi]=अनुप्रयोग मेन्यू डेमॉन +Name[hsb]=Demon za menije aplikacijow +Name[hu]=Alkalmazás menük démon +Name[ia]=Demone de menus de application +Name[id]=Daemon menu-menu aplikasi +Name[is]=Forritavalmyndamiðlari +Name[it]=Demone dei menu delle applicazioni +Name[ja]=アプリケーションメニューデーモン +Name[kk]=Қолданба мәзірінің қызметі +Name[ko]=프로그램 메뉴 데몬 +Name[lt]=Programos meniu tarnyba +Name[ml]=ആപ്ലിക്കേഷൻ മെനു ഡീമൺ +Name[mr]=अनुप्रयोग मेन्यू डीमन +Name[nb]=Daemon for programmenyer +Name[nds]=Programmmenü-Dämoon +Name[nl]=Daemon voor menu van toepassingen +Name[nn]=Programmeny-teneste +Name[pa]=ਐਪਲੀਕੇਸ਼ਨ ਮੈਨੂ ਡੈਮਨ +Name[pl]=Usługa menu programów +Name[pt]=Servidor dos menus da aplicação +Name[pt_BR]=Servidor dos menus do aplicativo +Name[ro]=Demon pentru meniurile aplicațiilor +Name[ru]=Фоновая служба меню приложений +Name[sk]=Démon ponúk aplikácie +Name[sl]=Odzadnji program za programske menije +Name[sr]=Демон програмских менија +Name[sr@ijekavian]=Демон програмских менија +Name[sr@ijekavianlatin]=Demon programskih menija +Name[sr@latin]=Demon programskih menija +Name[sv]=Demon för programmenyer +Name[ta]=செயலி பட்டிகளுக்கான பின்னணி சேவை +Name[tg]=Раванди дохилии феҳристҳои барнома +Name[tr]=Uygulama menü ardalan süreci +Name[uk]=Фонова служба меню програм +Name[vi]=Trình nền các trình đơn ứng dụng +Name[x-test]=xxApplication menus daemonxx +Name[zh_CN]=应用程序菜单守护程序 +Name[zh_TW]=應用程式選單伺服程式 +Comment=Transfers application's menu to the desktop +Comment[ar]=ينقل قائمة التطبيقات إلى سطح المكتب +Comment[az]=Tətbiqlər menyusunu İş masasında yerləşdirir +Comment[bs]=Prebacuje aplikacijski meni na radnu površinu +Comment[ca]=Transfereix els menús d'aplicació a l'escriptori +Comment[ca@valencia]=Transfereix els menús d'aplicació a l'escriptori +Comment[cs]=Přesouvá nabídku aplikací na plochu +Comment[da]=Overfører programmets menu til skrivebordet +Comment[de]=Überträgt Anwendungsmenüs auf die Arbeitsfläche +Comment[el]=Μεταφέρει το μενού της εφαρμογής στην επιφάνεια εργασίας +Comment[en_GB]=Transfers application's menu to the desktop +Comment[es]=Transfiere el menú de aplicaciones al escritorio +Comment[et]=Rakenduste menüü paigutamine töölauale +Comment[eu]=Aplikazioen menua mahaigainera transferitzen du +Comment[fi]=Siirtää sovelluksen valikon työpöydälle +Comment[fr]=Transfère le menu des applications sur le bureau +Comment[gl]=Transfire o menú de aplicación ao escritorio +Comment[he]=מעביר את התפריטים של היישום אל שולחן העבודה +Comment[hi]=अनुप्रयोग के मेन्यू को डेस्कटॉप पर स्थानांतरित करता है +Comment[hu]=Átviszi az alkalmazás menüjét az asztalra +Comment[ia]=Il transfere menu de applicationes al scriptorio +Comment[id]=Transfer menu aplikasi ke desktop +Comment[is]=Miðlar forritavalmyndum á skjáborðið +Comment[it]=Trasferisce al desktop i menu delle applicazioni +Comment[ja]=アプリケーションメニューをデスクトップに転送します +Comment[kk]=Қолданбаның мәзірін үстел бетіне тапсыру +Comment[ko]=프로그램 메뉴를 바탕 화면으로 보내기 +Comment[lt]=Perkelia programos meniu į darbalaukį +Comment[ml]=അപ്ലിക്കേഷന്റെ മെനു ഡെസ്ക്ടോപ്പിലേക്ക് മാറ്റുന്നു +Comment[mr]=अनुप्रयोग मेन्यूचे डेस्कटॉपवर स्थानान्तरण +Comment[nb]=Overfører programmets meny til skrivebordet +Comment[nds]=Dat Programmmenü na den Schriefdisch överdregen +Comment[nl]=Brengt het menu van de toepassing over naar het bureaublad +Comment[nn]=Overfører programmenyar til skrivebordet +Comment[pa]=ਐਪਲੀਕੇਸ਼ਨ ਦੇ ਮੇਨੂ ਨੂੰ ਡੈਸਕਟਾਪ ਉੱਤੇ ਭੇਜੋ +Comment[pl]=Przenosi menu aplikacji na pulpit +Comment[pt]=Transfere o menu da aplicação para o ecrã +Comment[pt_BR]=Transfere o menu do aplicativo para a área de trabalho +Comment[ro]=Transferă meniul aplicațiilor către birou +Comment[ru]=Перемещает меню приложения на рабочий стол +Comment[sk]=Presunúť ponuky aplikácie na plochu +Comment[sl]=Prenese programski meni na namizje +Comment[sr]=Пребацује меније програма на површ +Comment[sr@ijekavian]=Пребацује меније програма на површ +Comment[sr@ijekavianlatin]=Prebacuje menije programa na površ +Comment[sr@latin]=Prebacuje menije programa na površ +Comment[sv]=Överför programmets meny till skrivbordet +Comment[ta]=செயலியின் பட்டியை பணிமேடைக்கு மாற்றும் +Comment[tr]=Uygulamanın menüsünü masaüstüne aktarır +Comment[uk]=Передає меню програм на стільницю +Comment[vi]=Chuyển trình đơn của ứng dụng ra bàn làm việc +Comment[x-test]=xxTransfers application's menu to the desktopxx +Comment[zh_CN]=将应用程序的菜单转移到桌面上 +Comment[zh_TW]=將應用程式選單傳送到桌面 +X-KDE-ServiceTypes=KDEDModule +X-KDE-Library=appmenu +X-KDE-Kded-autoload=true +X-KDE-Kded-load-on-demand=false +X-KDE-Kded-phase=1 diff --git a/plasma/workspace/appmenu/appmenu.h b/plasma/workspace/appmenu/appmenu.h new file mode 100644 index 0000000000..43bcf32fb3 --- /dev/null +++ b/plasma/workspace/appmenu/appmenu.h @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include +#ifdef HAVE_X11 +#include +#endif + +#include + +#include "menuimporter.h" +#include + +class QDBusServiceWatcher; +class KDBusMenuImporter; +class AppmenuDBus; +class VerticalMenu; +namespace KWayland +{ +namespace Client +{ +class PlasmaShell; +}; +}; + +class AppMenuModule : public KDEDModule, protected QDBusContext +{ + Q_OBJECT +public: + AppMenuModule(QObject *parent, const QList &list); + ~AppMenuModule() override; + bool eventFilter(QObject *object, QEvent *event) override; + +Q_SIGNALS: + /** + * We do not know where is menu decoration button, so tell kwin to show menu + */ + void showRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + /** + * This signal is emitted whenever popup menu/menubar is shown + * Useful for decorations to know if menu button should look pressed + */ + void menuShown(const QString &service, const QDBusObjectPath &objectPath); + /** + * This signal is emitted whenever popup menu/menubar is hidden + * Useful for decorations to know if menu button should be release + */ + void menuHidden(const QString &service, const QDBusObjectPath &objectPath); + +private Q_SLOTS: + /** + * A new window was registered to AppMenu + * + * For compatibility this will set the DBus service name and menu object path as properties + * on the window so we keep working with clients that use the DBusMenu "properly". + */ + void slotWindowRegistered(WId id, const QString &serviceName, const QDBusObjectPath &menuObjectPath); + /** + * Show menu at QPoint(x,y) for DBus serviceName and menuObjectPath + * if x or y == -1, show in application window + */ + void slotShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + /** + * Reconfigure module + */ + void reconfigure(); + + void itemActivationRequested(int actionId, uint timeStamp); + +private: + void hideMenu(); + + void fakeUnityAboutToShow(const QString &service, const QDBusObjectPath &menuObjectPath); + + KDBusMenuImporter *getImporter(const QString &service, const QString &path); + void initMenuWayland(); + + MenuImporter *m_menuImporter = nullptr; + AppmenuDBus *m_appmenuDBus; + QDBusServiceWatcher *m_menuViewWatcher; + QPointer m_menu; + +#ifdef HAVE_X11 + xcb_connection_t *m_xcbConn = nullptr; +#endif + KWayland::Client::PlasmaShell *m_plasmashell = nullptr; +}; diff --git a/plasma/workspace/appmenu/appmenu.json b/plasma/workspace/appmenu/appmenu.json new file mode 100644 index 0000000000..f2d8eb0b16 --- /dev/null +++ b/plasma/workspace/appmenu/appmenu.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Description": "Transfers application's menu to the desktop", + "Description[ar]": "ينقل قائمة التطبيقات إلى سطح المكتب", + "Description[az]": "Tətbiqlər menyusunu İş masasında yerləşdirir", + "Description[ca]": "Transfereix els menús d'aplicació a l'escriptori", + "Description[cs]": "Přesouvá nabídku aplikací na plochu", + "Description[de]": "Überträgt Anwendungsmenüs auf die Arbeitsfläche", + "Description[en_GB]": "Transfers application's menu to the desktop", + "Description[es]": "Transfiere el menú de aplicaciones al escritorio", + "Description[eu]": "Aplikazioen menua mahaigainera transferitzen du", + "Description[fi]": "Siirtää sovelluksen valikon työpöydälle", + "Description[fr]": "Transfère le menu des applications sur le bureau.", + "Description[hu]": "Átviszi az alkalmazás menüjét az asztalra", + "Description[ia]": "Il transfere menu de applicationes al scriptorio", + "Description[it]": "Trasferisce al desktop i menu delle applicazioni", + "Description[ko]": "프로그램 메뉴를 바탕 화면으로 보내기", + "Description[lt]": "Perkelia programos meniu į darbalaukį", + "Description[nl]": "Brengt het menu van de toepassing over naar het bureaublad", + "Description[nn]": "Overfører programmenyar til skrivebordet", + "Description[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਦੇ ਮੇਨੂ ਨੂੰ ਡੈਸਕਟਾਪ ਉੱਤੇ ਭੇਜੋ", + "Description[pl]": "Przenosi menu aplikacji na pulpit", + "Description[pt_BR]": "Transfere o menu do aplicativo para a área de trabalho", + "Description[ro]": "Transferă meniul aplicațiilor către birou", + "Description[ru]": "Перемещает меню приложения на рабочий стол", + "Description[sk]": "Presunúť ponuky aplikácie na plochu", + "Description[sl]": "Prenese programski meni na namizje", + "Description[sv]": "Överför programmets meny till skrivbordet", + "Description[ta]": "செயலியின் பட்டியை பணிமேடையில் காட்டும்", + "Description[tr]": "Uygulamanın menüsünü masaüstüne aktarır", + "Description[uk]": "Передає меню програм на стільницю", + "Description[vi]": "Chuyển trình đơn của ứng dụng ra bàn làm việc", + "Description[x-test]": "xxTransfers application's menu to the desktopxx", + "Description[zh_CN]": "将应用程序的菜单转移到桌面上", + "Name": "Application menus daemon", + "Name[ar]": "عفريت قوائم التطبيقات", + "Name[az]": "Tətbiqlər menyusu fon xidməti", + "Name[bs]": "Demon za aplikacijske menije", + "Name[ca@valencia]": "Dimoni de menús d'aplicació", + "Name[ca]": "Dimoni de menús d'aplicació", + "Name[cs]": "Démon nabídky aplikací", + "Name[da]": "Dæmon til programmenuer", + "Name[de]": "Dienst für Anwendungsmenüs", + "Name[el]": "Δαίμων για τα μενού των εφαρμογών", + "Name[en_GB]": "Application menus daemon", + "Name[es]": "Demonio de menús de aplicación", + "Name[et]": "Rakenduste menüü deemon", + "Name[eu]": "Aplikazio-menuen daimona", + "Name[fi]": "Sovellusvalikkopalvelu", + "Name[fr]": "Démon de menus des applications", + "Name[gl]": "Servizo de menús da aplicación", + "Name[he]": "תהליך רקע של תפריט היישומים", + "Name[hi]": "अनुप्रयोग मेन्यू डेमॉन", + "Name[hsb]": "Demon za menije aplikacijow", + "Name[hu]": "Alkalmazás menük démon", + "Name[ia]": "Demone de menus de application", + "Name[id]": "Daemon menu-menu aplikasi", + "Name[is]": "Forritavalmyndamiðlari", + "Name[it]": "Demone dei menu delle applicazioni", + "Name[ja]": "アプリケーションメニューデーモン", + "Name[kk]": "Қолданба мәзірінің қызметі", + "Name[ko]": "프로그램 메뉴 데몬", + "Name[lt]": "Programos meniu tarnyba", + "Name[ml]": "ആപ്ലിക്കേഷൻ മെനു ഡീമൺ", + "Name[mr]": "अनुप्रयोग मेन्यू डीमन", + "Name[nb]": "Daemon for programmenyer", + "Name[nds]": "Programmmenü-Dämoon", + "Name[nl]": "Daemon voor menu van toepassingen", + "Name[nn]": "Programmeny-teneste", + "Name[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਮੈਨੂ ਡੈਮਨ", + "Name[pl]": "Usługa menu programów", + "Name[pt]": "Servidor dos menus da aplicação", + "Name[pt_BR]": "Servidor dos menus do aplicativo", + "Name[ro]": "Demon pentru meniurile aplicațiilor", + "Name[ru]": "Фоновая служба меню приложений", + "Name[sk]": "Démon ponúk aplikácie", + "Name[sl]": "Odzadnji program za programske menije", + "Name[sr@ijekavian]": "Демон програмских менија", + "Name[sr@ijekavianlatin]": "Demon programskih menija", + "Name[sr@latin]": "Demon programskih menija", + "Name[sr]": "Демон програмских менија", + "Name[sv]": "Demon för programmenyer", + "Name[ta]": "செயலி பட்டிகளுக்கான பின்னணி சேவை", + "Name[tg]": "Раванди дохилии феҳристҳои барнома", + "Name[tr]": "Uygulama menü araçları", + "Name[uk]": "Фонова служба меню програм", + "Name[vi]": "Trình nền các trình đơn ứng dụng", + "Name[x-test]": "xxApplication menus daemonxx", + "Name[zh_CN]": "应用程序菜单守护程序", + "Name[zh_TW]": "應用程式選單伺服程式" + }, + "X-KDE-Kded-autoload": true, + "X-KDE-Kded-load-on-demand": false, + "X-KDE-Kded-phase": 1 +} diff --git a/plasma/workspace/appmenu/appmenu_dbus.cpp b/plasma/workspace/appmenu/appmenu_dbus.cpp new file mode 100644 index 0000000000..1ed2ffbc84 --- /dev/null +++ b/plasma/workspace/appmenu/appmenu_dbus.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + + SPDX-License-Identifier: MIT +*/ + +#include "appmenu_dbus.h" +#include "appmenuadaptor.h" +#include "kdbusimporter.h" + +#include +#include +#include + +static const char *DBUS_SERVICE = "org.kde.kappmenu"; +static const char *DBUS_OBJECT_PATH = "/KAppMenu"; + +AppmenuDBus::AppmenuDBus(QObject *parent) + : QObject(parent) +{ +} + +AppmenuDBus::~AppmenuDBus() +{ +} + +bool AppmenuDBus::connectToBus(const QString &service, const QString &path) +{ + m_service = service.isEmpty() ? DBUS_SERVICE : service; + QString newPath = path.isEmpty() ? DBUS_OBJECT_PATH : path; + + if (!QDBusConnection::sessionBus().registerService(m_service)) { + return false; + } + new AppmenuAdaptor(this); + QDBusConnection::sessionBus().registerObject(newPath, this); + + return true; +} + +void AppmenuDBus::showMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId) +{ + Q_EMIT appShowMenu(x, y, serviceName, menuObjectPath, actionId); +} + +void AppmenuDBus::reconfigure() +{ + Q_EMIT reconfigured(); +} diff --git a/plasma/workspace/appmenu/appmenu_dbus.h b/plasma/workspace/appmenu/appmenu_dbus.h new file mode 100644 index 0000000000..64652ac26c --- /dev/null +++ b/plasma/workspace/appmenu/appmenu_dbus.h @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +// Qt +#include +#include +#include +#include +#include + +class AppmenuDBus : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + explicit AppmenuDBus(QObject *); + ~AppmenuDBus() override; + + bool connectToBus(const QString &service = QString(), const QString &path = QString()); + + /** + * DBus method showing menu at QPoint(x,y) for given DBus service name and menuObjectPath + * if x or y == -1, show in application window + */ + void showMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + /** + * DBus method reconfiguring kded module + */ + void reconfigure(); + +Q_SIGNALS: + /** + * This signal is emitted on showMenu() request + */ + void appShowMenu(int x, int y, const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + /** + * This signal is emitted on reconfigure() request + */ + void reconfigured(); + + // Dbus signals + /** + * This signal is emitted whenever kded want to show menu + * We do not know where is menu decoration button, so tell kwin to show menu + */ + void showRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + /** + * This signal is emitted whenever popup menu/menubar is shown + * Useful for decorations to know if menu button should look pressed + */ + void menuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + /** + * This signal is emitted whenever popup menu/menubar is hidden + * Useful for decorations to know if menu button should be release + */ + void menuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + +private: + QString m_service; +}; diff --git a/plasma/workspace/appmenu/com.canonical.AppMenu.Registrar.xml b/plasma/workspace/appmenu/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000000..bc2be43c19 --- /dev/null +++ b/plasma/workspace/appmenu/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window.  This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/plasma/workspace/appmenu/kdbusimporter.h b/plasma/workspace/appmenu/kdbusimporter.h new file mode 100644 index 0000000000..7626aa5426 --- /dev/null +++ b/plasma/workspace/appmenu/kdbusimporter.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include + +#include "verticalmenu.h" +#include + +class KDBusMenuImporter : public DBusMenuImporter +{ +public: + KDBusMenuImporter(const QString &service, const QString &path, QObject *parent) + : DBusMenuImporter(service, path, parent) + { + } + +protected: + QIcon iconForName(const QString &name) override + { + return QIcon::fromTheme(name); + } + + QMenu *createMenu(QWidget *parent) override + { + return new VerticalMenu(parent); + } +}; diff --git a/plasma/workspace/appmenu/menuimporter.cpp b/plasma/workspace/appmenu/menuimporter.cpp new file mode 100644 index 0000000000..ed51b7f604 --- /dev/null +++ b/plasma/workspace/appmenu/menuimporter.cpp @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#include "menuimporter.h" +#include "dbusmenutypes_p.h" +#include "menuimporteradaptor.h" + +#include +#include + +#include +#include + +static const char *DBUS_SERVICE = "com.canonical.AppMenu.Registrar"; +static const char *DBUS_OBJECT_PATH = "/com/canonical/AppMenu/Registrar"; + +MenuImporter::MenuImporter(QObject *parent) + : QObject(parent) + , m_serviceWatcher(new QDBusServiceWatcher(this)) +{ + qDBusRegisterMetaType(); + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MenuImporter::slotServiceUnregistered); +} + +MenuImporter::~MenuImporter() +{ + QDBusConnection::sessionBus().unregisterService(DBUS_SERVICE); +} + +bool MenuImporter::connectToBus() +{ + if (!QDBusConnection::sessionBus().registerService(DBUS_SERVICE)) { + return false; + } + new MenuImporterAdaptor(this); + QDBusConnection::sessionBus().registerObject(DBUS_OBJECT_PATH, this); + + return true; +} + +void MenuImporter::RegisterWindow(WId id, const QDBusObjectPath &path) +{ + KWindowInfo info(id, NET::WMWindowType, NET::WM2WindowClass); + NET::WindowTypes mask = NET::AllTypesMask; + auto type = info.windowType(mask); + + // Menu can try to register, right click in gimp for example + if (type != NET::Unknown && (type & (NET::Menu | NET::DropdownMenu | NET::PopupMenu))) { + return; + } + + if (path.path().isEmpty()) // prevent bad dbusmenu usage + return; + + QString service = message().service(); + + QString classClass = info.windowClassClass(); + m_windowClasses.insert(id, classClass); + m_menuServices.insert(id, service); + m_menuPaths.insert(id, path); + + if (!m_serviceWatcher->watchedServices().contains(service)) { + m_serviceWatcher->addWatchedService(service); + } + + Q_EMIT WindowRegistered(id, service, path); +} + +void MenuImporter::UnregisterWindow(WId id) +{ + m_menuServices.remove(id); + m_menuPaths.remove(id); + m_windowClasses.remove(id); + + Q_EMIT WindowUnregistered(id); +} + +QString MenuImporter::GetMenuForWindow(WId id, QDBusObjectPath &path) +{ + path = m_menuPaths.value(id); + return m_menuServices.value(id); +} + +void MenuImporter::slotServiceUnregistered(const QString &service) +{ + WId id = m_menuServices.key(service); + m_menuServices.remove(id); + m_menuPaths.remove(id); + m_windowClasses.remove(id); + Q_EMIT WindowUnregistered(id); + m_serviceWatcher->removeWatchedService(service); +} diff --git a/plasma/workspace/appmenu/menuimporter.h b/plasma/workspace/appmenu/menuimporter.h new file mode 100644 index 0000000000..026f757c2f --- /dev/null +++ b/plasma/workspace/appmenu/menuimporter.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +// Qt +#include +#include +#include +#include +#include // For WId + +class QDBusObjectPath; +class QDBusServiceWatcher; + +class MenuImporter : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + explicit MenuImporter(QObject *); + ~MenuImporter() override; + + bool connectToBus(); + + bool serviceExist(WId id) + { + return m_menuServices.contains(id); + } + QString serviceForWindow(WId id) + { + return m_menuServices.value(id); + } + + bool pathExist(WId id) + { + return m_menuPaths.contains(id); + } + QString pathForWindow(WId id) + { + return m_menuPaths.value(id).path(); + } + + QList ids() + { + return m_menuServices.keys(); + } + +Q_SIGNALS: + void WindowRegistered(WId id, const QString &service, const QDBusObjectPath &); + void WindowUnregistered(WId id); + +public Q_SLOTS: + Q_NOREPLY void RegisterWindow(WId id, const QDBusObjectPath &path); + Q_NOREPLY void UnregisterWindow(WId id); + QString GetMenuForWindow(WId id, QDBusObjectPath &path); + +private Q_SLOTS: + void slotServiceUnregistered(const QString &service); + +private: + QDBusServiceWatcher *m_serviceWatcher; + QHash m_menuServices; + QHash m_menuPaths; + QHash m_windowClasses; +}; diff --git a/plasma/workspace/appmenu/org.kde.kappmenu.xml b/plasma/workspace/appmenu/org.kde.kappmenu.xml new file mode 100644 index 0000000000..d29d3ee86b --- /dev/null +++ b/plasma/workspace/appmenu/org.kde.kappmenu.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/appmenu/verticalmenu.cpp b/plasma/workspace/appmenu/verticalmenu.cpp new file mode 100644 index 0000000000..c1d6ad661d --- /dev/null +++ b/plasma/workspace/appmenu/verticalmenu.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + + SPDX-License-Identifier: MIT +*/ + +#include "verticalmenu.h" + +#include +#include +#include +#include + +VerticalMenu::VerticalMenu(QWidget *parent) + : QMenu(parent) +{ +} + +VerticalMenu::~VerticalMenu() +{ +} diff --git a/plasma/workspace/appmenu/verticalmenu.h b/plasma/workspace/appmenu/verticalmenu.h new file mode 100644 index 0000000000..7960959e94 --- /dev/null +++ b/plasma/workspace/appmenu/verticalmenu.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include +#include + +class VerticalMenu : public QMenu +{ + Q_OBJECT +public: + explicit VerticalMenu(QWidget *parent = nullptr); + ~VerticalMenu() override; + + QString serviceName() const + { + return m_serviceName; + } + void setServiceName(const QString &serviceName) + { + m_serviceName = serviceName; + } + + QDBusObjectPath menuObjectPath() const + { + return m_menuObjectPath; + } + void setMenuObjectPath(const QDBusObjectPath &menuObjectPath) + { + m_menuObjectPath = menuObjectPath; + } + +private: + QString m_serviceName; + QDBusObjectPath m_menuObjectPath; +}; diff --git a/plasma/workspace/cmake/FindAppMenuGtkModule.cmake b/plasma/workspace/cmake/FindAppMenuGtkModule.cmake new file mode 100644 index 0000000000..199cbaa61f --- /dev/null +++ b/plasma/workspace/cmake/FindAppMenuGtkModule.cmake @@ -0,0 +1,41 @@ +#.rst: +# FindAppmenuGtkModule +# ----------- +# +# Try to find appmenu-gtk2-module and appmenu-gtk3-module. +# Once done this will define: +# +# ``AppMenuGtkModule_FOUND`` +# System has both appmenu-gtk2-module and appmenu-gtk3-module + +#============================================================================= +# SPDX-FileCopyrightText: 2018 Kai Uwe Broulik +# +# SPDX-License-Identifier: BSD-3-Clause + +find_library(AppMenuGtk2Module_LIBRARY libappmenu-gtk-module.so + PATH_SUFFIXES + gtk-2.0/modules +) + +find_library(AppMenuGtk3Module_LIBRARY libappmenu-gtk-module.so + PATH_SUFFIXES + gtk-3.0/modules +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AppMenuGtkModule + FOUND_VAR + AppMenuGtkModule_FOUND + REQUIRED_VARS + AppMenuGtk3Module_LIBRARY + AppMenuGtk2Module_LIBRARY +) + +mark_as_advanced(AppMenuGtk3Module_LIBRARY AppMenuGtk2Module_LIBRARY) + +include(FeatureSummary) +set_package_properties(AppMenuGtkModule PROPERTIES + URL "https://github.com/rilian-la-te/vala-panel-appmenu/tree/master/subprojects/appmenu-gtk-module" + DESCRIPTION "Application Menu GTK+ Module" +) diff --git a/plasma/workspace/cmake/FindKIOExtras.cmake b/plasma/workspace/cmake/FindKIOExtras.cmake new file mode 100644 index 0000000000..fe6622d6cb --- /dev/null +++ b/plasma/workspace/cmake/FindKIOExtras.cmake @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 Alexander Lohnau +# SPDX-License-Identifier: BSD-3-Clause + +find_path(KIOExtras_PATH thumbnail.so PATHS ${KDE_INSTALL_FULL_PLUGINDIR}/kf5/kio/) + +if (KIOExtras_PATH) + set(KIOExtras_FOUND TRUE) +endif() diff --git a/plasma/workspace/cmake/FindKIOFuse.cmake b/plasma/workspace/cmake/FindKIOFuse.cmake new file mode 100644 index 0000000000..b393c31a68 --- /dev/null +++ b/plasma/workspace/cmake/FindKIOFuse.cmake @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 David Edmundson +# SPDX-License-Identifier: BSD-3-Clause + +find_program(KIOFuse_PATH kio-fuse PATHS ${KDE_INSTALL_FULL_LIBEXECDIR}) + +if (KIOFuse_PATH) + set(KIOFuse_FOUND TRUE) +endif() diff --git a/plasma/workspace/cmake/FindLibdrm.cmake b/plasma/workspace/cmake/FindLibdrm.cmake new file mode 100644 index 0000000000..cf6e061349 --- /dev/null +++ b/plasma/workspace/cmake/FindLibdrm.cmake @@ -0,0 +1,105 @@ +#.rst: +# FindLibdrm +# ------- +# +# Try to find libdrm on a Unix system. +# +# This will define the following variables: +# +# ``Libdrm_FOUND`` +# True if (the requested version of) libdrm is available +# ``Libdrm_VERSION`` +# The version of libdrm +# ``Libdrm_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Libdrm::Libdrm`` +# target +# ``Libdrm_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Libdrm_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Libdrm_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Libdrm::Libdrm`` +# The libdrm library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014 Martin Gräßlin +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindLibdrm.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindLibdrm.cmake") +endif() + +if(NOT WIN32) + # Use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + pkg_check_modules(PKG_Libdrm QUIET libdrm) + + set(Libdrm_DEFINITIONS ${PKG_Libdrm_CFLAGS_OTHER}) + set(Libdrm_VERSION ${PKG_Libdrm_VERSION}) + + find_path(Libdrm_INCLUDE_DIR + NAMES + xf86drm.h + HINTS + ${PKG_Libdrm_INCLUDE_DIRS} + ) + find_library(Libdrm_LIBRARY + NAMES + drm + HINTS + ${PKG_Libdrm_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Libdrm + FOUND_VAR + Libdrm_FOUND + REQUIRED_VARS + Libdrm_LIBRARY + Libdrm_INCLUDE_DIR + VERSION_VAR + Libdrm_VERSION + ) + + if(Libdrm_FOUND AND NOT TARGET Libdrm::Libdrm) + add_library(Libdrm::Libdrm UNKNOWN IMPORTED) + set_target_properties(Libdrm::Libdrm PROPERTIES + IMPORTED_LOCATION "${Libdrm_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Libdrm_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}" + INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}/libdrm" + ) + endif() + + mark_as_advanced(Libdrm_LIBRARY Libdrm_INCLUDE_DIR) + + # compatibility variables + set(Libdrm_LIBRARIES ${Libdrm_LIBRARY}) + set(Libdrm_INCLUDE_DIRS ${Libdrm_INCLUDE_DIR} "${Libdrm_INCLUDE_DIR}/libdrm") + set(Libdrm_VERSION_STRING ${Libdrm_VERSION}) + +else() + message(STATUS "FindLibdrm.cmake cannot find libdrm on Windows systems.") + set(Libdrm_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(Libdrm PROPERTIES + URL "https://wiki.freedesktop.org/dri/" + DESCRIPTION "Userspace interface to kernel DRM services" +) diff --git a/plasma/workspace/cmake/FindQalculate.cmake b/plasma/workspace/cmake/FindQalculate.cmake new file mode 100644 index 0000000000..52457c2cbf --- /dev/null +++ b/plasma/workspace/cmake/FindQalculate.cmake @@ -0,0 +1,79 @@ +# - Try to find libqalculate +# Input variables +# +# QALCULATE_MIN_VERSION - minimal version of libqalculate +# QALCULATE_FIND_REQUIRED - fail if can't find libqalculate +# +# Once done this will define +# +# QALCULATE_FOUND - system has libqalculate +# QALCULATE_CFLAGS - libqalculate cflags +# QALCULATE_LIBRARIES - libqalculate libraries +# +# SPDX-FileCopyrightText: 2007 Vladimir Kuznetsov +# +# SPDX-License-Identifier: BSD-3-Clause + +if(QALCULATE_CFLAGS AND QALCULATE_LIBRARIES) + + # in cache already + set(QALCULATE_FOUND TRUE) + +else(QALCULATE_CFLAGS AND QALCULATE_LIBRARIES) + if(NOT WIN32) + find_package(PkgConfig) + + if(QALCULATE_MIN_VERSION) + pkg_check_modules(_pc_QALCULATE libqalculate>=${QALCULATE_MIN_VERSION}) + else(QALCULATE_MIN_VERSION) + pkg_check_modules(_pc_QALCULATE libqalculate) + endif(QALCULATE_MIN_VERSION) + + if(_pc_QALCULATE_FOUND) + if(${_pc_QALCULATE_VERSION} VERSION_LESS 2.0.0) + pkg_check_modules(_pc_CLN cln) + endif() + set(QALCULATE_CFLAGS ${_pc_QALCULATE_CFLAGS}) + endif() + + find_library(QALCULATE_LIBRARIES + NAMES + qalculate + PATHS + ${_pc_QALCULATE_LIBRARY_DIRS} + ${LIB_INSTALL_DIR} + ) + + find_path(QALCULATE_INCLUDE_DIR + NAMES + libqalculate + PATHS + ${_pc_QALCULATE_INCLUDE_DIRS} + ${INCLUDE_INSTALL_DIR} + ) + + if(_pc_QALCULATE_FOUND) + if(${_pc_QALCULATE_VERSION} VERSION_LESS 2.0.0) + find_library(CLN_LIBRARIES + NAMES + cln + PATHS + ${_pc_CLN_LIBRARY_DIRS} + ${LIB_INSTALL_DIR} + ) + endif() + endif() + + else(NOT WIN32) + # XXX: currently no libqalculate on windows + set(QALCULATE_FOUND FALSE) + + endif(NOT WIN32) + + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(Qalculate DEFAULT_MSG QALCULATE_LIBRARIES ) + + mark_as_advanced(QALCULATE_CFLAGS QALCULATE_LIBRARIES) + +endif(QALCULATE_CFLAGS AND QALCULATE_LIBRARIES) + diff --git a/plasma/workspace/components/CMakeLists.txt b/plasma/workspace/components/CMakeLists.txt new file mode 100644 index 0000000000..3fcd082d10 --- /dev/null +++ b/plasma/workspace/components/CMakeLists.txt @@ -0,0 +1,9 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasmashellprivateplugin\") + +install(DIRECTORY workspace/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/workspace/components) +install(DIRECTORY dialogs/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/workspace/dialogs) +add_subdirectory(containmentlayoutmanager) +add_subdirectory(shellprivate) +add_subdirectory(keyboardlayout) +add_subdirectory(sessionsprivate) +add_subdirectory(lookandfeelqml) diff --git a/plasma/workspace/components/Messages.sh b/plasma/workspace/components/Messages.sh new file mode 100644 index 0000000000..b806b8facd --- /dev/null +++ b/plasma/workspace/components/Messages.sh @@ -0,0 +1,4 @@ +#! /bin/sh +$EXTRACTRC `find . -name \*.kcfg` >>rc.cpp +$XGETTEXT `find . -name \*.cpp` -o $podir/plasmashellprivateplugin.pot +rm -f rc.cpp diff --git a/plasma/workspace/components/containmentlayoutmanager/CMakeLists.txt b/plasma/workspace/components/containmentlayoutmanager/CMakeLists.txt new file mode 100644 index 0000000000..bd8708af99 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/CMakeLists.txt @@ -0,0 +1,32 @@ + + +set(containmentlayoutmanagerplugin_SRCS + containmentlayoutmanagerplugin.cpp + appletcontainer.cpp + configoverlay.cpp + appletslayout.cpp + abstractlayoutmanager.cpp + gridlayoutmanager.cpp + itemcontainer.cpp + resizehandle.cpp + ) + +add_library(containmentlayoutmanagerplugin ${containmentlayoutmanagerplugin_SRCS}) + +target_link_libraries(containmentlayoutmanagerplugin + PUBLIC + Qt::Core + PRIVATE + Qt::Qml Qt::Quick + KF5::Plasma KF5::PlasmaQuick + ) + +ecm_qt_declare_logging_category(containmentlayoutmanagerplugin + HEADER containmentlayoutmanager_debug.h + IDENTIFIER CONTAINMENTLAYOUTMANAGER_DEBUG + CATEGORY_NAME org.kde.plasma.containmentlayoutmanager +) + +install(TARGETS containmentlayoutmanagerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/containmentlayoutmanager) + +install(DIRECTORY qml/ DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/containmentlayoutmanager) diff --git a/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.cpp b/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.cpp new file mode 100644 index 0000000000..acf3c46c0c --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "abstractlayoutmanager.h" +#include "itemcontainer.h" + +#include + +AbstractLayoutManager::AbstractLayoutManager(AppletsLayout *layout) + : QObject(layout) + , m_layout(layout) +{ +} + +AbstractLayoutManager::~AbstractLayoutManager() +{ +} + +AppletsLayout *AbstractLayoutManager::layout() const +{ + return m_layout; +} + +QSizeF AbstractLayoutManager::cellSize() const +{ + return m_cellSize; +} + +QSizeF AbstractLayoutManager::cellAlignedContainingSize(const QSizeF &size) const +{ + return QSizeF(m_cellSize.width() * ceil(size.width() / m_cellSize.width()), m_cellSize.height() * ceil(size.height() / m_cellSize.height())); +} + +void AbstractLayoutManager::setCellSize(const QSizeF &size) +{ + m_cellSize = size; +} + +QRectF AbstractLayoutManager::candidateGeometry(ItemContainer *item) const +{ + const QRectF originalItemRect = QRectF(item->x(), item->y(), item->width(), item->height()); + + // TODO: a default minimum size + QSizeF minimumSize = QSize(m_layout->minimumItemWidth(), m_layout->minimumItemHeight()); + if (item->layoutAttached()) { + minimumSize = QSizeF(qMax(minimumSize.width(), item->layoutAttached()->property("minimumWidth").toReal()), + qMax(minimumSize.height(), item->layoutAttached()->property("minimumHeight").toReal())); + } + + const QRectF ltrRect = nextAvailableSpace(item, minimumSize, AppletsLayout::LeftToRight); + const QRectF rtlRect = nextAvailableSpace(item, minimumSize, AppletsLayout::RightToLeft); + const QRectF ttbRect = nextAvailableSpace(item, minimumSize, AppletsLayout::TopToBottom); + const QRectF bttRect = nextAvailableSpace(item, minimumSize, AppletsLayout::BottomToTop); + + // Take the closest rect, unless the item prefers a particular positioning strategy + QMap distances; + if (!ltrRect.isEmpty()) { + const int dist = + item->preferredLayoutDirection() == AppletsLayout::LeftToRight ? 0 : QPointF(originalItemRect.center() - ltrRect.center()).manhattanLength(); + distances[dist] = ltrRect; + } + if (!rtlRect.isEmpty()) { + const int dist = + item->preferredLayoutDirection() == AppletsLayout::RightToLeft ? 0 : QPointF(originalItemRect.center() - rtlRect.center()).manhattanLength(); + distances[dist] = rtlRect; + } + if (!ttbRect.isEmpty()) { + const int dist = + item->preferredLayoutDirection() == AppletsLayout::TopToBottom ? 0 : QPointF(originalItemRect.center() - ttbRect.center()).manhattanLength(); + distances[dist] = ttbRect; + } + if (!bttRect.isEmpty()) { + const int dist = + item->preferredLayoutDirection() == AppletsLayout::BottomToTop ? 0 : QPointF(originalItemRect.center() - bttRect.center()).manhattanLength(); + distances[dist] = bttRect; + } + + if (distances.isEmpty()) { + // Failure to layout, completely full + return originalItemRect; + } else { + return distances.first(); + } +} + +void AbstractLayoutManager::positionItem(ItemContainer *item) +{ + // Give it a sane size if uninitialized: this may change size hints + if (item->width() <= 0 || item->height() <= 0) { + item->setSize(QSizeF(qMax(m_layout->minimumItemWidth(), m_layout->defaultItemWidth()), // + qMax(m_layout->minimumItemHeight(), m_layout->defaultItemHeight()))); + } + + QRectF candidate = candidateGeometry(item); + // Use setProperty to allow Behavior on to take effect + item->setProperty("x", candidate.topLeft().x()); + item->setProperty("y", candidate.topLeft().y()); + item->setSize(candidate.size()); +} + +void AbstractLayoutManager::positionItemAndAssign(ItemContainer *item) +{ + releaseSpace(item); + positionItem(item); + assignSpace(item); +} + +bool AbstractLayoutManager::assignSpace(ItemContainer *item) +{ + if (assignSpaceImpl(item)) { + Q_EMIT layoutNeedsSaving(); + return true; + } else { + return false; + } +} + +void AbstractLayoutManager::releaseSpace(ItemContainer *item) +{ + releaseSpaceImpl(item); + Q_EMIT layoutNeedsSaving(); +} + +void AbstractLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_UNUSED(newGeometry); + Q_UNUSED(oldGeometry); + // NOTE: Empty base implementation, don't put anything here +} + +#include "moc_abstractlayoutmanager.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.h b/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.h new file mode 100644 index 0000000000..6c2d428c61 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/abstractlayoutmanager.h @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "appletslayout.h" +#include + +class ItemContainer; + +class AbstractLayoutManager : public QObject +{ + Q_OBJECT + +public: + AbstractLayoutManager(AppletsLayout *layout); + ~AbstractLayoutManager(); + + AppletsLayout *layout() const; + + void setCellSize(const QSizeF &size); + QSizeF cellSize() const; + + /** + * A size aligned to the gid that fully contains the given size + */ + QSizeF cellAlignedContainingSize(const QSizeF &size) const; + + /** + * Positions the item, does *not* assign the space as taken + */ + void positionItem(ItemContainer *item); + + /** + * Positions the item and assigns the space as taken by this item + */ + void positionItemAndAssign(ItemContainer *item); + + /** + * Set the space of item's rect as occupied by item. + * The operation may fail if some space of the item's geometry is already occupied. + * @returns true if the operation succeeded + */ + bool assignSpace(ItemContainer *item); + + /** + * If item is occupying space, set it as available + */ + void releaseSpace(ItemContainer *item); + + // VIRTUALS + virtual QString serializeLayout() const = 0; + + virtual void parseLayout(const QString &savedLayout) = 0; + + virtual void layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry); + + /** + * true if the item is managed by the grid + */ + virtual bool itemIsManaged(ItemContainer *item) = 0; + + /** + * Forget about layout information and relayout all items based solely on their current geometry + */ + virtual void resetLayout() = 0; + + /** + * Forget about layout information and relayout all items based on their stored geometry first, and if that fails from their current geometry + */ + virtual void resetLayoutFromConfig() = 0; + + /** + * Restores an item geometry from the serialized config + * parseLayout needs to be called before this + * @returns true if the item was stored in the config + * and the restore has been performed. + * Otherwise, the item is not touched and returns false + */ + virtual bool restoreItem(ItemContainer *item) = 0; + + /** + * @returns true if the given rectangle is all free space + */ + virtual bool isRectAvailable(const QRectF &rect) = 0; + +Q_SIGNALS: + /** + * Emitted when the layout has been changed and now needs saving + */ + void layoutNeedsSaving(); + +protected: + /** + * Subclasses implement their assignSpace logic here + */ + virtual bool assignSpaceImpl(ItemContainer *item) = 0; + + /** + * Subclasses implement their releasespace logic here + */ + virtual void releaseSpaceImpl(ItemContainer *item) = 0; + + /** + * @returns a rectangle big at least as minimumSize, trying to be as near as possible to the current item's geometry, displaced in the direction we asked, + * forwards or backwards + * @param item the item container we want to place an item in + * @param minimumSize the minimum size we need to make sure is available + * @param direction the preferred item layout direction, can be Closest, LeftToRight, RightToLeft, TopToBottom, and BottomToTop + */ + virtual QRectF nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const = 0; + +private: + QRectF candidateGeometry(ItemContainer *item) const; + + AppletsLayout *m_layout; + + // size in pixels of a crid cell + QSizeF m_cellSize; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/appletcontainer.cpp b/plasma/workspace/components/containmentlayoutmanager/appletcontainer.cpp new file mode 100644 index 0000000000..59d0cec351 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/appletcontainer.cpp @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "appletcontainer.h" +#include "containmentlayoutmanager_debug.h" + +#include +#include + +#include +#include + +AppletContainer::AppletContainer(QQuickItem *parent) + : ItemContainer(parent) +{ + connect(this, &AppletContainer::contentItemChanged, this, [this]() { + if (m_appletItem) { + disconnect(m_appletItem->applet(), &Plasma::Applet::busyChanged, this, nullptr); + } + m_appletItem = qobject_cast(contentItem()); + + connectBusyIndicator(); + connectConfigurationRequired(); + + Q_EMIT appletChanged(); + }); +} + +AppletContainer::~AppletContainer() +{ +} + +void AppletContainer::componentComplete() +{ + connectBusyIndicator(); + connectConfigurationRequired(); + ItemContainer::componentComplete(); +} + +PlasmaQuick::AppletQuickItem *AppletContainer::applet() +{ + return m_appletItem; +} + +QQmlComponent *AppletContainer::busyIndicatorComponent() const +{ + return m_busyIndicatorComponent; +} + +void AppletContainer::setBusyIndicatorComponent(QQmlComponent *component) +{ + if (m_busyIndicatorComponent == component) { + return; + } + + m_busyIndicatorComponent = component; + + if (m_busyIndicatorItem) { + m_busyIndicatorItem->deleteLater(); + m_busyIndicatorItem = nullptr; + } + + Q_EMIT busyIndicatorComponentChanged(); +} + +void AppletContainer::connectBusyIndicator() +{ + if (m_appletItem && !m_busyIndicatorItem) { + Q_ASSERT(m_appletItem->applet()); + connect(m_appletItem->applet(), &Plasma::Applet::busyChanged, this, [this]() { + if (!m_busyIndicatorComponent || !m_appletItem->applet()->isBusy() || m_busyIndicatorItem) { + return; + } + + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_busyIndicatorComponent->beginCreate(context); + m_busyIndicatorItem = qobject_cast(instance); + + if (!m_busyIndicatorItem) { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: busyIndicatorComponent not of Item type"; + if (instance) { + instance->deleteLater(); + } + return; + } + + m_busyIndicatorItem->setParentItem(this); + m_busyIndicatorItem->setZ(999); + m_busyIndicatorComponent->completeCreate(); + }); + } +} + +QQmlComponent *AppletContainer::configurationRequiredComponent() const +{ + return m_configurationRequiredComponent; +} + +void AppletContainer::setConfigurationRequiredComponent(QQmlComponent *component) +{ + if (m_configurationRequiredComponent == component) { + return; + } + + m_configurationRequiredComponent = component; + + if (m_configurationRequiredItem) { + m_configurationRequiredItem->deleteLater(); + m_configurationRequiredItem = nullptr; + } + + Q_EMIT configurationRequiredComponentChanged(); +} + +void AppletContainer::connectConfigurationRequired() +{ + if (m_appletItem && !m_configurationRequiredItem) { + Q_ASSERT(m_appletItem->applet()); + + auto syncConfigRequired = [this]() { + if (!m_configurationRequiredComponent || !m_appletItem->applet()->configurationRequired() || m_configurationRequiredItem) { + return; + } + + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_configurationRequiredComponent->beginCreate(context); + m_configurationRequiredItem = qobject_cast(instance); + + if (!m_configurationRequiredItem) { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: configurationRequiredComponent not of Item type"; + if (instance) { + instance->deleteLater(); + } + return; + } + + m_configurationRequiredItem->setParentItem(this); + m_configurationRequiredItem->setZ(998); + m_configurationRequiredComponent->completeCreate(); + }; + + connect(m_appletItem->applet(), &Plasma::Applet::configurationRequiredChanged, this, syncConfigRequired); + + if (m_appletItem->applet()->configurationRequired()) { + syncConfigRequired(); + } + } +} + +#include "moc_appletcontainer.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/appletcontainer.h b/plasma/workspace/components/containmentlayoutmanager/appletcontainer.h new file mode 100644 index 0000000000..030f1c0776 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/appletcontainer.h @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "itemcontainer.h" + +#include +#include + +namespace PlasmaQuick +{ +class AppletQuickItem; +} + +class AppletContainer : public ItemContainer +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(PlasmaQuick::AppletQuickItem *applet READ applet NOTIFY appletChanged) + + Q_PROPERTY(QQmlComponent *busyIndicatorComponent READ busyIndicatorComponent WRITE setBusyIndicatorComponent NOTIFY busyIndicatorComponentChanged) + + Q_PROPERTY(QQmlComponent *configurationRequiredComponent READ configurationRequiredComponent WRITE setConfigurationRequiredComponent NOTIFY + configurationRequiredComponentChanged) + +public: + AppletContainer(QQuickItem *parent = nullptr); + ~AppletContainer(); + + PlasmaQuick::AppletQuickItem *applet(); + + QQmlComponent *busyIndicatorComponent() const; + void setBusyIndicatorComponent(QQmlComponent *comp); + + QQmlComponent *configurationRequiredComponent() const; + void setConfigurationRequiredComponent(QQmlComponent *comp); + +protected: + void componentComplete() override; + +Q_SIGNALS: + void appletChanged(); + void busyIndicatorComponentChanged(); + void configurationRequiredComponentChanged(); + +private: + void connectBusyIndicator(); + void connectConfigurationRequired(); + + QPointer m_appletItem; + QPointer m_busyIndicatorComponent; + QQuickItem *m_busyIndicatorItem = nullptr; + QPointer m_configurationRequiredComponent; + QQuickItem *m_configurationRequiredItem = nullptr; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/appletslayout.cpp b/plasma/workspace/components/containmentlayoutmanager/appletslayout.cpp new file mode 100644 index 0000000000..d6ea1d0352 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/appletslayout.cpp @@ -0,0 +1,755 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "appletslayout.h" +#include "appletcontainer.h" +#include "containmentlayoutmanager_debug.h" +#include "gridlayoutmanager.h" + +#include +#include +#include +#include + +// Plasma +#include +#include +#include + +AppletsLayout::AppletsLayout(QQuickItem *parent) + : QQuickItem(parent) +{ + m_layoutManager = new GridLayoutManager(this); + + setFlags(QQuickItem::ItemIsFocusScope); + setAcceptedMouseButtons(Qt::LeftButton); + + m_saveLayoutTimer = new QTimer(this); + m_saveLayoutTimer->setSingleShot(true); + m_saveLayoutTimer->setInterval(100); + connect(m_layoutManager, &AbstractLayoutManager::layoutNeedsSaving, m_saveLayoutTimer, QOverload<>::of(&QTimer::start)); + connect(m_saveLayoutTimer, &QTimer::timeout, this, [this]() { + // We can't assume m_containment to be valid: if we load in a plasmoid that can run also + // in "applet" mode, m_containment will never be valid + if (!m_containment) { + return; + } + // We can't save the layout during bootup, for performance reasons and to avoid race consitions as much as possible, so if we needto save and still + // starting up, don't actually savenow, but we will when Corona::startupCompleted is emitted + + if (!m_configKey.isEmpty() && m_containment && m_containment->corona()->isStartupCompleted()) { + const QString serializedConfig = m_layoutManager->serializeLayout(); + m_containment->config().writeEntry(m_configKey, serializedConfig); + m_containment->config().writeEntry(m_fallbackConfigKey, serializedConfig); + // FIXME: something more efficient + m_layoutManager->parseLayout(serializedConfig); + m_savedSize = size(); + m_containment->corona()->requireConfigSync(); + } + }); + + m_layoutChangeTimer = new QTimer(this); + m_layoutChangeTimer->setSingleShot(true); + m_layoutChangeTimer->setInterval(100); + connect(m_layoutChangeTimer, &QTimer::timeout, this, [this]() { + // We can't assume m_containment to be valid: if we load in a plasmoid that can run also + // in "applet" mode, m_containment will never be valid + if (!m_containment) { + return; + } + const QString &serializedConfig = m_containment->config().readEntry(m_configKey, ""); + if ((m_layoutChanges & ConfigKeyChange) && !serializedConfig.isEmpty()) { + if (!m_configKey.isEmpty() && m_containment) { + m_layoutManager->parseLayout(serializedConfig); + + if (width() > 0 && height() > 0) { + m_layoutManager->resetLayoutFromConfig(); + m_savedSize = size(); + } + } + } else if (m_layoutChanges & SizeChange) { + const QRect newGeom(x(), y(), width(), height()); + // The size has been restored from the last one it has been saved: restore that exact same layout + if (newGeom.size() == m_savedSize) { + m_layoutManager->resetLayoutFromConfig(); + + // If the resize is consequence of a screen resolution change, queue a relayout maintaining the distance between screen edges + } else if (!m_geometryBeforeResolutionChange.isEmpty()) { + m_layoutManager->layoutGeometryChanged(newGeom, m_geometryBeforeResolutionChange); + m_geometryBeforeResolutionChange = QRectF(); + + // Heuristically relayout items only when the plasma startup is fully completed + } else { + polish(); + } + } + m_layoutChanges = NoChange; + }); + m_pressAndHoldTimer = new QTimer(this); + m_pressAndHoldTimer->setSingleShot(true); + connect(m_pressAndHoldTimer, &QTimer::timeout, this, [this]() { + setEditMode(true); + }); +} + +AppletsLayout::~AppletsLayout() +{ +} + +PlasmaQuick::AppletQuickItem *AppletsLayout::containment() const +{ + return m_containmentItem; +} + +void AppletsLayout::setContainment(PlasmaQuick::AppletQuickItem *containmentItem) +{ + // Forbid changing containmentItem at runtime + if (m_containmentItem || containmentItem == m_containmentItem || !containmentItem->applet() || !containmentItem->applet()->isContainment()) { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: cannot change the containment to AppletsLayout"; + return; + } + + // Can't assign containments that aren't parents + QQuickItem *candidate = parentItem(); + while (candidate) { + if (candidate == m_containmentItem) { + break; + } + candidate = candidate->parentItem(); + } + if (candidate != m_containmentItem) { + return; + } + + m_containmentItem = containmentItem; + m_containment = static_cast(m_containmentItem->applet()); + + connect(m_containmentItem, SIGNAL(appletAdded(QObject *, int, int)), this, SLOT(appletAdded(QObject *, int, int))); + + connect(m_containmentItem, SIGNAL(appletRemoved(QObject *)), this, SLOT(appletRemoved(QObject *))); + + Q_EMIT containmentChanged(); +} + +QString AppletsLayout::configKey() const +{ + return m_configKey; +} + +void AppletsLayout::setConfigKey(const QString &key) +{ + if (m_configKey == key) { + return; + } + + m_configKey = key; + + // Reloading everything from the new config is expansive, event compress it + m_layoutChanges |= ConfigKeyChange; + m_layoutChangeTimer->start(); + + Q_EMIT configKeyChanged(); +} + +QString AppletsLayout::fallbackConfigKey() const +{ + return m_fallbackConfigKey; +} + +void AppletsLayout::setFallbackConfigKey(const QString &key) +{ + if (m_fallbackConfigKey == key) { + return; + } + + m_fallbackConfigKey = key; + + Q_EMIT fallbackConfigKeyChanged(); +} + +QJSValue AppletsLayout::acceptsAppletCallback() const +{ + return m_acceptsAppletCallback; +} + +qreal AppletsLayout::minimumItemWidth() const +{ + return m_minimumItemSize.width(); +} + +void AppletsLayout::setMinimumItemWidth(qreal width) +{ + if (qFuzzyCompare(width, m_minimumItemSize.width())) { + return; + } + + m_minimumItemSize.setWidth(width); + + Q_EMIT minimumItemWidthChanged(); +} + +qreal AppletsLayout::minimumItemHeight() const +{ + return m_minimumItemSize.height(); +} + +void AppletsLayout::setMinimumItemHeight(qreal height) +{ + if (qFuzzyCompare(height, m_minimumItemSize.height())) { + return; + } + + m_minimumItemSize.setHeight(height); + + Q_EMIT minimumItemHeightChanged(); +} + +qreal AppletsLayout::defaultItemWidth() const +{ + return m_defaultItemSize.width(); +} + +void AppletsLayout::setDefaultItemWidth(qreal width) +{ + if (qFuzzyCompare(width, m_defaultItemSize.width())) { + return; + } + + m_defaultItemSize.setWidth(width); + + Q_EMIT defaultItemWidthChanged(); +} + +qreal AppletsLayout::defaultItemHeight() const +{ + return m_defaultItemSize.height(); +} + +void AppletsLayout::setDefaultItemHeight(qreal height) +{ + if (qFuzzyCompare(height, m_defaultItemSize.height())) { + return; + } + + m_defaultItemSize.setHeight(height); + + Q_EMIT defaultItemHeightChanged(); +} + +qreal AppletsLayout::cellWidth() const +{ + return m_layoutManager->cellSize().width(); +} + +void AppletsLayout::setCellWidth(qreal width) +{ + if (qFuzzyCompare(width, m_layoutManager->cellSize().width())) { + return; + } + + m_layoutManager->setCellSize(QSizeF(width, m_layoutManager->cellSize().height())); + + Q_EMIT cellWidthChanged(); +} + +qreal AppletsLayout::cellHeight() const +{ + return m_layoutManager->cellSize().height(); +} + +void AppletsLayout::setCellHeight(qreal height) +{ + if (qFuzzyCompare(height, m_layoutManager->cellSize().height())) { + return; + } + + m_layoutManager->setCellSize(QSizeF(m_layoutManager->cellSize().width(), height)); + + Q_EMIT cellHeightChanged(); +} + +void AppletsLayout::setAcceptsAppletCallback(const QJSValue &callback) +{ + if (m_acceptsAppletCallback.strictlyEquals(callback)) { + return; + } + + if (!callback.isNull() && !callback.isCallable()) { + return; + } + + m_acceptsAppletCallback = callback; + + Q_EMIT acceptsAppletCallbackChanged(); +} + +QQmlComponent *AppletsLayout::appletContainerComponent() const +{ + return m_appletContainerComponent; +} + +void AppletsLayout::setAppletContainerComponent(QQmlComponent *component) +{ + if (m_appletContainerComponent == component) { + return; + } + + m_appletContainerComponent = component; + + Q_EMIT appletContainerComponentChanged(); +} + +AppletsLayout::EditModeCondition AppletsLayout::editModeCondition() const +{ + return m_editModeCondition; +} + +void AppletsLayout::setEditModeCondition(AppletsLayout::EditModeCondition condition) +{ + if (m_editModeCondition == condition) { + return; + } + + if (m_editModeCondition == Locked) { + setEditMode(false); + } + + m_editModeCondition = condition; + + Q_EMIT editModeConditionChanged(); +} + +bool AppletsLayout::editMode() const +{ + return m_editMode; +} + +void AppletsLayout::setEditMode(bool editMode) +{ + if (m_editMode == editMode) { + return; + } + + m_editMode = editMode; + + Q_EMIT editModeChanged(); +} + +ItemContainer *AppletsLayout::placeHolder() const +{ + return m_placeHolder; +} + +void AppletsLayout::setPlaceHolder(ItemContainer *placeHolder) +{ + if (m_placeHolder == placeHolder) { + return; + } + + m_placeHolder = placeHolder; + m_placeHolder->setParentItem(this); + m_placeHolder->setZ(9999); + m_placeHolder->setOpacity(false); + + Q_EMIT placeHolderChanged(); +} + +QQuickItem *AppletsLayout::eventManagerToFilter() const +{ + return m_eventManagerToFilter; +} + +void AppletsLayout::setEventManagerToFilter(QQuickItem *item) +{ + if (m_eventManagerToFilter == item) { + return; + } + + m_eventManagerToFilter = item; + setFiltersChildMouseEvents(m_eventManagerToFilter); + Q_EMIT eventManagerToFilterChanged(); +} + +void AppletsLayout::save() +{ + m_saveLayoutTimer->start(); +} + +void AppletsLayout::showPlaceHolderAt(const QRectF &geom) +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setPosition(geom.topLeft()); + m_placeHolder->setSize(geom.size()); + + m_layoutManager->positionItem(m_placeHolder); + + m_placeHolder->setProperty("opacity", 1); +} + +void AppletsLayout::showPlaceHolderForItem(ItemContainer *item) +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setPreferredLayoutDirection(item->preferredLayoutDirection()); + m_placeHolder->setPosition(item->position()); + m_placeHolder->setSize(item->size()); + + m_layoutManager->positionItem(m_placeHolder); + + m_placeHolder->setProperty("opacity", 1); +} + +void AppletsLayout::hidePlaceHolder() +{ + if (!m_placeHolder) { + return; + } + + m_placeHolder->setProperty("opacity", 0); +} + +bool AppletsLayout::isRectAvailable(qreal x, qreal y, qreal width, qreal height) +{ + return m_layoutManager->isRectAvailable(QRectF(x, y, width, height)); +} + +bool AppletsLayout::itemIsManaged(ItemContainer *item) +{ + if (!item) { + return false; + } + + return m_layoutManager->itemIsManaged(item); +} + +void AppletsLayout::positionItem(ItemContainer *item) +{ + if (!item) { + return; + } + + item->setParent(this); + m_layoutManager->positionItemAndAssign(item); +} + +void AppletsLayout::restoreItem(ItemContainer *item) +{ + m_layoutManager->restoreItem(item); +} + +void AppletsLayout::releaseSpace(ItemContainer *item) +{ + if (!item) { + return; + } + + m_layoutManager->releaseSpace(item); +} + +void AppletsLayout::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + // Ignore completely moves without resize + if (newGeometry.size() == oldGeometry.size()) { + QQuickItem::geometryChanged(newGeometry, oldGeometry); + return; + } + + // Don't care for anything happening before startup completion + if (!m_containment || !m_containment->corona() || !m_containment->corona()->isStartupCompleted()) { + QQuickItem::geometryChanged(newGeometry, oldGeometry); + return; + } + + // Only do a layouting procedure if we received a valid size + if (!newGeometry.isEmpty()) { + m_layoutChanges |= SizeChange; + m_layoutChangeTimer->start(); + } + + QQuickItem::geometryChanged(newGeometry, oldGeometry); +} + +void AppletsLayout::updatePolish() +{ + m_layoutManager->resetLayout(); + m_savedSize = size(); +} + +void AppletsLayout::componentComplete() +{ + if (!m_containment || !m_containmentItem) { + QQuickItem::componentComplete(); + return; + } + + if (!m_configKey.isEmpty()) { + const QString &serializedConfig = m_containment->config().readEntry(m_configKey, ""); + if (!serializedConfig.isEmpty()) { + m_layoutManager->parseLayout(serializedConfig); + } else { + m_layoutManager->parseLayout(m_containment->config().readEntry(m_fallbackConfigKey, "")); + } + } + + const QList appletObjects = m_containmentItem->property("applets").value>(); + + for (auto *obj : appletObjects) { + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(obj); + + if (!obj) { + continue; + } + + AppletContainer *container = createContainerForApplet(appletItem); + if (width() > 0 && height() > 0) { + m_layoutManager->positionItemAndAssign(container); + } + } + + // layout all extra non applet items + if (width() > 0 && height() > 0) { + for (auto *child : childItems()) { + ItemContainer *item = qobject_cast(child); + if (item && item != m_placeHolder && !m_layoutManager->itemIsManaged(item)) { + m_layoutManager->positionItemAndAssign(item); + } + } + } + + if (m_containment && m_containment->corona()) { + // We inhibit save during startup, so actually save now that startup is completed + connect(m_containment->corona(), &Plasma::Corona::startupCompleted, this, [this]() { + save(); + }); + // When the screen geometry changes, we need to know the geometry just before it did, so we can apply out heuristic of keeping the distance with borders + // constant + connect(m_containment->corona(), &Plasma::Corona::screenGeometryChanged, this, [this](int id) { + if (m_containment->screen() == id) { + m_geometryBeforeResolutionChange = QRectF(x(), y(), width(), height()); + } + }); + } + QQuickItem::componentComplete(); +} + +bool AppletsLayout::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (item != m_eventManagerToFilter) { + return QQuickItem::childMouseEventFilter(item, event); + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *me = static_cast(event); + if (me->buttons() & Qt::LeftButton) { + mousePressEvent(me); + } + break; + } + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + mouseMoveEvent(me); + break; + } + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(event); + mouseReleaseEvent(me); + break; + } + case QEvent::UngrabMouse: + mouseUngrabEvent(); + break; + default: + break; + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void AppletsLayout::mousePressEvent(QMouseEvent *event) +{ + forceActiveFocus(Qt::MouseFocusReason); + + if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { + return; + } + + if (!m_editMode && m_editModeCondition == AppletsLayout::AfterPressAndHold) { + m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + + m_mouseDownWasEditMode = m_editMode; + m_mouseDownPosition = event->windowPos(); + + // event->setAccepted(false); +} + +void AppletsLayout::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { + return; + } + + if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + m_pressAndHoldTimer->stop(); + } +} + +void AppletsLayout::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_editMode + && m_mouseDownWasEditMode + // By only accepting synthetyzed events, this makes the + // close by tapping in any empty area only work with real + // touch events, as we want a different behavior between desktop + // and tablet mode + && (event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt) + && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() < QGuiApplication::styleHints()->startDragDistance()) { + setEditMode(false); + } + + m_pressAndHoldTimer->stop(); + + if (!m_editMode) { + for (auto *child : childItems()) { + ItemContainer *item = qobject_cast(child); + if (item && item != m_placeHolder) { + item->setEditMode(false); + } + } + } +} + +void AppletsLayout::mouseUngrabEvent() +{ + m_pressAndHoldTimer->stop(); +} + +void AppletsLayout::appletAdded(QObject *applet, int x, int y) +{ + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); + + // maybe even an assert? + if (!appletItem) { + return; + } + + if (m_acceptsAppletCallback.isCallable()) { + QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); + Q_ASSERT(engine); + QJSValueList args; + args << engine->newQObject(applet) << QJSValue(x) << QJSValue(y); + + if (!m_acceptsAppletCallback.call(args).toBool()) { + Q_EMIT appletRefused(applet, x, y); + return; + } + } + + AppletContainer *container = createContainerForApplet(appletItem); + container->setPosition(QPointF(x, y)); + container->setVisible(true); + + m_layoutManager->positionItemAndAssign(container); +} + +void AppletsLayout::appletRemoved(QObject *applet) +{ + PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); + + // maybe even an assert? + if (!appletItem) { + return; + } + + AppletContainer *container = m_containerForApplet.value(appletItem); + if (!container) { + return; + } + + m_layoutManager->releaseSpace(container); + m_containerForApplet.remove(appletItem); + appletItem->setParentItem(this); + container->deleteLater(); +} + +AppletContainer *AppletsLayout::createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem) +{ + AppletContainer *container = m_containerForApplet.value(appletItem); + + if (container) { + return container; + } + + bool createdFromQml = true; + + if (m_appletContainerComponent) { + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_appletContainerComponent->beginCreate(context); + container = qobject_cast(instance); + if (container) { + container->setParentItem(this); + } else { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: provided component not an AppletContainer instance"; + if (instance) { + instance->deleteLater(); + } + createdFromQml = false; + } + } + + if (!container) { + container = new AppletContainer(this); + } + + container->setVisible(false); + + const QSizeF appletSize = appletItem->size(); + container->setContentItem(appletItem); + + m_containerForApplet[appletItem] = container; + container->setLayout(this); + container->setKey(QLatin1String("Applet-") + QString::number(appletItem->applet()->id())); + + const bool geometryWasSaved = m_layoutManager->restoreItem(container); + + if (!geometryWasSaved) { + container->setPosition(QPointF(appletItem->x() - container->leftPadding(), appletItem->y() - container->topPadding())); + + if (!appletSize.isEmpty()) { + container->setSize(QSizeF(qMax(m_minimumItemSize.width(), appletSize.width() + container->leftPadding() + container->rightPadding()), + qMax(m_minimumItemSize.height(), appletSize.height() + container->topPadding() + container->bottomPadding()))); + } + } + + if (m_appletContainerComponent && createdFromQml) { + m_appletContainerComponent->completeCreate(); + } + + // NOTE: This has to be done here as we need the component completed to have all the bindings evaluated + if (!geometryWasSaved && appletSize.isEmpty()) { + if (container->initialSize().width() > m_minimumItemSize.width() && container->initialSize().height() > m_minimumItemSize.height()) { + const QSizeF size = m_layoutManager->cellAlignedContainingSize(container->initialSize()); + container->setSize(size); + } else { + container->setSize( + QSizeF(qMax(m_minimumItemSize.width(), m_defaultItemSize.width()), qMax(m_minimumItemSize.height(), m_defaultItemSize.height()))); + } + } + + container->setVisible(true); + appletItem->setVisible(true); + + return container; +} + +#include "moc_appletslayout.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/appletslayout.h b/plasma/workspace/components/containmentlayoutmanager/appletslayout.h new file mode 100644 index 0000000000..309a8944fe --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/appletslayout.h @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class QTimer; + +namespace Plasma +{ +class Containment; +} + +namespace PlasmaQuick +{ +class AppletQuickItem; +} + +class AbstractLayoutManager; +class AppletContainer; +class ItemContainer; + +class AppletsLayout : public QQuickItem +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString configKey READ configKey WRITE setConfigKey NOTIFY configKeyChanged) + + // A config key that can be used as fallback when loading and configKey is not found + // Is always a backup of the last used configKey. Useful when the configkey depends + // from the screen size and plasma starts on an "unexpected" size + Q_PROPERTY(QString fallbackConfigKey READ fallbackConfigKey WRITE setFallbackConfigKey NOTIFY fallbackConfigKeyChanged) + + Q_PROPERTY(PlasmaQuick::AppletQuickItem *containment READ containment WRITE setContainment NOTIFY containmentChanged) + + Q_PROPERTY(QJSValue acceptsAppletCallback READ acceptsAppletCallback WRITE setAcceptsAppletCallback NOTIFY acceptsAppletCallbackChanged) + + Q_PROPERTY(qreal minimumItemWidth READ minimumItemWidth WRITE setMinimumItemWidth NOTIFY minimumItemWidthChanged) + + Q_PROPERTY(qreal minimumItemHeight READ minimumItemHeight WRITE setMinimumItemHeight NOTIFY minimumItemHeightChanged) + + Q_PROPERTY(qreal defaultItemWidth READ defaultItemWidth WRITE setDefaultItemWidth NOTIFY defaultItemWidthChanged) + + Q_PROPERTY(qreal defaultItemHeight READ defaultItemHeight WRITE setDefaultItemHeight NOTIFY defaultItemHeightChanged) + + Q_PROPERTY(qreal cellWidth READ cellWidth WRITE setCellWidth NOTIFY cellWidthChanged) + + Q_PROPERTY(qreal cellHeight READ cellHeight WRITE setCellHeight NOTIFY cellHeightChanged) + + Q_PROPERTY(QQmlComponent *appletContainerComponent READ appletContainerComponent WRITE setAppletContainerComponent NOTIFY appletContainerComponentChanged) + + Q_PROPERTY(ItemContainer *placeHolder READ placeHolder WRITE setPlaceHolder NOTIFY placeHolderChanged); + + /** + * if the applets layout contains some kind of main MouseArea, + * MouseEventListener or Flickable, we want to filter its events to make the + * long mouse press work + */ + Q_PROPERTY(QQuickItem *eventManagerToFilter READ eventManagerToFilter WRITE setEventManagerToFilter NOTIFY eventManagerToFilterChanged); + + Q_PROPERTY(AppletsLayout::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) + Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) + +public: + enum PreferredLayoutDirection { + Closest = 0, + LeftToRight, + RightToLeft, + TopToBottom, + BottomToTop, + }; + Q_ENUM(PreferredLayoutDirection) + + enum EditModeCondition { + Locked = 0, + Manual, + AfterPressAndHold, + }; + Q_ENUM(EditModeCondition) + + enum LayoutChange { + NoChange = 0, + SizeChange = 1, + ConfigKeyChange = 2, + }; + Q_DECLARE_FLAGS(LayoutChanges, LayoutChange) + + AppletsLayout(QQuickItem *parent = nullptr); + ~AppletsLayout(); + + // QML setters and getters + QString configKey() const; + void setConfigKey(const QString &key); + + QString fallbackConfigKey() const; + void setFallbackConfigKey(const QString &key); + + PlasmaQuick::AppletQuickItem *containment() const; + void setContainment(PlasmaQuick::AppletQuickItem *containment); + + QJSValue acceptsAppletCallback() const; + void setAcceptsAppletCallback(const QJSValue &callback); + + qreal minimumItemWidth() const; + void setMinimumItemWidth(qreal width); + + qreal minimumItemHeight() const; + void setMinimumItemHeight(qreal height); + + qreal defaultItemWidth() const; + void setDefaultItemWidth(qreal width); + + qreal defaultItemHeight() const; + void setDefaultItemHeight(qreal height); + + qreal cellWidth() const; + void setCellWidth(qreal width); + + qreal cellHeight() const; + void setCellHeight(qreal height); + + QQmlComponent *appletContainerComponent() const; + void setAppletContainerComponent(QQmlComponent *component); + + ItemContainer *placeHolder() const; + void setPlaceHolder(ItemContainer *placeHolder); + + QQuickItem *eventManagerToFilter() const; + void setEventManagerToFilter(QQuickItem *item); + + EditModeCondition editModeCondition() const; + void setEditModeCondition(EditModeCondition condition); + + bool editMode() const; + void setEditMode(bool edit); + + Q_INVOKABLE void save(); + Q_INVOKABLE void showPlaceHolderAt(const QRectF &geom); + Q_INVOKABLE void showPlaceHolderForItem(ItemContainer *item); + Q_INVOKABLE void hidePlaceHolder(); + + Q_INVOKABLE bool isRectAvailable(qreal x, qreal y, qreal width, qreal height); + Q_INVOKABLE bool itemIsManaged(ItemContainer *item); + Q_INVOKABLE void positionItem(ItemContainer *item); + Q_INVOKABLE void restoreItem(ItemContainer *item); + Q_INVOKABLE void releaseSpace(ItemContainer *item); + +Q_SIGNALS: + /** + * An applet has been refused by the layout: acceptsAppletCallback + * returned false and will need to be managed in a different way + */ + void appletRefused(QObject *applet, int x, int y); + + void configKeyChanged(); + void fallbackConfigKeyChanged(); + void containmentChanged(); + void minimumItemWidthChanged(); + void minimumItemHeightChanged(); + void defaultItemWidthChanged(); + void defaultItemHeightChanged(); + void cellWidthChanged(); + void cellHeightChanged(); + void acceptsAppletCallbackChanged(); + void appletContainerComponentChanged(); + void placeHolderChanged(); + void eventManagerToFilterChanged(); + void editModeConditionChanged(); + void editModeChanged(); + +protected: + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void updatePolish() override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + // void classBegin() override; + void componentComplete() override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + +private Q_SLOTS: + void appletAdded(QObject *applet, int x, int y); + void appletRemoved(QObject *applet); + +private: + AppletContainer *createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem); + + QString m_configKey; + QString m_fallbackConfigKey; + QTimer *m_saveLayoutTimer; + QTimer *m_layoutChangeTimer; + LayoutChanges m_layoutChanges = NoChange; + + PlasmaQuick::AppletQuickItem *m_containmentItem = nullptr; + Plasma::Containment *m_containment = nullptr; + QQmlComponent *m_appletContainerComponent = nullptr; + + AbstractLayoutManager *m_layoutManager = nullptr; + + QPointer m_placeHolder; + QPointer m_eventManagerToFilter; + + QTimer *m_pressAndHoldTimer; + + QJSValue m_acceptsAppletCallback; + + AppletsLayout::EditModeCondition m_editModeCondition = AppletsLayout::Manual; + + QHash m_containerForApplet; + + QSizeF m_minimumItemSize; + QSizeF m_defaultItemSize; + QSizeF m_savedSize; + QRectF m_geometryBeforeResolutionChange; + + QPointF m_mouseDownPosition = QPoint(-1, -1); + bool m_mouseDownWasEditMode = false; + bool m_editMode = false; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(AppletsLayout::LayoutChanges) diff --git a/plasma/workspace/components/containmentlayoutmanager/configoverlay.cpp b/plasma/workspace/components/containmentlayoutmanager/configoverlay.cpp new file mode 100644 index 0000000000..0fb755a0fe --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/configoverlay.cpp @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "configoverlay.h" + +#include + +ConfigOverlay::ConfigOverlay(QQuickItem *parent) + : QQuickItem(parent) +{ + m_hideTimer = new QTimer(this); + m_hideTimer->setSingleShot(true); + m_hideTimer->setInterval(600); + connect(m_hideTimer, &QTimer::timeout, this, [this]() { + setVisible(false); + }); +} + +ConfigOverlay::~ConfigOverlay() +{ +} + +bool ConfigOverlay::open() const +{ + return m_open; +} + +void ConfigOverlay::setOpen(bool open) +{ + if (open == m_open) { + return; + } + + m_open = open; + + if (open) { + m_hideTimer->stop(); + setVisible(true); + } else { + m_hideTimer->start(); + } + + Q_EMIT openChanged(); +} + +bool ConfigOverlay::touchInteraction() const +{ + return m_touchInteraction; +} +void ConfigOverlay::setTouchInteraction(bool touch) +{ + if (touch == m_touchInteraction) { + return; + } + + m_touchInteraction = touch; + Q_EMIT touchInteractionChanged(); +} + +ItemContainer *ConfigOverlay::itemContainer() const +{ + return m_itemContainer; +} + +void ConfigOverlay::setItemContainer(ItemContainer *container) +{ + if (container == m_itemContainer) { + return; + } + + if (m_itemContainer) { + disconnect(m_itemContainer, nullptr, this, nullptr); + } + + m_itemContainer = container; + + if (!m_itemContainer || !m_itemContainer->layout()) { + return; + } + + m_leftAvailableSpace = qMax(0.0, m_itemContainer->x()); + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + m_topAvailableSpace = qMax(0.0, m_itemContainer->y()); + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + Q_EMIT leftAvailableSpaceChanged(); + Q_EMIT rightAvailableSpaceChanged(); + Q_EMIT topAvailableSpaceChanged(); + Q_EMIT bottomAvailableSpaceChanged(); + + connect(m_itemContainer.data(), &ItemContainer::xChanged, this, [this]() { + m_leftAvailableSpace = qMax(0.0, m_itemContainer->x()); + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + Q_EMIT leftAvailableSpaceChanged(); + Q_EMIT rightAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::yChanged, this, [this]() { + m_topAvailableSpace = qMax(0.0, m_itemContainer->y()); + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + Q_EMIT topAvailableSpaceChanged(); + Q_EMIT bottomAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::widthChanged, this, [this]() { + m_rightAvailableSpace = qMax(0.0, m_itemContainer->layout()->width() - (m_itemContainer->x() + m_itemContainer->width())); + Q_EMIT rightAvailableSpaceChanged(); + }); + + connect(m_itemContainer.data(), &ItemContainer::heightChanged, this, [this]() { + m_bottomAvailableSpace = qMax(0.0, m_itemContainer->layout()->height() - (m_itemContainer->y() + m_itemContainer->height())); + Q_EMIT bottomAvailableSpaceChanged(); + }); + Q_EMIT itemContainerChanged(); +} + +#include "moc_configoverlay.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/configoverlay.h b/plasma/workspace/components/containmentlayoutmanager/configoverlay.h new file mode 100644 index 0000000000..70b3799a73 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/configoverlay.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "itemcontainer.h" + +class ConfigOverlay : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(bool open READ open WRITE setOpen NOTIFY openChanged) + Q_PROPERTY(ItemContainer *itemContainer READ itemContainer NOTIFY itemContainerChanged) + Q_PROPERTY(qreal leftAvailableSpace READ leftAvailableSpace NOTIFY leftAvailableSpaceChanged); + Q_PROPERTY(qreal topAvailableSpace READ topAvailableSpace NOTIFY topAvailableSpaceChanged); + Q_PROPERTY(qreal rightAvailableSpace READ rightAvailableSpace NOTIFY rightAvailableSpaceChanged); + Q_PROPERTY(qreal bottomAvailableSpace READ bottomAvailableSpace NOTIFY bottomAvailableSpaceChanged); + Q_PROPERTY(bool touchInteraction READ touchInteraction NOTIFY touchInteractionChanged) + +public: + ConfigOverlay(QQuickItem *parent = nullptr); + ~ConfigOverlay(); + + ItemContainer *itemContainer() const; + // NOTE: setter not accessible from QML by purpose + void setItemContainer(ItemContainer *container); + + bool open() const; + void setOpen(bool open); + + qreal leftAvailableSpace() + { + return m_leftAvailableSpace; + } + qreal topAvailableSpace() + { + return m_topAvailableSpace; + } + qreal rightAvailableSpace() + { + return m_rightAvailableSpace; + } + qreal bottomAvailableSpace() + { + return m_bottomAvailableSpace; + } + + bool touchInteraction() const; + // This only usable from C++ + void setTouchInteraction(bool touch); + +Q_SIGNALS: + void openChanged(); + void itemContainerChanged(); + void leftAvailableSpaceChanged(); + void topAvailableSpaceChanged(); + void rightAvailableSpaceChanged(); + void bottomAvailableSpaceChanged(); + void touchInteractionChanged(); + +private: + QPointer m_itemContainer; + qreal m_leftAvailableSpace = 0; + qreal m_topAvailableSpace = 0; + qreal m_rightAvailableSpace = 0; + qreal m_bottomAvailableSpace = 0; + + QTimer *m_hideTimer = nullptr; + + QList m_oldTouchPoints; + + bool m_open = false; + bool m_touchInteraction = false; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp b/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp new file mode 100644 index 0000000000..9303db96ea --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.cpp @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "containmentlayoutmanagerplugin.h" + +#include +#include + +#include "appletcontainer.h" +#include "appletslayout.h" +#include "configoverlay.h" +#include "itemcontainer.h" +#include "resizehandle.h" + +void ContainmentLayoutManagerPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.plasma.private.containmentlayoutmanager")); + + qmlRegisterType(uri, 1, 0, "AppletsLayout"); + qmlRegisterType(uri, 1, 0, "AppletContainer"); + qmlRegisterType(uri, 1, 0, "ConfigOverlay"); + qmlRegisterType(uri, 1, 0, "ItemContainer"); + qmlRegisterType(uri, 1, 0, "ResizeHandle"); + + // qmlProtectModule(uri, 1); +} + +#include "moc_containmentlayoutmanagerplugin.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h b/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h new file mode 100644 index 0000000000..4b60948e45 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/containmentlayoutmanagerplugin.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +class ContainmentLayoutManagerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.cpp b/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.cpp new file mode 100644 index 0000000000..187161c1d8 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.cpp @@ -0,0 +1,552 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "gridlayoutmanager.h" +#include "appletslayout.h" +#include "containmentlayoutmanager_debug.h" + +#include + +GridLayoutManager::GridLayoutManager(AppletsLayout *layout) + : AbstractLayoutManager(layout) +{ +} + +GridLayoutManager::~GridLayoutManager() +{ +} + +QString GridLayoutManager::serializeLayout() const +{ + QString result; + + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + result += itemCont->key() + QLatin1Char(':') + QString::number(itemCont->x()) + QLatin1Char(',') + QString::number(itemCont->y()) + QLatin1Char(',') + + QString::number(itemCont->width()) + QLatin1Char(',') + QString::number(itemCont->height()) + QLatin1Char(',') + + QString::number(itemCont->rotation()) + QLatin1Char(';'); + } + } + + return result; +} + +void GridLayoutManager::parseLayout(const QString &savedLayout) +{ + m_parsedConfig.clear(); + const QStringList itemsConfigs = savedLayout.split(QLatin1Char(';')); + + for (const auto &itemString : itemsConfigs) { + QStringList itemConfig = itemString.split(QLatin1Char(':')); + if (itemConfig.count() != 2) { + continue; + } + + QString id = itemConfig[0]; + QStringList itemGeom = itemConfig[1].split(QLatin1Char(',')); + if (itemGeom.count() != 5) { + continue; + } + + m_parsedConfig[id] = {itemGeom[0].toDouble(), itemGeom[1].toDouble(), itemGeom[2].toDouble(), itemGeom[3].toDouble(), itemGeom[4].toDouble()}; + } +} + +bool GridLayoutManager::itemIsManaged(ItemContainer *item) +{ + return m_pointsForItem.contains(item); +} + +inline void maintainItemEdgeAlignment(ItemContainer *item, const QRectF &newRect, const QRectF &oldRect) +{ + const qreal leftDist = item->x() - oldRect.x(); + const qreal hCenterDist = item->x() + item->width() / 2 - oldRect.center().x(); + const qreal rightDist = oldRect.right() - item->x() - item->width(); + + qreal hMin = qMin(qMin(qAbs(leftDist), qAbs(hCenterDist)), qAbs(rightDist)); + if (qFuzzyCompare(hMin, qAbs(leftDist))) { + // Right alignment, do nothing + } else if (qFuzzyCompare(hMin, qAbs(hCenterDist))) { + item->setX(newRect.center().x() - item->width() / 2 + hCenterDist); + } else if (qFuzzyCompare(hMin, qAbs(rightDist))) { + item->setX(newRect.right() - item->width() - rightDist); + } + + const qreal topDist = item->y() - oldRect.y(); + const qreal vCenterDist = item->y() + item->height() / 2 - oldRect.center().y(); + const qreal bottomDist = oldRect.bottom() - item->y() - item->height(); + + qreal vMin = qMin(qMin(qAbs(topDist), qAbs(vCenterDist)), qAbs(bottomDist)); + + if (qFuzzyCompare(vMin, qAbs(topDist))) { + // Top alignment, do nothing + } else if (qFuzzyCompare(vMin, qAbs(vCenterDist))) { + item->setY(newRect.center().y() - item->height() / 2 + vCenterDist); + } else if (qFuzzyCompare(vMin, qAbs(bottomDist))) { + item->setY(newRect.bottom() - item->height() - bottomDist); + } +} + +void GridLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + m_grid.clear(); + m_pointsForItem.clear(); + for (auto *item : layout()->childItems()) { + // Stash the old config + // m_parsedConfig[item->key()] = {item->x(), item->y(), item->width(), item->height(), item->rotation()}; + // Move the item to maintain the distance with the anchors point + auto *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + maintainItemEdgeAlignment(itemCont, newGeometry, oldGeometry); + // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize + positionItem(itemCont); + assignSpaceImpl(itemCont); + } + } +} + +void GridLayoutManager::resetLayout() +{ + m_grid.clear(); + m_pointsForItem.clear(); + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize + positionItem(itemCont); + assignSpaceImpl(itemCont); + } + } +} + +void GridLayoutManager::resetLayoutFromConfig() +{ + m_grid.clear(); + m_pointsForItem.clear(); + QList missingItems; + + for (auto *item : layout()->childItems()) { + ItemContainer *itemCont = qobject_cast(item); + if (itemCont && itemCont != layout()->placeHolder()) { + if (!restoreItem(itemCont)) { + missingItems << itemCont; + } + } + } + + for (auto *item : qAsConst(missingItems)) { + // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize + positionItem(item); + assignSpaceImpl(item); + } +} + +bool GridLayoutManager::restoreItem(ItemContainer *item) +{ + auto it = m_parsedConfig.find(item->key()); + + if (it != m_parsedConfig.end()) { + // Actual restore + item->setPosition(QPointF(it.value().x, it.value().y)); + item->setSize(QSizeF(it.value().width, it.value().height)); + item->setRotation(it.value().rotation); + + // NOTE: do not use positionItemAndAssign here, because we do not want to Q_EMIT layoutNeedsSaving, to not save after resize + // If size is empty the layout is not in a valid state and probably startup is not completed yet + if (!layout()->size().isEmpty()) { + releaseSpaceImpl(item); + positionItem(item); + assignSpaceImpl(item); + } + + return true; + } + + return false; +} + +bool GridLayoutManager::isRectAvailable(const QRectF &rect) +{ + // TODO: define directions in which it can grow + if (rect.x() < 0 || rect.y() < 0 || rect.x() + rect.width() > layout()->width() || rect.y() + rect.height() > layout()->height()) { + return false; + } + + const QRect cellItemGeom = cellBasedGeometry(rect); + + for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { + for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { + if (!isCellAvailable(QPair(row, column))) { + return false; + } + } + } + return true; +} + +bool GridLayoutManager::assignSpaceImpl(ItemContainer *item) +{ + // Don't Q_EMIT extra layoutneedssaving signals + releaseSpaceImpl(item); + if (!isRectAvailable(itemGeometry(item))) { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Trying to take space not available" << item; + return false; + } + + const QRect cellItemGeom = cellBasedGeometry(itemGeometry(item)); + + for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { + for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { + QPair cell(row, column); + m_grid.insert(cell, item); + m_pointsForItem[item].insert(cell); + } + } + + // Reorder items tab order + for (auto *i2 : layout()->childItems()) { + ItemContainer *item2 = qobject_cast(i2); + if (item2 && item2->parentItem() == item->parentItem() && item != item2 && item2 != layout()->placeHolder() && item->y() < item2->y() + item2->height() + && item->x() <= item2->x()) { + item->stackBefore(item2); + break; + } + } + + if (item->layoutAttached()) { + connect(item, &ItemContainer::sizeHintsChanged, this, [this, item]() { + adjustToItemSizeHints(item); + }); + } + + return true; +} + +void GridLayoutManager::releaseSpaceImpl(ItemContainer *item) +{ + auto it = m_pointsForItem.find(item); + + if (it == m_pointsForItem.end()) { + return; + } + + for (const auto &point : it.value()) { + m_grid.remove(point); + } + + m_pointsForItem.erase(it); + + disconnect(item, &ItemContainer::sizeHintsChanged, this, nullptr); +} + +int GridLayoutManager::rows() const +{ + return layout()->height() / cellSize().height(); +} + +int GridLayoutManager::columns() const +{ + return layout()->width() / cellSize().width(); +} + +void GridLayoutManager::adjustToItemSizeHints(ItemContainer *item) +{ + if (!item->layoutAttached() || item->editMode()) { + return; + } + + bool changed = false; + + // Minimum + const qreal newMinimumHeight = item->layoutAttached()->property("minimumHeight").toReal(); + const qreal newMinimumWidth = item->layoutAttached()->property("minimumWidth").toReal(); + + if (newMinimumHeight > item->height()) { + item->setHeight(newMinimumHeight); + changed = true; + } + if (newMinimumWidth > item->width()) { + item->setWidth(newMinimumWidth); + changed = true; + } + + // Preferred + const qreal newPreferredHeight = item->layoutAttached()->property("preferredHeight").toReal(); + const qreal newPreferredWidth = item->layoutAttached()->property("preferredWidth").toReal(); + + if (newPreferredHeight > item->height()) { + item->setHeight(layout()->cellHeight() * ceil(newPreferredHeight / layout()->cellHeight())); + changed = true; + } + if (newPreferredWidth > item->width()) { + item->setWidth(layout()->cellWidth() * ceil(newPreferredWidth / layout()->cellWidth())); + changed = true; + } + + /*// Maximum : IGNORE? + const qreal newMaximumHeight = item->layoutAttached()->property("preferredHeight").toReal(); + const qreal newMaximumWidth = item->layoutAttached()->property("preferredWidth").toReal(); + + if (newMaximumHeight > 0 && newMaximumHeight < height()) { + item->setHeight(newMaximumHeight); + changed = true; + } + if (newMaximumHeight > 0 && newMaximumWidth < width()) { + item->setWidth(newMaximumWidth); + changed = true; + }*/ + + // Relayout if anything changed + if (changed && itemIsManaged(item)) { + releaseSpace(item); + positionItem(item); + assignSpace(item); + } +} + +QRect GridLayoutManager::cellBasedGeometry(const QRectF &geom) const +{ + return QRect(round(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), + round(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), + round((qreal)geom.width() / cellSize().width()), + round((qreal)geom.height() / cellSize().height())); +} + +QRect GridLayoutManager::cellBasedBoundingGeometry(const QRectF &geom) const +{ + return QRect(floor(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), + floor(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), + ceil((qreal)geom.width() / cellSize().width()), + ceil((qreal)geom.height() / cellSize().height())); +} + +bool GridLayoutManager::isOutOfBounds(const QPair &cell) const +{ + return cell.first < 0 || cell.second < 0 || cell.first >= rows() || cell.second >= columns(); +} + +bool GridLayoutManager::isCellAvailable(const QPair &cell) const +{ + return !isOutOfBounds(cell) && !m_grid.contains(cell); +} + +QRectF GridLayoutManager::itemGeometry(QQuickItem *item) const +{ + return QRectF(item->x(), item->y(), item->width(), item->height()); +} + +QPair GridLayoutManager::nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + --nCell.first; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + ++nCell.first; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.second; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.second; + break; + } + + return nCell; +} + +QPair GridLayoutManager::nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + while (!isOutOfBounds(nCell)) { + nCell = nextCell(nCell, direction); + + if (isOutOfBounds(nCell)) { + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + nCell.first = rows() - 1; + --nCell.second; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + nCell.first = 0; + ++nCell.second; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.first; + nCell.second = columns() - 1; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.first; + nCell.second = 0; + break; + } + } + + if (isCellAvailable(nCell)) { + return nCell; + } + } + + return QPair(-1, -1); +} + +QPair GridLayoutManager::nextTakenCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + while (!isOutOfBounds(nCell)) { + nCell = nextCell(nCell, direction); + + if (isOutOfBounds(nCell)) { + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + nCell.first = rows() - 1; + --nCell.second; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + nCell.first = 0; + ++nCell.second; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.first; + nCell.second = columns() - 1; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.first; + nCell.second = 0; + break; + } + } + + if (!isCellAvailable(nCell)) { + return nCell; + } + } + + return QPair(-1, -1); +} + +int GridLayoutManager::freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + + int avail = 0; + + while (isCellAvailable(nCell)) { + ++avail; + nCell = nextCell(nCell, direction); + } + + return avail; +} + +QRectF GridLayoutManager::nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const +{ + // The mionimum size in grid units + const QSize minimumGridSize(ceil((qreal)minimumSize.width() / cellSize().width()), ceil((qreal)minimumSize.height() / cellSize().height())); + + QRect itemCellGeom = cellBasedGeometry(itemGeometry(item)); + itemCellGeom.setWidth(qMax(itemCellGeom.width(), minimumGridSize.width())); + itemCellGeom.setHeight(qMax(itemCellGeom.height(), minimumGridSize.height())); + + QSize partialSize; + + QPair cell(itemCellGeom.y(), itemCellGeom.x()); + if (direction == AppletsLayout::AppletsLayout::RightToLeft) { + cell.second += itemCellGeom.width(); + } else if (direction == AppletsLayout::AppletsLayout::BottomToTop) { + cell.first += itemCellGeom.height(); + } + + if (!isCellAvailable(cell)) { + cell = nextAvailableCell(cell, direction); + } + + while (!isOutOfBounds(cell)) { + if (direction == AppletsLayout::LeftToRight || direction == AppletsLayout::RightToLeft) { + partialSize = QSize(INT_MAX, 0); + + int currentRow = cell.first; + for (; currentRow < cell.first + itemCellGeom.height(); ++currentRow) { + const int freeRow = freeSpaceInDirection(QPair(currentRow, cell.second), direction); + + partialSize.setWidth(qMin(partialSize.width(), freeRow)); + + if (freeRow > 0) { + partialSize.setHeight(partialSize.height() + 1); + } else if (partialSize.height() < minimumGridSize.height()) { + break; + } + + if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { + break; + } else if (partialSize.width() < minimumGridSize.width()) { + break; + } + } + + if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { + const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); + const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); + + if (direction == AppletsLayout::RightToLeft) { + return QRectF((cell.second + 1) * cellSize().width() - width, cell.first * cellSize().height(), width, height); + // AppletsLayout::LeftToRight + } else { + return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); + } + } else { + cell = nextAvailableCell(nextTakenCell(cell, direction), direction); + } + + } else if (direction == AppletsLayout::TopToBottom || direction == AppletsLayout::BottomToTop) { + partialSize = QSize(0, INT_MAX); + + int currentColumn = cell.second; + for (; currentColumn < cell.second + itemCellGeom.width(); ++currentColumn) { + const int freeColumn = freeSpaceInDirection(QPair(cell.first, currentColumn), direction); + + partialSize.setHeight(qMin(partialSize.height(), freeColumn)); + + if (freeColumn > 0) { + partialSize.setWidth(partialSize.width() + 1); + } else if (partialSize.width() < minimumGridSize.width()) { + break; + } + + if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { + break; + } else if (partialSize.height() < minimumGridSize.height()) { + break; + } + } + + if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { + const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); + const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); + + if (direction == AppletsLayout::BottomToTop) { + return QRectF(cell.second * cellSize().width(), (cell.first + 1) * cellSize().height() - height, width, height); + // AppletsLayout::TopToBottom: + } else { + return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); + } + } else { + cell = nextAvailableCell(nextTakenCell(cell, direction), direction); + } + } + } + + // We didn't manage to find layout space, return invalid geometry + return QRectF(); +} + +#include "moc_gridlayoutmanager.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.h b/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.h new file mode 100644 index 0000000000..532c90d0f1 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/gridlayoutmanager.h @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "abstractlayoutmanager.h" +#include "appletcontainer.h" + +class AppletsLayout; +class ItemContainer; + +struct Geom { + qreal x; + qreal y; + qreal width; + qreal height; + qreal rotation; +}; + +class GridLayoutManager : public AbstractLayoutManager +{ + Q_OBJECT + +public: + GridLayoutManager(AppletsLayout *layout); + ~GridLayoutManager(); + + void layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + QString serializeLayout() const override; + void parseLayout(const QString &savedLayout) override; + + bool itemIsManaged(ItemContainer *item) override; + + void resetLayout() override; + void resetLayoutFromConfig() override; + + bool restoreItem(ItemContainer *item) override; + + bool isRectAvailable(const QRectF &rect) override; + +protected: + // The rectangle as near as possible to the current item geometry which can fit it + QRectF nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const override; + + bool assignSpaceImpl(ItemContainer *item) override; + void releaseSpaceImpl(ItemContainer *item) override; + +private: + // Total cell rows + inline int rows() const; + + // Total cell columns + inline int columns() const; + + // Converts the item pixel-based geometry to a cellsize-based geometry + inline QRect cellBasedGeometry(const QRectF &geom) const; + + // Converts the item pixel-based geometry to a cellsize-based geometry + // This is the bounding geometry, usually larger than cellBasedGeometry + inline QRect cellBasedBoundingGeometry(const QRectF &geom) const; + + // true if the cell is out of the bounds of the containment + inline bool isOutOfBounds(const QPair &cell) const; + + // True if the space for the given cell is available + inline bool isCellAvailable(const QPair &cell) const; + + // Returns the qrect geometry for an item + inline QRectF itemGeometry(QQuickItem *item) const; + + // The next cell given the direction + QPair nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + // The next cell that is available given the direction + QPair nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + // The next cell that is has something in it given the direction + QPair nextTakenCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + // How many cells are available in the row starting from the given cell and direction + int freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + + /** + * This reacts to changes in size hints by an item + */ + void adjustToItemSizeHints(ItemContainer *item); + + // What is the item that occupies the point. The point is expressed in cells rather than pixels. a qpair rather a QPointF as QHash doesn't support + // identification by QPointF + QHash, ItemContainer *> m_grid; + QHash>> m_pointsForItem; + + QHash m_parsedConfig; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/itemcontainer.cpp b/plasma/workspace/components/containmentlayoutmanager/itemcontainer.cpp new file mode 100644 index 0000000000..09ba8199a1 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/itemcontainer.cpp @@ -0,0 +1,768 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemcontainer.h" +#include "configoverlay.h" +#include "containmentlayoutmanager_debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +ItemContainer::ItemContainer(QQuickItem *parent) + : QQuickItem(parent) +{ + setFiltersChildMouseEvents(true); + setFlags(QQuickItem::ItemIsFocusScope); + setActiveFocusOnTab(true); + setAcceptedMouseButtons(Qt::LeftButton); + + setLayout(qobject_cast(parent)); + + m_editModeTimer = new QTimer(this); + m_editModeTimer->setSingleShot(true); + + connect(this, &QQuickItem::parentChanged, this, [this]() { + setLayout(qobject_cast(parentItem())); + }); + + connect(m_editModeTimer, &QTimer::timeout, this, [this]() { + setEditMode(true); + }); + + setKeepMouseGrab(true); + m_sizeHintAdjustTimer = new QTimer(this); + m_sizeHintAdjustTimer->setSingleShot(true); + m_sizeHintAdjustTimer->setInterval(0); + + connect(m_sizeHintAdjustTimer, &QTimer::timeout, this, &ItemContainer::sizeHintsChanged); +} + +ItemContainer::~ItemContainer() +{ + disconnect(this, &QQuickItem::parentChanged, this, nullptr); + + if (m_contentItem) { + m_contentItem->setEnabled(true); + } +} + +QString ItemContainer::key() const +{ + return m_key; +} + +void ItemContainer::setKey(const QString &key) +{ + if (m_key == key) { + return; + } + + m_key = key; + + Q_EMIT keyChanged(); +} + +bool ItemContainer::editMode() const +{ + return m_editMode; +} + +bool ItemContainer::dragActive() const +{ + return m_dragActive; +} + +void ItemContainer::cancelEdit() +{ + m_editModeTimer->stop(); + m_mouseDown = false; + setEditMode(false); +} + +void ItemContainer::setEditMode(bool editMode) +{ + if (m_editMode == editMode) { + return; + } + + if (editMode && editModeCondition() == Locked) { + return; + } + + m_editMode = editMode; + + if (m_editModeCondition != AfterMouseOver || (m_layout && m_layout->editMode())) { + m_contentItem->setEnabled(!editMode); + } + + if (editMode) { + setZ(1); + } else { + setZ(0); + } + + if (m_mouseDown) { + sendUngrabRecursive(m_contentItem); + grabMouse(); + } + + if (m_dragActive != editMode && m_mouseDown) { + m_dragActive = editMode && m_mouseDown; + Q_EMIT dragActiveChanged(); + } + + setConfigOverlayVisible(editMode); + + Q_EMIT editModeChanged(editMode); +} + +ItemContainer::EditModeCondition ItemContainer::editModeCondition() const +{ + if (m_layout && m_layout->editModeCondition() == AppletsLayout::Locked) { + return Locked; + } + + return m_editModeCondition; +} + +void ItemContainer::setEditModeCondition(EditModeCondition condition) +{ + if (condition == m_editModeCondition) { + return; + } + + if (condition == Locked) { + setEditMode(false); + } + + m_editModeCondition = condition; + + setAcceptHoverEvents(condition == AfterMouseOver || (m_layout && m_layout->editMode())); + + Q_EMIT editModeConditionChanged(); +} + +AppletsLayout::PreferredLayoutDirection ItemContainer::preferredLayoutDirection() const +{ + return m_preferredLayoutDirection; +} + +void ItemContainer::setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction) +{ + if (direction == m_preferredLayoutDirection) { + return; + } + + m_preferredLayoutDirection = direction; + + Q_EMIT preferredLayoutDirectionChanged(); +} + +void ItemContainer::setLayout(AppletsLayout *layout) +{ + if (m_layout == layout) { + return; + } + + if (m_layout) { + disconnect(m_layout, &AppletsLayout::editModeConditionChanged, this, nullptr); + disconnect(m_layout, &AppletsLayout::editModeChanged, this, nullptr); + + if (m_editMode) { + m_layout->hidePlaceHolder(); + } + } + + m_layout = layout; + + if (!layout) { + Q_EMIT layoutChanged(); + return; + } + + if (parentItem() != layout) { + setParentItem(layout); + } + + connect(m_layout, &AppletsLayout::editModeConditionChanged, this, [this]() { + if (m_layout->editModeCondition() == AppletsLayout::Locked) { + setEditMode(false); + } + if ((m_layout->editModeCondition() == AppletsLayout::Locked) != (m_editModeCondition == ItemContainer::Locked)) { + Q_EMIT editModeConditionChanged(); + } + }); + connect(m_layout, &AppletsLayout::editModeChanged, this, [this]() { + setAcceptHoverEvents(m_editModeCondition == AfterMouseOver || m_layout->editMode()); + }); + Q_EMIT layoutChanged(); +} + +AppletsLayout *ItemContainer::layout() const +{ + return m_layout; +} + +void ItemContainer::syncChildItemsGeometry(const QSizeF &size) +{ + if (m_contentItem) { + m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); + + m_contentItem->setSize(QSizeF(size.width() - m_leftPadding - m_rightPadding, size.height() - m_topPadding - m_bottomPadding)); + } + + if (m_backgroundItem) { + m_backgroundItem->setPosition(QPointF(0, 0)); + m_backgroundItem->setSize(size); + } + + if (m_configOverlay) { + m_configOverlay->setPosition(QPointF(0, 0)); + m_configOverlay->setSize(size); + } +} + +QQmlComponent *ItemContainer::configOverlayComponent() const +{ + return m_configOverlayComponent; +} + +void ItemContainer::setConfigOverlayComponent(QQmlComponent *component) +{ + if (component == m_configOverlayComponent) { + return; + } + + m_configOverlayComponent = component; + if (m_configOverlay) { + m_configOverlay->deleteLater(); + m_configOverlay = nullptr; + } + + Q_EMIT configOverlayComponentChanged(); +} + +ConfigOverlay *ItemContainer::configOverlayItem() const +{ + return m_configOverlay; +} + +QSizeF ItemContainer::initialSize() const +{ + return m_initialSize; +} + +void ItemContainer::setInitialSize(const QSizeF &size) +{ + if (m_initialSize == size) { + return; + } + + m_initialSize = size; + + Q_EMIT initialSizeChanged(); +} + +bool ItemContainer::configOverlayVisible() const +{ + return m_configOverlay && m_configOverlay->open(); +} + +void ItemContainer::setConfigOverlayVisible(bool visible) +{ + if (!m_configOverlayComponent) { + return; + } + + if (visible == configOverlayVisible()) { + return; + } + + if (visible && !m_configOverlay) { + QQmlContext *context = QQmlEngine::contextForObject(this); + Q_ASSERT(context); + QObject *instance = m_configOverlayComponent->beginCreate(context); + m_configOverlay = qobject_cast(instance); + + if (!m_configOverlay) { + qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: Applet configOverlay not of ConfigOverlay type"; + if (instance) { + instance->deleteLater(); + } + return; + } + + m_configOverlay->setVisible(false); + m_configOverlay->setItemContainer(this); + m_configOverlay->setParentItem(this); + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + m_configOverlay->setZ(999); + m_configOverlay->setPosition(QPointF(0, 0)); + m_configOverlay->setSize(size()); + + m_configOverlayComponent->completeCreate(); + + connect(m_configOverlay, &ConfigOverlay::openChanged, this, [this]() { + Q_EMIT configOverlayVisibleChanged(m_configOverlay->open()); + }); + + Q_EMIT configOverlayItemChanged(); + } + + if (m_configOverlay) { + m_configOverlay->setOpen(visible); + } +} + +void ItemContainer::contentData_append(QQmlListProperty *prop, QObject *object) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return; + } + + // QQuickItem *item = qobject_cast(object); + container->m_contentData.append(object); +} + +int ItemContainer::contentData_count(QQmlListProperty *prop) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return 0; + } + + return container->m_contentData.count(); +} + +QObject *ItemContainer::contentData_at(QQmlListProperty *prop, int index) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return nullptr; + } + + if (index < 0 || index >= container->m_contentData.count()) { + return nullptr; + } + return container->m_contentData.value(index); +} + +void ItemContainer::contentData_clear(QQmlListProperty *prop) +{ + ItemContainer *container = static_cast(prop->object); + if (!container) { + return; + } + + return container->m_contentData.clear(); +} + +QQmlListProperty ItemContainer::contentData() +{ + return QQmlListProperty(this, nullptr, contentData_append, contentData_count, contentData_at, contentData_clear); +} + +void ItemContainer::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + syncChildItemsGeometry(newGeometry.size()); + QQuickItem::geometryChanged(newGeometry, oldGeometry); + Q_EMIT contentWidthChanged(); + Q_EMIT contentHeightChanged(); +} + +void ItemContainer::componentComplete() +{ + if (!m_contentItem) { + // qWarning()<<"Creating default contentItem"; + m_contentItem = new QQuickItem(this); + syncChildItemsGeometry(size()); + } + + for (auto *o : qAsConst(m_contentData)) { + QQuickItem *item = qobject_cast(o); + if (item) { + item->setParentItem(m_contentItem); + } + } + + // Search for the Layout attached property + // Qt6: this should become public api + // https://bugreports.qt.io/browse/QTBUG-77103 + for (auto *o : children()) { + if (o->inherits("QQuickLayoutAttached")) { + m_layoutAttached = o; + } + } + + if (m_layoutAttached) { + // NOTE: new syntax cannot be used because we don't have access to the QQuickLayoutAttached class + connect(m_layoutAttached, SIGNAL(minimumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(minimumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + + connect(m_layoutAttached, SIGNAL(preferredHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(preferredWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + + connect(m_layoutAttached, SIGNAL(maximumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); + connect(m_layoutAttached, SIGNAL(maximumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); + } + QQuickItem::componentComplete(); +} + +void ItemContainer::sendUngrabRecursive(QQuickItem *item) +{ + if (!item || !item->window()) { + return; + } + + for (auto *child : item->childItems()) { + sendUngrabRecursive(child); + } + + QEvent ev(QEvent::UngrabMouse); + + QCoreApplication::sendEvent(item, &ev); +} + +bool ItemContainer::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + // Don't filter the configoverlay + if (item == m_configOverlay || (m_configOverlay && m_configOverlay->isAncestorOf(item)) || (!m_editMode && m_editModeCondition == Manual)) { + return QQuickItem::childMouseEventFilter(item, event); + } + + // give more time before closing + if (m_closeEditModeTimer && m_closeEditModeTimer->isActive()) { + m_closeEditModeTimer->start(); + } + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *me = static_cast(event); + if (me->button() != Qt::LeftButton && !(me->buttons() & Qt::LeftButton)) { + return QQuickItem::childMouseEventFilter(item, event); + } + forceActiveFocus(Qt::MouseFocusReason); + m_mouseDown = true; + m_mouseSynthetizedFromTouch = me->source() == Qt::MouseEventSynthesizedBySystem || me->source() == Qt::MouseEventSynthesizedByQt; + if (m_configOverlay) { + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + } + + const bool wasEditMode = m_editMode; + if (m_layout && m_layout->editMode()) { + setEditMode(true); + } else if (m_editModeCondition == AfterPressAndHold) { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + m_lastMousePosition = me->windowPos(); + m_mouseDownPosition = me->windowPos(); + + if (m_editMode && !wasEditMode) { + event->accept(); + return true; + } + + } else if (event->type() == QEvent::MouseMove) { + QMouseEvent *me = static_cast(event); + + if (!m_editMode && QPointF(me->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + m_editModeTimer->stop(); + } else if (m_editMode) { + event->accept(); + } + + } else if (event->type() == QEvent::MouseButtonRelease) { + m_editModeTimer->stop(); + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + ungrabMouse(); + event->accept(); + m_dragActive = false; + if (m_editMode) { + Q_EMIT dragActiveChanged(); + } + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +void ItemContainer::mousePressEvent(QMouseEvent *event) +{ + forceActiveFocus(Qt::MouseFocusReason); + + if (!m_editMode && m_editModeCondition == Manual) { + return; + } + + m_mouseDown = true; + m_mouseSynthetizedFromTouch = event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt; + if (m_configOverlay) { + m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); + } + + if (m_layout && m_layout->editMode()) { + setEditMode(true); + } + + if (m_editMode) { + grabMouse(); + setCursor(Qt::ClosedHandCursor); + m_dragActive = true; + Q_EMIT dragActiveChanged(); + } else if (m_editModeCondition == AfterPressAndHold) { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } + + m_lastMousePosition = event->windowPos(); + m_mouseDownPosition = event->windowPos(); + event->accept(); +} + +void ItemContainer::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + + if (!m_layout || (!m_editMode && m_editModeCondition == Manual)) { + return; + } + + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + m_editModeTimer->stop(); + ungrabMouse(); + + if (m_editMode && !m_layout->itemIsManaged(this)) { + m_layout->hidePlaceHolder(); + m_layout->positionItem(this); + } + + m_dragActive = false; + if (m_editMode) { + Q_EMIT dragActiveChanged(); + setCursor(Qt::OpenHandCursor); + } + event->accept(); +} + +void ItemContainer::mouseMoveEvent(QMouseEvent *event) +{ + if ((event->button() == Qt::NoButton && event->buttons() == Qt::NoButton) || (!m_editMode && m_editModeCondition == Manual)) { + return; + } + + if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { + if (m_editModeCondition == AfterPress) { + setEditMode(true); + } else { + m_editModeTimer->stop(); + } + } + + if (!m_editMode) { + return; + } + + if (m_layout && m_layout->itemIsManaged(this)) { + m_layout->releaseSpace(this); + grabMouse(); + m_dragActive = true; + Q_EMIT dragActiveChanged(); + + } else { + setPosition(QPointF(x() + event->windowPos().x() - m_lastMousePosition.x(), y() + event->windowPos().y() - m_lastMousePosition.y())); + + if (m_layout) { + m_layout->showPlaceHolderForItem(this); + } + + Q_EMIT userDrag(QPointF(x(), y()), event->pos()); + } + m_lastMousePosition = event->windowPos(); + event->accept(); +} + +void ItemContainer::mouseUngrabEvent() +{ + m_mouseDown = false; + m_mouseSynthetizedFromTouch = false; + m_editModeTimer->stop(); + ungrabMouse(); + + if (m_layout && m_editMode && !m_layout->itemIsManaged(this)) { + m_layout->hidePlaceHolder(); + m_layout->positionItem(this); + } + + m_dragActive = false; + if (m_editMode) { + Q_EMIT dragActiveChanged(); + } +} + +void ItemContainer::hoverEnterEvent(QHoverEvent *event) +{ + Q_UNUSED(event); + + if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { + return; + } + + if (m_closeEditModeTimer) { + m_closeEditModeTimer->stop(); + } + + if (m_layout->editMode()) { + setCursor(Qt::OpenHandCursor); + setEditMode(true); + } else { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } +} + +void ItemContainer::hoverLeaveEvent(QHoverEvent *event) +{ + Q_UNUSED(event); + + if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { + return; + } + + m_editModeTimer->stop(); + if (!m_closeEditModeTimer) { + m_closeEditModeTimer = new QTimer(this); + m_closeEditModeTimer->setSingleShot(true); + m_closeEditModeTimer->setInterval(500); + connect(m_closeEditModeTimer, &QTimer::timeout, this, [this]() { + setEditMode(false); + }); + } + m_closeEditModeTimer->start(); +} + +QQuickItem *ItemContainer::contentItem() const +{ + return m_contentItem; +} + +void ItemContainer::setContentItem(QQuickItem *item) +{ + if (m_contentItem == item) { + return; + } + + m_contentItem = item; + item->setParentItem(this); + m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); + + m_contentItem->setSize(QSizeF(width() - m_leftPadding - m_rightPadding, height() - m_topPadding - m_bottomPadding)); + + Q_EMIT contentItemChanged(); +} + +QQuickItem *ItemContainer::background() const +{ + return m_backgroundItem; +} + +void ItemContainer::setBackground(QQuickItem *item) +{ + if (m_backgroundItem == item) { + return; + } + + m_backgroundItem = item; + m_backgroundItem->setParentItem(this); + m_backgroundItem->setPosition(QPointF(0, 0)); + m_backgroundItem->setSize(size()); + + Q_EMIT backgroundChanged(); +} + +int ItemContainer::leftPadding() const +{ + return m_leftPadding; +} + +void ItemContainer::setLeftPadding(int padding) +{ + if (m_leftPadding == padding) { + return; + } + + m_leftPadding = padding; + syncChildItemsGeometry(size()); + Q_EMIT leftPaddingChanged(); + Q_EMIT contentWidthChanged(); +} + +int ItemContainer::topPadding() const +{ + return m_topPadding; +} + +void ItemContainer::setTopPadding(int padding) +{ + if (m_topPadding == padding) { + return; + } + + m_topPadding = padding; + syncChildItemsGeometry(size()); + Q_EMIT topPaddingChanged(); + Q_EMIT contentHeightChanged(); +} + +int ItemContainer::rightPadding() const +{ + return m_rightPadding; +} + +void ItemContainer::setRightPadding(int padding) +{ + if (m_rightPadding == padding) { + return; + } + + m_rightPadding = padding; + syncChildItemsGeometry(size()); + Q_EMIT rightPaddingChanged(); + Q_EMIT contentWidthChanged(); +} + +int ItemContainer::bottomPadding() const +{ + return m_bottomPadding; +} + +void ItemContainer::setBottomPadding(int padding) +{ + if (m_bottomPadding == padding) { + return; + } + + m_bottomPadding = padding; + syncChildItemsGeometry(size()); + Q_EMIT bottomPaddingChanged(); + Q_EMIT contentHeightChanged(); +} + +int ItemContainer::contentWidth() const +{ + return width() - m_leftPadding - m_rightPadding; +} + +int ItemContainer::contentHeight() const +{ + return height() - m_topPadding - m_bottomPadding; +} + +#include "moc_itemcontainer.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/itemcontainer.h b/plasma/workspace/components/containmentlayoutmanager/itemcontainer.h new file mode 100644 index 0000000000..cfea041d61 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/itemcontainer.h @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "appletslayout.h" + +class QTimer; + +class ConfigOverlay; + +class ItemContainer : public QQuickItem +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(AppletsLayout *layout READ layout NOTIFY layoutChanged) + // TODO: make it unchangeable? probably not + Q_PROPERTY(QString key READ key WRITE setKey NOTIFY keyChanged) + Q_PROPERTY(ItemContainer::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) + Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) + Q_PROPERTY(bool dragActive READ dragActive NOTIFY dragActiveChanged) + Q_PROPERTY(AppletsLayout::PreferredLayoutDirection preferredLayoutDirection READ preferredLayoutDirection WRITE setPreferredLayoutDirection NOTIFY + preferredLayoutDirectionChanged) + + Q_PROPERTY(QQmlComponent *configOverlayComponent READ configOverlayComponent WRITE setConfigOverlayComponent NOTIFY configOverlayComponentChanged) + Q_PROPERTY(bool configOverlayVisible READ configOverlayVisible WRITE setConfigOverlayVisible NOTIFY configOverlayVisibleChanged) + Q_PROPERTY(QQuickItem *configOverlayItem READ configOverlayItem NOTIFY configOverlayItemChanged) + + /** + * Initial size this container asks to have upon creation. only positive values are considered + */ + Q_PROPERTY(QSizeF initialSize READ initialSize WRITE setInitialSize NOTIFY initialSizeChanged) + // From there mostly a clone of QQC2 Control + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged) + Q_PROPERTY(QQuickItem *background READ background WRITE setBackground NOTIFY backgroundChanged) + + /** + * Padding adds a space between each edge of the content item and the background item, effectively controlling the size of the content item. + */ + Q_PROPERTY(int leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged) + Q_PROPERTY(int rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged) + Q_PROPERTY(int topPadding READ topPadding WRITE setTopPadding NOTIFY topPaddingChanged) + Q_PROPERTY(int bottomPadding READ bottomPadding WRITE setBottomPadding NOTIFY bottomPaddingChanged) + + /** + * The size of the contents: the size of this item minus the padding + */ + Q_PROPERTY(int contentWidth READ contentWidth NOTIFY contentWidthChanged) + Q_PROPERTY(int contentHeight READ contentHeight NOTIFY contentHeightChanged) + + Q_PROPERTY(QQmlListProperty contentData READ contentData FINAL) + // Q_CLASSINFO("DeferredPropertyNames", "background,contentItem") + Q_CLASSINFO("DefaultProperty", "contentData") + +public: + enum EditModeCondition { + Locked = AppletsLayout::EditModeCondition::Locked, + Manual = AppletsLayout::EditModeCondition::Manual, + AfterPressAndHold = AppletsLayout::EditModeCondition::AfterPressAndHold, + AfterPress, + AfterMouseOver, + }; + Q_ENUMS(EditModeCondition) + + ItemContainer(QQuickItem *parent = nullptr); + ~ItemContainer(); + + QQmlListProperty contentData(); + + QString key() const; + void setKey(const QString &id); + + bool editMode() const; + void setEditMode(bool edit); + + bool dragActive() const; + + Q_INVOKABLE void cancelEdit(); + + EditModeCondition editModeCondition() const; + void setEditModeCondition(EditModeCondition condition); + + AppletsLayout::PreferredLayoutDirection preferredLayoutDirection() const; + void setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction); + + QQmlComponent *configOverlayComponent() const; + void setConfigOverlayComponent(QQmlComponent *component); + + bool configOverlayVisible() const; + void setConfigOverlayVisible(bool visible); + + // TODO: keep this accessible? + ConfigOverlay *configOverlayItem() const; + + QSizeF initialSize() const; + void setInitialSize(const QSizeF &size); + + // Control-like api + QQuickItem *contentItem() const; + void setContentItem(QQuickItem *item); + + QQuickItem *background() const; + void setBackground(QQuickItem *item); + + // Setters and getters for the padding + int leftPadding() const; + void setLeftPadding(int padding); + + int topPadding() const; + void setTopPadding(int padding); + + int rightPadding() const; + void setRightPadding(int padding); + + int bottomPadding() const; + void setBottomPadding(int padding); + + int contentWidth() const; + int contentHeight() const; + + AppletsLayout *layout() const; + + // Not for QML + void setLayout(AppletsLayout *layout); + + QObject *layoutAttached() const + { + return m_layoutAttached; + } + +protected: + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + // void classBegin() override; + void componentComplete() override; + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + +Q_SIGNALS: + + /** + * The user manually dragged the ItemContainer around + * @param newPosition new position of the ItemContainer in parent coordinates + * @param dragCenter position in ItemContainer coordinates of the drag hotspot, i.e. where the user pressed the mouse or the + * finger over the ItemContainer + */ + void userDrag(const QPointF &newPosition, const QPointF &dragCenter); + + void dragActiveChanged(); + + /** + * The attached layout object changed some of its size hints + */ + void sizeHintsChanged(); + + // QML property notifiers + void layoutChanged(); + void keyChanged(); + void editModeConditionChanged(); + void editModeChanged(bool editMode); + void preferredLayoutDirectionChanged(); + void configOverlayComponentChanged(); + void configOverlayItemChanged(); + void initialSizeChanged(); + void configOverlayVisibleChanged(bool configOverlayVisile); + + void backgroundChanged(); + void contentItemChanged(); + void leftPaddingChanged(); + void rightPaddingChanged(); + void topPaddingChanged(); + void bottomPaddingChanged(); + void contentWidthChanged(); + void contentHeightChanged(); + +private: + void syncChildItemsGeometry(const QSizeF &size); + void sendUngrabRecursive(QQuickItem *item); + + // internal accessorts for the contentData QProperty + static void contentData_append(QQmlListProperty *prop, QObject *object); + static int contentData_count(QQmlListProperty *prop); + static QObject *contentData_at(QQmlListProperty *prop, int index); + static void contentData_clear(QQmlListProperty *prop); + + QPointer m_contentItem; + QPointer m_backgroundItem; + + // Internal implementation detail: this is used to reparent all items to contentItem + QList m_contentData; + + /** + * Padding adds a space between each edge of the content item and the background item, effectively controlling the size of the content item. + */ + int m_leftPadding = 0; + int m_rightPadding = 0; + int m_topPadding = 0; + int m_bottomPadding = 0; + + QString m_key; + + QPointer m_layout; + QTimer *m_editModeTimer = nullptr; + QTimer *m_closeEditModeTimer = nullptr; + QTimer *m_sizeHintAdjustTimer = nullptr; + QObject *m_layoutAttached = nullptr; + EditModeCondition m_editModeCondition = Manual; + QSizeF m_initialSize; + + QPointer m_configOverlayComponent; + ConfigOverlay *m_configOverlay = nullptr; + + QPointF m_lastMousePosition = QPoint(-1, -1); + QPointF m_mouseDownPosition = QPoint(-1, -1); + AppletsLayout::PreferredLayoutDirection m_preferredLayoutDirection = AppletsLayout::Closest; + bool m_editMode = false; + bool m_mouseDown = false; + bool m_mouseSynthetizedFromTouch = false; + bool m_dragActive = false; +}; diff --git a/plasma/workspace/components/containmentlayoutmanager/qml/BasicAppletContainer.qml b/plasma/workspace/components/containmentlayoutmanager/qml/BasicAppletContainer.qml new file mode 100644 index 0000000000..a9e6d0a83f --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/qml/BasicAppletContainer.qml @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager +import org.kde.kirigami 2.11 as Kirigami + +ContainmentLayoutManager.AppletContainer { + id: appletContainer + editModeCondition: plasmoid.immutable + ? ContainmentLayoutManager.ItemContainer.Manual + : ContainmentLayoutManager.ItemContainer.AfterPressAndHold + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: (contentItem.effectiveBackgroundHints & PlasmaCore.Types.ShadowBackground) + && !(contentItem.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) + && !(contentItem.effectiveBackgroundHints & PlasmaCore.Types.TranslucentBackground) + ? Kirigami.Theme.Complementary + : Kirigami.Theme.Window + + PlasmaCore.ColorScope.inherit: false + PlasmaCore.ColorScope.colorGroup: Kirigami.Theme.colorSet == Kirigami.Theme.Complementary + ? PlasmaCore.Theme.ComplementaryColorGroup + : PlasmaCore.Theme.NormalColorGroup + + onFocusChanged: { + if (!focus && !dragActive) { + editMode = false; + } + } + Layout.minimumWidth: { + if (!applet) { + return leftPadding + rightPadding; + } + + if (applet.preferredRepresentation != applet.fullRepresentation + && applet.compactRepresentationItem + ) { + return applet.compactRepresentationItem.Layout.minimumWidth + leftPadding + rightPadding; + } else { + return applet.Layout.minimumWidth + leftPadding + rightPadding; + } + } + Layout.minimumHeight: { + if (!applet) { + return topPadding + bottomPadding; + } + + if (applet.preferredRepresentation != applet.fullRepresentation + && applet.compactRepresentationItem + ) { + return applet.compactRepresentationItem.Layout.minimumHeight + topPadding + bottomPadding; + } else { + return applet.Layout.minimumHeight + topPadding + bottomPadding; + } + } + + Layout.preferredWidth: Math.max(applet.Layout.minimumWidth, applet.Layout.preferredWidth) + Layout.preferredHeight: Math.max(applet.Layout.minimumHeight, applet.Layout.preferredHeight) + + Layout.maximumWidth: applet.Layout.maximumWidth + Layout.maximumHeight: applet.Layout.maximumHeight + + leftPadding: background.margins.left + topPadding: background.margins.top + rightPadding: background.margins.right + bottomPadding: background.margins.bottom + + // render via a layer if we're at an angle + // resize handles are rendered outside this item, so also disable when they're showing to avoid clipping + layer.enabled: (rotation % 90 != 0) && !(configOverlayItem && configOverlayItem.visible) + layer.smooth: true + + initialSize.width: applet.switchWidth + leftPadding + rightPadding + initialSize.height: applet.switchHeight + topPadding + bottomPadding + + onRotationChanged: background.syncBlurEnabled() + + background: PlasmaCore.FrameSvgItem { + id: background + imagePath: { + if (!contentItem) { + return ""; + } + if (contentItem.effectiveBackgroundHints & PlasmaCore.Types.TranslucentBackground) { + return "widgets/translucentbackground"; + } else if (contentItem.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) { + return "widgets/background"; + } else { + return ""; + } + } + + property bool blurEnabled: false + function syncBlurEnabled() { + blurEnabled = appletContainer.rotation === 0 && plasmoid.GraphicsInfo.api !== GraphicsInfo.Software && hasElementPrefix("blurred"); + } + prefix: blurEnabled ? "blurred" : "" + Component.onCompleted: syncBlurEnabled() + + onRepaintNeeded: syncBlurEnabled() + + DropShadow { + anchors { + fill: parent + leftMargin: appletContainer.leftPadding + topMargin: appletContainer.topPadding + rightMargin: appletContainer.rightPadding + bottomMargin: appletContainer.bottomPadding + } + z: -1 + horizontalOffset: 0 + verticalOffset: 1 + + radius: 4 + samples: 9 + spread: 0.35 + + color: Qt.rgba(0, 0, 0, 0.5) + opacity: 1 + + source: contentItem && contentItem.effectiveBackgroundHints & PlasmaCore.Types.ShadowBackground ? contentItem : null + visible: source != null + } + + OpacityMask { + id: mask + enabled: visible + rotation: appletContainer.rotation + Component.onCompleted: mask.parent = plasmoid + width: appletContainer.width + height: appletContainer.height + x: appletContainer.Kirigami.ScenePosition.x + Math.max(0, -appletContainer.x) + y: appletContainer.Kirigami.ScenePosition.y + Math.max(0, -appletContainer.y) + + visible: background.blurEnabled && (appletContainer.applet.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) + z: -2 + source: blur + maskSource: + ShaderEffectSource { + width: mask.width + height: mask.height + sourceRect: Qt.rect(Math.max(0, -appletContainer.x), + Math.max(0, -appletContainer.y), + width, height); + sourceItem: PlasmaCore.FrameSvgItem { + imagePath: "widgets/background" + prefix: "blurred-mask" + parent: appletContainer.background + anchors.fill: parent + visible: false + } + } + + FastBlur { + id: blur + anchors.fill: parent + + radius: 128 + visible: false + + source: ShaderEffectSource { + width: blur.width + height: blur.height + sourceRect: Qt.rect(Math.max(0, appletContainer.x), + Math.max(0, appletContainer.y), + appletContainer.width - Math.max(0, - (appletContainer.parent.width - appletContainer.x - appletContainer.width)), + appletContainer.height - Math.max(0, - (appletContainer.parent.height - appletContainer.y - appletContainer.height))); + sourceItem: plasmoid.wallpaper + } + } + } + } + + busyIndicatorComponent: PlasmaComponents.BusyIndicator { + anchors.centerIn: parent + visible: applet.busy + running: visible + } + configurationRequiredComponent: PlasmaComponents.Button { + anchors.centerIn: parent + text: i18n("Configure…") + icon.name: "configure" + visible: applet.configurationRequired + onClicked: applet.action("configure").trigger(); + } +} diff --git a/plasma/workspace/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml b/plasma/workspace/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml new file mode 100644 index 0000000000..b05ee2d5ef --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/qml/ConfigOverlayWithHandles.qml @@ -0,0 +1,152 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + +import "private" + +ContainmentLayoutManager.ConfigOverlay { + id: overlay + + opacity: open + Behavior on opacity { + OpacityAnimator { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } + + MultiPointTouchArea { + anchors.fill: parent + property real previousMinX + property real previousMinY + property real previousMaxX + property real previousMaxY + property bool pinching: false + mouseEnabled: false + maximumTouchPoints: 2 + touchPoints: [ + TouchPoint { id: point1 }, + TouchPoint { id: point2 } + ] + + onPressed: { + overlay.itemContainer.layout.releaseSpace(overlay.itemContainer); + previousMinX = point1.sceneX; + previousMinY = point1.sceneY; + } + + onUpdated: { + var minX; + var minY; + var maxX; + var maxY; + + if (point1.pressed && point2.pressed) { + minX = Math.min(point1.sceneX, point2.sceneX); + minY = Math.min(point1.sceneY, point2.sceneY); + + maxX = Math.max(point1.sceneX, point2.sceneX); + maxY = Math.max(point1.sceneY, point2.sceneY); + } else { + minX = point1.pressed ? point1.sceneX : point2.sceneX; + minY = point1.pressed ? point1.sceneY : point2.sceneY; + maxX = -1; + maxY = -1; + } + + if (pinching == (point1.pressed && point2.pressed)) { + overlay.itemContainer.x += minX - previousMinX; + overlay.itemContainer.y += minY - previousMinY; + + if (pinching) { + overlay.itemContainer.width += maxX - previousMaxX + previousMinX - minX; + overlay.itemContainer.height += maxY - previousMaxY + previousMinY - minY; + } + overlay.itemContainer.layout.showPlaceHolderForItem(overlay.itemContainer); + } + + pinching = point1.pressed && point2.pressed + previousMinX = minX; + previousMinY = minY; + previousMaxX = maxX; + previousMaxY = maxY; + } + onReleased: { + if (point1.pressed || point2.pressed) { + return; + } + overlay.itemContainer.layout.positionItem(overlay.itemContainer); + overlay.itemContainer.layout.hidePlaceHolder(); + pinching = false + } + onCanceled: released() + } + + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.TopLeft + anchors { + horizontalCenter: parent.left + verticalCenter: parent.top + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Left + anchors { + horizontalCenter: parent.left + verticalCenter: parent.verticalCenter + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.BottomLeft + anchors { + horizontalCenter: parent.left + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Bottom + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.BottomRight + anchors { + horizontalCenter: parent.right + verticalCenter: parent.bottom + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Right + anchors { + horizontalCenter: parent.right + verticalCenter: parent.verticalCenter + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.TopRight + anchors { + horizontalCenter: parent.right + verticalCenter: parent.top + } + } + BasicResizeHandle { + resizeCorner: ContainmentLayoutManager.ResizeHandle.Top + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.top + } + } +} + diff --git a/plasma/workspace/components/containmentlayoutmanager/qml/PlaceHolder.qml b/plasma/workspace/components/containmentlayoutmanager/qml/PlaceHolder.qml new file mode 100644 index 0000000000..b9ae726722 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/qml/PlaceHolder.qml @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager + +ContainmentLayoutManager.ItemContainer { + enabled: false + PlasmaCore.FrameSvgItem { + anchors.fill:parent + imagePath: "widgets/viewitem" + prefix: "hover" + opacity: 0.5 + } + Behavior on opacity { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } +} diff --git a/plasma/workspace/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml b/plasma/workspace/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml new file mode 100644 index 0000000000..23a9cc4852 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/qml/private/BasicResizeHandle.qml @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.12 + +import org.kde.plasma.private.containmentlayoutmanager 1.0 as ContainmentLayoutManager +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.14 as Kirigami + +ContainmentLayoutManager.ResizeHandle { + id: handle + width: overlay.touchInteraction ? PlasmaCore.Units.gridUnit * 2 : PlasmaCore.Units.gridUnit + height: width + z: 999 + + Kirigami.ShadowedRectangle { + anchors.fill: parent + color: resizeBlocked ? PlasmaCore.Theme.negativeTextColor : PlasmaCore.Theme.backgroundColor + + radius: width + + shadow.size: PlasmaCore.Units.smallSpacing + shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.2) + shadow.yOffset: PlasmaCore.Units.devicePixelRatio * 2 + + border.width: PlasmaCore.Units.devicePixelRatio + border.color: Qt.tint(Kirigami.Theme.textColor, + Qt.rgba(color.r, color.g, color.b, 0.3)) + } + Rectangle { + anchors { + fill: parent + margins: PlasmaCore.Units.devicePixelRatio / 2 + } + border { + width: PlasmaCore.Units.devicePixelRatio / 2 + color: Qt.rgba(1, 1, 1, 0.2) + } + gradient: Gradient { + GradientStop { position: 0.0; color: handle.pressed ? Qt.rgba(0, 0, 0, 0.15) : Qt.rgba(1, 1, 1, 0.05) } + GradientStop { position: 1.0; color: handle.pressed ? Qt.rgba(0, 0, 0, 0.15) : Qt.rgba(0, 0, 0, 0.05) } + } + + radius: width + } + scale: overlay.open ? 1 : 0 + Behavior on scale { + NumberAnimation { + duration: PlasmaCore.Units.longDuration + easing.type: Easing.InOutQuad + } + } +} + diff --git a/plasma/workspace/components/containmentlayoutmanager/qml/qmldir b/plasma/workspace/components/containmentlayoutmanager/qml/qmldir new file mode 100644 index 0000000000..d7299a5281 --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/qml/qmldir @@ -0,0 +1,7 @@ +module org.kde.plasma.private.containmentlayoutmanager + +plugin containmentlayoutmanagerplugin +BasicAppletContainer 1.0 BasicAppletContainer.qml +ConfigOverlayWithHandles 1.0 ConfigOverlayWithHandles.qml +PlaceHolder 1.0 PlaceHolder.qml + diff --git a/plasma/workspace/components/containmentlayoutmanager/resizehandle.cpp b/plasma/workspace/components/containmentlayoutmanager/resizehandle.cpp new file mode 100644 index 0000000000..a2c66e067d --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/resizehandle.cpp @@ -0,0 +1,248 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resizehandle.h" + +#include +#include + +ResizeHandle::ResizeHandle(QQuickItem *parent) + : QQuickItem(parent) +{ + setAcceptedMouseButtons(Qt::LeftButton); + + QQuickItem *candidate = parent; + while (candidate) { + ConfigOverlay *overlay = qobject_cast(candidate); + if (overlay) { + setConfigOverlay(overlay); + break; + } + + candidate = candidate->parentItem(); + } + + connect(this, &QQuickItem::parentChanged, this, [this]() { + QQuickItem *candidate = parentItem(); + while (candidate) { + ConfigOverlay *overlay = qobject_cast(candidate); + if (overlay) { + setConfigOverlay(overlay); + break; + } + + candidate = candidate->parentItem(); + } + }); + + auto syncCursor = [this]() { + switch (m_resizeCorner) { + case Left: + case Right: + setCursor(QCursor(Qt::SizeHorCursor)); + break; + case Top: + case Bottom: + setCursor(QCursor(Qt::SizeVerCursor)); + break; + case TopLeft: + case BottomRight: + setCursor(QCursor(Qt::SizeFDiagCursor)); + break; + case TopRight: + case BottomLeft: + default: + setCursor(Qt::SizeBDiagCursor); + } + }; + + syncCursor(); + connect(this, &ResizeHandle::resizeCornerChanged, this, syncCursor); +} + +ResizeHandle::~ResizeHandle() +{ +} + +bool ResizeHandle::resizeBlocked() const +{ + return m_resizeWidthBlocked || m_resizeHeightBlocked; +} + +void ResizeHandle::setPressed(bool pressed) +{ + if (pressed == m_pressed) { + return; + } + + m_pressed = pressed; + Q_EMIT pressedChanged(); +} + +bool ResizeHandle::isPressed() const +{ + return m_pressed; +} + +bool ResizeHandle::resizeLeft() const +{ + return m_resizeCorner == Left || m_resizeCorner == TopLeft || m_resizeCorner == BottomLeft; +} + +bool ResizeHandle::resizeTop() const +{ + return m_resizeCorner == Top || m_resizeCorner == TopLeft || m_resizeCorner == TopRight; +} + +bool ResizeHandle::resizeRight() const +{ + return m_resizeCorner == Right || m_resizeCorner == TopRight || m_resizeCorner == BottomRight; +} + +bool ResizeHandle::resizeBottom() const +{ + return m_resizeCorner == Bottom || m_resizeCorner == BottomLeft || m_resizeCorner == BottomRight; +} + +void ResizeHandle::setResizeBlocked(bool width, bool height) +{ + if (m_resizeWidthBlocked == width && m_resizeHeightBlocked == height) { + return; + } + + m_resizeWidthBlocked = width; + m_resizeHeightBlocked = height; + + Q_EMIT resizeBlockedChanged(); +} + +void ResizeHandle::mousePressEvent(QMouseEvent *event) +{ + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + if (!itemContainer) { + return; + } + m_mouseDownPosition = event->windowPos(); + m_mouseDownGeometry = QRectF(itemContainer->x(), itemContainer->y(), itemContainer->width(), itemContainer->height()); + setResizeBlocked(false, false); + setPressed(true); + event->accept(); +} + +void ResizeHandle::mouseMoveEvent(QMouseEvent *event) +{ + if (!m_configOverlay || !m_configOverlay->itemContainer()) { + return; + } + + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + AppletsLayout *layout = itemContainer->layout(); + + if (!layout) { + return; + } + + layout->releaseSpace(itemContainer); + const QPointF difference = m_mouseDownPosition - event->windowPos(); + + QSizeF minimumSize = QSize(layout->minimumItemWidth(), layout->minimumItemHeight()); + if (itemContainer->layoutAttached()) { + minimumSize.setWidth(qMax(minimumSize.width(), itemContainer->layoutAttached()->property("minimumWidth").toReal())); + minimumSize.setHeight(qMax(minimumSize.height(), itemContainer->layoutAttached()->property("minimumHeight").toReal())); + } + + // Now make minimumSize an integer number of cells + minimumSize.setWidth(ceil(minimumSize.width() / layout->cellWidth()) * layout->cellWidth()); + minimumSize.setHeight(ceil(minimumSize.height() / layout->cellWidth()) * layout->cellHeight()); + + // Horizontal resize + if (resizeLeft()) { + const qreal width = qMax(minimumSize.width(), m_mouseDownGeometry.width() + difference.x()); + const qreal x = m_mouseDownGeometry.x() + (m_mouseDownGeometry.width() - width); + + // -1 to have a bit of margins around + if (layout->isRectAvailable(x - 1, m_mouseDownGeometry.y(), width, m_mouseDownGeometry.height())) { + itemContainer->setX(x); + itemContainer->setWidth(width); + setResizeBlocked(m_mouseDownGeometry.width() + difference.x() < minimumSize.width(), m_resizeHeightBlocked); + } else { + setResizeBlocked(true, m_resizeHeightBlocked); + } + } else if (resizeRight()) { + const qreal width = qMax(minimumSize.width(), m_mouseDownGeometry.width() - difference.x()); + + if (layout->isRectAvailable(m_mouseDownGeometry.x(), m_mouseDownGeometry.y(), width, m_mouseDownGeometry.height())) { + itemContainer->setWidth(width); + setResizeBlocked(m_mouseDownGeometry.width() - difference.x() < minimumSize.width(), m_resizeHeightBlocked); + } else { + setResizeBlocked(true, m_resizeHeightBlocked); + } + } + + // Vertical Resize + if (resizeTop()) { + const qreal height = qMax(minimumSize.height(), m_mouseDownGeometry.height() + difference.y()); + const qreal y = m_mouseDownGeometry.y() + (m_mouseDownGeometry.height() - height); + + // -1 to have a bit of margins around + if (layout->isRectAvailable(m_mouseDownGeometry.x(), y - 1, m_mouseDownGeometry.width(), m_mouseDownGeometry.height())) { + itemContainer->setY(y); + itemContainer->setHeight(height); + setResizeBlocked(m_resizeWidthBlocked, m_mouseDownGeometry.height() + difference.y() < minimumSize.height()); + } else { + setResizeBlocked(m_resizeWidthBlocked, true); + } + } else if (resizeBottom()) { + const qreal height = qMax(minimumSize.height(), m_mouseDownGeometry.height() - difference.y()); + + if (layout->isRectAvailable(m_mouseDownGeometry.x(), m_mouseDownGeometry.y(), m_mouseDownGeometry.width(), height)) { + itemContainer->setHeight(qMax(height, minimumSize.height())); + setResizeBlocked(m_resizeWidthBlocked, m_mouseDownGeometry.height() - difference.y() < minimumSize.height()); + } else { + setResizeBlocked(m_resizeWidthBlocked, true); + } + } + + event->accept(); +} + +void ResizeHandle::mouseReleaseEvent(QMouseEvent *event) +{ + setPressed(false); + if (!m_configOverlay || !m_configOverlay->itemContainer()) { + return; + } + + ItemContainer *itemContainer = m_configOverlay->itemContainer(); + AppletsLayout *layout = itemContainer->layout(); + + if (!layout) { + return; + } + + layout->positionItem(itemContainer); + + event->accept(); + + setResizeBlocked(false, false); + Q_EMIT resizeBlockedChanged(); +} + +void ResizeHandle::mouseUngrabEvent() +{ + setPressed(false); +} + +void ResizeHandle::setConfigOverlay(ConfigOverlay *handle) +{ + if (handle == m_configOverlay) { + return; + } + + m_configOverlay = handle; +} + +#include "moc_resizehandle.cpp" diff --git a/plasma/workspace/components/containmentlayoutmanager/resizehandle.h b/plasma/workspace/components/containmentlayoutmanager/resizehandle.h new file mode 100644 index 0000000000..f7e1e9f0fb --- /dev/null +++ b/plasma/workspace/components/containmentlayoutmanager/resizehandle.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2019 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "configoverlay.h" + +class ResizeHandle : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(Corner resizeCorner MEMBER m_resizeCorner NOTIFY resizeCornerChanged) + Q_PROPERTY(bool resizeBlocked READ resizeBlocked NOTIFY resizeBlockedChanged) + Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged) + +public: + enum Corner { + Left = 0, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + }; + Q_ENUMS(Corner) + + ResizeHandle(QQuickItem *parent = nullptr); + ~ResizeHandle(); + + bool resizeBlocked() const; + + void setPressed(bool pressed); + bool isPressed() const; + +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + +Q_SIGNALS: + void resizeCornerChanged(); + void resizeBlockedChanged(); + void pressedChanged(); + +private: + void setConfigOverlay(ConfigOverlay *configOverlay); + + inline bool resizeLeft() const; + inline bool resizeTop() const; + inline bool resizeRight() const; + inline bool resizeBottom() const; + void setResizeBlocked(bool width, bool height); + + QPointF m_mouseDownPosition; + QRectF m_mouseDownGeometry; + + QPointer m_configOverlay; + Corner m_resizeCorner = Left; + bool m_resizeWidthBlocked = false; + bool m_resizeHeightBlocked = false; + bool m_pressed = false; +}; diff --git a/plasma/workspace/components/dialogs/SystemDialog.qml b/plasma/workspace/components/dialogs/SystemDialog.qml new file mode 100644 index 0000000000..cc3e8d2498 --- /dev/null +++ b/plasma/workspace/components/dialogs/SystemDialog.qml @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import QtGraphicalEffects 1.12 +import org.kde.kirigami 2.18 as Kirigami +import org.kde.plasma.lookandfeel 1.0 + +/** + * Component to create CSD dialogs that come from the system. + */ +Kirigami.AbstractApplicationWindow { + id: root + + title: mainText + /** + * Main text of the dialog. + */ + property string mainText: title + + /** + * Subtitle of the dialog. + */ + property string subtitle: "" + + /** + * This property holds the icon used in the dialog. + */ + property string iconName: "" + + /** + * This property holds the list of actions for this dialog. + * + * Each action will be rendered as a button that the user will be able + * to click. + */ + property list actions + + default property Item mainItem + + /** + * This property holds the QQC2 DialogButtonBox used in the footer of the dialog. + */ + readonly property DialogButtonBox dialogButtonBox: contentDialog.item.dialogButtonBox + + /** + * Provides dialogButtonBox.standardButtons + * + * Useful to be able to set it as dialogButtonBox will be null as the object gets built + */ + property variant standardButtons: contentDialog.item ? contentDialog.item.dialogButtonBox.standardButtons : undefined + + /** + * Controls whether the accept button is enabled + */ + property bool acceptable: true + + + /** + * The layout of the action buttons in the footer of the dialog. + * + * By default, if there are more than 3 actions, it will have `Qt.Vertical`. + * + * Otherwise, with zero to 2 actions, it will have `Qt.Horizontal`. + * + * This will only affect mobile dialogs. + */ + property int /*Qt.Orientation*/ layout: actions.length > 3 ? Qt.Vertical : Qt.Horizontal + + flags: contentDialog.item.flags + width: contentDialog.implicitWidth + height: contentDialog.implicitHeight + visible: false + minimumHeight: contentDialog.item.minimumHeight + minimumWidth: contentDialog.item.minimumWidth + + function present() { + contentDialog.item.present() + } + signal accept() + signal reject() + property bool accepted: false + onAccept: { + accepted = true + close() + } + onReject: close() + + onVisibleChanged: { + if (!visible && !accepted) { + root.reject() + } + width = Qt.binding(() => { return contentDialog.implicitWidth }) + height = Qt.binding(() => { return contentDialog.implicitHeight }) + } + + Binding { + target: dialogButtonBox.standardButton(DialogButtonBox.Ok) + property: "enabled" + when: dialogButtonBox.standardButton(DialogButtonBox.Ok) + value: root.acceptable + } + + Loader { + id: contentDialog + anchors.fill: parent + Component.onCompleted: { + var component = LookAndFeel.fileUrl("systemdialogscript") + setSource(component, { + window: root, + mainText: root.mainText, + subtitle: root.subtitle, + actions: root.actions, + iconName: root.iconName, + mainItem: root.mainItem, + standardButtons: root.standardButtons + }) + } + + focus: true + + function accept() { + const button = dialogButtonBox.standardButton(DialogButtonBox.Ok); + if (button && button.enabled) { + root.accept() + } + } + Keys.onEnterPressed: accept() + Keys.onReturnPressed: accept() + Keys.onEscapePressed: root.reject() + } +} diff --git a/plasma/workspace/components/dialogs/examples/test.qml b/plasma/workspace/components/dialogs/examples/test.qml new file mode 100644 index 0000000000..ed46b83f32 --- /dev/null +++ b/plasma/workspace/components/dialogs/examples/test.qml @@ -0,0 +1,360 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import QtQuick.Dialogs 1.2 as Dialogs +import QtGraphicalEffects 1.12 +import org.kde.kirigami 2.19 as Kirigami +import org.kde.plasma.workspace.dialogs 1.0 + +Kirigami.AbstractApplicationWindow { + id: root + + width: 600 + height: 600 + + SystemDialog { + id: simple + mainText: "Reset Data" + subtitle: "This will reset all of your data." + iconName: "documentinfo" + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + } + + SystemDialog { + id: simpleList + mainText: "Reset Data" + subtitle: "This will reset all of your data." + iconName: "documentinfo" + + ListView { + Layout.fillWidth: true + implicitHeight: 300 + + model: ListModel { + ListElement { + display: "banana" + } + ListElement { + display: "banana1" + } + ListElement { + display: "banana2" + } + ListElement { + display: "banana3" + } + } + delegate: Kirigami.BasicListItem { + icon: "kate" + label: display + highlighted: false + checkable: true + } + } + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + } + + SystemDialog { + id: desktopPolkit + mainText: "Authentication Required" + subtitle: "Authentication is needed to run `/usr/bin/ls` as the super user." + iconName: "im-user-online" + + Kirigami.PasswordField {} + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + actions: [ + Kirigami.Action { + text: "Details" + iconName: "documentinfo" + onTriggered: desktopPolkit.close() + } + ] + } + + SystemDialog { + id: xdgDialog + mainText: "Wallet access" + subtitle: "Share your wallet with 'Somebody'." + iconName: "kwallet" + acceptable: false + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + Component.onCompleted: { + dialogButtonBox.standardButton(DialogButtonBox.Ok).text = "Share" + } + actions: [ + Kirigami.Action { + text: "Something Happens" + iconName: "documentinfo" + onTriggered: xdgDialog.acceptable = true + } + ] + } + + SystemDialog { + id: appchooser + title: "Open with..." + iconName: "applications-all" + ColumnLayout { + Text { + text: "height: " + parent.height + " / " + xdgDialog.height + } + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 3 + + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at viverra mi. Maecenas volutpat et nisi ac scelerisque. Mauris pulvinar blandit dapibus. Nulla facilisi. Donec congue imperdiet maximus. Aliquam gravida velit sed mattis convallis. Nam id nisi egestas nibh ultrices varius quis at sapien." + wrapMode: Text.WordWrap + + onLinkActivated: { + AppChooserData.openDiscover() + } + } + + Frame { + id: viewBackground + Layout.fillWidth: true + Layout.fillHeight: true + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + background: Rectangle { + color: Kirigami.Theme.backgroundColor + property color borderColor: Kirigami.Theme.textColor + border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3) + } + + ScrollView { + anchors.fill: parent + implicitHeight: grid.cellHeight * 3 + + GridView { + id: grid + + cellHeight: Kirigami.Units.iconSizes.huge + 50 + cellWidth: Kirigami.Units.iconSizes.huge + 80 + + model: ListModel { + ListElement { + display: "banana" + } + ListElement { + display: "banana1" + } + ListElement { + display: "banana2" + } + ListElement { + display: "banana3" + } + } + delegate: Rectangle { + color: "blue" + height: grid.cellHeight + width: grid.cellWidth + + Kirigami.Icon { + source: "kalgebra" + } + } + } + } + } + + Button { + id: showAllAppsButton + Layout.alignment: Qt.AlignHCenter + icon.name: "view-more-symbolic" + text: "Show More" + + onClicked: { + visible = false + } + } + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + visible: !showAllAppsButton.visible + opacity: visible + } + } + } + + SystemDialog { + id: mobilePolkit + mainText: "Authentication Required" + subtitle: "Authentication is needed to run `/usr/bin/ls` as the super user." + + ColumnLayout { + width: Kirigami.Units.gridUnit * 20 + + Kirigami.Avatar { + implicitHeight: Kirigami.Units.iconSizes.medium + implicitWidth: Kirigami.Units.iconSizes.medium + Layout.alignment: Qt.AlignHCenter + } + Kirigami.PasswordField { + Layout.fillWidth: true + } + } + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + actions: [ + Kirigami.Action { + text: "Details" + iconName: "documentinfo" + onTriggered: mobilePolkit.close() + } + ] + } + + SystemDialog { + id: sim + mainText: "SIM Locked" + subtitle: "Please enter your SIM PIN in order to unlock it." + + width: Kirigami.Units.gridUnit * 20 + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + + Kirigami.PasswordField { + Layout.fillWidth: true + } + } + + SystemDialog { + id: device + mainText: "Device Request" + subtitle: "Allow PureMaps to access your location?" + + layout: Qt.Vertical + + actions: [ + Kirigami.Action { + text: "Allow all the time" + onTriggered: device.accept() + }, + Kirigami.Action { + text: "Allow only while the app is in use" + onTriggered: device.accept() + }, + Kirigami.Action { + text: "Deny" + onTriggered: device.accept() + } + ] + } + + SystemDialog { + id: wifi + mainText: "eduroam" + + Kirigami.FormLayout { + ComboBox { + model: ["PEAP"] + Layout.fillWidth: true + Kirigami.FormData.label: "EAP method:" + currentIndex: 0 + } + ComboBox { + model: ["MSCHAPV2"] + Layout.fillWidth: true + Kirigami.FormData.label: "Phase 2 authentication:" + currentIndex: 0 + } + TextField { + Kirigami.FormData.label: "Domain:" + Layout.fillWidth: true + text: "" + } + TextField { + Kirigami.FormData.label: "Identity:" + Layout.fillWidth: true + } + TextField { + Kirigami.FormData.label: "Username:" + Layout.fillWidth: true + } + Kirigami.PasswordField { + Kirigami.FormData.label: "Password:" + Layout.fillWidth: true + } + } + + standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel + Component.onCompleted: { + dialogButtonBox.standardButton(DialogButtonBox.Ok).text = "Save" + } + } + + ColumnLayout { + anchors.fill: parent + Button { + text: "Simple dialog (Desktop)" + onClicked: { + simple.present() + } + } + Button { + text: "Simple List" + onClicked: { + simpleList.present() + } + } + Button { + text: "Polkit dialog (Desktop)" + onClicked: { + desktopPolkit.present() + } + } + Button { + text: "App Chooser(-ish)" + onClicked: { + appchooser.present() + } + } + Button { + text: "XDG dialog (Desktop)" + onClicked: { + xdgDialog.present() + } + } + Button { + text: "Polkit dialog (Mobile)" + onClicked: { + mobilePolkit.present() + } + } + Button { + text: "SIM PIN dialog (Mobile)" + onClicked: { + sim.present() + } + } + Button { + text: "Device request dialog (Mobile)" + onClicked: { + device.present() + } + } + Button { + text: "Wifi Dialog (Mobile)" + onClicked: { + wifi.present() + } + } + } +} + diff --git a/plasma/workspace/components/dialogs/qmldir b/plasma/workspace/components/dialogs/qmldir new file mode 100644 index 0000000000..5f16337ba2 --- /dev/null +++ b/plasma/workspace/components/dialogs/qmldir @@ -0,0 +1,3 @@ +module org.kde.plasma.workspace.dialogs + +SystemDialog 1.0 SystemDialog.qml diff --git a/plasma/workspace/components/keyboardlayout/CMakeLists.txt b/plasma/workspace/components/keyboardlayout/CMakeLists.txt new file mode 100644 index 0000000000..99b816c4f1 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/CMakeLists.txt @@ -0,0 +1,29 @@ +set(keyboardlayoutplugin_SRCS + keyboardlayout.cpp + keyboardlayoutplugin.cpp + layoutnames.cpp + virtualkeyboard.cpp +) + +ecm_qt_declare_logging_category(keyboardlayoutplugin_SRCS HEADER debug.h + IDENTIFIER KEYBOARD_LAYOUT + CATEGORY_NAME kde.keyboardlayout + DEFAULT_SEVERITY Info) + +set_source_files_properties(org.kde.KeyboardLayouts.xml + PROPERTIES INCLUDE layoutnames.h) + +qt_add_dbus_interface(keyboardlayoutplugin_SRCS "org.kde.KeyboardLayouts.xml" keyboard_layout_interface) +qt_add_dbus_interface(keyboardlayoutplugin_SRCS "${KWIN_VIRTUALKEYBOARD_INTERFACE}" virtualkeyboard_interface) + +add_library(keyboardlayoutplugin SHARED ${keyboardlayoutplugin_SRCS}) + +target_link_libraries(keyboardlayoutplugin Qt::Core + Qt::DBus + Qt::Qml) + +set(keyboardlayoutplugin_PATH /org/kde/plasma/workspace/keyboardlayout) +install(TARGETS keyboardlayoutplugin + DESTINATION ${KDE_INSTALL_QMLDIR}${keyboardlayoutplugin_PATH}) +install(FILES qmldir + DESTINATION ${KDE_INSTALL_QMLDIR}${keyboardlayoutplugin_PATH}) diff --git a/plasma/workspace/components/keyboardlayout/keyboardlayout.cpp b/plasma/workspace/components/keyboardlayout/keyboardlayout.cpp new file mode 100644 index 0000000000..c2ee0f97b9 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/keyboardlayout.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2020 Andrey Butirsky + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "keyboardlayout.h" +#include "keyboard_layout_interface.h" + +#include + +template<> +inline void KeyboardLayout::requestDBusData() +{ + if (mIface) + requestDBusData(mIface->getLayout(), mLayout, &KeyboardLayout::layoutChanged); +} + +template<> +inline void KeyboardLayout::requestDBusData() +{ + if (mIface) + requestDBusData(mIface->getLayoutsList(), mLayoutsList, &KeyboardLayout::layoutsListChanged); +} + +KeyboardLayout::KeyboardLayout(QObject *parent) + : QObject(parent) + , mIface(nullptr) +{ + LayoutNames::registerMetaType(); + + mIface = new OrgKdeKeyboardLayoutsInterface(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QDBusConnection::sessionBus(), this); + if (!mIface->isValid()) { + delete mIface; + mIface = nullptr; + return; + } + + connect(mIface, &OrgKdeKeyboardLayoutsInterface::layoutChanged, this, [this](uint index) { + mLayout = index; + Q_EMIT layoutChanged(); + }); + + connect(mIface, &OrgKdeKeyboardLayoutsInterface::layoutListChanged, this, [this]() { + requestDBusData(); + requestDBusData(); + }); + + Q_EMIT mIface->OrgKdeKeyboardLayoutsInterface::layoutListChanged(); +} + +KeyboardLayout::~KeyboardLayout() +{ +} + +void KeyboardLayout::switchToNextLayout() +{ + if (mIface) + mIface->switchToNextLayout(); +} + +void KeyboardLayout::switchToPreviousLayout() +{ + if (mIface) + mIface->switchToPreviousLayout(); +} + +void KeyboardLayout::setLayout(uint index) +{ + if (mIface) + mIface->setLayout(index); +} + +template +void KeyboardLayout::requestDBusData(QDBusPendingReply pendingReply, T &out, void (KeyboardLayout::*notify)()) +{ + connect(new QDBusPendingCallWatcher(pendingReply, this), &QDBusPendingCallWatcher::finished, this, [this, &out, notify](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(KEYBOARD_LAYOUT) << reply.error().message(); + } + out = reply.value(); + Q_EMIT(this->*notify)(); + + watcher->deleteLater(); + }); +} diff --git a/plasma/workspace/components/keyboardlayout/keyboardlayout.h b/plasma/workspace/components/keyboardlayout/keyboardlayout.h new file mode 100644 index 0000000000..4cdd67c466 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/keyboardlayout.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + SPDX-FileCopyrightText: 2020 Andrey Butirsky + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +#include "debug.h" + +class OrgKdeKeyboardLayoutsInterface; +class LayoutNames; + +class KeyboardLayout : public QObject +{ + Q_OBJECT + + Q_PROPERTY(uint layout MEMBER mLayout WRITE setLayout NOTIFY layoutChanged) + + Q_PROPERTY(const QVector &layoutsList READ getLayoutsList NOTIFY layoutsListChanged) + +public: + explicit KeyboardLayout(QObject *parent = nullptr); + ~KeyboardLayout() override; + +Q_SIGNALS: + void layoutChanged(); + void layoutsListChanged(); + +protected: + Q_INVOKABLE void switchToNextLayout(); + Q_INVOKABLE void switchToPreviousLayout(); + +private: + void setLayout(uint index); + const QVector &getLayoutsList() const + { + return mLayoutsList; + } + + enum DBusData { Layout, LayoutsList }; + + template + void requestDBusData(QDBusPendingReply pendingReply, T &out, void (KeyboardLayout::*notify)()); + template + void requestDBusData(); + + uint mLayout; + QVector mLayoutsList; + OrgKdeKeyboardLayoutsInterface *mIface; +}; diff --git a/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.cpp b/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.cpp new file mode 100644 index 0000000000..bdce48484b --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.cpp @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Yrjölä + + SPDX-License-Identifier: MIT +*/ + +#include "keyboardlayoutplugin.h" +#include "keyboardlayout.h" +#include "virtualkeyboard.h" + +#include + +void KeyboardLayoutPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.workspace.keyboardlayout")); + + qmlRegisterType(uri, 1, 0, "KeyboardLayout"); + qmlRegisterSingletonType(uri, 1, 0, "KWinVirtualKeyboard", [](QQmlEngine *, QJSEngine *) -> QObject * { + return new KwinVirtualKeyboardInterface; + }); +} diff --git a/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.h b/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.h new file mode 100644 index 0000000000..e79f242601 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/keyboardlayoutplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Yrjölä + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include + +class KeyboardLayoutPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/components/keyboardlayout/layoutnames.cpp b/plasma/workspace/components/keyboardlayout/layoutnames.cpp new file mode 100644 index 0000000000..bc754b8c29 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/layoutnames.cpp @@ -0,0 +1,24 @@ +#include "layoutnames.h" +#include + +void LayoutNames::registerMetaType() +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); +} + +QDBusArgument &operator<<(QDBusArgument &argument, const LayoutNames &layoutNames) +{ + argument.beginStructure(); + argument << layoutNames.shortName << layoutNames.displayName << layoutNames.longName; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, LayoutNames &layoutNames) +{ + argument.beginStructure(); + argument >> layoutNames.shortName >> layoutNames.displayName >> layoutNames.longName; + argument.endStructure(); + return argument; +} diff --git a/plasma/workspace/components/keyboardlayout/layoutnames.h b/plasma/workspace/components/keyboardlayout/layoutnames.h new file mode 100644 index 0000000000..da1301b617 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/layoutnames.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class QDBusArgument; + +class LayoutNames +{ + Q_GADGET + + Q_PROPERTY(QString shortName MEMBER shortName) + Q_PROPERTY(QString displayName MEMBER displayName) + Q_PROPERTY(QString longName MEMBER longName) + +public: + static void registerMetaType(); + + QString shortName; + QString displayName; + QString longName; +}; +Q_DECLARE_METATYPE(LayoutNames) + +QDBusArgument &operator<<(QDBusArgument &argument, const LayoutNames &layoutNames); +const QDBusArgument &operator>>(const QDBusArgument &argument, LayoutNames &layoutNames); diff --git a/plasma/workspace/components/keyboardlayout/org.kde.KeyboardLayouts.xml b/plasma/workspace/components/keyboardlayout/org.kde.KeyboardLayouts.xml new file mode 100644 index 0000000000..1126e403c0 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/org.kde.KeyboardLayouts.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/components/keyboardlayout/qmldir b/plasma/workspace/components/keyboardlayout/qmldir new file mode 100644 index 0000000000..ef02a24701 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/qmldir @@ -0,0 +1,3 @@ +module org.kde.plasma.workspace.keyboardlayout + +plugin keyboardlayoutplugin diff --git a/plasma/workspace/components/keyboardlayout/virtualkeyboard.cpp b/plasma/workspace/components/keyboardlayout/virtualkeyboard.cpp new file mode 100644 index 0000000000..1951b79e09 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/virtualkeyboard.cpp @@ -0,0 +1,12 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "virtualkeyboard.h" + +KwinVirtualKeyboardInterface::KwinVirtualKeyboardInterface() + : OrgKdeKwinVirtualKeyboardInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/VirtualKeyboard"), QDBusConnection::sessionBus()) +{ +} diff --git a/plasma/workspace/components/keyboardlayout/virtualkeyboard.h b/plasma/workspace/components/keyboardlayout/virtualkeyboard.h new file mode 100644 index 0000000000..a3abbe6dc7 --- /dev/null +++ b/plasma/workspace/components/keyboardlayout/virtualkeyboard.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#pragma once + +#include "virtualkeyboard_interface.h" + +class KwinVirtualKeyboardInterface : public OrgKdeKwinVirtualKeyboardInterface +{ + Q_OBJECT + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged) + Q_PROPERTY(bool available READ available NOTIFY availableChanged) +public: + KwinVirtualKeyboardInterface(); +}; diff --git a/plasma/workspace/components/lookandfeelqml/CMakeLists.txt b/plasma/workspace/components/lookandfeelqml/CMakeLists.txt new file mode 100644 index 0000000000..f78aa84e0c --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/CMakeLists.txt @@ -0,0 +1,15 @@ +set(lookandfeelqmlplugin_SRCS + lookandfeelqmlplugin.cpp + kpackageinterface.cpp +) + +add_library(lookandfeelqmlplugin SHARED ${lookandfeelqmlplugin_SRCS}) +target_link_libraries(lookandfeelqmlplugin + Qt::Qml + KF5::ConfigCore + KF5::Package +) + +install(TARGETS lookandfeelqmlplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/lookandfeel) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/lookandfeel) diff --git a/plasma/workspace/components/lookandfeelqml/kpackageinterface.cpp b/plasma/workspace/components/lookandfeelqml/kpackageinterface.cpp new file mode 100644 index 0000000000..4881ff61bb --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/kpackageinterface.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "kpackageinterface.h" + +KPackageInterface::KPackageInterface(const KPackage::Package &package) + : m_package(package) +{ + Q_ASSERT(m_package.isValid()); +} + +QUrl KPackageInterface::fileUrl(const QByteArray &key) const +{ + return m_package.fileUrl(key); +} diff --git a/plasma/workspace/components/lookandfeelqml/kpackageinterface.h b/plasma/workspace/components/lookandfeelqml/kpackageinterface.h new file mode 100644 index 0000000000..34c4cb721f --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/kpackageinterface.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#pragma once + +#include +#include + +class KPackageInterface : public QObject +{ + Q_OBJECT +public: + KPackageInterface(const KPackage::Package &package); + + Q_INVOKABLE QUrl fileUrl(const QByteArray &key) const; + +private: + const KPackage::Package m_package; +}; diff --git a/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.cpp b/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.cpp new file mode 100644 index 0000000000..6c7d760b09 --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.cpp @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "lookandfeelqmlplugin.h" + +#include "kpackageinterface.h" +#include +#include +#include +#include + +void SessionsPrivatePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.lookandfeel")); + + qmlRegisterSingletonType(uri, 1, 0, "LookAndFeel", [](QQmlEngine *, QJSEngine *) { + KConfigGroup cg(KSharedConfig::openConfig(), "KDE"); + const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); + const auto package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), packageName); + return new KPackageInterface(package); + }); +} diff --git a/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.h b/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.h new file mode 100644 index 0000000000..64823086f7 --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/lookandfeelqmlplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#pragma once + +#include + +class SessionsPrivatePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/components/lookandfeelqml/qmldir b/plasma/workspace/components/lookandfeelqml/qmldir new file mode 100644 index 0000000000..3568e10c21 --- /dev/null +++ b/plasma/workspace/components/lookandfeelqml/qmldir @@ -0,0 +1,2 @@ +module org.kde.plasma.lookandfeel +plugin lookandfeelqmlplugin diff --git a/plasma/workspace/components/sessionsprivate/CMakeLists.txt b/plasma/workspace/components/sessionsprivate/CMakeLists.txt new file mode 100644 index 0000000000..64e9e94ede --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/CMakeLists.txt @@ -0,0 +1,27 @@ +set(sessionsprivateplugin_SRCS + sessionsmodel.cpp + sessionsprivateplugin.cpp +) + +qt_add_dbus_interface(sessionsprivateplugin_SRCS ${SCREENSAVER_DBUS_INTERFACE} screensaver_interface) + +kconfig_add_kcfg_files(sessionsprivateplugin_SRCS kscreensaversettings.kcfgc) + +add_library(sessionsprivateplugin SHARED ${sessionsprivateplugin_SRCS}) +target_link_libraries(sessionsprivateplugin + Qt::Core + Qt::DBus + Qt::Quick + Qt::Qml + Qt::Gui + KF5::CoreAddons + KF5::ConfigCore + KF5::ConfigGui + KF5::I18n + PW::KWorkspace +) + +install(TARGETS sessionsprivateplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/sessions) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/sessions) + diff --git a/plasma/workspace/components/sessionsprivate/kscreenlockersettings.kcfg b/plasma/workspace/components/sessionsprivate/kscreenlockersettings.kcfg new file mode 100644 index 0000000000..4d42dbb9d3 --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/kscreenlockersettings.kcfg @@ -0,0 +1,47 @@ + + + + + + true + + Sets whether the screen will be locked after the specified time. + + + 5 + 1 + + Sets the minutes after which the screen is locked. + + + true + + + + + 5 + 0 + 300 + + + + + true + + + + + + + + + + + + + + + diff --git a/plasma/workspace/components/sessionsprivate/kscreensaversettings.kcfgc b/plasma/workspace/components/sessionsprivate/kscreensaversettings.kcfgc new file mode 100644 index 0000000000..eca6b96cb0 --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/kscreensaversettings.kcfgc @@ -0,0 +1,4 @@ +File=kscreenlockersettings.kcfg +ClassName=KScreenSaverSettings +Singleton=true +Mutators=true diff --git a/plasma/workspace/components/sessionsprivate/qmldir b/plasma/workspace/components/sessionsprivate/qmldir new file mode 100644 index 0000000000..12de2f5ead --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/qmldir @@ -0,0 +1,2 @@ +module org.kde.plasma.private.sessions +plugin sessionsprivateplugin diff --git a/plasma/workspace/components/sessionsprivate/sessionsmodel.cpp b/plasma/workspace/components/sessionsprivate/sessionsmodel.cpp new file mode 100644 index 0000000000..4a221cec5f --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/sessionsmodel.cpp @@ -0,0 +1,291 @@ +/* + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#include "sessionsmodel.h" + +#include +#include +#include + +#include "kscreensaversettings.h" + +#include "screensaver_interface.h" + +SessionsModel::SessionsModel(QObject *parent) + : QAbstractListModel(parent) + , m_screensaverInterface( + new org::freedesktop::ScreenSaver(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus(), this)) +{ + reload(); + + // wait for the screen locker to be ready before actually switching + connect(m_screensaverInterface, &org::freedesktop::ScreenSaver::ActiveChanged, this, [this](bool active) { + if (active) { + if (m_pendingVt) { + m_displayManager.switchVT(m_pendingVt); + Q_EMIT switchedUser(m_pendingVt); + } else if (m_pendingReserve) { + m_displayManager.startReserve(); + Q_EMIT startedNewSession(); + } + + m_pendingVt = 0; + m_pendingReserve = false; + } + }); +} + +bool SessionsModel::canSwitchUser() const +{ + return const_cast(this)->m_displayManager.isSwitchable() && KAuthorized::authorizeAction(QLatin1String("switch_user")); +} + +bool SessionsModel::canStartNewSession() const +{ + return const_cast(this)->m_displayManager.numReserve() > 0 && KAuthorized::authorizeAction(QLatin1String("start_new_session")); +} + +bool SessionsModel::shouldLock() const +{ + return m_shouldLock; +} + +bool SessionsModel::includeUnusedSessions() const +{ + return m_includeUnusedSessions; +} + +void SessionsModel::setIncludeUnusedSessions(bool includeUnusedSessions) +{ + if (m_includeUnusedSessions != includeUnusedSessions) { + m_includeUnusedSessions = includeUnusedSessions; + + reload(); + + Q_EMIT includeUnusedSessionsChanged(); + } +} + +void SessionsModel::switchUser(int vt, bool shouldLock) +{ + if (vt < 0) { + startNewSession(shouldLock); + return; + } + + if (!canSwitchUser()) { + return; + } + + if (!shouldLock) { + m_displayManager.switchVT(vt); + Q_EMIT switchedUser(vt); + return; + } + + checkScreenLocked([this, vt](bool locked) { + if (locked) { + // already locked, switch right away + m_displayManager.switchVT(vt); + Q_EMIT switchedUser(vt); + } else { + m_pendingReserve = false; + m_pendingVt = vt; + + Q_EMIT aboutToLockScreen(); + m_screensaverInterface->Lock(); + } + }); +} + +void SessionsModel::startNewSession(bool shouldLock) +{ + if (!canStartNewSession()) { + return; + } + + if (!shouldLock) { + m_displayManager.startReserve(); + Q_EMIT startedNewSession(); + return; + } + + checkScreenLocked([this](bool locked) { + if (locked) { + // already locked, switch right away + m_displayManager.startReserve(); + Q_EMIT startedNewSession(); + } else { + m_pendingReserve = true; + m_pendingVt = 0; + + Q_EMIT aboutToLockScreen(); + m_screensaverInterface->Lock(); + } + }); +} + +void SessionsModel::reload() +{ + static QHash kusers; + + const bool oldShouldLock = m_shouldLock; + m_shouldLock = KAuthorized::authorizeAction(QStringLiteral("lock_screen")) && KScreenSaverSettings::autolock(); + if (m_shouldLock != oldShouldLock) { + Q_EMIT shouldLockChanged(); + } + + SessList sessions; + m_displayManager.localSessions(sessions); + + const int oldCount = m_data.count(); + + beginResetModel(); + + m_data.clear(); + m_data.reserve(sessions.count()); + + foreach (const SessEnt &session, sessions) { + if (!session.vt || session.self) { + continue; + } + + if (!m_includeUnusedSessions && session.session.isEmpty()) { + continue; + } + + SessionEntry entry; + entry.name = session.user; + entry.displayNumber = session.display; + entry.vtNumber = session.vt; + entry.session = session.session; + entry.isTty = session.tty; + + auto it = kusers.constFind(session.user); + if (it != kusers.constEnd()) { + entry.realName = it->property(KUser::FullName).toString(); + entry.icon = it->faceIconPath(); + } else { + KUser user(session.user); + entry.realName = user.property(KUser::FullName).toString(); + entry.icon = user.faceIconPath(); + kusers.insert(session.user, user); + } + + m_data.append(entry); + } + + endResetModel(); + + if (oldCount != m_data.count()) { + Q_EMIT countChanged(); + } +} + +void SessionsModel::checkScreenLocked(const std::function &cb) +{ + auto reply = m_screensaverInterface->GetActive(); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [cb](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + cb(reply.value()); + } + watcher->deleteLater(); + }); +} + +void SessionsModel::setShowNewSessionEntry(bool showNewSessionEntry) +{ + if (!canStartNewSession()) { + return; + } + + if (showNewSessionEntry == m_showNewSessionEntry) { + return; + } + + int row = m_data.size(); + if (showNewSessionEntry) { + beginInsertRows(QModelIndex(), row, row); + m_showNewSessionEntry = showNewSessionEntry; + endInsertRows(); + } else { + beginRemoveRows(QModelIndex(), row, row); + m_showNewSessionEntry = showNewSessionEntry; + endRemoveRows(); + } + Q_EMIT countChanged(); +} + +QVariant SessionsModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() > rowCount(QModelIndex())) { + return QVariant(); + } + + if (index.row() == m_data.count()) { + switch (static_cast(role)) { + case Role::RealName: + return i18n("New Session"); + case Role::IconName: + return QStringLiteral("system-switch-user"); + case Role::Name: + return i18n("New Session"); + case Role::DisplayNumber: + return 0; // NA + case Role::VtNumber: + return -1; // an invalid VtNumber - which we'll use to indicate it's to start a new session + case Role::Session: + return 0; // NA + case Role::IsTty: + return false; // NA + default: + return QVariant(); + } + } + + const SessionEntry &item = m_data.at(index.row()); + + switch (static_cast(role)) { + case Role::RealName: + return item.realName; + case Role::Icon: + return item.icon; + case Role::Name: + return item.name; + case Role::DisplayNumber: + return item.displayNumber; + case Role::VtNumber: + return item.vtNumber; + case Role::Session: + return item.session; + case Role::IsTty: + return item.isTty; + default: + return QVariant(); + } +} + +int SessionsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data.count() + (m_showNewSessionEntry ? 1 : 0); +} + +QHash SessionsModel::roleNames() const +{ + return { + {static_cast(Role::Name), QByteArrayLiteral("name")}, + {static_cast(Role::RealName), QByteArrayLiteral("realName")}, + {static_cast(Role::Icon), QByteArrayLiteral("icon")}, + {static_cast(Role::IconName), QByteArrayLiteral("iconName")}, + {static_cast(Role::DisplayNumber), QByteArrayLiteral("displayNumber")}, + {static_cast(Role::VtNumber), QByteArrayLiteral("vtNumber")}, + {static_cast(Role::Session), QByteArrayLiteral("session")}, + {static_cast(Role::IsTty), QByteArrayLiteral("isTty")}, + }; +} diff --git a/plasma/workspace/components/sessionsprivate/sessionsmodel.h b/plasma/workspace/components/sessionsprivate/sessionsmodel.h new file mode 100644 index 0000000000..a3049d80ef --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/sessionsmodel.h @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include + +#include + +#include + +class OrgFreedesktopScreenSaverInterface; +namespace org +{ +namespace freedesktop +{ +using ScreenSaver = ::OrgFreedesktopScreenSaverInterface; +} +} + +struct SessionEntry { + QString realName; + QString icon; + QString name; + QString displayNumber; + QString session; + int vtNumber; + bool isTty; +}; + +class KDisplayManager; + +class SessionsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool canSwitchUser READ canSwitchUser CONSTANT) + Q_PROPERTY(bool canStartNewSession READ canStartNewSession CONSTANT) + Q_PROPERTY(bool shouldLock READ shouldLock NOTIFY shouldLockChanged) + Q_PROPERTY(bool showNewSessionEntry MEMBER m_showNewSessionEntry WRITE setShowNewSessionEntry NOTIFY showNewSessionEntryChanged) + Q_PROPERTY(bool includeUnusedSessions READ includeUnusedSessions WRITE setIncludeUnusedSessions NOTIFY includeUnusedSessionsChanged) + + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + explicit SessionsModel(QObject *parent = nullptr); + ~SessionsModel() override = default; + + enum class Role { + RealName = Qt::DisplayRole, + Icon = Qt::DecorationRole, // path to a file + Name = Qt::UserRole + 1, + DisplayNumber, + VtNumber, + Session, + IsTty, + IconName, // name of an icon + }; + + bool canSwitchUser() const; + bool canStartNewSession() const; + bool shouldLock() const; + bool includeUnusedSessions() const; + + void setShowNewSessionEntry(bool showNewSessionEntry); + void setIncludeUnusedSessions(bool includeUnusedSessions); + + Q_INVOKABLE void reload(); + Q_INVOKABLE void switchUser(int vt, bool shouldLock = false); + Q_INVOKABLE void startNewSession(bool shouldLock = false); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +Q_SIGNALS: + void shouldLockChanged(); + void showNewSessionEntryChanged(); + void countChanged(); + void includeUnusedSessionsChanged(); + + void switchedUser(int vt); + void startedNewSession(); + void aboutToLockScreen(); + +private: + void checkScreenLocked(const std::function &cb); + + KDisplayManager m_displayManager; + + QVector m_data; + + bool m_shouldLock = true; + + int m_pendingVt = 0; + bool m_pendingReserve = false; + + bool m_showNewSessionEntry = false; + bool m_includeUnusedSessions = true; + + org::freedesktop::ScreenSaver *m_screensaverInterface = nullptr; +}; diff --git a/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.cpp b/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.cpp new file mode 100644 index 0000000000..43b9881758 --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#include "sessionsprivateplugin.h" + +#include + +#include "sessionsmodel.h" +#include + +void SessionsPrivatePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.sessions")); + + qmlRegisterType(uri, 2, 0, "SessionManagement"); + qmlRegisterType(uri, 2, 0, "SessionsModel"); +} diff --git a/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.h b/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.h new file mode 100644 index 0000000000..9afa8e6b76 --- /dev/null +++ b/plasma/workspace/components/sessionsprivate/sessionsprivateplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include + +class SessionsPrivatePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/components/shellprivate/CMakeLists.txt b/plasma/workspace/components/shellprivate/CMakeLists.txt new file mode 100644 index 0000000000..64ecf659f1 --- /dev/null +++ b/plasma/workspace/components/shellprivate/CMakeLists.txt @@ -0,0 +1,41 @@ + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-shellprivate.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-shellprivate.h) + +set(plasmashellprivateplugin_SRCS + widgetexplorer/kcategorizeditemsviewmodels.cpp + widgetexplorer/plasmaappletitemmodel.cpp + widgetexplorer/openwidgetassistant.cpp + widgetexplorer/widgetexplorer.cpp + shellprivateplugin.cpp +) + +add_library(plasmashellprivateplugin SHARED ${plasmashellprivateplugin_SRCS}) +target_link_libraries(plasmashellprivateplugin + Qt::Core + Qt::Quick + Qt::Qml + Qt::Gui + Qt::Widgets + Qt::Quick + Qt::Qml + KF5::Plasma + KF5::PlasmaQuick + KF5::I18n + KF5::Service + KF5::NewStuff + KF5::KIOFileWidgets + KF5::WindowSystem + KF5::Declarative + KF5::Activities + KF5::TextWidgets +) + +install(TARGETS plasmashellprivateplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/shell) +install(FILES + wallpaperplugin.knsrc + widgetexplorer/plasmoids.knsrc + DESTINATION ${KDE_INSTALL_KNSRCDIR} +) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/shell) + diff --git a/plasma/workspace/components/shellprivate/config-shellprivate.h.cmake b/plasma/workspace/components/shellprivate/config-shellprivate.h.cmake new file mode 100644 index 0000000000..f4dbf74a94 --- /dev/null +++ b/plasma/workspace/components/shellprivate/config-shellprivate.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 KF5TextEditor_FOUND diff --git a/plasma/workspace/components/shellprivate/qmldir b/plasma/workspace/components/shellprivate/qmldir new file mode 100644 index 0000000000..6bdee35231 --- /dev/null +++ b/plasma/workspace/components/shellprivate/qmldir @@ -0,0 +1,2 @@ +module org.kde.plasma.private.shell +plugin plasmashellprivateplugin diff --git a/plasma/workspace/components/shellprivate/shellprivateplugin.cpp b/plasma/workspace/components/shellprivate/shellprivateplugin.cpp new file mode 100644 index 0000000000..34e1de92aa --- /dev/null +++ b/plasma/workspace/components/shellprivate/shellprivateplugin.cpp @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: MIT +*/ + +#include "shellprivateplugin.h" +#include "config-shellprivate.h" + +#include + +#include "widgetexplorer/widgetexplorer.h" +#include + +void PlasmaShellPrivatePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.private.shell")); + + qmlRegisterAnonymousType("", 1); + qmlRegisterType(uri, 2, 0, "WidgetExplorer"); +} diff --git a/plasma/workspace/components/shellprivate/shellprivateplugin.h b/plasma/workspace/components/shellprivate/shellprivateplugin.h new file mode 100644 index 0000000000..8343674826 --- /dev/null +++ b/plasma/workspace/components/shellprivate/shellprivateplugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include + +class PlasmaShellPrivatePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/components/shellprivate/wallpaperplugin.knsrc b/plasma/workspace/components/shellprivate/wallpaperplugin.knsrc new file mode 100644 index 0000000000..3ed59f75d5 --- /dev/null +++ b/plasma/workspace/components/shellprivate/wallpaperplugin.knsrc @@ -0,0 +1,49 @@ +[KNewStuff3] +Name=Wallpaper Plugins +Name[ar]=محلق خلفية +Name[az]=Divar kağızı əlavəsi +Name[ca]=Connectors de fons de pantalla +Name[ca@valencia]=Connectors de fons de pantalla +Name[cs]=Moduly tapet +Name[da]=Baggrundsbillede-plugins +Name[de]=Hintergrundbild-Module +Name[el]=Πρόσθετα για ταπετσαρίες +Name[en_GB]=Wallpaper Plugins +Name[es]=Complementos de fondos del escritorio +Name[et]=Taustapildipluginad +Name[eu]=Horma-paper pluginak +Name[fi]=Taustakuvaliitännäiset +Name[fr]=Modules externes de fonds d'écran +Name[gl]=Complementos de fondo de escritorio +Name[hi]=वॉलपेपर प्लगइन +Name[hu]=Háttérképmodulok +Name[ia]=Plugins de tapete de papiro +Name[id]=Plugin-plugin Wallpaper +Name[it]=Estensioni per lo sfondo +Name[ko]=배경 그림 플러그인 +Name[lt]=Darbalaukio fono papildiniai +Name[ml]=വാള്‍പേപ്പര്‍ പ്ലഗ്ഗിനുകള്‍ +Name[nl]=Achtergrond-plug-ins +Name[nn]=Tillegg for bakgrunnsbilete +Name[pa]=ਵਾਲਪੇਪਰ ਪਲੱਗਇਨਾਂ +Name[pl]=Wtyczki tapety +Name[pt]='Plugins' de Papel de Parede +Name[pt_BR]=Plugins de papéis de parede +Name[ro]=Extensii de tapet +Name[ru]=Подключаемые модули, предоставляющие обои рабочего стола +Name[sk]=Doplnky tapety +Name[sl]=Vtičniki ozadij +Name[sv]=Insticksprogram för skrivbordsunderlägg +Name[ta]=பின்னணிப் பட செருகுநிரல்கள் +Name[tr]=Duvar Kağıdı Eklentileri +Name[uk]=Додатки зображень тла +Name[vi]=Phần cài cắm phông nền +Name[x-test]=xxWallpaper Pluginsxx +Name[zh_CN]=壁纸插件 +Name[zh_TW]=桌布外掛程式 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Plasma Wallpaper Plugin +StandardResource=tmp +Uncompress=kpackage +KPackageType=Plasma/Wallpaper diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels.cpp b/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels.cpp new file mode 100644 index 0000000000..0ec4109cea --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels.cpp @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kcategorizeditemsviewmodels_p.h" +#include +#include + +#define COLUMN_COUNT 4 + +namespace KCategorizedItemsViewModels +{ +// AbstractItem + +QString AbstractItem::name() const +{ + return text(); +} + +QString AbstractItem::id() const +{ + QString plugin = data().toMap()[QStringLiteral("pluginName")].toString(); + + if (plugin.isEmpty()) { + return name(); + } + + return plugin; +} + +QString AbstractItem::description() const +{ + return QLatin1String(""); +} + +bool AbstractItem::isFavorite() const +{ + return passesFiltering(Filter(QStringLiteral("favorite"), true)); +} + +int AbstractItem::running() const +{ + return 0; +} + +bool AbstractItem::matches(const QString &pattern) const +{ + return name().contains(pattern, Qt::CaseInsensitive) || description().contains(pattern, Qt::CaseInsensitive); +} + +// DefaultFilterModel + +DefaultFilterModel::DefaultFilterModel(QObject *parent) + : QStandardItemModel(0, 1, parent) +{ + setHeaderData(1, Qt::Horizontal, i18n("Filters")); + + connect(this, &QAbstractItemModel::modelReset, this, &DefaultFilterModel::countChanged); + connect(this, &QAbstractItemModel::rowsInserted, this, &DefaultFilterModel::countChanged); + connect(this, &QAbstractItemModel::rowsRemoved, this, &DefaultFilterModel::countChanged); +} + +QHash DefaultFilterModel::roleNames() const +{ + static QHash newRoleNames; + if (newRoleNames.isEmpty()) { + newRoleNames = QAbstractItemModel::roleNames(); + newRoleNames[FilterTypeRole] = "filterType"; + newRoleNames[FilterDataRole] = "filterData"; + newRoleNames[SeparatorRole] = "separator"; + } + return newRoleNames; +} + +void DefaultFilterModel::addFilter(const QString &caption, const Filter &filter, const QIcon &icon) +{ + QList newRow; + QStandardItem *item = new QStandardItem(caption); + item->setData(QVariant::fromValue(filter)); + if (!icon.isNull()) { + item->setIcon(icon); + } + item->setData(filter.first, FilterTypeRole); + item->setData(filter.second, FilterDataRole); + + newRow << item; + appendRow(newRow); +} + +void DefaultFilterModel::addSeparator(const QString &caption) +{ + QList newRow; + QStandardItem *item = new QStandardItem(caption); + item->setEnabled(false); + item->setData(true, SeparatorRole); + + newRow << item; + appendRow(newRow); +} + +QVariantHash DefaultFilterModel::get(int row) const +{ + QModelIndex idx = index(row, 0); + QVariantHash hash; + + const QHash roles = roleNames(); + for (QHash::const_iterator i = roles.constBegin(); i != roles.constEnd(); ++i) { + hash[i.value()] = data(idx, i.key()); + } + + return hash; +} + +// DefaultItemFilterProxyModel + +DefaultItemFilterProxyModel::DefaultItemFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +void DefaultItemFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + QStandardItemModel *model = qobject_cast(sourceModel); + + if (!model) { + qWarning() << "Expecting a QStandardItemModel!"; + return; + } + + QSortFilterProxyModel::setSourceModel(model); + connect(this, &QAbstractItemModel::modelReset, this, &DefaultItemFilterProxyModel::countChanged); + connect(this, &QAbstractItemModel::rowsInserted, this, &DefaultItemFilterProxyModel::countChanged); + connect(this, &QAbstractItemModel::rowsRemoved, this, &DefaultItemFilterProxyModel::countChanged); +} + +QAbstractItemModel *DefaultItemFilterProxyModel::sourceModel() const +{ + return QSortFilterProxyModel::sourceModel(); +} + +int DefaultItemFilterProxyModel::columnCount(const QModelIndex &index) const +{ + Q_UNUSED(index); + return COLUMN_COUNT; +} + +QVariant DefaultItemFilterProxyModel::data(const QModelIndex &index, int role) const +{ + return QSortFilterProxyModel::data(index, role); +} + +bool DefaultItemFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QStandardItemModel *model = (QStandardItemModel *)sourceModel(); + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + + AbstractItem *item = (AbstractItem *)model->itemFromIndex(index); + // qDebug() << "ITEM " << (item ? "IS NOT " : "IS") << " NULL\n"; + + return item && (m_filter.first.isEmpty() || item->passesFiltering(m_filter)) && (m_searchPattern.isEmpty() || item->matches(m_searchPattern)); +} + +QVariantHash DefaultItemFilterProxyModel::get(int row) const +{ + QModelIndex idx = index(row, 0); + QVariantHash hash; + + const QHash roles = roleNames(); + for (QHash::const_iterator i = roles.constBegin(); i != roles.constEnd(); ++i) { + hash[i.value()] = data(idx, i.key()); + } + + return hash; +} + +bool DefaultItemFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + return sourceModel()->data(left).toString().localeAwareCompare(sourceModel()->data(right).toString()) < 0; +} + +void DefaultItemFilterProxyModel::setSearchTerm(const QString &pattern) +{ + m_searchPattern = pattern; + invalidateFilter(); + Q_EMIT searchTermChanged(pattern); +} + +QString DefaultItemFilterProxyModel::searchTerm() const +{ + return m_searchPattern; +} + +void DefaultItemFilterProxyModel::setFilter(const Filter &filter) +{ + m_filter = filter; + invalidateFilter(); + Q_EMIT filterChanged(); +} + +void DefaultItemFilterProxyModel::setFilterType(const QString type) +{ + m_filter.first = type; + invalidateFilter(); + Q_EMIT filterChanged(); +} + +QString DefaultItemFilterProxyModel::filterType() const +{ + return m_filter.first; +} + +void DefaultItemFilterProxyModel::setFilterQuery(const QVariant query) +{ + m_filter.second = query; + invalidateFilter(); + Q_EMIT filterChanged(); +} + +QVariant DefaultItemFilterProxyModel::filterQuery() const +{ + return m_filter.second; +} + +} diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels_p.h b/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels_p.h new file mode 100644 index 0000000000..ff922caf66 --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/kcategorizeditemsviewmodels_p.h @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +namespace KCategorizedItemsViewModels +{ +typedef QPair Filter; + +/** + * Abstract class that needs to be implemented and used with the ItemModel + */ +class AbstractItem : public QStandardItem +{ +public: + /** + * Returns a localized string - name of the item + */ + virtual QString name() const; + + /** + * Returns a unique id related to this item + */ + virtual QString id() const; + + /** + * Returns a localized string - description of the item + */ + virtual QString description() const; + + /** + * Returns if the item is flagged as favorite + * Default implementation checks if the item passes the Filter("favorite", "1") filter + */ + virtual bool isFavorite() const; + + /** + * Returns the item's number of running applets + * Default implementation just returns 0 + */ + virtual int running() const; + + /** + * Returns if the item contains string specified by pattern. + * Default implementation checks whether name or description contain the + * string (not needed to be exactly that string) + */ + virtual bool matches(const QString &pattern) const; + + /** + * sets the number of running applets for the item + */ + virtual void setRunning(int count) = 0; + + /** + * Returns if the item passes the filter specified + */ + virtual bool passesFiltering(const Filter &filter) const = 0; + +private: +}; + +/** + * The default implementation of the model containing filters + */ +class DefaultFilterModel : public QStandardItemModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) +public: + enum Roles { + FilterTypeRole = Qt::UserRole + 1, + FilterDataRole = Qt::UserRole + 2, + SeparatorRole = Qt::UserRole + 3, + }; + explicit DefaultFilterModel(QObject *parent = nullptr); + + QHash roleNames() const override; + + /** + * Adds a filter to the model + * @param caption The localized string to be displayed as a name of the filter + * @param filter The filter structure + * @param icon The filter icon + */ + void addFilter(const QString &caption, const Filter &filter, const QIcon &icon = QIcon()); + + /** + * Adds a separator to the model + * @param caption The localized string to be displayed as a name of the separator + */ + void addSeparator(const QString &caption); + + int count() + { + return rowCount(QModelIndex()); + } + + Q_INVOKABLE QVariantHash get(int i) const; + +Q_SIGNALS: + void countChanged(); +}; + +/** + * Default filter proxy model. + */ +class DefaultItemFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString searchTerm READ searchTerm WRITE setSearchTerm NOTIFY searchTermChanged) + Q_PROPERTY(QString filterType READ filterType WRITE setFilterType NOTIFY filterChanged) + Q_PROPERTY(QVariant filterQuery READ filterQuery WRITE setFilterQuery NOTIFY filterChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit DefaultItemFilterProxyModel(QObject *parent = nullptr); + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + + void setSearchTerm(const QString &pattern); + QString searchTerm() const; + + void setFilterType(const QString type); + QString filterType() const; + + void setFilterQuery(const QVariant query); + QVariant filterQuery() const; + + void setFilter(const Filter &filter); + + void setSourceModel(QAbstractItemModel *sourceModel) override; + + QAbstractItemModel *sourceModel() const; + + int columnCount(const QModelIndex &index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int count() + { + return rowCount(QModelIndex()); + } + + Q_INVOKABLE QVariantHash get(int i) const; + +Q_SIGNALS: + void searchTermChanged(const QString &term); + void filterChanged(); + void countChanged(); + +private: + Filter m_filter; + QString m_searchPattern; +}; + +} // end of namespace + +Q_DECLARE_METATYPE(KCategorizedItemsViewModels::Filter) diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant.cpp b/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant.cpp new file mode 100644 index 0000000000..7534dded95 --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "openwidgetassistant_p.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace Plasma +{ +OpenWidgetAssistant::OpenWidgetAssistant(QWidget *parent) + : KAssistantDialog(parent) + , m_fileWidget(nullptr) + , m_filePageWidget(nullptr) +{ + m_filePageWidget = new QWidget(this); + + QVBoxLayout *layout = new QVBoxLayout(m_filePageWidget); + m_fileWidget = new KFileWidget(QUrl(), m_filePageWidget); + m_fileWidget->setOperationMode(KFileWidget::Opening); + m_fileWidget->setMode(KFile::File | KFile::ExistingOnly); + connect(this, SIGNAL(user1Clicked()), m_fileWidget, SLOT(slotOk())); + connect(m_fileWidget, SIGNAL(accepted()), this, SLOT(finished())); + layout->addWidget(m_fileWidget); + + m_fileWidget->setFilter(QString()); + QStringList mimes; + mimes << QStringLiteral("application/x-plasma"); + m_fileWidget->setMimeFilter(mimes); + + m_filePage = new KPageWidgetItem(m_filePageWidget, i18n("Select Plasmoid File")); + addPage(m_filePage); + + resize(QSize(560, 400).expandedTo(minimumSizeHint())); +} + +void OpenWidgetAssistant::slotHelpClicked() +{ + // enable it when doc will created +} + +void OpenWidgetAssistant::finished() +{ + m_fileWidget->accept(); // how interesting .. accept() must be called before the state is set + QString packageFilePath = m_fileWidget->selectedFile(); + if (packageFilePath.isEmpty()) { + // TODO: user visible error handling + qDebug() << "hm. no file path?"; + return; + } + + KPackage::Package installer = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet")); + + if (!installer.install(packageFilePath)) { + KMessageBox::error(this, i18n("Installing the package %1 failed.", packageFilePath), i18n("Installation Failure")); + } +} + +} // Plasma namespace diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant_p.h b/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant_p.h new file mode 100644 index 0000000000..ac1050379e --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/openwidgetassistant_p.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class KFileWidget; +class QListWidget; + +namespace Plasma +{ +class OpenWidgetAssistant : public KAssistantDialog +{ + Q_OBJECT + +public: + explicit OpenWidgetAssistant(QWidget *parent); + +protected Q_SLOTS: + void finished(); + void slotHelpClicked(); + +private: + KPageWidgetItem *m_filePage; + KFileWidget *m_fileWidget; + QWidget *m_filePageWidget; +}; + +} // Plasma namespace diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel.cpp b/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel.cpp new file mode 100644 index 0000000000..73baa8b792 --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel.cpp @@ -0,0 +1,420 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "plasmaappletitemmodel_p.h" + +#include +#include +#include + +#include "config-workspace.h" +#include +#include +#include +#include +#include +#include +#include + +PlasmaAppletItem::PlasmaAppletItem(const KPluginMetaData &info) + : AbstractItem() + , m_info(info) + , m_runningCount(0) + , m_local(false) +{ + const QString api(m_info.value(QStringLiteral("X-Plasma-API"))); + if (!api.isEmpty()) { + const QString _f = PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/" + info.pluginId() + '/'; + QFileInfo dir(QStandardPaths::locate(QStandardPaths::QStandardPaths::GenericDataLocation, _f, QStandardPaths::LocateDirectory)); + m_local = dir.exists() && dir.isWritable(); + } + + setText(m_info.name() + " - " + m_info.category().toLower()); + + if (QIcon::hasThemeIcon(info.pluginId())) { + setIcon(QIcon::fromTheme(info.pluginId())); + } else if (!m_info.iconName().isEmpty()) { + setIcon(QIcon::fromTheme(info.iconName())); + } else { + setIcon(QIcon::fromTheme(QStringLiteral("application-x-plasma"))); + } + + // set plugininfo parts as roles in the model, only way qml can understand it + setData(name(), PlasmaAppletItemModel::NameRole); + setData(pluginName(), PlasmaAppletItemModel::PluginNameRole); + setData(description(), PlasmaAppletItemModel::DescriptionRole); + setData(category().toLower(), PlasmaAppletItemModel::CategoryRole); + setData(license(), PlasmaAppletItemModel::LicenseRole); + setData(website(), PlasmaAppletItemModel::WebsiteRole); + setData(version(), PlasmaAppletItemModel::VersionRole); + setData(author(), PlasmaAppletItemModel::AuthorRole); + setData(email(), PlasmaAppletItemModel::EmailRole); + setData(0, PlasmaAppletItemModel::RunningRole); + setData(m_local, PlasmaAppletItemModel::LocalRole); +} + +QString PlasmaAppletItem::pluginName() const +{ + return m_info.pluginId(); +} + +QString PlasmaAppletItem::name() const +{ + return m_info.name(); +} + +QString PlasmaAppletItem::description() const +{ + return m_info.description(); +} + +QString PlasmaAppletItem::license() const +{ + return m_info.license(); +} + +QString PlasmaAppletItem::category() const +{ + return m_info.category(); +} + +QString PlasmaAppletItem::website() const +{ + return m_info.website(); +} + +QString PlasmaAppletItem::version() const +{ + return m_info.version(); +} + +QString PlasmaAppletItem::author() const +{ + if (m_info.authors().isEmpty()) { + return QString(); + } + + return m_info.authors().constFirst().name(); +} + +QString PlasmaAppletItem::email() const +{ + if (m_info.authors().isEmpty()) { + return QString(); + } + + return m_info.authors().constFirst().emailAddress(); +} + +int PlasmaAppletItem::running() const +{ + return m_runningCount; +} + +void PlasmaAppletItem::setRunning(int count) +{ + m_runningCount = count; + setData(count, PlasmaAppletItemModel::RunningRole); + emitDataChanged(); +} + +bool PlasmaAppletItem::matches(const QString &pattern) const +{ + const QJsonObject rawData = m_info.rawData(); + const QString keywordsList = KJsonUtils::readTranslatedString(rawData, QStringLiteral("Keywords")); + auto keywords = keywordsList.splitRef(QLatin1Char(';'), Qt::SkipEmptyParts); + + // Add English name and keywords so users in other languages won't have to switch IME when searching. + if (!QLocale().name().startsWith(QLatin1String("en_"))) { + const QString name(rawData[QStringLiteral("KPlugin")][QStringLiteral("Name")].toString()); + keywords << &name << m_info.value(QStringLiteral("Keywords"), QString()).splitRef(QLatin1Char(';'), Qt::SkipEmptyParts); + } + + for (const auto &keyword : keywords) { + if (keyword.startsWith(pattern, Qt::CaseInsensitive)) { + return true; + } + } + + return AbstractItem::matches(pattern); +} + +bool PlasmaAppletItem::isLocal() const +{ + return m_local; +} + +bool PlasmaAppletItem::passesFiltering(const KCategorizedItemsViewModels::Filter &filter) const +{ + if (filter.first == QLatin1String("running")) { + return running(); + } else if (filter.first == QLatin1String("local")) { + return isLocal(); + } else if (filter.first == QLatin1String("category")) { + return m_info.category().toLower() == filter.second; + } else { + return false; + } +} + +QMimeData *PlasmaAppletItem::mimeData() const +{ + QMimeData *data = new QMimeData(); + QByteArray appletName; + appletName += pluginName().toUtf8(); + data->setData(mimeTypes().at(0), appletName); + return data; +} + +QStringList PlasmaAppletItem::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("text/x-plasmoidservicename"); + return types; +} + +QVariant PlasmaAppletItem::data(int role) const +{ + switch (role) { + case PlasmaAppletItemModel::ScreenshotRole: + // null = not yet done, empty = tried and failed + if (m_screenshot.isNull()) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet")); + pkg.setDefaultPackageRoot(QStringLiteral("plasma/plasmoids")); + pkg.setPath(m_info.pluginId()); + if (pkg.isValid()) { + const_cast(this)->m_screenshot = pkg.filePath("screenshot"); + } else { + const_cast(this)->m_screenshot = QString(); + } + } else if (m_screenshot.isEmpty()) { + return QVariant(); + } + return m_screenshot; + + case Qt::DecorationRole: { + // null = not yet done, empty = tried and failed + if (m_icon.isNull()) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet")); + pkg.setDefaultPackageRoot(QStringLiteral("plasma/plasmoids")); + pkg.setPath(m_info.pluginId()); + if (pkg.isValid() && pkg.metadata().iconName().startsWith(QLatin1String("/"))) { + const_cast(this)->m_icon = pkg.filePath("", pkg.metadata().iconName().toUtf8()); + } else { + const_cast(this)->m_icon = QString(); + return AbstractItem::data(role); + } + } + if (m_icon.isEmpty()) { + return AbstractItem::data(role); + } + return QIcon(m_icon); + } + + default: + return AbstractItem::data(role); + } +} + +// PlasmaAppletItemModel + +PlasmaAppletItemModel::PlasmaAppletItemModel(QObject *parent) + : QStandardItemModel(parent) + , m_startupCompleted(false) +{ + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &PlasmaAppletItemModel::populateModel); + + setSortRole(Qt::DisplayRole); +} + +QHash PlasmaAppletItemModel::roleNames() const +{ + QHash newRoleNames = QAbstractItemModel::roleNames(); + newRoleNames[NameRole] = "name"; + newRoleNames[PluginNameRole] = "pluginName"; + newRoleNames[DescriptionRole] = "description"; + newRoleNames[CategoryRole] = "category"; + newRoleNames[LicenseRole] = "license"; + newRoleNames[WebsiteRole] = "website"; + newRoleNames[VersionRole] = "version"; + newRoleNames[AuthorRole] = "author"; + newRoleNames[EmailRole] = "email"; + newRoleNames[RunningRole] = "running"; + newRoleNames[LocalRole] = "local"; + newRoleNames[ScreenshotRole] = "screenshot"; + return newRoleNames; +} + +void PlasmaAppletItemModel::populateModel() +{ + clear(); + + auto filter = [this](const KPluginMetaData &plugin) -> bool { + const QStringList provides = plugin.value(QStringLiteral("X-Plasma-Provides"), QStringList()); + + if (!m_provides.isEmpty()) { + const bool providesFulfilled = std::any_of(m_provides.cbegin(), m_provides.cend(), [&provides](const QString &p) { + return provides.contains(p); + }); + + if (!providesFulfilled) { + return false; + } + } + + if (!plugin.isValid() || plugin.rawData().value(QStringLiteral("NoDisplay")).toBool() || plugin.category() == QLatin1String("Containments")) { + // we don't want to show the hidden category + return false; + } + + static const auto formFactors = KDeclarative::KDeclarative::runtimePlatform(); + // If runtimePlatformis not defined, accept everything + bool inFormFactor = formFactors.isEmpty(); + + for (const QString &formFactor : formFactors) { + if (plugin.formFactors().isEmpty() || plugin.formFactors().contains(formFactor)) { + inFormFactor = true; + break; + } + } + + if (!inFormFactor) { + return false; + } + + return true; + }; + + const QList packages = + KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/Applet"), QStringLiteral("plasma/plasmoids"), filter); + + for (const KPluginMetaData &plugin : packages) { + appendRow(new PlasmaAppletItem(plugin)); + } + + Q_EMIT modelPopulated(); +} + +void PlasmaAppletItemModel::setRunningApplets(const QHash &apps) +{ + // for each item, find that string and set the count + for (int r = 0; r < rowCount(); ++r) { + QStandardItem *i = item(r); + PlasmaAppletItem *p = dynamic_cast(i); + + if (p) { + const int running = apps.value(p->pluginName()); + p->setRunning(running); + } + } +} + +void PlasmaAppletItemModel::setRunningApplets(const QString &name, int count) +{ + for (int r = 0; r < rowCount(); ++r) { + QStandardItem *i = item(r); + PlasmaAppletItem *p = dynamic_cast(i); + if (p && p->pluginName() == name) { + p->setRunning(count); + } + } +} + +QStringList PlasmaAppletItemModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral("text/x-plasmoidservicename"); + return types; +} + +QSet PlasmaAppletItemModel::categories() const +{ + QSet cats; + for (int r = 0; r < rowCount(); ++r) { + QStandardItem *i = item(r); + PlasmaAppletItem *p = dynamic_cast(i); + if (p) { + cats.insert(p->category().toLower()); + } + } + + return cats; +} + +QMimeData *PlasmaAppletItemModel::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.count() <= 0) { + return nullptr; + } + + QStringList types = mimeTypes(); + + if (types.isEmpty()) { + return nullptr; + } + + QMimeData *data = new QMimeData(); + + QString format = types.at(0); + + QByteArray appletNames; + int lastRow = -1; + for (const QModelIndex &index : indexes) { + if (index.row() == lastRow) { + continue; + } + + lastRow = index.row(); + PlasmaAppletItem *selectedItem = (PlasmaAppletItem *)itemFromIndex(index); + appletNames += '\n' + selectedItem->pluginName().toUtf8(); + // qDebug() << selectedItem->pluginName() << index.column() << index.row(); + } + + data->setData(format, appletNames); + return data; +} + +QStringList PlasmaAppletItemModel::provides() const +{ + return m_provides; +} + +void PlasmaAppletItemModel::setProvides(const QStringList &provides) +{ + if (m_provides == provides) { + return; + } + + m_provides = provides; + if (m_startupCompleted) { + populateModel(); + } +} + +void PlasmaAppletItemModel::setApplication(const QString &app) +{ + m_application = app; + if (m_startupCompleted) { + populateModel(); + } +} + +bool PlasmaAppletItemModel::startupCompleted() const +{ + return m_startupCompleted; +} + +void PlasmaAppletItemModel::setStartupCompleted(bool complete) +{ + m_startupCompleted = complete; +} + +QString &PlasmaAppletItemModel::Application() +{ + return m_application; +} + +//#include diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel_p.h b/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel_p.h new file mode 100644 index 0000000000..5b324476bd --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/plasmaappletitemmodel_p.h @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "kcategorizeditemsviewmodels_p.h" +#include +#include + +class PlasmaAppletItemModel; + +/** + * Implementation of the KCategorizedItemsViewModels::AbstractItem + */ +class PlasmaAppletItem : public KCategorizedItemsViewModels::AbstractItem +{ +public: + explicit PlasmaAppletItem(const KPluginMetaData &info); + + QString pluginName() const; + QString name() const override; + QString category() const; + QString description() const override; + QString license() const; + QString website() const; + QString version() const; + QString author() const; + QString email() const; + QVariant data(int role = Qt::UserRole + 1) const override; + + int running() const override; + bool isLocal() const; + bool matches(const QString &pattern) const override; + + // set how many instances of this applet are running + void setRunning(int count) override; + bool passesFiltering(const KCategorizedItemsViewModels::Filter &filter) const override; + QMimeData *mimeData() const; + QStringList mimeTypes() const; + +private: + KPluginMetaData m_info; + QString m_screenshot; + QString m_icon; + int m_runningCount; + bool m_local; +}; + +class PlasmaAppletItemModel : public QStandardItemModel +{ + Q_OBJECT + +public: + enum Roles { + NameRole = Qt::UserRole + 1, + PluginNameRole = Qt::UserRole + 2, + DescriptionRole = Qt::UserRole + 3, + CategoryRole = Qt::UserRole + 4, + LicenseRole = Qt::UserRole + 5, + WebsiteRole = Qt::UserRole + 6, + VersionRole = Qt::UserRole + 7, + AuthorRole = Qt::UserRole + 8, + EmailRole = Qt::UserRole + 9, + RunningRole = Qt::UserRole + 10, + LocalRole = Qt::UserRole + 11, + ScreenshotRole = Qt::UserRole + 12, + }; + + explicit PlasmaAppletItemModel(QObject *parent = nullptr); + + QStringList mimeTypes() const override; + QSet categories() const; + + QMimeData *mimeData(const QModelIndexList &indexes) const override; + + void setApplication(const QString &app); + void setRunningApplets(const QHash &apps); + void setRunningApplets(const QString &name, int count); + + QString &Application(); + + QStringList provides() const; + void setProvides(const QStringList &provides); + + QHash roleNames() const override; + + bool startupCompleted() const; + void setStartupCompleted(bool complete); + +Q_SIGNALS: + void modelPopulated(); + +private: + QString m_application; + QStringList m_provides; + KConfigGroup m_configGroup; + bool m_startupCompleted : 1; + +private Q_SLOTS: + void populateModel(); +}; diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/plasmoids.knsrc b/plasma/workspace/components/shellprivate/widgetexplorer/plasmoids.knsrc new file mode 100644 index 0000000000..06a50f4e9e --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/plasmoids.knsrc @@ -0,0 +1,57 @@ +[KNewStuff3] +Name=Plasma Widgets +Name[ar]=ودجات بلازما +Name[ast]=Widgets pa Plasma +Name[az]=Plasma Vidjetləri +Name[ca]=Ginys del Plasma +Name[ca@valencia]=Ginys («widgets») de Plasma +Name[cs]=Widgety pro Plasmu +Name[da]=Plasma-widgets +Name[de]=Plasma-Miniprogramme +Name[el]=Γραφικά συστατικά Plasma +Name[en_GB]=Plasma Widgets +Name[es]=Elementos gráficos de Plasma +Name[et]=Plasma vidinad +Name[eu]=Plasmako trepetak +Name[fi]=Plasma-sovelmat +Name[fr]=Composants graphiques Plasma +Name[gl]=Trebellos de Plasma +Name[he]=יישומוני Plasma +Name[hi]=प्लाज़्मा विजेट +Name[hu]=Plasma widgetek +Name[ia]=Widgets de Plasma +Name[id]=Widget Plasma +Name[it]=Oggetti di Plasma +Name[ko]=Plasma 위젯 +Name[lt]=Plasma valdikliai +Name[lv]=Plasma logdaļas +Name[ml]=പ്ലാസ്മ വിഡ്ജെറ്റുകള്‍ +Name[nl]=Plasma widgets +Name[nn]=Plasma-element +Name[pa]=ਪਲਾਜ਼ਮਾ ਵਿਜੈਟ +Name[pl]=Elementy interfejsu Plazmy +Name[pt]=Elementos do Plasma +Name[pt_BR]=Widgets do Plasma +Name[ro]=Controale grafice Plasma +Name[ru]=Виджеты Plasma +Name[sk]=Miniaplikácie Plasmy +Name[sl]=Gradniki Plasme +Name[sr]=плазма виџети +Name[sr@ijekavian]=плазма виџети +Name[sr@ijekavianlatin]=plasma vidžeti +Name[sr@latin]=plasma vidžeti +Name[sv]=Plasma grafiska komponenter +Name[ta]=பிளாஸ்மா பிளாஸ்மாய்டுகள் +Name[tg]=Виҷетҳои Плазма +Name[tr]=Plasma Gereçleri +Name[uk]=Віджети Плазми +Name[vi]=Phụ kiện Plasma +Name[x-test]=xxPlasma Widgetsxx +Name[zh_CN]=Plasma 小部件 +Name[zh_TW]=Plasma 元件 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Plasma 5 Plasmoid +StandardResource=tmp +Uncompress=kpackage +KPackageType=Plasma/Applet diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.cpp b/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.cpp new file mode 100644 index 0000000000..6c1485bfd4 --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.cpp @@ -0,0 +1,517 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + SPDX-FileCopyrightText: 2009 Ana Cecília Martins + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "widgetexplorer.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "config-workspace.h" +#include "kcategorizeditemsviewmodels_p.h" +#include "openwidgetassistant_p.h" + +using namespace KActivities; +using namespace KCategorizedItemsViewModels; +using namespace Plasma; + +WidgetAction::WidgetAction(QObject *parent) + : QAction(parent) +{ +} + +WidgetAction::WidgetAction(const QIcon &icon, const QString &text, QObject *parent) + : QAction(icon, text, parent) +{ +} + +class WidgetExplorerPrivate +{ +public: + WidgetExplorerPrivate(WidgetExplorer *w) + : q(w) + , containment(nullptr) + , itemModel(w) + , filterModel(w) + , activitiesConsumer(new KActivities::Consumer()) + { + QObject::connect(activitiesConsumer.data(), &Consumer::currentActivityChanged, q, [this] { + initRunningApplets(); + }); + } + + void initFilters(); + void initRunningApplets(); + void screenAdded(int screen); + void screenRemoved(int screen); + void containmentDestroyed(); + + void addContainment(Containment *containment); + void removeContainment(Containment *containment); + + /** + * Tracks a new running applet + */ + void appletAdded(Plasma::Applet *applet); + + /** + * A running applet is no more + */ + void appletRemoved(Plasma::Applet *applet); + + WidgetExplorer *q; + QString application; + Plasma::Containment *containment; + + QHash runningApplets; // applet name => count + // extra hash so we can look up the names of deleted applets + QHash appletNames; + QPointer openAssistant; + KPackage::Package *package; + + PlasmaAppletItemModel itemModel; + KCategorizedItemsViewModels::DefaultFilterModel filterModel; + bool showSpecialFilters = true; + DefaultItemFilterProxyModel filterItemModel; + static QPointer newStuffDialog; + + QScopedPointer activitiesConsumer; +}; + +QPointer WidgetExplorerPrivate::newStuffDialog; + +void WidgetExplorerPrivate::initFilters() +{ + filterModel.clear(); + + filterModel.addFilter(i18n("All Widgets"), KCategorizedItemsViewModels::Filter(), QIcon::fromTheme(QStringLiteral("plasma"))); + + if (showSpecialFilters) { + // Filters: Special + filterModel.addFilter(i18n("Running"), + KCategorizedItemsViewModels::Filter(QStringLiteral("running"), true), + QIcon::fromTheme(QStringLiteral("dialog-ok"))); + + filterModel.addFilter(i18nc("@item:inmenu used in the widget filter. Filter widgets that can be un-installed from the system, which are usually installed by the user to a local place.", "Uninstallable"), + KCategorizedItemsViewModels::Filter(QStringLiteral("local"), true), + QIcon::fromTheme(QStringLiteral("edit-delete"))); + + filterModel.addSeparator(i18n("Categories:")); + } + + typedef QPair catPair; + QMap categories; + QSet existingCategories = itemModel.categories(); + QStringList cats; + const QList list = PluginLoader::self()->listAppletMetaData(QString()); + + for (auto &plugin : list) { + if (!plugin.isValid()) { + continue; + } + if (plugin.rawData().value("NoDisplay").toBool() || plugin.category() == QLatin1String("Containments") || plugin.category().isEmpty()) { + // we don't want to show the hidden category + continue; + } + const QString c = plugin.category(); + if (-1 == cats.indexOf(c)) { + cats << c; + } + } + for (const QString &category : qAsConst(cats)) { + const QString lowerCaseCat = category.toLower(); + if (existingCategories.contains(lowerCaseCat)) { + const QString trans = i18nd("libplasma5", category.toLocal8Bit()); + categories.insert(trans.toLower(), qMakePair(trans, lowerCaseCat)); + } + } + + for (const catPair &category : qAsConst(categories)) { + filterModel.addFilter(category.first, KCategorizedItemsViewModels::Filter(QStringLiteral("category"), category.second)); + } +} + +void WidgetExplorer::classBegin() +{ +} + +void WidgetExplorer::componentComplete() +{ + d->itemModel.setStartupCompleted(true); + setApplication(); + d->initRunningApplets(); +} + +QObject *WidgetExplorer::widgetsModel() const +{ + return &d->filterItemModel; +} + +QObject *WidgetExplorer::filterModel() const +{ + return &d->filterModel; +} + +bool WidgetExplorer::showSpecialFilters() const +{ + return d->showSpecialFilters; +} + +void WidgetExplorer::setShowSpecialFilters(bool show) +{ + if (d->showSpecialFilters != show) { + d->showSpecialFilters = show; + d->initFilters(); + Q_EMIT showSpecialFiltersChanged(); + } +} + +QList WidgetExplorer::widgetsMenuActions() +{ + QList actionList; + + WidgetAction *action = nullptr; + + if (KAuthorized::authorize(KAuthorized::GHNS)) { + action = new WidgetAction(QIcon::fromTheme(QStringLiteral("internet-services")), i18n("Download New Plasma Widgets"), this); + connect(action, &QAction::triggered, this, &WidgetExplorer::downloadWidgets); + actionList << action; + } + + action = new WidgetAction(this); + action->setSeparator(true); + actionList << action; + + action = new WidgetAction(QIcon::fromTheme(QStringLiteral("package-x-generic")), i18n("Install Widget From Local File…"), this); + QObject::connect(action, &QAction::triggered, this, &WidgetExplorer::openWidgetFile); + actionList << action; + + return actionList; +} + +void WidgetExplorerPrivate::initRunningApplets() +{ + // get applets from corona, count them, send results to model + if (!containment) { + return; + } + + Plasma::Corona *c = containment->corona(); + + // we've tried our best to get a corona + // we don't want just one containment, we want them all + if (!c) { + qWarning() << "WidgetExplorer failed to find corona"; + return; + } + appletNames.clear(); + runningApplets.clear(); + + QObject::connect(c, &Plasma::Corona::screenAdded, q, [this](int screen) { + screenAdded(screen); + }); + QObject::connect(c, &Plasma::Corona::screenRemoved, q, [this](int screen) { + screenRemoved(screen); + }); + + const QList containments = c->containments(); + for (Containment *containment : containments) { + if (containment->containmentType() == Plasma::Types::DesktopContainment && containment->activity() != activitiesConsumer->currentActivity()) { + continue; + } + if (containment->screen() != -1) { + addContainment(containment); + } + } + + // qDebug() << runningApplets; + itemModel.setRunningApplets(runningApplets); +} + +void WidgetExplorerPrivate::screenAdded(int screen) +{ + const QList containments = containment->corona()->containments(); + for (auto c : containments) { + if (c->screen() == screen) { + addContainment(c); + } + } + itemModel.setRunningApplets(runningApplets); +} + +void WidgetExplorerPrivate::screenRemoved(int screen) +{ + const QList containments = containment->corona()->containments(); + for (auto c : containments) { + if (c->lastScreen() == screen) { + removeContainment(c); + } + } + itemModel.setRunningApplets(runningApplets); +} + +void WidgetExplorerPrivate::addContainment(Containment *containment) +{ + QObject::connect(containment, SIGNAL(appletAdded(Plasma::Applet *)), q, SLOT(appletAdded(Plasma::Applet *))); + QObject::connect(containment, SIGNAL(appletRemoved(Plasma::Applet *)), q, SLOT(appletRemoved(Plasma::Applet *))); + + foreach (Applet *applet, containment->applets()) { + if (applet->pluginMetaData().isValid()) { + Containment *childContainment = applet->property("containment").value(); + if (childContainment) { + addContainment(childContainment); + } + runningApplets[applet->pluginMetaData().pluginId()]++; + } else { + qDebug() << "Invalid plugin metadata. :("; + } + } +} + +void WidgetExplorerPrivate::removeContainment(Plasma::Containment *containment) +{ + containment->disconnect(q); + const QList applets = containment->applets(); + for (auto applet : applets) { + if (applet->pluginMetaData().isValid()) { + Containment *childContainment = applet->property("containment").value(); + if (childContainment) { + removeContainment(childContainment); + } + runningApplets[applet->pluginMetaData().pluginId()]--; + } + } +} + +void WidgetExplorerPrivate::containmentDestroyed() +{ + containment = nullptr; +} + +void WidgetExplorerPrivate::appletAdded(Plasma::Applet *applet) +{ + if (!applet->pluginMetaData().isValid()) { + return; + } + QString name = applet->pluginMetaData().pluginId(); + + runningApplets[name]++; + appletNames.insert(applet, name); + itemModel.setRunningApplets(name, runningApplets[name]); +} + +void WidgetExplorerPrivate::appletRemoved(Plasma::Applet *applet) +{ + QString name = appletNames.take(applet); + + int count = 0; + if (runningApplets.contains(name)) { + count = runningApplets[name] - 1; + + if (count < 1) { + runningApplets.remove(name); + } else { + runningApplets[name] = count; + } + } + + itemModel.setRunningApplets(name, count); +} + +// WidgetExplorer + +WidgetExplorer::WidgetExplorer(QObject *parent) + : QObject(parent) + , d(new WidgetExplorerPrivate(this)) +{ + d->filterItemModel.setSortCaseSensitivity(Qt::CaseInsensitive); + d->filterItemModel.setDynamicSortFilter(true); + d->filterItemModel.setSourceModel(&d->itemModel); + d->filterItemModel.sort(0); +} + +WidgetExplorer::~WidgetExplorer() +{ + delete d; +} + +void WidgetExplorer::setApplication(const QString &app) +{ + if (d->application == app && !app.isEmpty()) { + return; + } + + d->application = app; + d->itemModel.setApplication(app); + d->initFilters(); + + d->itemModel.setRunningApplets(d->runningApplets); + Q_EMIT applicationChanged(); +} + +QString WidgetExplorer::application() +{ + return d->application; +} + +QStringList WidgetExplorer::provides() const +{ + return d->itemModel.provides(); +} + +void WidgetExplorer::setProvides(const QStringList &provides) +{ + if (d->itemModel.provides() == provides) { + return; + } + + d->itemModel.setProvides(provides); + Q_EMIT providesChanged(); +} + +void WidgetExplorer::setContainment(Plasma::Containment *containment) +{ + if (d->containment != containment) { + if (d->containment) { + d->containment->disconnect(this); + } + + d->containment = containment; + + if (d->containment) { + connect(d->containment, SIGNAL(destroyed(QObject *)), this, SLOT(containmentDestroyed())); + connect(d->containment, &Applet::immutabilityChanged, this, &WidgetExplorer::immutabilityChanged); + } + + d->initRunningApplets(); + Q_EMIT containmentChanged(); + } +} + +Containment *WidgetExplorer::containment() const +{ + return d->containment; +} + +Plasma::Corona *WidgetExplorer::corona() const +{ + if (d->containment) { + return d->containment->corona(); + } + + return nullptr; +} + +void WidgetExplorer::addApplet(const QString &pluginName) +{ + const QString p = PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/" + pluginName; + qWarning() << "--------> load applet: " << pluginName << " relpath: " << p; + + QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory); + + qDebug() << " .. pathes: " << dirs; + if (!dirs.count()) { + qWarning() << "Failed to find plasmoid path for " << pluginName; + return; + } + + if (d->containment) { + d->containment->createApplet(dirs.first()); + } +} + +void WidgetExplorer::immutabilityChanged(Plasma::Types::ImmutabilityType type) +{ + if (type != Plasma::Types::Mutable) { + Q_EMIT shouldClose(); + } +} + +void WidgetExplorer::downloadWidgets() +{ + if (!d->newStuffDialog) { + d->newStuffDialog = new KNS3::QtQuickDialogWrapper(QLatin1String("plasmoids.knsrc")); + } + d->newStuffDialog->open(); + + Q_EMIT shouldClose(); +} + +void WidgetExplorer::openWidgetFile() +{ + Plasma::OpenWidgetAssistant *assistant = d->openAssistant.data(); + if (!assistant) { + assistant = new Plasma::OpenWidgetAssistant(nullptr); + d->openAssistant = assistant; + } + + KWindowSystem::setOnDesktop(assistant->winId(), KWindowSystem::currentDesktop()); + assistant->setAttribute(Qt::WA_DeleteOnClose, true); + assistant->show(); + assistant->raise(); + assistant->setFocus(); + + Q_EMIT shouldClose(); +} + +void WidgetExplorer::uninstall(const QString &pluginName) +{ + static const QString packageRoot = + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + PLASMA_RELATIVE_DATA_INSTALL_DIR "/plasmoids/"; + + KPackage::PackageStructure *structure = KPackage::PackageLoader::self()->loadPackageStructure(QStringLiteral("Plasma/Applet")); + + KPackage::Package pkg(structure); + pkg.uninstall(pluginName, packageRoot); + + // FIXME: moreefficient way rather a linear scan? + for (int i = 0; i < d->itemModel.rowCount(); ++i) { + QStandardItem *item = d->itemModel.item(i); + if (item->data(PlasmaAppletItemModel::PluginNameRole).toString() == pluginName) { + d->itemModel.takeRow(i); + break; + } + } + + // now remove all instances of that applet + if (corona()) { + const auto &containments = corona()->containments(); + + for (Containment *c : containments) { + const auto &applets = c->applets(); + + for (Applet *applet : applets) { + const auto &appletInfo = applet->pluginMetaData(); + + if (appletInfo.isValid() && appletInfo.pluginId() == pluginName) { + applet->destroy(); + } + } + } + } +} + +#include "moc_widgetexplorer.cpp" diff --git a/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.h b/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.h new file mode 100644 index 0000000000..efbab6acf0 --- /dev/null +++ b/plasma/workspace/components/shellprivate/widgetexplorer/widgetexplorer.h @@ -0,0 +1,159 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + SPDX-FileCopyrightText: 2009 Ana Cecília Martins + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "plasmaappletitemmodel_p.h" + +namespace Plasma +{ +class Corona; +class Containment; +class Applet; +} +class WidgetExplorerPrivate; + +// We need to access the separator property that is not exported by QAction +class WidgetAction : public QAction +{ + Q_OBJECT + Q_PROPERTY(bool separator READ isSeparator WRITE setSeparator NOTIFY separatorChanged) + +public: + explicit WidgetAction(QObject *parent = nullptr); + WidgetAction(const QIcon &icon, const QString &text, QObject *parent); + +Q_SIGNALS: + void separatorChanged(); +}; + +class WidgetExplorer : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * Model that lists all applets + */ + Q_PROPERTY(QObject *widgetsModel READ widgetsModel CONSTANT) + + /** + * Model that lists all applets filters and categories + */ + Q_PROPERTY(QObject *filterModel READ filterModel CONSTANT) + + /** + * Whether to show special filters such as "Running" and "Uninstallable" in the filterModel. + */ + Q_PROPERTY(bool showSpecialFilters READ showSpecialFilters WRITE setShowSpecialFilters NOTIFY showSpecialFiltersChanged) + + /** + * Actions for adding widgets, like download plasma widgets, download google gadgets, install from local file + */ + Q_PROPERTY(QList widgetsMenuActions READ widgetsMenuActions NOTIFY widgetsMenuActionsChanged) + + /** + * The application that owns the widget list. different application may show different lists + */ + Q_PROPERTY(QString application READ application WRITE setApplication NOTIFY applicationChanged) + + /** + * Set the features the listed applets must provide: needed for listing alternatives + * to a particular applet + */ + Q_PROPERTY(QStringList provides READ provides WRITE setProvides NOTIFY providesChanged) + + Q_PROPERTY(Plasma::Containment *containment READ containment WRITE setContainment NOTIFY containmentChanged) + +public: + explicit WidgetExplorer(QObject *parent = nullptr); + ~WidgetExplorer() override; + + QString application(); + + /** + * Populates the widget list for the given application. This must be called + * before the widget explorer will be usable as the widget list will remain + * empty up to that point. + * + * @arg application the application which the widgets should be loaded for. + */ + void setApplication(const QString &application = QString()); + + QStringList provides() const; + void setProvides(const QStringList &provides); + + /** + * Changes the current default containment to add applets to + * + * @arg containment the new default + */ + void setContainment(Plasma::Containment *containment); + + /** + * @return the current default containment to add applets to + */ + Plasma::Containment *containment() const; + /** + * @return the current corona this widget is added to + */ + Plasma::Corona *corona() const; + + QObject *widgetsModel() const; + QObject *filterModel() const; + + bool showSpecialFilters() const; + void setShowSpecialFilters(bool show); + + QList widgetsMenuActions(); + + /** + * Uninstall a plasmoid with a given plugin name. only user-installed ones are uninstallable + */ + Q_INVOKABLE void uninstall(const QString &pluginName); + + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void widgetsMenuActionsChanged(); + void extraActionsChanged(); + void shouldClose(); + void viewChanged(); + void applicationChanged(); + void containmentChanged(); + void providesChanged(); + +public Q_SLOTS: + /** + * Adds currently selected applets + */ + void addApplet(const QString &pluginName); + void openWidgetFile(); + void downloadWidgets(); + +Q_SIGNALS: + void showSpecialFiltersChanged() const; + +protected Q_SLOTS: + void immutabilityChanged(Plasma::Types::ImmutabilityType); + +private: + Q_PRIVATE_SLOT(d, void appletAdded(Plasma::Applet *)) + Q_PRIVATE_SLOT(d, void appletRemoved(Plasma::Applet *)) + Q_PRIVATE_SLOT(d, void containmentDestroyed()) + + WidgetExplorerPrivate *const d; + friend class WidgetExplorerPrivate; +}; diff --git a/plasma/workspace/components/tests/sessions.qml b/plasma/workspace/components/tests/sessions.qml new file mode 100644 index 0000000000..fe8cdd48a2 --- /dev/null +++ b/plasma/workspace/components/tests/sessions.qml @@ -0,0 +1,15 @@ +import QtQuick 2.15 +import org.kde.plasma.private.sessions 2.0 + +ListView +{ + width: 500 + height: 500 + + model: SessionsModel{} + + delegate: Text { + text: model.name + " " + model.session + " " + model.displayNumber + " VT" +model.vtNumber + } + +} diff --git a/plasma/workspace/components/workspace/BatteryIcon.qml b/plasma/workspace/components/workspace/BatteryIcon.qml new file mode 100644 index 0000000000..e9e52bccbf --- /dev/null +++ b/plasma/workspace/components/workspace/BatteryIcon.qml @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2013 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +Item { + property bool hasBattery + property int percent + property bool pluggedIn + property string batteryType + + PlasmaCore.Svg { + id: svg + imagePath: "icons/battery" + colorGroup: PlasmaCore.ColorScope.colorGroup + onRepaintNeeded: { // needed to detect the hint item go away when theme changes + batterySvg.visible = Qt.binding(function() { return !otherBatteriesSvg.visible && (!svg.hasElement("hint-dont-superimpose-fill") || !hasBattery); }) + } + } + + PlasmaCore.SvgItem { + id: batterySvg + anchors.centerIn: parent + width: PlasmaCore.Units.roundToIconSize(Math.min(parent.width, parent.height)) + height: width + svg: svg + elementId: "Battery" + visible: !otherBatteriesSvg.visible && (!svg.hasElement("hint-dont-superimpose-fill") || !hasBattery) + } + + PlasmaCore.SvgItem { + id: fillSvg + anchors.fill: batterySvg + svg: svg + elementId: hasBattery ? fillElement(percent) : "Unavailable" + visible: !otherBatteriesSvg.visible + } + + function fillElement(p) { + // We switched from having steps of 20 for the battery percentage to a more accurate + // step of 10. This means we break other and older themes. + // If the Fill10 element is not found, it is likely that the theme doesn't support + // that and we use the older method of obtaining the fill element. + if (!svg.hasElement("Fill10")) { + print("No Fill10 element found in your theme's battery.svg - Using legacy 20% steps for battery icon"); + if (p >= 90) { + return "Fill100"; + } else if (p >= 70) { + return "Fill80"; + } else if (p >= 50) { + return "Fill60"; + } else if (p > 20) { + return "Fill40"; + } else if (p >= 10) { + return "Fill20"; + } else { + return "Fill0"; + } + } else { + if (p >= 95) { + return "Fill100"; + } else if (p >= 85) { + return "Fill90"; + } else if (p >= 75) { + return "Fill80"; + } else if (p >= 65) { + return "Fill70"; + } else if (p >= 55) { + return "Fill60"; + } else if (p >= 45) { + return "Fill50"; + } else if (p >= 35) { + return "Fill40"; + } else if (p >= 25) { + return "Fill30"; + } else if (p >= 15) { + return "Fill20"; + } else if (p > 5) { + return "Fill10"; + } else { + return "Fill0"; + } + } + } + + PlasmaCore.SvgItem { + anchors.fill: batterySvg + svg: svg + elementId: "AcAdapter" + visible: pluggedIn && !otherBatteriesSvg.visible + } + + PlasmaCore.IconItem { + id: otherBatteriesSvg + anchors.fill: batterySvg + source: elementForType(batteryType) + visible: source !== "" + } + + function elementForType(t) { + switch(t) { + case "Mouse": + return "input-mouse-battery"; + case "Keyboard": + return "input-keyboard-battery"; + case "Pda": + return "phone-battery"; + case "Phone": + return "phone-battery"; + case "Ups": + return "battery-ups"; + case "GamingInput": + return "input-gaming-battery"; + case "Bluetooth": + return "preferences-system-bluetooth-battery"; + default: + return ""; + } + } +} diff --git a/plasma/workspace/components/workspace/KeyboardLayoutSwitcher.qml b/plasma/workspace/components/workspace/KeyboardLayoutSwitcher.qml new file mode 100644 index 0000000000..4561ec8518 --- /dev/null +++ b/plasma/workspace/components/workspace/KeyboardLayoutSwitcher.qml @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + SPDX-FileCopyrightText: 2020 Andrey Butirsky + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.12 +import org.kde.plasma.workspace.keyboardlayout 1.0 + +MouseArea { + property alias keyboardLayout: keyboardLayout + readonly property bool hasMultipleKeyboardLayouts: keyboardLayout.layoutsList.length > 1 + readonly property var layoutNames: keyboardLayout.layoutsList.length ? keyboardLayout.layoutsList[keyboardLayout.layout] + : { shortName: "", displayName: "", longName: "" } + + onClicked: keyboardLayout.switchToNextLayout() + + property int wheelDelta: 0 + + onWheel: { + // Magic number 120 for common "one click" + // See: https://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop + var delta = wheel.angleDelta.y || wheel.angleDelta.x; + wheelDelta += delta; + while (wheelDelta >= 120) { + wheelDelta -= 120; + keyboardLayout.switchToPreviousLayout(); + } + while (wheelDelta <= -120) { + wheelDelta += 120; + keyboardLayout.switchToNextLayout(); + } + } + + KeyboardLayout { + id: keyboardLayout + } +} diff --git a/plasma/workspace/components/workspace/qmldir b/plasma/workspace/components/workspace/qmldir new file mode 100644 index 0000000000..9e70915ae8 --- /dev/null +++ b/plasma/workspace/components/workspace/qmldir @@ -0,0 +1,4 @@ +module org.kde.plasma.workspace.components + +BatteryIcon 2.0 BatteryIcon.qml +KeyboardLayoutSwitcher 2.0 KeyboardLayoutSwitcher.qml diff --git a/plasma/workspace/config-X11.h.cmake b/plasma/workspace/config-X11.h.cmake new file mode 100644 index 0000000000..0749ee256f --- /dev/null +++ b/plasma/workspace/config-X11.h.cmake @@ -0,0 +1,8 @@ +/* Define to 1 if you have Xcursor */ +#cmakedefine HAVE_XCURSOR 1 + +/* Define if you have the XFixes extension */ +#cmakedefine HAVE_XFIXES 1 + +/* Define if you have X11 at all */ +#cmakedefine01 HAVE_X11 diff --git a/plasma/workspace/config-appstream.h.cmake b/plasma/workspace/config-appstream.h.cmake new file mode 100644 index 0000000000..f0e89921a0 --- /dev/null +++ b/plasma/workspace/config-appstream.h.cmake @@ -0,0 +1 @@ +#cmakedefine HAVE_APPSTREAMQT 1 diff --git a/plasma/workspace/config-unix.h.cmake b/plasma/workspace/config-unix.h.cmake new file mode 100644 index 0000000000..d1d31812b5 --- /dev/null +++ b/plasma/workspace/config-unix.h.cmake @@ -0,0 +1,3 @@ + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LIMITS_H 1 diff --git a/plasma/workspace/config-workspace.h.cmake b/plasma/workspace/config-workspace.h.cmake new file mode 100644 index 0000000000..0e08829ade --- /dev/null +++ b/plasma/workspace/config-workspace.h.cmake @@ -0,0 +1,14 @@ +/* config-workspace.h. Generated by cmake from config-workspace.h.cmake */ + +/* Defines if your system has the libfontconfig library */ +#cmakedefine HAVE_FONTCONFIG 1 + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_SYS_TIME_H 1 + +/* place where plasma-frameworks things are installed */ +#define PLASMA_RELATIVE_DATA_INSTALL_DIR "@PLASMA_RELATIVE_DATA_INSTALL_DIR@" + +#define WORKSPACE_VERSION_STRING "${PROJECT_VERSION}" + +#cmakedefine HAVE_PACKAGEKIT "${HAVE_PACKAGEKIT}" diff --git a/plasma/workspace/containmentactions/CMakeLists.txt b/plasma/workspace/containmentactions/CMakeLists.txt new file mode 100644 index 0000000000..a3749a197b --- /dev/null +++ b/plasma/workspace/containmentactions/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(contextmenu) +add_subdirectory(switchdesktop) +add_subdirectory(switchactivity) +add_subdirectory(paste) +add_subdirectory(switchwindow) +add_subdirectory(applauncher) diff --git a/plasma/workspace/containmentactions/applauncher/CMakeLists.txt b/plasma/workspace/containmentactions/applauncher/CMakeLists.txt new file mode 100644 index 0000000000..d1dc662e0d --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_containmentactions_applauncher\") + +set(applauncher_SRCS + launch.cpp +) +ki18n_wrap_ui(applauncher_SRCS config.ui) + +kcoreaddons_add_plugin(plasma_containmentactions_applauncher SOURCES ${applauncher_SRCS} INSTALL_NAMESPACE "plasma/containmentactions") + +target_link_libraries(plasma_containmentactions_applauncher KF5::Plasma KF5::KIOCore KF5::KIOWidgets KF5::I18n) diff --git a/plasma/workspace/containmentactions/applauncher/Messages.sh b/plasma/workspace/containmentactions/applauncher/Messages.sh new file mode 100644 index 0000000000..3611977bd6 --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp +$XGETTEXT *.cpp -o $podir/plasma_containmentactions_applauncher.pot diff --git a/plasma/workspace/containmentactions/applauncher/config.ui b/plasma/workspace/containmentactions/applauncher/config.ui new file mode 100644 index 0000000000..d2389aeb06 --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/config.ui @@ -0,0 +1,25 @@ + + + Config + + + + 0 + 0 + 397 + 123 + + + + + + + Show applications by name + + + + + + + + diff --git a/plasma/workspace/containmentactions/applauncher/launch.cpp b/plasma/workspace/containmentactions/applauncher/launch.cpp new file mode 100644 index 0000000000..88880351ae --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/launch.cpp @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "launch.h" + +#include + +#include +#include + +AppLauncher::AppLauncher(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) + , m_group(new KServiceGroup(QStringLiteral("/"))) +{ +} + +AppLauncher::~AppLauncher() +{ +} + +void AppLauncher::init(const KConfigGroup &) +{ +} + +QList AppLauncher::contextualActions() +{ + qDeleteAll(m_actions); + m_actions.clear(); + makeMenu(nullptr, m_group); + + return m_actions; +} + +void AppLauncher::makeMenu(QMenu *menu, const KServiceGroup::Ptr group) +{ + const auto entries = group->entries(true, true, true); + for (const KSycocaEntry::Ptr &p : entries) { + if (p->isType(KST_KService)) { + const KService::Ptr service(static_cast(p.data())); + + QString text = service->name(); + if (!m_showAppsByName && !service->genericName().isEmpty()) { + text = service->genericName(); + } + + QAction *action = new QAction(QIcon::fromTheme(service->icon()), text, this); + connect(action, &QAction::triggered, [action]() { + KService::Ptr service = KService::serviceByStorageId(action->data().toString()); + auto job = new KIO::ApplicationLauncherJob(service); + job->start(); + }); + action->setData(service->storageId()); + if (menu) { + menu->addAction(action); + } else { + m_actions << action; + } + } else if (p->isType(KST_KServiceGroup)) { + const KServiceGroup::Ptr service(static_cast(p.data())); + if (service->childCount() == 0) { + continue; + } + QAction *action = new QAction(QIcon::fromTheme(service->icon()), service->caption(), this); + QMenu *subMenu = new QMenu(); + makeMenu(subMenu, service); + action->setMenu(subMenu); + if (menu) { + menu->addAction(action); + } else { + m_actions << action; + } + } else if (p->isType(KST_KServiceSeparator)) { + if (menu) { + menu->addSeparator(); + } + } + } +} + +QWidget *AppLauncher::createConfigurationInterface(QWidget *parent) +{ + QWidget *widget = new QWidget(parent); + m_ui.setupUi(widget); + widget->setWindowTitle(i18nc("plasma_containmentactions_applauncher", "Configure Application Launcher Plugin")); + + m_ui.showAppsByName->setChecked(m_showAppsByName); + + return widget; +} + +void AppLauncher::configurationAccepted() +{ + m_showAppsByName = m_ui.showAppsByName->isChecked(); +} + +void AppLauncher::restore(const KConfigGroup &config) +{ + m_showAppsByName = config.readEntry(QStringLiteral("showAppsByName"), false); +} + +void AppLauncher::save(KConfigGroup &config) +{ + config.writeEntry(QStringLiteral("showAppsByName"), m_showAppsByName); +} + +K_PLUGIN_CLASS_WITH_JSON(AppLauncher, "plasma-containmentactions-applauncher.json") + +#include "launch.moc" diff --git a/plasma/workspace/containmentactions/applauncher/launch.h b/plasma/workspace/containmentactions/applauncher/launch.h new file mode 100644 index 0000000000..c1e505f715 --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/launch.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +#include + +#include "ui_config.h" + +class QAction; +class QMenu; + +class AppLauncher : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + AppLauncher(QObject *parent, const QVariantList &args); + ~AppLauncher() override; + + void init(const KConfigGroup &config); + + QList contextualActions() override; + + QWidget *createConfigurationInterface(QWidget *parent) override; + void configurationAccepted() override; + + void restore(const KConfigGroup &config) override; + void save(KConfigGroup &config) override; + +protected: + void makeMenu(QMenu *menu, const KServiceGroup::Ptr group); + +private: + KServiceGroup::Ptr m_group; + QList m_actions; + + Ui::Config m_ui; + bool m_showAppsByName = false; +}; diff --git a/plasma/workspace/containmentactions/applauncher/plasma-containmentactions-applauncher.json b/plasma/workspace/containmentactions/applauncher/plasma-containmentactions-applauncher.json new file mode 100644 index 0000000000..f73d5a2ea5 --- /dev/null +++ b/plasma/workspace/containmentactions/applauncher/plasma-containmentactions-applauncher.json @@ -0,0 +1,164 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Simple application launcher", + "Description[ar]": "مُطلِق تطبيقات بسيط", + "Description[az]": "Sadə tətbiq başladıcısı", + "Description[ca]": "Llançador d'aplicacions senzill", + "Description[cs]": "Jednoduchý spouštěč aplikací", + "Description[de]": "Einfacher Anwendungsstarter", + "Description[en_GB]": "Simple application launcher", + "Description[es]": "Lanzador de aplicaciones sencillo", + "Description[eu]": "Aplikazio-abiarazle soila", + "Description[fi]": "Yksinkertainen sovelluskäynnistin", + "Description[fr]": "Lanceur simple d'applications", + "Description[hu]": "Egyszerű alkalmazásindító", + "Description[ia]": "Simple lanceator de application", + "Description[it]": "Semplice avviatore di applicazioni", + "Description[ko]": "간단한 프로그램 실행기", + "Description[lt]": "Paprasta programų paleidyklė", + "Description[nl]": "Eenvoudige programmastarter", + "Description[nn]": "Enkel programstartar", + "Description[pa]": "ਸਧਾਰਨ ਐਪਲੀਕੇਸ਼ਨ ਲਾਂਚਰ", + "Description[pl]": "Proste uruchamianie programów", + "Description[pt_BR]": "Lançador de aplicativos simples", + "Description[ro]": "Lansator de aplicații simplu", + "Description[ru]": "Упрощённое меню запуска приложений", + "Description[sk]": "Jednoduchý spúšťač aplikácií", + "Description[sl]": "Preprost zaganjalnik programov", + "Description[sv]": "Enkel programstart", + "Description[ta]": "எளிய செயலி ஏவி", + "Description[tr]": "Basit uygulama başlatıcı", + "Description[uk]": "Простий інструмент запуску програм", + "Description[vi]": "Trình khởi chạy ứng dụng đơn giản", + "Description[x-test]": "xxSimple application launcherxx", + "Description[zh_CN]": "简易程序启动器", + "EnabledByDefault": true, + "Icon": "preferences-desktop-launch-feedback", + "Id": "org.kde.applauncher", + "License": "GPL", + "Name": "Application Launcher", + "Name[af]": "Programlanseerder", + "Name[ar]": "مُطلِق التطبيقات", + "Name[ast]": "Llanzador d'aplicaciones", + "Name[az]": "Tətbiq Başladıcı", + "Name[be@latin]": "Uklučeńnie aplikacyi", + "Name[be]": "Запуск праграмаў", + "Name[bg]": "Стартиране на програми", + "Name[bn]": "অ্যাপলিকেশন লঞ্চার", + "Name[bn_IN]": "অ্যাপ্লিকেশন সঞ্চালনকারী", + "Name[bs]": "Pokretač programa", + "Name[ca@valencia]": "Llançador d'aplicacions", + "Name[ca]": "Llançador d'aplicacions", + "Name[cs]": "Spouštěč aplikací", + "Name[csb]": "Zrëszôcz programów", + "Name[da]": "Programmenu", + "Name[de]": "Anwendungsstarter", + "Name[el]": "Εκτέλεση εφαρμογών", + "Name[en_GB]": "Application Launcher", + "Name[eo]": "Aplikaĵolanĉilo", + "Name[es]": "Lanzador de aplicaciones", + "Name[et]": "Rakenduste käivitaja", + "Name[eu]": "Aplikazio-abiarazlea", + "Name[fa]": "راه‌انداز برنامه", + "Name[fi]": "Sovelluskäynnistin", + "Name[fr]": "Lanceur d'application", + "Name[fy]": "In applikaashe starter", + "Name[ga]": "Tosaitheoir Feidhmchlár", + "Name[gl]": "Iniciador de aplicacións", + "Name[gu]": "કાર્યક્રમ ચલાવનાર", + "Name[he]": "משגר יישומים", + "Name[hi]": "अनुप्रयोग चालक", + "Name[hne]": "अनुपरयोग चालू करइया", + "Name[hr]": "Pokretač aplikacija", + "Name[hsb]": "Startowar za aplikacije", + "Name[hu]": "Programindító", + "Name[ia]": "Lanceator de application", + "Name[id]": "Peluncur Aplikasi", + "Name[is]": "Forrita-Ræsir", + "Name[it]": "Avviatore di applicazioni", + "Name[ja]": "Kickoff アプリケーションランチャー", + "Name[kk]": "Қолданбаны жегу", + "Name[km]": "កម្មវិធី​ចាប់ផ្ដើម​កម្មវិធី​", + "Name[kn]": "ಅನ್ವಯ ಪ್ರಕ್ಷೇಪಕ (ಲಾಚರ್)", + "Name[ko]": "프로그램 실행기", + "Name[ku]": "Deskpêkerê Sepanan", + "Name[lt]": "Programų paleidyklė", + "Name[lv]": "Programmu palaidējs", + "Name[mai]": "अनुप्रयोग चालक", + "Name[mk]": "Стартувач на апликации", + "Name[ml]": "പ്രയോഗവിക്ഷേപിണി", + "Name[mr]": "अनुप्रयोग प्रक्षेपक", + "Name[nb]": "Programstarter", + "Name[nds]": "Programmoproper", + "Name[ne]": "अनुप्रयोक सुरुआतकर्ता", + "Name[nl]": "Programmastarter", + "Name[nn]": "Program­startar", + "Name[or]": "ପ୍ରୟୋଗ ଆରମ୍ଭକର୍ତ୍ତା", + "Name[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਲਾਂਚਰ", + "Name[pl]": "Uruchamiacz programów", + "Name[pt]": "Lançador de Aplicações", + "Name[pt_BR]": "Lançador de aplicativos", + "Name[ro]": "Lansator de aplicații", + "Name[ru]": "Меню запуска приложений", + "Name[se]": "Prográmmaálggaheaddji", + "Name[si]": "යෙදුම් ඇරඹුම", + "Name[sk]": "Spúšťač aplikácií", + "Name[sl]": "Zaganjalnik programov", + "Name[sr@ijekavian]": "покретач програма", + "Name[sr@ijekavianlatin]": "pokretač programa", + "Name[sr@latin]": "pokretač programa", + "Name[sr]": "покретач програма", + "Name[sv]": "Starta program", + "Name[ta]": "செயலி ஏவி", + "Name[te]": "అనువర్తనం దించునది", + "Name[th]": "ตัวเรียกใช้งานโปรแกรม", + "Name[tr]": "Uygulama Başlatıcısı", + "Name[ug]": "پروگرامما ئىجرا قىلغۇچ", + "Name[uk]": "Інструмент запуску програм", + "Name[uz@cyrillic]": "Дастурларни ишга туширувчи", + "Name[uz]": "Dasturlarni ishga tushiruvchi", + "Name[vi]": "Trình khởi chạy ứng dụng", + "Name[wa]": "Enondeu di programe", + "Name[x-test]": "xxApplication Launcherxx", + "Name[zh_CN]": "程序启动器", + "Name[zh_TW]": "應用程式啟動器", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-HasConfigurationInterface": true +} diff --git a/plasma/workspace/containmentactions/contextmenu/CMakeLists.txt b/plasma/workspace/containmentactions/contextmenu/CMakeLists.txt new file mode 100644 index 0000000000..f9914aa20a --- /dev/null +++ b/plasma/workspace/containmentactions/contextmenu/CMakeLists.txt @@ -0,0 +1,25 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_containmentactions_contextmenu\") + +include_directories(${plasma-workspace_SOURCE_DIR}/libkworkspace) + +set(contextmenu_SRCS + menu.cpp +) + +set(krunner_xml ${plasma-workspace_SOURCE_DIR}/krunner/dbus/org.kde.krunner.App.xml) +qt_add_dbus_interface(contextmenu_SRCS ${krunner_xml} krunner_interface) + +qt_add_dbus_interface(contextmenu_SRCS ${SCREENSAVER_DBUS_INTERFACE} screensaver_interface) + +kcoreaddons_add_plugin(plasma_containmentactions_contextmenu SOURCES ${contextmenu_SRCS} INSTALL_NAMESPACE "plasma/containmentactions") + +target_link_libraries(plasma_containmentactions_contextmenu + Qt::DBus + KF5::Activities + KF5::I18n + KF5::GlobalAccel + KF5::Plasma + KF5::XmlGui + KF5::KIOCore + KF5::KIOGui + PW::KWorkspace) diff --git a/plasma/workspace/containmentactions/contextmenu/Messages.sh b/plasma/workspace/containmentactions/contextmenu/Messages.sh new file mode 100644 index 0000000000..bc81a94389 --- /dev/null +++ b/plasma/workspace/containmentactions/contextmenu/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_containmentactions_contextmenu.pot diff --git a/plasma/workspace/containmentactions/contextmenu/menu.cpp b/plasma/workspace/containmentactions/contextmenu/menu.cpp new file mode 100644 index 0000000000..6af47b99e4 --- /dev/null +++ b/plasma/workspace/containmentactions/contextmenu/menu.cpp @@ -0,0 +1,336 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "menu.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "krunner_interface.h" +#include "kworkspace.h" +#include + +ContextMenu::ContextMenu(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) + , m_runCommandAction(nullptr) + , m_lockScreenAction(nullptr) + , m_logoutAction(nullptr) + , m_configureDisplaysAction(nullptr) + , m_separator1(nullptr) + , m_separator2(nullptr) + , m_separator3(nullptr) + , m_buttons(nullptr) + , m_session(new SessionManagement(this)) +{ +} + +ContextMenu::~ContextMenu() +{ +} + +void ContextMenu::restore(const KConfigGroup &config) +{ + Plasma::Containment *c = containment(); + Q_ASSERT(c); + + m_actions.clear(); + m_actionOrder.clear(); + QHash actions; + QSet disabled; + + // clang-format off + // because it really wants to mangle this nice aligned list + if (c->containmentType() == Plasma::Types::PanelContainment || c->containmentType() == Plasma::Types::CustomPanelContainment) { + m_actionOrder << QStringLiteral("add widgets") + << QStringLiteral("_context") + << QStringLiteral("configure") + << QStringLiteral("remove"); + } else { + actions.insert(QStringLiteral("configure shortcuts"), false); + m_actionOrder << QStringLiteral("configure") + << QStringLiteral("_display_settings") + << QStringLiteral("configure shortcuts") + << QStringLiteral("_sep1") + << QStringLiteral("_context") + << QStringLiteral("_run_command") + << QStringLiteral("add widgets") + << QStringLiteral("_add panel") + << QStringLiteral("manage activities") + << QStringLiteral("remove") + << QStringLiteral("edit mode") + << QStringLiteral("_sep2") + << QStringLiteral("_lock_screen") + << QStringLiteral("_logout") + << QStringLiteral("_sep3") + << QStringLiteral("_wallpaper"); + disabled.insert(QStringLiteral("configure shortcuts")); + disabled.insert(QStringLiteral("_run_command")); + disabled.insert(QStringLiteral("run associated application")); + } + // clang-format on + + for (const QString &name : qAsConst(m_actionOrder)) { + actions.insert(name, !disabled.contains(name)); + } + + QHashIterator it(actions); + while (it.hasNext()) { + it.next(); + m_actions.insert(it.key(), config.readEntry(it.key(), it.value())); + } + + // everything below should only happen once, so check for it + if (!m_runCommandAction) { + m_runCommandAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Show KRunner"), this); + m_runCommandAction->setIcon(QIcon::fromTheme(QStringLiteral("plasma-search"))); + m_runCommandAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("krunner.desktop"), QStringLiteral("_launch")).value(0)); + connect(m_runCommandAction, &QAction::triggered, this, &ContextMenu::runCommand); + + m_lockScreenAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Lock Screen"), this); + m_lockScreenAction->setIcon(QIcon::fromTheme(QStringLiteral("system-lock-screen"))); + m_lockScreenAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("ksmserver"), QStringLiteral("Lock Session")).value(0)); + m_lockScreenAction->setEnabled(m_session->canLock()); + connect(m_session, &SessionManagement::canLockChanged, this, [this]() { + m_lockScreenAction->setEnabled(m_session->canLock()); + }); + connect(m_lockScreenAction, &QAction::triggered, m_session, &SessionManagement::lock); + + m_logoutAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Leave…"), this); + m_logoutAction->setIcon(QIcon::fromTheme(QStringLiteral("system-log-out"))); + m_logoutAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("ksmserver"), QStringLiteral("Log Out")).value(0)); + m_logoutAction->setEnabled(m_session->canLogout()); + connect(m_session, &SessionManagement::canLogoutChanged, this, [this]() { + m_logoutAction->setEnabled(m_session->canLogout()); + }); + connect(m_logoutAction, &QAction::triggered, this, &ContextMenu::startLogout); + + m_configureDisplaysAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Configure Display Settings…"), this); + m_configureDisplaysAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-display"))); + connect(m_configureDisplaysAction, &QAction::triggered, this, &ContextMenu::configureDisplays); + + m_separator1 = new QAction(this); + m_separator1->setSeparator(true); + m_separator2 = new QAction(this); + m_separator2->setSeparator(true); + m_separator3 = new QAction(this); + m_separator3->setSeparator(true); + } +} + +QList ContextMenu::contextualActions() +{ + Plasma::Containment *c = containment(); + Q_ASSERT(c); + QList actions; + foreach (const QString &name, m_actionOrder) { + if (!m_actions.value(name)) { + continue; + } + + if (name == QLatin1String("_context")) { + actions << c->contextualActions(); + } + if (name == QLatin1String("_wallpaper")) { + if (!c->wallpaper().isEmpty()) { + QObject *wallpaperGraphicsObject = c->property("wallpaperGraphicsObject").value(); + if (wallpaperGraphicsObject) { + actions << wallpaperGraphicsObject->property("contextualActions").value>(); + } + } + } else if (QAction *a = action(name)) { + // Bug 364292: show "Remove this Panel" option only when panelcontroller is opened + if (name != QLatin1String("remove") || c->isUserConfiguring() + || (c->containmentType() != Plasma::Types::PanelContainment && c->containmentType() != Plasma::Types::CustomPanelContainment + && c->corona()->immutability() != Plasma::Types::Mutable)) { + actions << a; + } + } + } + + return actions; +} + +QAction *ContextMenu::action(const QString &name) +{ + Plasma::Containment *c = containment(); + Q_ASSERT(c); + if (name == QLatin1String("_sep1")) { + return m_separator1; + } else if (name == QLatin1String("_sep2")) { + return m_separator2; + } else if (name == QLatin1String("_sep3")) { + return m_separator3; + } else if (name == QLatin1String("_add panel")) { + if (c->corona() && c->corona()->immutability() == Plasma::Types::Mutable) { + return c->corona()->actions()->action(QStringLiteral("add panel")); + } + } else if (name == QLatin1String("_run_command")) { + if (KAuthorized::authorizeAction(QStringLiteral("run_command")) && KAuthorized::authorize(QStringLiteral("run_command"))) { + return m_runCommandAction; + } + } else if (name == QLatin1String("_lock_screen")) { + if (KAuthorized::authorizeAction(QStringLiteral("lock_screen"))) { + return m_lockScreenAction; + } + } else if (name == QLatin1String("_logout")) { + if (KAuthorized::authorize(QStringLiteral("logout"))) { + return m_logoutAction; + } + } else if (name == QLatin1String("_display_settings")) { + if (KAuthorized::authorizeControlModule(QStringLiteral("kcm_kscreen.desktop")) && KService::serviceByStorageId(QStringLiteral("kcm_kscreen"))) { + return m_configureDisplaysAction; + } + } else if (name == QLatin1String("edit mode")) { + if (c->corona()) { + return c->corona()->actions()->action(QStringLiteral("edit mode")); + } + } else if (name == QLatin1String("manage activities")) { + if (c->corona()) { + // Don't show the action if there's only one activity since in this + // case it's clear that the user doesn't use activities + if (KActivities::Consumer().activities().length() == 1) { + return nullptr; + } + + return c->corona()->actions()->action(QStringLiteral("manage activities")); + } + } else { + // FIXME: remove action: make removal of current activity possible + return c->actions()->action(name); + } + return nullptr; +} + +void ContextMenu::runCommand() +{ + if (!KAuthorized::authorizeAction(QStringLiteral("run_command"))) { + return; + } + + QString interface(QStringLiteral("org.kde.krunner")); + org::kde::krunner::App krunner(interface, QStringLiteral("/App"), QDBusConnection::sessionBus()); + krunner.display(); +} + +void ContextMenu::startLogout() +{ + KConfig config(QStringLiteral("ksmserverrc")); + const auto group = config.group("General"); + switch (group.readEntry("shutdownType", int(KWorkSpace::ShutdownTypeNone))) { + case int(KWorkSpace::ShutdownTypeHalt): + m_session->requestShutdown(); + break; + case int(KWorkSpace::ShutdownTypeReboot): + m_session->requestReboot(); + break; + default: + m_session->requestLogout(); + break; + } +} + +// FIXME: this function contains some code copied from KCMShell::openSystemSettings() +// which is not publicly available to C++ code right now. Eventually we should +// move that code into KIO so it's accessible to everyone, and then call that +// function instead of this one +void ContextMenu::configureDisplays() +{ + const QString systemSettings = QStringLiteral("systemsettings"); + const QString kscreenKCM = QStringLiteral("kcm_kscreen"); + + KIO::CommandLauncherJob *job = nullptr; + + // Open in System Settings if it's available + if (KService::serviceByDesktopName(systemSettings)) { + job = new KIO::CommandLauncherJob(QStringLiteral("systemsettings5"), {kscreenKCM}); + job->setDesktopName(systemSettings); + } else { + job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), {kscreenKCM}); + } + job->start(); +} + +QWidget *ContextMenu::createConfigurationInterface(QWidget *parent) +{ + QWidget *widget = new QWidget(parent); + QVBoxLayout *lay = new QVBoxLayout(); + widget->setLayout(lay); + widget->setWindowTitle(i18nc("plasma_containmentactions_contextmenu", "Configure Contextual Menu Plugin")); + m_buttons = new QButtonGroup(widget); + m_buttons->setExclusive(false); + + foreach (const QString &name, m_actionOrder) { + QCheckBox *item = nullptr; + + if (name == QLatin1String("_context")) { + item = new QCheckBox(widget); + // FIXME better text + item->setText(i18nc("plasma_containmentactions_contextmenu", "[Other Actions]")); + } else if (name == QLatin1String("_wallpaper")) { + item = new QCheckBox(widget); + item->setText(i18nc("plasma_containmentactions_contextmenu", "Wallpaper Actions")); + item->setIcon(QIcon::fromTheme(QStringLiteral("user-desktop"))); + } else if (name == QLatin1String("_sep1") || name == QLatin1String("_sep2") || name == QLatin1String("_sep3")) { + item = new QCheckBox(widget); + item->setText(i18nc("plasma_containmentactions_contextmenu", "[Separator]")); + } else { + QAction *a = action(name); + if (a) { + item = new QCheckBox(widget); + item->setText(a->text()); + item->setIcon(a->icon()); + } + } + + if (item) { + item->setChecked(m_actions.value(name)); + item->setProperty("actionName", name); + lay->addWidget(item); + m_buttons->addButton(item); + } + } + + return widget; +} + +void ContextMenu::configurationAccepted() +{ + QList buttons = m_buttons->buttons(); + QListIterator it(buttons); + while (it.hasNext()) { + QAbstractButton *b = it.next(); + if (b) { + m_actions.insert(b->property("actionName").toString(), b->isChecked()); + } + } +} + +void ContextMenu::save(KConfigGroup &config) +{ + QHashIterator it(m_actions); + while (it.hasNext()) { + it.next(); + config.writeEntry(it.key(), it.value()); + } +} + +K_PLUGIN_CLASS_WITH_JSON(ContextMenu, "plasma-containmentactions-contextmenu.json") + +#include "menu.moc" diff --git a/plasma/workspace/containmentactions/contextmenu/menu.h b/plasma/workspace/containmentactions/contextmenu/menu.h new file mode 100644 index 0000000000..7296b458fd --- /dev/null +++ b/plasma/workspace/containmentactions/contextmenu/menu.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class SessionManagement; + +class ContextMenu : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + ContextMenu(QObject *parent, const QVariantList &args); + ~ContextMenu() override; + + void restore(const KConfigGroup &) override; + + QList contextualActions() override; + QAction *action(const QString &name); + + QWidget *createConfigurationInterface(QWidget *parent) override; + void configurationAccepted() override; + void save(KConfigGroup &config) override; + +public Q_SLOTS: + void runCommand(); + void startLogout(); + void configureDisplays(); + +private: + QAction *m_runCommandAction; + QAction *m_lockScreenAction; + QAction *m_logoutAction; + QAction *m_configureDisplaysAction; + QAction *m_separator1; + QAction *m_separator2; + QAction *m_separator3; + + // action name and whether it is enabled or not + QHash m_actions; + QStringList m_actionOrder; + QButtonGroup *m_buttons; + SessionManagement *m_session; +}; diff --git a/plasma/workspace/containmentactions/contextmenu/plasma-containmentactions-contextmenu.json b/plasma/workspace/containmentactions/contextmenu/plasma-containmentactions-contextmenu.json new file mode 100644 index 0000000000..105e7b508a --- /dev/null +++ b/plasma/workspace/containmentactions/contextmenu/plasma-containmentactions-contextmenu.json @@ -0,0 +1,150 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "The menu that normally shows on right-click", + "Description[ar]": "القائمة التي تظهر عادةً عند الضغط باليمين", + "Description[az]": "Adi halda sağ kliklə açılan menyu", + "Description[ca]": "Menú que es mostra normalment amb un clic del botó dret", + "Description[cs]": "Nabídka která se normálně zobrazuje po stisknutí pravého tlačítka", + "Description[de]": "Das Menü, das für gewöhnlich durch einen Rechtsklick geöffnet wird", + "Description[en_GB]": "The menu that normally shows on right-click", + "Description[es]": "El menú que se suele mostrar al hacer clic derecho", + "Description[eu]": "Eskuineko botoiarekin klik egitean agertu ohi den menua", + "Description[fi]": "Tavallisesti hiiren oikealla painikkeella näytettävä valikko", + "Description[fr]": "Le menu s'affichant normalement lors d'un clic droit.", + "Description[hu]": "A jobb egérkattintásra megjelenő menü", + "Description[ia]": "Le menu que normalmente monstra sur le pression dextere", + "Description[it]": "Il menu che normalmente appare al clic destro", + "Description[ko]": "오른쪽 단추를 눌렀을 때 나타나는 메뉴", + "Description[lt]": "Meniu, kuris įprastai rodomas spustelėjus dešiniuoju pelės mygtuku", + "Description[nl]": "Het menu dat normaal verschijnt bij rechtsklikken", + "Description[nn]": "Menyen som vanlegvis vert vist ved høgreklikking", + "Description[pa]": "ਮੇਨੂ, ਜੋ ਕਿ ਸੱਜੇ ਕਲਿੱਕ ਨਾਲ ਦਿਖਾਈ ਦਿੰਦਾ ਹੈ", + "Description[pl]": "Wyświetla menu normalnie wyświetlane po naciśnięciu prawym przyciskiem myszy", + "Description[pt_BR]": "O menu que normalmente aparece ao clicar com o botão direito", + "Description[ro]": "Meniul ce apare de obicei la clic-dreapta", + "Description[ru]": "Меню, которое обычно открывается правой кнопкой мыши", + "Description[sk]": "Ponuka, ktorá sa zvyčajne zobrazuje po kliknutí pravým tlačidlom myši", + "Description[sl]": "Meni, ki se običajno prikaže ob desnem kliku miške", + "Description[sv]": "Meny som normalt visas med högerklick", + "Description[ta]": "வலது-க்ளிக் செய்யும்போது காட்டப்படும் பட்டி", + "Description[tr]": "Normalde sağ tıklama ile gösterilen menü", + "Description[uk]": "Меню, яке буде показано у відповідь на клацання правою кнопкою миші", + "Description[vi]": "Trình đơn thường thấy khi bấm phải", + "Description[x-test]": "xxThe menu that normally shows on right-clickxx", + "Description[zh_CN]": "通常在点击右键时显示的菜单", + "EnabledByDefault": true, + "Icon": "help-contextual", + "Id": "org.kde.contextmenu", + "License": "GPL", + "Name": "Standard Menu", + "Name[ar]": "قائمة قياسية", + "Name[ast]": "Menú estándar", + "Name[az]": "Standart Menyu", + "Name[bg]": "Стандартно меню", + "Name[bs]": "Standardni meni", + "Name[ca@valencia]": "Menú estàndard", + "Name[ca]": "Menú estàndard", + "Name[cs]": "Standardní nabídka", + "Name[csb]": "Standardowé menu", + "Name[da]": "Standardmenu", + "Name[de]": "Standard-Menü", + "Name[el]": "Τυπικό Μενού", + "Name[en_GB]": "Standard Menu", + "Name[eo]": "Defaŭlta Menuo", + "Name[es]": "Menú estándar", + "Name[et]": "Standardmenüü", + "Name[eu]": "Menu estandarra", + "Name[fi]": "Vakiovalikko", + "Name[fr]": "Menu standard", + "Name[fy]": "Standert menu", + "Name[ga]": "Gnáthroghchlár", + "Name[gl]": "Menú estándar", + "Name[he]": "תפריט רגיל", + "Name[hi]": "मानक मीनु", + "Name[hr]": "Standardni izbornik", + "Name[hsb]": "Standardny meni", + "Name[hu]": "Standard menü", + "Name[ia]": "Menu standard", + "Name[id]": "Menu Standar", + "Name[is]": "Staðalvalmynd", + "Name[it]": "Menu standard", + "Name[ja]": "標準メニュー", + "Name[ka]": "სტანდარტული მენიუ", + "Name[kk]": "Стандартты мәзір", + "Name[km]": "ម៉ឺនុយ​ស្តង់ដារ", + "Name[kn]": "ಶಿಷ್ಟ ಪರಿವಿಡಿ", + "Name[ko]": "표준 메뉴", + "Name[lt]": "Standartinis meniu", + "Name[lv]": "Konteksta izvēlne", + "Name[mk]": "Стандардно мени", + "Name[ml]": "സ്റ്റാന്‍ഡര്‍ഡ് മെനു", + "Name[mr]": "प्रमाणित मेन्यू", + "Name[nb]": "Standardmeny", + "Name[nds]": "Standardmenü", + "Name[nl]": "Standaardmenu", + "Name[nn]": "Standardmeny", + "Name[pa]": "ਸਟੈਂਡਰਡ ਮੇਨੂ", + "Name[pl]": "Menu standardowe", + "Name[pt]": "Menu Normal", + "Name[pt_BR]": "Menu padrão", + "Name[ro]": "Meniu standard", + "Name[ru]": "Стандартное контекстное меню", + "Name[si]": "සම්මත මෙනුව", + "Name[sk]": "Štandardná ponuka", + "Name[sl]": "Običajni meni", + "Name[sr@ijekavian]": "стандардни мени", + "Name[sr@ijekavianlatin]": "standardni meni", + "Name[sr@latin]": "standardni meni", + "Name[sr]": "стандардни мени", + "Name[sv]": "Standardmeny", + "Name[ta]": "செந்தர பட்டி", + "Name[tg]": "Феҳристи стандартӣ", + "Name[th]": "เมนูมาตรฐาน", + "Name[tr]": "Standart Menü", + "Name[ug]": "ئۆلچەملىك تىزىملىك", + "Name[uk]": "Стандартне меню", + "Name[vi]": "Trình đơn chuẩn", + "Name[wa]": "Dressêye sitandård", + "Name[x-test]": "xxStandard Menuxx", + "Name[zh_CN]": "标准菜单", + "Name[zh_TW]": "標準選單", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-HasConfigurationInterface": true +} diff --git a/plasma/workspace/containmentactions/paste/CMakeLists.txt b/plasma/workspace/containmentactions/paste/CMakeLists.txt new file mode 100644 index 0000000000..8d78b3e0a4 --- /dev/null +++ b/plasma/workspace/containmentactions/paste/CMakeLists.txt @@ -0,0 +1,13 @@ +kcoreaddons_add_plugin(plasma_containmentactions_paste SOURCES paste.cpp INSTALL_NAMESPACE "plasma/containmentactions") +target_link_libraries(plasma_containmentactions_paste + Qt::Gui + Qt::Widgets + KF5::Plasma + KF5::KIOCore +) + +ecm_qt_declare_logging_category(plasma_containmentactions_paste + HEADER containmentactions_paste_debug.h + IDENTIFIER CONTAINMENTACTIONS_PASTE_DEBUG + CATEGORY_NAME org.kde.plasma.containmentactions_paste +) diff --git a/plasma/workspace/containmentactions/paste/paste.cpp b/plasma/workspace/containmentactions/paste/paste.cpp new file mode 100644 index 0000000000..16f5ecc3e9 --- /dev/null +++ b/plasma/workspace/containmentactions/paste/paste.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "paste.h" +#include "containmentactions_paste_debug.h" + +#include +#include +#include + +#include + +#include +#include + +Paste::Paste(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) +{ + m_action = new QAction(this); + QObject::connect(m_action, &QAction::triggered, this, &Paste::doPaste); +} + +QList Paste::contextualActions() +{ + QList actions; + actions << m_action; + + return actions; +} + +void Paste::doPaste() +{ + qCWarning(CONTAINMENTACTIONS_PASTE_DEBUG) << "Paste at" << m_action->data(); + + if (!m_action->data().canConvert()) { + return; + } + + QPoint pos = m_action->data().value(); + Plasma::Containment *c = containment(); + Q_ASSERT(c); + + // get the actual graphic object of the containment + QObject *graphicObject = c->property("_plasma_graphicObject").value(); + if (!graphicObject) { + return; + } + + QClipboard *clipboard = QGuiApplication::clipboard(); + // FIXME: can be the const_cast avoided? + QMimeData *mimeData = const_cast(clipboard->mimeData(QClipboard::Selection)); + // TODO if that's not supported (ie non-linux) should we try clipboard instead of selection? + + graphicObject->metaObject() + ->invokeMethod(graphicObject, "processMimeData", Qt::DirectConnection, Q_ARG(QMimeData *, mimeData), Q_ARG(int, pos.x()), Q_ARG(int, pos.y())); +} + +K_PLUGIN_CLASS_WITH_JSON(Paste, "plasma-containmentactions-paste.json") + +#include "paste.moc" diff --git a/plasma/workspace/containmentactions/paste/paste.h b/plasma/workspace/containmentactions/paste/paste.h new file mode 100644 index 0000000000..53a23bf94b --- /dev/null +++ b/plasma/workspace/containmentactions/paste/paste.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class QAction; + +class Paste : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + Paste(QObject *parent, const QVariantList &args); + + QList contextualActions() override; + +private Q_SLOTS: + void doPaste(); + +private: + QAction *m_action; +}; diff --git a/plasma/workspace/containmentactions/paste/plasma-containmentactions-paste.json b/plasma/workspace/containmentactions/paste/plasma-containmentactions-paste.json new file mode 100644 index 0000000000..f0efa947e2 --- /dev/null +++ b/plasma/workspace/containmentactions/paste/plasma-containmentactions-paste.json @@ -0,0 +1,149 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Creates a widget from the contents of the clipboard", + "Description[ar]": "أنشئ ودجة من محتويات الحافظة", + "Description[az]": "Mübadilə buferinin tərkiblərindən vidjet yaradır", + "Description[ca]": "Crea un giny a partir del contingut del porta-retalls", + "Description[cs]": "Vytvoří widget z obsahu schránky", + "Description[de]": "Erzeugt ein Miniprogramm aus dem Inhalt der Zwischenablage", + "Description[en_GB]": "Creates a widget from the contents of the clipboard", + "Description[es]": "Crea un elemento gráfico con los contenidos del portapapeles", + "Description[eu]": "Arbelaren edukitik trepeta bat sortzen du", + "Description[fi]": "Luo leikepöydän sisällöstä sovelman", + "Description[fr]": "Crée un composant graphique à partir du contenu du presse-papier.", + "Description[hu]": "Készít egy widgetet a vágólap tartalmából", + "Description[ia]": "Il crea elemento graphic (Widget) ex le contentos del area de transferentia (clipboard)", + "Description[it]": "Crea un oggetto dai contenuti degli appunti", + "Description[ko]": "클립보드 내용을 기반으로 위젯을 만듭니다", + "Description[lt]": "Sukuria valdiklį iš iškarpinės turinio", + "Description[nl]": "Maakt een widget aan uit de inhoud van het klembord", + "Description[nn]": "Lagar eit element frå innhaldet på utklippstavla", + "Description[pa]": "ਕਲਿੱਪਬੋਰਡ ਦੀ ਸਮੱਗਰੀ ਤੋਂ ਵਿਜੈੱਟ ਬਣਾਓ", + "Description[pl]": "Wyświetla zawartość schowka", + "Description[pt_BR]": "Cria um widget a partir do conteúdo da área de transferência", + "Description[ro]": "Creează un control din conținutul clipboard-ului", + "Description[ru]": "Создание виджета с содержимым буфера обмена", + "Description[sk]": "Vytvorí miniaplikáciu z obsahu schránky", + "Description[sl]": "Ustvari gradnik iz vsebine odložišča", + "Description[sv]": "Skapa komponent från klippbordets innehåll", + "Description[ta]": "பிடிப்புப்பலகையின் உள்ளடக்கத்திலிருந்து ஒரு பிளாஸ்மாய்டை உருவாக்கும்", + "Description[tr]": "Pano içeriğinden bir araç takımı oluşturur", + "Description[uk]": "Створює віджет з вмістом буфера обміну даними", + "Description[vi]": "Tạo một phụ kiện từ nội dung của bảng nháp", + "Description[x-test]": "xxCreates a widget from the contents of the clipboardxx", + "Description[zh_CN]": "用剪贴板内容创建小部件", + "EnabledByDefault": true, + "Icon": "edit-paste", + "Id": "org.kde.paste", + "License": "GPL", + "Name": "Paste", + "Name[ar]": "ألصق", + "Name[az]": "Mətn Yerləşdirmə", + "Name[bg]": "Поставяне", + "Name[bn]": "পেস্ট", + "Name[bs]": "Naljepljivanje", + "Name[ca@valencia]": "Apega", + "Name[ca]": "Enganxa", + "Name[cs]": "Vložit", + "Name[csb]": "Wlepi", + "Name[da]": "Indsæt", + "Name[de]": "Einfügen", + "Name[el]": "Επικόλληση", + "Name[en_GB]": "Paste", + "Name[eo]": "Glui", + "Name[es]": "Pegar", + "Name[et]": "Asetamine", + "Name[eu]": "Itsatsi", + "Name[fa]": "چسباندن", + "Name[fi]": "Liitä", + "Name[fr]": "Coller", + "Name[fy]": "Plakke", + "Name[ga]": "Greamaigh", + "Name[gl]": "Pegar", + "Name[gu]": "ચોટાડો", + "Name[he]": "הדבקה", + "Name[hi]": "चिपकाएँ", + "Name[hr]": "Umetni", + "Name[hsb]": "Zasunyć", + "Name[hu]": "Beillesztés", + "Name[ia]": "Colla", + "Name[id]": "Tempel", + "Name[is]": "Líma", + "Name[it]": "Incolla", + "Name[ja]": "貼り付け", + "Name[kk]": "Орналастыру", + "Name[km]": "បិទភ្ជាប់", + "Name[kn]": "ಅಂಟಿಸು", + "Name[ko]": "붙여넣기", + "Name[lt]": "Įdėti", + "Name[lv]": "Ielīmēt", + "Name[ml]": "ഒട്ടിയ്ക്കുക", + "Name[mr]": "चिटकवा", + "Name[nb]": "Lim inn", + "Name[nds]": "Infögen", + "Name[nl]": "Plakken", + "Name[nn]": "Tekstbitar", + "Name[pa]": "ਚੇਪੋ", + "Name[pl]": "Wklej", + "Name[pt]": "Colar", + "Name[pt_BR]": "Colar", + "Name[ro]": "Lipire", + "Name[ru]": "Вставка текста", + "Name[si]": "අලවන්න", + "Name[sk]": "Vložiť", + "Name[sl]": "Prilepi", + "Name[sr@ijekavian]": "наљепљивање", + "Name[sr@ijekavianlatin]": "naljepljivanje", + "Name[sr@latin]": "nalepljivanje", + "Name[sr]": "налепљивање", + "Name[sv]": "Klistra in", + "Name[ta]": "ஒட்டு", + "Name[tg]": "Гузоштан", + "Name[th]": "วาง", + "Name[tr]": "Yapıştır", + "Name[ug]": "چاپلا", + "Name[uk]": "Вставка", + "Name[vi]": "Dán", + "Name[wa]": "Aclaper", + "Name[x-test]": "xxPastexx", + "Name[zh_CN]": "粘贴", + "Name[zh_TW]": "貼上", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/containmentactions/switchactivity/CMakeLists.txt b/plasma/workspace/containmentactions/switchactivity/CMakeLists.txt new file mode 100644 index 0000000000..715e626b21 --- /dev/null +++ b/plasma/workspace/containmentactions/switchactivity/CMakeLists.txt @@ -0,0 +1,8 @@ +kcoreaddons_add_plugin(plasma_containmentactions_switchactivity SOURCES switch.cpp INSTALL_NAMESPACE "plasma/containmentactions") + +target_link_libraries(plasma_containmentactions_switchactivity + Qt::Widgets + KF5::Plasma + KF5::KIOCore + KF5::Activities + PW::KWorkspace) diff --git a/plasma/workspace/containmentactions/switchactivity/Messages.sh b/plasma/workspace/containmentactions/switchactivity/Messages.sh new file mode 100644 index 0000000000..3c54a5268f --- /dev/null +++ b/plasma/workspace/containmentactions/switchactivity/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_containmentactions_switchactivity.pot diff --git a/plasma/workspace/containmentactions/switchactivity/plasma-containmentactions-switchactivity.json b/plasma/workspace/containmentactions/switchactivity/plasma-containmentactions-switchactivity.json new file mode 100644 index 0000000000..8304b3668e --- /dev/null +++ b/plasma/workspace/containmentactions/switchactivity/plasma-containmentactions-switchactivity.json @@ -0,0 +1,146 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Switch to another activity", + "Description[ar]": "بدّل إلى نشاط آخر", + "Description[az]": "Başqa İş Otağına keçidi təmin edir", + "Description[ca]": "Canvia a una altra activitat", + "Description[cs]": "Přepnout na jinou aktivitu", + "Description[de]": "Zu einer anderen Aktivität wechseln", + "Description[en_GB]": "Switch to another activity", + "Description[es]": "Cambiar a otra actividad", + "Description[eu]": "Aldatu beste jarduera batera", + "Description[fi]": "Vaihda toiseen aktiviteettiin", + "Description[fr]": "Basculer vers une autre activité", + "Description[hu]": "Váltás másik aktivitásra", + "Description[ia]": "Commuta a altere activitate", + "Description[it]": "Passa ad un'altra attività", + "Description[ko]": "다른 활동으로 전환합니다", + "Description[lt]": "Perjungti į kitą veiklą", + "Description[nl]": "Schakel naar een andere activiteit", + "Description[nn]": "Byt til ein annan aktivitet", + "Description[pa]": "ਹੋਰ ਐਕਟਵਿਟੀ ਉੱਤੇ ਜਾਓ", + "Description[pl]": "Przełącza między aktywnościami", + "Description[pt_BR]": "Alterna para outra atividade", + "Description[ro]": "Comută la altă activitate", + "Description[ru]": "Переключение на другую комнату", + "Description[sk]": "Prepne na inú aktivitu", + "Description[sl]": "Preklopi na drugo dejavnost", + "Description[sv]": "Byt till en annan aktivitet", + "Description[ta]": "வேறு செயல்பாட்டிற்கு தாவு", + "Description[tr]": "Başka bir etkinliğe geç", + "Description[uk]": "Перемкнутися на інший простір дій", + "Description[vi]": "Chuyển sang một Hoạt động khác", + "Description[x-test]": "xxSwitch to another activityxx", + "Description[zh_CN]": "切换到另一个活动", + "EnabledByDefault": true, + "Icon": "dashboard-show", + "Id": "switchactivity", + "License": "GPL", + "Name": "Switch Activity", + "Name[ar]": "بدّل النشاط", + "Name[az]": "İş Otaqlarına keçid", + "Name[bg]": "Превключване на дейност", + "Name[bs]": "Prebacivanje aktivnosti", + "Name[ca@valencia]": "Canvi d'activitat", + "Name[ca]": "Canvi d'activitat", + "Name[cs]": "Přepnout aktivitu", + "Name[da]": "Skift aktivitet", + "Name[de]": "Aktivität wechseln", + "Name[el]": "Εναλλαγή δραστηριότητας", + "Name[en_GB]": "Switch Activity", + "Name[eo]": "Ŝalti Aktivecon", + "Name[es]": "Cambiar actividad", + "Name[et]": "Tegevuse vahetamine", + "Name[eu]": "Jardueraz aldatu", + "Name[fi]": "Vaihda aktiviteettia", + "Name[fr]": "Changer d'activité", + "Name[fy]": "Aktiviteit wikselje", + "Name[ga]": "Athraigh Gníomhaíocht", + "Name[gl]": "Cambiar de actividade", + "Name[he]": "מחליף פעילות", + "Name[hi]": "क्रिया बदलें", + "Name[hr]": "Prebaci aktivnost", + "Name[hsb]": "K druhej aktiwiće hić", + "Name[hu]": "Aktivitásváltás", + "Name[ia]": "Commuta activitate", + "Name[id]": "Alihkan Aktivitas", + "Name[is]": "Skipta um virkni", + "Name[it]": "Cambia attività", + "Name[ja]": "アクティビティを切り替え", + "Name[kk]": "Белсенділікті ауыстыру", + "Name[km]": "ប្ដូរ​សកម្មភាព", + "Name[kn]": "ಚಟುವಟಿಕೆಯನ್ನು ಬದಲಾಯಿಸು", + "Name[ko]": "활동 전환", + "Name[lt]": "Perjungti veiklą", + "Name[lv]": "Pārslēgt aktivitāti", + "Name[mk]": "Смени активност", + "Name[ml]": "പ്രക്രിയ മാറ്റുക", + "Name[mr]": "वेगळ्या कार्यपध्दती वर जा", + "Name[nb]": "Bytt aktivitet", + "Name[nds]": "Aktiviteet wesseln", + "Name[nl]": "Activiteit omschakelen", + "Name[nn]": "Byt aktivitet", + "Name[pa]": "ਐਕਟਵਿਟੀ ਬਦਲੋ", + "Name[pl]": "Przełącznik aktywności", + "Name[pt]": "Mudar de Actividade", + "Name[pt_BR]": "Alternar atividade", + "Name[ro]": "Comutare activitate", + "Name[ru]": "Переключение комнат", + "Name[si]": "කාර්‍යය මාරුකරන්න", + "Name[sk]": "Prepnúť aktivitu", + "Name[sl]": "Preklopi med dejavnostmi", + "Name[sr@ijekavian]": "пребацивање активности", + "Name[sr@ijekavianlatin]": "prebacivanje aktivnosti", + "Name[sr@latin]": "prebacivanje aktivnosti", + "Name[sr]": "пребацивање активности", + "Name[sv]": "Byt aktivitet", + "Name[ta]": "செயல்பாட்டை மாற்று", + "Name[tg]": "Гузариши фаъолият", + "Name[th]": "สลับกิจกรรม", + "Name[tr]": "Etkinlik Değiştir", + "Name[ug]": "پائالىيەت ئالماشتۇر", + "Name[uk]": "Перемкнути простір дій", + "Name[vi]": "Chuyển Hoạt động", + "Name[wa]": "Potchî a ene ôte activité", + "Name[x-test]": "xxSwitch Activityxx", + "Name[zh_CN]": "切换活动", + "Name[zh_TW]": "切換活動", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/containmentactions/switchactivity/switch.cpp b/plasma/workspace/containmentactions/switchactivity/switch.cpp new file mode 100644 index 0000000000..f13a87aa29 --- /dev/null +++ b/plasma/workspace/containmentactions/switchactivity/switch.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "switch.h" + +#include + +#include +#include + +#include +#include +#include +#include + +Q_DECLARE_METATYPE(QWeakPointer) + +SwitchActivity::SwitchActivity(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) +{ +} + +SwitchActivity::~SwitchActivity() +{ +} + +void SwitchActivity::makeMenu() +{ + qDeleteAll(m_actions); + m_actions.clear(); + foreach (const QString &id, m_consumer.activities(KActivities::Info::Running)) { + KActivities::Info info(id); + QAction *action = new QAction(QIcon::fromTheme(info.icon()), info.name(), this); + action->setData(id); + + if (id == m_consumer.currentActivity()) { + QFont font = action->font(); + font.setBold(true); + action->setFont(font); + } + + connect(action, &QAction::triggered, [=]() { + switchTo(action); + }); + + m_actions << action; + } +} + +QList SwitchActivity::contextualActions() +{ + makeMenu(); + + return m_actions; +} + +void SwitchActivity::switchTo(QAction *action) +{ + if (!action) { + return; + } + + m_controller.setCurrentActivity(action->data().toString()); +} + +void SwitchActivity::performNextAction() +{ + const QStringList activities = m_consumer.activities(KActivities::Info::Running); + + int i = activities.indexOf(m_consumer.currentActivity()); + + i = (i + 1) % activities.size(); + m_controller.setCurrentActivity(activities[i]); +} + +void SwitchActivity::performPreviousAction() +{ + const QStringList activities = m_consumer.activities(KActivities::Info::Running); + + int i = activities.indexOf(m_consumer.currentActivity()); + + i = (i + activities.size() - 1) % activities.size(); + m_controller.setCurrentActivity(activities[i]); +} + +K_PLUGIN_CLASS_WITH_JSON(SwitchActivity, "plasma-containmentactions-switchactivity.json") + +#include "switch.moc" diff --git a/plasma/workspace/containmentactions/switchactivity/switch.h b/plasma/workspace/containmentactions/switchactivity/switch.h new file mode 100644 index 0000000000..2cb25aeada --- /dev/null +++ b/plasma/workspace/containmentactions/switchactivity/switch.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +class QAction; + +class SwitchActivity : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + SwitchActivity(QObject *parent, const QVariantList &args); + ~SwitchActivity() override; + + QList contextualActions() override; + + void performNextAction() override; + void performPreviousAction() override; + +private Q_SLOTS: + void switchTo(QAction *action); + void makeMenu(); + +private: + QList m_actions; + KActivities::Consumer m_consumer; + KActivities::Controller m_controller; +}; diff --git a/plasma/workspace/containmentactions/switchdesktop/CMakeLists.txt b/plasma/workspace/containmentactions/switchdesktop/CMakeLists.txt new file mode 100644 index 0000000000..c45c425fc7 --- /dev/null +++ b/plasma/workspace/containmentactions/switchdesktop/CMakeLists.txt @@ -0,0 +1,8 @@ +kcoreaddons_add_plugin(plasma_containmentactions_switchdesktop SOURCES desktop.cpp INSTALL_NAMESPACE "plasma/containmentactions") + +target_link_libraries(plasma_containmentactions_switchdesktop + Qt::Widgets + KF5::Plasma + PW::LibTaskManager + ) + diff --git a/plasma/workspace/containmentactions/switchdesktop/Messages.sh b/plasma/workspace/containmentactions/switchdesktop/Messages.sh new file mode 100644 index 0000000000..de79ab5efb --- /dev/null +++ b/plasma/workspace/containmentactions/switchdesktop/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_containmentactions_switchdesktop.pot diff --git a/plasma/workspace/containmentactions/switchdesktop/desktop.cpp b/plasma/workspace/containmentactions/switchdesktop/desktop.cpp new file mode 100644 index 0000000000..8ea9857238 --- /dev/null +++ b/plasma/workspace/containmentactions/switchdesktop/desktop.cpp @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "desktop.h" + +#include + +#include + +using namespace TaskManager; + +SwitchDesktop::SwitchDesktop(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) + , m_virtualDesktopInfo(new VirtualDesktopInfo(this)) +{ +} + +SwitchDesktop::~SwitchDesktop() +{ +} + +QList SwitchDesktop::contextualActions() +{ + const int numDesktops = m_virtualDesktopInfo->numberOfDesktops(); + const QVariantList &desktopIds = m_virtualDesktopInfo->desktopIds(); + const QStringList &desktopNames = m_virtualDesktopInfo->desktopNames(); + const QVariant ¤tDesktop = m_virtualDesktopInfo->currentDesktop(); + + QList actions; + actions.reserve(numDesktops); + + if (m_actions.count() < numDesktops) { + for (int i = m_actions.count(); i < numDesktops; ++i) { + QAction *action = new QAction(this); + connect(action, &QAction::triggered, this, &SwitchDesktop::switchTo); + m_actions[i] = action; + } + } else if (m_actions.count() > numDesktops) { + for (int i = m_actions.count(); i > numDesktops; --i) { + delete m_actions.take(i - 1); + } + } + + for (int i = 0; i < numDesktops; ++i) { + QAction *action = m_actions.value(i); + action->setText(QStringLiteral("%1: %2").arg(QString::number(i), desktopNames.at(i))); + action->setData(desktopIds.at(i)); + action->setEnabled(desktopIds.at(i) != currentDesktop); + actions << action; + } + + return actions; +} + +void SwitchDesktop::switchTo() +{ + const QAction *action = qobject_cast(sender()); + + if (!action) { + return; + } + + m_virtualDesktopInfo->requestActivate(action->data()); +} + +void SwitchDesktop::performNextAction() +{ + const QVariantList &desktopIds = m_virtualDesktopInfo->desktopIds(); + if (desktopIds.isEmpty()) { + return; + } + + const QVariant ¤tDesktop = m_virtualDesktopInfo->currentDesktop(); + const int currentDesktopIndex = desktopIds.indexOf(currentDesktop); + + int nextDesktopIndex = currentDesktopIndex + 1; + + if (nextDesktopIndex == desktopIds.count()) { + if (m_virtualDesktopInfo->navigationWrappingAround()) { + nextDesktopIndex = 0; + } else { + return; + } + } + + m_virtualDesktopInfo->requestActivate(desktopIds.at(nextDesktopIndex)); +} + +void SwitchDesktop::performPreviousAction() +{ + const QVariantList &desktopIds = m_virtualDesktopInfo->desktopIds(); + if (desktopIds.isEmpty()) { + return; + } + const QVariant ¤tDesktop = m_virtualDesktopInfo->currentDesktop(); + const int currentDesktopIndex = desktopIds.indexOf(currentDesktop); + + int previousDesktopIndex = currentDesktopIndex - 1; + + if (previousDesktopIndex < 0) { + if (m_virtualDesktopInfo->navigationWrappingAround()) { + previousDesktopIndex = desktopIds.count() - 1; + } else { + return; + } + } + + m_virtualDesktopInfo->requestActivate(desktopIds.at(previousDesktopIndex)); +} + +K_PLUGIN_CLASS_WITH_JSON(SwitchDesktop, "plasma-containmentactions-switchdesktop.json") + +#include "desktop.moc" diff --git a/plasma/workspace/containmentactions/switchdesktop/desktop.h b/plasma/workspace/containmentactions/switchdesktop/desktop.h new file mode 100644 index 0000000000..c97956b86d --- /dev/null +++ b/plasma/workspace/containmentactions/switchdesktop/desktop.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class QAction; + +namespace TaskManager +{ +class VirtualDesktopInfo; +} + +class SwitchDesktop : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + SwitchDesktop(QObject *parent, const QVariantList &args); + ~SwitchDesktop() override; + + QList contextualActions() override; + + void performNextAction() override; + void performPreviousAction() override; + +private Q_SLOTS: + void switchTo(); + +private: + QHash m_actions; + TaskManager::VirtualDesktopInfo *m_virtualDesktopInfo; +}; diff --git a/plasma/workspace/containmentactions/switchdesktop/plasma-containmentactions-switchdesktop.json b/plasma/workspace/containmentactions/switchdesktop/plasma-containmentactions-switchdesktop.json new file mode 100644 index 0000000000..1a7479fb64 --- /dev/null +++ b/plasma/workspace/containmentactions/switchdesktop/plasma-containmentactions-switchdesktop.json @@ -0,0 +1,148 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Switch to another virtual desktop", + "Description[ar]": "بدّل إلى سطح مكتب وهمي آخر", + "Description[az]": "Başqa İş Masasına keçidi təmin edir", + "Description[ca]": "Canvia a un altre escriptori virtual", + "Description[cs]": "Přepnout na jinou virtuální plochu", + "Description[de]": "Zu einer anderen virtuellen Arbeitsfläche wechseln", + "Description[en_GB]": "Switch to another virtual desktop", + "Description[es]": "Cambiar a otro escritorio virtual", + "Description[eu]": "Aldatu alegiazko beste mahaigain batera", + "Description[fi]": "Vaihda toiselle virtuaalityöpöydälle", + "Description[fr]": "Basculer vers un autre bureau virtuel", + "Description[hu]": "Váltás másik virtuális asztalra", + "Description[ia]": "Commuta a altere scriptorio virtual", + "Description[it]": "Passa ad un altro desktop virtuale", + "Description[ko]": "다른 가상 바탕 화면으로 전환합니다", + "Description[lt]": "Perjungti į kitą virtualų darbalaukį", + "Description[nl]": "Schakel naar een andere virtueel bureaublad", + "Description[nn]": "Byt til eit anna virtuelt skrivebord", + "Description[pa]": "ਹੋਰ ਵੁਰਚੁਅਲ ਡੈਸਕਟਾਪਾਂ ਉੱਤੇ ਜਾਓ", + "Description[pl]": "Przełącza między pulpitami", + "Description[pt_BR]": "Alterna para outra área de trabalho", + "Description[ro]": "Comută la alt birou virtual", + "Description[ru]": "Переключение на другой рабочий стол", + "Description[sk]": "Prepne na inú virtuálnu plochu", + "Description[sl]": "Preklopi na drugo navidezno namizje", + "Description[sv]": "Byt till ett annat virtuellt skrivbord", + "Description[ta]": "வேறு பணிமேடைக்குத் தாவு", + "Description[tr]": "Başka bir sanal masaüstüne geç", + "Description[uk]": "Перемкнутися на іншу віртуальну стільницю", + "Description[vi]": "Chuyển sang một bàn làm việc ảo khác", + "Description[x-test]": "xxSwitch to another virtual desktopxx", + "Description[zh_CN]": "切换到另一个虚拟桌面", + "EnabledByDefault": true, + "Icon": "user-desktop", + "Id": "org.kde.switchdesktop", + "License": "GPL", + "Name": "Switch Desktop", + "Name[ar]": "بدّل سطح المكتب", + "Name[az]": "İş Masasına keçid", + "Name[bg]": "Превключване на работен плот", + "Name[bn]": "অন্য ডেস্কটপ", + "Name[bs]": "Prebacivanje površi", + "Name[ca@valencia]": "Canvi d'escriptori", + "Name[ca]": "Canvi d'escriptori", + "Name[cs]": "Přepnout plochu", + "Name[da]": "Skift skrivebord", + "Name[de]": "Arbeitsfläche wechseln", + "Name[el]": "Εναλλαγή Επιφάνειας εργασίας", + "Name[en_GB]": "Switch Desktop", + "Name[eo]": "Ŝalti Labortablon", + "Name[es]": "Cambiar escritorio", + "Name[et]": "Töölaua vahetamine", + "Name[eu]": "Aldatu mahaigainez", + "Name[fi]": "Vaihda työpöytää", + "Name[fr]": "Changer de bureau", + "Name[fy]": "Buroblêd wikselje", + "Name[ga]": "Athraigh an Deasc", + "Name[gl]": "Cambiar de escritorio", + "Name[gu]": "ડેસ્કટોપ બદલો", + "Name[he]": "מחליף שולחנות עבודה", + "Name[hi]": "डेस्कटॉप बदलें", + "Name[hr]": "Prebaci radnu površinu", + "Name[hsb]": "Na hinaši dźěłowy powjerch hić", + "Name[hu]": "Asztalváltás", + "Name[ia]": "Commuta scriptorio", + "Name[id]": "Alihkan Desktop", + "Name[is]": "Skipta um skjáborð", + "Name[it]": "Cambia desktop", + "Name[ja]": "デスクトップを切り替え", + "Name[kk]": "Жұмыс үстелді ауыстыру", + "Name[km]": "ប្ដូរ​ផ្ទៃតុ", + "Name[kn]": "ಗಣಕತೆರೆಯನ್ನು ಬದಲಾಯಿಸು", + "Name[ko]": "바탕 화면 전환", + "Name[lt]": "Perjungti darbalaukį", + "Name[lv]": "Pārslēgt darbvirsmu", + "Name[mk]": "Смени раб. површина", + "Name[ml]": "പണിയിടം മാറുക", + "Name[mr]": "वेगळ्या डेस्कटॉप वर जा", + "Name[nb]": "Bytt skrivebord", + "Name[nds]": "Schriefdisch wesseln", + "Name[nl]": "Bureaublad omschakelen", + "Name[nn]": "Byt skrivebord", + "Name[pa]": "ਡੈਸਕਟਾਪ ਬਦਲੋ", + "Name[pl]": "Przełącznik pulpitów", + "Name[pt]": "Mudar de Ecrã", + "Name[pt_BR]": "Alternar área de trabalho", + "Name[ro]": "Comutare birou", + "Name[ru]": "Переключение рабочих столов", + "Name[si]": "වැඩතලය මාරුකරන්න", + "Name[sk]": "Prepnúť plochu", + "Name[sl]": "Preklopi med namizji", + "Name[sr@ijekavian]": "пребацивање површи", + "Name[sr@ijekavianlatin]": "prebacivanje površi", + "Name[sr@latin]": "prebacivanje površi", + "Name[sr]": "пребацивање површи", + "Name[sv]": "Byt skrivbord", + "Name[ta]": "பணிமேடையை மாற்று", + "Name[tg]": "Гузариши мизи корӣ", + "Name[th]": "สลับพื้นที่ทำงาน", + "Name[tr]": "Masaüstünü Değiştir", + "Name[ug]": "ئۈستەلئۈستى ئالماشتۇر", + "Name[uk]": "Перемкнути стільницю", + "Name[vi]": "Chuyển bàn làm việc", + "Name[wa]": "Potchî a èn ôte sicribanne", + "Name[x-test]": "xxSwitch Desktopxx", + "Name[zh_CN]": "切换桌面", + "Name[zh_TW]": "切換桌面", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/containmentactions/switchwindow/CMakeLists.txt b/plasma/workspace/containmentactions/switchwindow/CMakeLists.txt new file mode 100644 index 0000000000..f32f62f740 --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/CMakeLists.txt @@ -0,0 +1,14 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_containmentactions_switchwindow\") + +set(switchwindow_SRCS + switch.cpp +) +ki18n_wrap_ui(switchwindow_SRCS config.ui) + +kcoreaddons_add_plugin(plasma_containmentactions_switchwindow SOURCES ${switchwindow_SRCS} INSTALL_NAMESPACE "plasma/containmentactions") + +target_link_libraries(plasma_containmentactions_switchwindow + Qt::Widgets + KF5::Plasma + KF5::I18n + PW::LibTaskManager) diff --git a/plasma/workspace/containmentactions/switchwindow/Messages.sh b/plasma/workspace/containmentactions/switchwindow/Messages.sh new file mode 100644 index 0000000000..7f28e587fb --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp +$XGETTEXT *.cpp -o $podir/plasma_containmentactions_switchwindow.pot diff --git a/plasma/workspace/containmentactions/switchwindow/config.ui b/plasma/workspace/containmentactions/switchwindow/config.ui new file mode 100644 index 0000000000..a3b6b120d8 --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/config.ui @@ -0,0 +1,38 @@ + + Config + + + + 0 + 0 + 388 + 108 + + + + + + + Display all windows in one list + + + + + + + Display a submenu for each desktop + + + + + + + Display only the current desktop's windows + + + + + + + + diff --git a/plasma/workspace/containmentactions/switchwindow/plasma-containmentactions-switchwindow.json b/plasma/workspace/containmentactions/switchwindow/plasma-containmentactions-switchwindow.json new file mode 100644 index 0000000000..582cc956c4 --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/plasma-containmentactions-switchwindow.json @@ -0,0 +1,146 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Show a list of windows to switch to", + "Description[ar]": "أظهر قائمة النوافذ للتبديل بينها", + "Description[az]": "Bütün pəncərələrin siyahısı", + "Description[ca]": "Mostra una llista de les finestres per a canviar", + "Description[cs]": "Zobrazit seznam oken k přepnutí", + "Description[de]": "Eine Liste aller Fenster anzeigen", + "Description[en_GB]": "Show a list of windows to switch to", + "Description[es]": "Mostrar una lista de ventanas a las que cambiar", + "Description[eu]": "Erakutsi aldatzeko hauta ditzakezun leihoen zerrenda bat", + "Description[fi]": "Näytä luettelo ikkunoista, joihin voi vaihtaa", + "Description[fr]": "Afficher une liste de fenêtres vers lesquelles basculer", + "Description[hu]": "Ablakok listájának megjelenítése váltáshoz", + "Description[ia]": "Monstra un lista de fenestras ubi commutar", + "Description[it]": "Mostra un elenco di finestre a cui passare", + "Description[ko]": "전환할 수 있는 창 목록을 표시합니다", + "Description[lt]": "Rodyti langų, į kuriuos perjungti, sąrašą", + "Description[nl]": "Toon een lijst met vensters waarnaar om te schakelen", + "Description[nn]": "Vis ei oversikt over vindauge å byta til", + "Description[pl]": "Przełącza między oknami", + "Description[pt_BR]": "Exibe uma lista de janelas para alternar", + "Description[ro]": "Arată o listă de ferestre de comutat", + "Description[ru]": "Показать список окон", + "Description[sk]": "Zobrazenie zoznamu okien na prepnutie", + "Description[sl]": "Prikaži seznam oken kamor se je mogoče preklopiti", + "Description[sv]": "Visa en lista med fönster att byta till", + "Description[ta]": "தாவுவதற்காக சாளரங்களை பட்டியலிடு", + "Description[tr]": "Geçmek için pencereler listesini göster", + "Description[uk]": "Показує список вікон для перемикання", + "Description[vi]": "Hiện một danh sách các cửa sổ để chuyển", + "Description[x-test]": "xxShow a list of windows to switch toxx", + "Description[zh_CN]": "显示可供切换的窗口列表", + "EnabledByDefault": true, + "Icon": "preferences-system-windows", + "Id": "switchwindow", + "License": "GPL", + "Name": "Switch Window", + "Name[ar]": "بدّل النافذة", + "Name[az]": "Pəncərə dəyişmə", + "Name[bg]": "Превключване на прозорци", + "Name[bs]": "Prebacivanje prozora", + "Name[ca@valencia]": "Canvi de finestra", + "Name[ca]": "Canvi de finestra", + "Name[cs]": "Přepnout okna", + "Name[da]": "Skift vindue", + "Name[de]": "Fenster wechseln", + "Name[el]": "Εναλλαγή παραθύρου", + "Name[en_GB]": "Switch Window", + "Name[eo]": "Ŝalti fenestron", + "Name[es]": "Cambiar ventana", + "Name[et]": "Akna vahetamine", + "Name[eu]": "Aldatu leihoz", + "Name[fi]": "Vaihda ikkunaa", + "Name[fr]": "Changer de fenêtre", + "Name[fy]": "Finster wikselje", + "Name[ga]": "Athraigh an Fhuinneog", + "Name[gl]": "Tocar de xanela", + "Name[he]": "מחליף חלונות", + "Name[hi]": "विंडो बदलें", + "Name[hr]": "Prebaci prozor", + "Name[hsb]": "Na hinaše wokno hić", + "Name[hu]": "Ablakváltás", + "Name[ia]": "Commuta fenestra", + "Name[id]": "Alihkan Window", + "Name[is]": "Skipta um glugga", + "Name[it]": "Cambia finestra", + "Name[ja]": "ウィンドウを切り替え", + "Name[kk]": "Терезені ауыстыру", + "Name[km]": "ប្ដូរ​បង្អួច", + "Name[kn]": "ಕಿಟಿಕಿಯನ್ನು ಬದಲಾಯಿಸು", + "Name[ko]": "창 전환", + "Name[lt]": "Perjungti langą", + "Name[lv]": "Pārslēgt logu", + "Name[mk]": "Смени прозорец", + "Name[ml]": "ജാലകം മാറുക", + "Name[mr]": "वेगळ्या चौकट वर जा", + "Name[nb]": "Bytt vindu", + "Name[nds]": "Finster wesseln", + "Name[nl]": "Venster omschakelen", + "Name[nn]": "Byt vindauge", + "Name[pa]": "ਵਿੰਡੋ ਬਦਲੋ", + "Name[pl]": "Przełącznik okien", + "Name[pt]": "Mudar de Janela", + "Name[pt_BR]": "Alternar janela", + "Name[ro]": "Comutare fereastră", + "Name[ru]": "Переключение окон", + "Name[si]": "කවුළුව මාරුකරන්න", + "Name[sk]": "Prepnúť okno", + "Name[sl]": "Preklopi med okni", + "Name[sr@ijekavian]": "пребацивање прозора", + "Name[sr@ijekavianlatin]": "prebacivanje prozora", + "Name[sr@latin]": "prebacivanje prozora", + "Name[sr]": "пребацивање прозора", + "Name[sv]": "Byt fönster", + "Name[ta]": "வேறு சாளரத்துக்குத் தாவு", + "Name[tg]": "Гузариши равзана", + "Name[th]": "สลับหน้าต่าง", + "Name[tr]": "Pencereyi Değiştir", + "Name[ug]": "كۆزنەك ئالماشتۇر", + "Name[uk]": "Перемкнути вікно", + "Name[vi]": "Chuyển cửa sổ", + "Name[wa]": "Passer a ene ôte finiesse", + "Name[x-test]": "xxSwitch Windowxx", + "Name[zh_CN]": "切换窗口", + "Name[zh_TW]": "切換視窗", + "Version": "pre0.1", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-HasConfigurationInterface": true +} diff --git a/plasma/workspace/containmentactions/switchwindow/switch.cpp b/plasma/workspace/containmentactions/switchwindow/switch.cpp new file mode 100644 index 0000000000..4b5054bce0 --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/switch.cpp @@ -0,0 +1,267 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "switch.h" + +#include "abstracttasksmodel.h" +#include "activityinfo.h" +#include "tasksmodel.h" +#include "virtualdesktopinfo.h" +#include + +#include +#include + +using namespace TaskManager; + +ActivityInfo *SwitchWindow::s_activityInfo = nullptr; +TasksModel *SwitchWindow::s_tasksModel = nullptr; +int SwitchWindow::s_instanceCount = 0; + +SwitchWindow::SwitchWindow(QObject *parent, const QVariantList &args) + : Plasma::ContainmentActions(parent, args) + , m_mode(AllFlat) + , m_virtualDesktopInfo(new VirtualDesktopInfo(this)) +{ + ++s_instanceCount; + + if (!s_activityInfo) { + s_activityInfo = new ActivityInfo(); + } + + if (!s_tasksModel) { + s_tasksModel = new TasksModel(); + + s_tasksModel->setGroupMode(TasksModel::GroupDisabled); + + s_tasksModel->setActivity(s_activityInfo->currentActivity()); + s_tasksModel->setFilterByActivity(true); + connect(s_activityInfo, &ActivityInfo::currentActivityChanged, this, []() { + s_tasksModel->setActivity(s_activityInfo->currentActivity()); + }); + } +} + +SwitchWindow::~SwitchWindow() +{ + --s_instanceCount; + + if (!s_instanceCount) { + delete s_activityInfo; + s_activityInfo = nullptr; + delete s_tasksModel; + s_tasksModel = nullptr; + } + + qDeleteAll(m_actions); +} + +void SwitchWindow::restore(const KConfigGroup &config) +{ + m_mode = (MenuMode)config.readEntry("mode", (int)AllFlat); +} + +QWidget *SwitchWindow::createConfigurationInterface(QWidget *parent) +{ + QWidget *widget = new QWidget(parent); + m_ui.setupUi(widget); + widget->setWindowTitle(i18nc("plasma_containmentactions_switchwindow", "Configure Switch Window Plugin")); + switch (m_mode) { + case AllFlat: + m_ui.flatButton->setChecked(true); + break; + case DesktopSubmenus: + m_ui.subButton->setChecked(true); + break; + case CurrentDesktop: + m_ui.curButton->setChecked(true); + break; + } + return widget; +} + +void SwitchWindow::configurationAccepted() +{ + if (m_ui.flatButton->isChecked()) { + m_mode = AllFlat; + } else if (m_ui.subButton->isChecked()) { + m_mode = DesktopSubmenus; + } else { + m_mode = CurrentDesktop; + } +} + +void SwitchWindow::save(KConfigGroup &config) +{ + config.writeEntry("mode", (int)m_mode); +} + +void SwitchWindow::makeMenu() +{ + qDeleteAll(m_actions); + m_actions.clear(); + + if (s_tasksModel->rowCount() == 0) { + return; + } + + QMultiMap desktops; + QList allDesktops; + + // Make all the window actions. + for (int i = 0; i < s_tasksModel->rowCount(); ++i) { + const QModelIndex &idx = s_tasksModel->index(i, 0); + + if (!idx.data(AbstractTasksModel::IsWindow).toBool()) { + continue; + } + + const QString &name = idx.data().toString(); + + if (name.isEmpty()) { + continue; + } + + QAction *action = new QAction(name, this); + action->setIcon(idx.data(Qt::DecorationRole).value()); + action->setData(idx.data(AbstractTasksModel::WinIdList).toList()); + + const QStringList &desktopList = idx.data(AbstractTasksModel::VirtualDesktops).toStringList(); + + for (const QString &desktop : desktopList) { + desktops.insert(desktop, action); + } + + if (idx.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool()) { + allDesktops << action; + } + + connect(action, &QAction::triggered, [=]() { + switchTo(action); + }); + } + + // Sort into menu(s). + if (m_mode == CurrentDesktop) { + const QString ¤tDesktop = m_virtualDesktopInfo->currentDesktop().toString(); + + QAction *a = new QAction(i18nc("plasma_containmentactions_switchwindow", "Windows"), this); + a->setSeparator(true); + m_actions << a; + m_actions << desktops.values(currentDesktop); + m_actions << allDesktops; + + } else { + const QVariantList &desktopIds = m_virtualDesktopInfo->desktopIds(); + const QStringList &desktopNames = m_virtualDesktopInfo->desktopNames(); + + if (m_mode == AllFlat) { + for (int i = 0; i < desktopIds.count(); ++i) { + const QString &desktop = desktopIds.at(i).toString(); + + if (desktops.contains(desktop)) { + const QString &name = QStringLiteral("%1: %2").arg(QString::number(i + 1), desktopNames.at(i)); + QAction *a = new QAction(name, this); + a->setSeparator(true); + m_actions << a; + m_actions << desktops.values(desktop); + } + } + + if (allDesktops.count()) { + QAction *a = new QAction(i18nc("plasma_containmentactions_switchwindow", "All Desktops"), this); + a->setSeparator(true); + m_actions << a; + m_actions << allDesktops; + } + } else { // Submenus. + for (int i = 0; i < desktopIds.count(); ++i) { + const QString &desktop = desktopIds.at(i).toString(); + + if (desktops.contains(desktop)) { + const QString &name = QStringLiteral("%1: %2").arg(QString::number(i + 1), desktopNames.at(i)); + QMenu *subMenu = new QMenu(name); + subMenu->addActions(desktops.values(desktop)); + + QAction *a = new QAction(name, this); + a->setMenu(subMenu); + m_actions << a; + } + } + + if (allDesktops.count()) { + QMenu *subMenu = new QMenu(i18nc("plasma_containmentactions_switchwindow", "All Desktops")); + subMenu->addActions(allDesktops); + QAction *a = new QAction(i18nc("plasma_containmentactions_switchwindow", "All Desktops"), this); + a->setMenu(subMenu); + m_actions << a; + } + } + } +} + +QList SwitchWindow::contextualActions() +{ + makeMenu(); + return m_actions; +} + +void SwitchWindow::switchTo(QAction *action) +{ + const QVariantList &idList = action->data().toList(); + + for (int i = 0; i < s_tasksModel->rowCount(); ++i) { + const QModelIndex &idx = s_tasksModel->index(i, 0); + + if (idList == idx.data(AbstractTasksModel::WinIdList).toList()) { + s_tasksModel->requestActivate(idx); + + return; + } + } +} + +void SwitchWindow::performNextAction() +{ + doSwitch(true); +} + +void SwitchWindow::performPreviousAction() +{ + doSwitch(false); +} + +void SwitchWindow::doSwitch(bool up) +{ + const QModelIndex &activeTask = s_tasksModel->activeTask(); + + if (!activeTask.isValid()) { + return; + } + + if (up) { + const QModelIndex &next = activeTask.sibling(activeTask.row() + 1, 0); + + if (next.isValid()) { + s_tasksModel->requestActivate(next); + } else if (s_tasksModel->rowCount() > 1) { + s_tasksModel->requestActivate(s_tasksModel->index(0, 0)); + } + } else { + const QModelIndex &previous = activeTask.sibling(activeTask.row() - 1, 0); + + if (previous.isValid()) { + s_tasksModel->requestActivate(previous); + } else if (s_tasksModel->rowCount() > 1) { + s_tasksModel->requestActivate(s_tasksModel->index(s_tasksModel->rowCount() - 1, 0)); + } + } +} + +K_PLUGIN_CLASS_WITH_JSON(SwitchWindow, "plasma-containmentactions-switchwindow.json") + +#include "switch.moc" diff --git a/plasma/workspace/containmentactions/switchwindow/switch.h b/plasma/workspace/containmentactions/switchwindow/switch.h new file mode 100644 index 0000000000..fa41cc49d6 --- /dev/null +++ b/plasma/workspace/containmentactions/switchwindow/switch.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "ui_config.h" +#include + +class QAction; + +namespace TaskManager +{ +class ActivityInfo; +class TasksModel; +class VirtualDesktopInfo; +} + +class SwitchWindow : public Plasma::ContainmentActions +{ + Q_OBJECT +public: + SwitchWindow(QObject *parent, const QVariantList &args); + ~SwitchWindow() override; + + void restore(const KConfigGroup &config) override; + QWidget *createConfigurationInterface(QWidget *parent) override; + void configurationAccepted() override; + void save(KConfigGroup &config) override; + + void performNextAction() override; + void performPreviousAction() override; + void doSwitch(bool up); + QList contextualActions() override; + +private: + void makeMenu(); + +private Q_SLOTS: + void switchTo(QAction *action); + +private: + enum MenuMode { + AllFlat = 0, + DesktopSubmenus, + CurrentDesktop, + }; + + QList m_actions; + Ui::Config m_ui; + MenuMode m_mode; + + TaskManager::VirtualDesktopInfo *m_virtualDesktopInfo; + + static TaskManager::ActivityInfo *s_activityInfo; + static TaskManager::TasksModel *s_tasksModel; + static int s_instanceCount; +}; diff --git a/plasma/workspace/dataengines/CMakeLists.txt b/plasma/workspace/dataengines/CMakeLists.txt new file mode 100644 index 0000000000..74a05eca17 --- /dev/null +++ b/plasma/workspace/dataengines/CMakeLists.txt @@ -0,0 +1,31 @@ + +add_subdirectory(applicationjobs) +if (KF5Activities_FOUND) + add_subdirectory(activities) +endif () +add_subdirectory(apps) +add_subdirectory(devicenotifications) +add_subdirectory(dict) +add_subdirectory(executable) +add_subdirectory(favicons) +add_subdirectory(filebrowser) +if (KF5NetworkManagerQt_FOUND) + add_subdirectory(geolocation) +endif () +add_subdirectory(hotplug) +add_subdirectory(keystate) +add_subdirectory(mpris2) +add_subdirectory(notifications) +add_subdirectory(packagekit) +add_subdirectory(places) +add_subdirectory(powermanagement) +add_subdirectory(soliddevice) + +add_subdirectory(time) +add_subdirectory(weather) +add_subdirectory(statusnotifieritem) + +if(NOT WIN32) + add_subdirectory(mouse) + add_subdirectory(systemmonitor) +endif() diff --git a/plasma/workspace/dataengines/Mainpage.dox b/plasma/workspace/dataengines/Mainpage.dox new file mode 100644 index 0000000000..52dcca0608 --- /dev/null +++ b/plasma/workspace/dataengines/Mainpage.dox @@ -0,0 +1,10 @@ +/** +* @mainpage Engines +* +* Plasma engines power widgets. +* +*/ + +// DOXYGEN_SET_PROJECT_NAME = Engines +// DOXYGEN_SET_RECURSIVE = YES +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/plasma/workspace/dataengines/activities/ActivityData.cpp b/plasma/workspace/dataengines/activities/ActivityData.cpp new file mode 100644 index 0000000000..1b643b04d6 --- /dev/null +++ b/plasma/workspace/dataengines/activities/ActivityData.cpp @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2011 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ActivityData.h" + +#include +#include + +class ActivityDataStaticInit +{ +public: + ActivityDataStaticInit() + { + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + } + + static ActivityDataStaticInit _instance; +}; + +ActivityDataStaticInit ActivityDataStaticInit::_instance; + +ActivityData::ActivityData() +{ +} + +ActivityData::ActivityData(const ActivityData &source) +{ + score = source.score; + id = source.id; +} + +ActivityData &ActivityData::operator=(const ActivityData &source) +{ + if (&source != this) { + score = source.score; + id = source.id; + } + + return *this; +} + +QDBusArgument &operator<<(QDBusArgument &arg, const ActivityData r) +{ + arg.beginStructure(); + + arg << r.id; + arg << r.score; + + arg.endStructure(); + + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, ActivityData &r) +{ + arg.beginStructure(); + + arg >> r.id; + arg >> r.score; + + arg.endStructure(); + + return arg; +} + +QDebug operator<<(QDebug dbg, const ActivityData &r) +{ + dbg << "ActivityData(" << r.score << r.id << ")"; + return dbg.space(); +} diff --git a/plasma/workspace/dataengines/activities/ActivityData.h b/plasma/workspace/dataengines/activities/ActivityData.h new file mode 100644 index 0000000000..e569c8b377 --- /dev/null +++ b/plasma/workspace/dataengines/activities/ActivityData.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2011 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class ActivityData +{ +public: + ActivityData(); + ActivityData(const ActivityData &source); + ActivityData &operator=(const ActivityData &source); + + double score; + QString id; +}; + +typedef QList ActivityDataList; +Q_DECLARE_METATYPE(ActivityData) +Q_DECLARE_METATYPE(ActivityDataList) + +QDBusArgument &operator<<(QDBusArgument &arg, const ActivityData); +const QDBusArgument &operator>>(const QDBusArgument &arg, ActivityData &rec); + +QDebug operator<<(QDebug dbg, const ActivityData &r); diff --git a/plasma/workspace/dataengines/activities/CMakeLists.txt b/plasma/workspace/dataengines/activities/CMakeLists.txt new file mode 100644 index 0000000000..dcd9dd82a1 --- /dev/null +++ b/plasma/workspace/dataengines/activities/CMakeLists.txt @@ -0,0 +1,28 @@ +set(activity_engine_SRCS + ActivityData.cpp + activityengine.cpp + activityservice.cpp + activityjob.cpp) + +set_source_files_properties(org.kde.ActivityManager.ActivityRanking.xml PROPERTIES INCLUDE "ActivityData.h") +qt_add_dbus_interface( + activity_engine_SRCS org.kde.ActivityManager.ActivityRanking.xml + ActivityRankingInterface + ) + +add_library(plasma_engine_activities MODULE ${activity_engine_SRCS}) +target_link_libraries(plasma_engine_activities + KF5::CoreAddons + KF5::Plasma + KF5::Activities + KF5::I18n + KF5::Service + Qt::DBus + Qt::Widgets + ) + +install(TARGETS plasma_engine_activities + DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + +install(FILES activities.operations + DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/activities/activities.operations b/plasma/workspace/dataengines/activities/activities.operations new file mode 100644 index 0000000000..c438c50624 --- /dev/null +++ b/plasma/workspace/dataengines/activities/activities.operations @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/activities/activityengine.cpp b/plasma/workspace/dataengines/activities/activityengine.cpp new file mode 100644 index 0000000000..68b668253c --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityengine.cpp @@ -0,0 +1,245 @@ +/* + SPDX-FileCopyrightText: 2010 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "activityengine.h" +#include "activityservice.h" + +#include +#include + +#include +#include + +#define ACTIVITYMANAGER_SERVICE "org.kde.kactivitymanagerd" +#define ACTIVITYRANKING_OBJECT "/ActivityRanking" + +ActivityEngine::ActivityEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + Q_UNUSED(args); + init(); +} + +void ActivityEngine::init() +{ + if (qApp->applicationName() == QLatin1String("plasma-netbook")) { + // hack for the netbook + // FIXME can I read a setting or something instead? + } else { + m_activityController = new KActivities::Controller(this); + m_currentActivity = m_activityController->currentActivity(); + const QStringList activities = m_activityController->activities(); + // setData("allActivities", activities); + for (const QString &id : activities) { + insertActivity(id); + } + + connect(m_activityController, &KActivities::Controller::activityAdded, this, &ActivityEngine::activityAdded); + connect(m_activityController, &KActivities::Controller::activityRemoved, this, &ActivityEngine::activityRemoved); + connect(m_activityController, &KActivities::Controller::currentActivityChanged, this, &ActivityEngine::currentActivityChanged); + + // some convenience sources for times when checking every activity source would suck + // it starts with _ so that it can easily be filtered out of sources() + // maybe I should just make it not included in sources() instead? + m_runningActivities = m_activityController->activities(KActivities::Info::Running); + setData(QStringLiteral("Status"), QStringLiteral("Current"), m_currentActivity); + setData(QStringLiteral("Status"), QStringLiteral("Running"), m_runningActivities); + + m_watcher = new QDBusServiceWatcher(ACTIVITYMANAGER_SERVICE, + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, + this); + + connect(m_watcher, &QDBusServiceWatcher::serviceRegistered, this, &ActivityEngine::enableRanking); + connect(m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, &ActivityEngine::disableRanking); + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ACTIVITYMANAGER_SERVICE)) { + enableRanking(); + } + } +} + +void ActivityEngine::insertActivity(const QString &id) +{ + // id -> name, icon, state + KActivities::Info *activity = new KActivities::Info(id, this); + m_activities[id] = activity; + setData(id, QStringLiteral("Name"), activity->name()); + setData(id, QStringLiteral("Icon"), activity->icon()); + setData(id, QStringLiteral("Current"), m_currentActivity == id); + + QString state; + switch (activity->state()) { + case KActivities::Info::Running: + state = QLatin1String("Running"); + break; + case KActivities::Info::Starting: + state = QLatin1String("Starting"); + break; + case KActivities::Info::Stopping: + state = QLatin1String("Stopping"); + break; + case KActivities::Info::Stopped: + state = QLatin1String("Stopped"); + break; + case KActivities::Info::Invalid: + default: + state = QLatin1String("Invalid"); + } + setData(id, QStringLiteral("State"), state); + setData(id, QStringLiteral("Score"), m_activityScores.value(id)); + + connect(activity, &KActivities::Info::infoChanged, this, &ActivityEngine::activityDataChanged); + connect(activity, &KActivities::Info::stateChanged, this, &ActivityEngine::activityStateChanged); + + m_runningActivities << id; +} + +void ActivityEngine::disableRanking() +{ + delete m_activityRankingClient; +} + +void ActivityEngine::enableRanking() +{ + m_activityRankingClient = new org::kde::ActivityManager::ActivityRanking(ACTIVITYMANAGER_SERVICE, ACTIVITYRANKING_OBJECT, QDBusConnection::sessionBus()); + connect(m_activityRankingClient, &org::kde::ActivityManager::ActivityRanking::rankingChanged, this, &ActivityEngine::rankingChanged); + + QDBusMessage msg = QDBusMessage::createMethodCall(ACTIVITYMANAGER_SERVICE, + ACTIVITYRANKING_OBJECT, + QStringLiteral("org.kde.ActivityManager.ActivityRanking"), + QStringLiteral("activities")); + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &ActivityEngine::activityScoresReply); +} + +void ActivityEngine::activityScoresReply(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qDebug() << "Error getting activity scores: " << reply.error().message(); + } else { + setActivityScores(reply.value()); + } + + watcher->deleteLater(); +} + +void ActivityEngine::rankingChanged(const QStringList &topActivities, const ActivityDataList &activities) +{ + Q_UNUSED(topActivities) + + setActivityScores(activities); +} + +void ActivityEngine::setActivityScores(const ActivityDataList &activities) +{ + QSet presentActivities; + m_activityScores.clear(); + + for (const ActivityData &activity : activities) { + if (m_activities.contains(activity.id)) { + setData(activity.id, QStringLiteral("Score"), activity.score); + } + presentActivities.insert(activity.id); + m_activityScores[activity.id] = activity.score; + } + + const auto controllerActivities = m_activityController->activities(); + for (const QString &activityId : controllerActivities) { + if (!presentActivities.contains(activityId) && m_activities.contains(activityId)) { + setData(activityId, QStringLiteral("Score"), 0); + } + } +} + +void ActivityEngine::activityAdded(const QString &id) +{ + insertActivity(id); + setData(QStringLiteral("Status"), QStringLiteral("Running"), m_runningActivities); +} + +void ActivityEngine::activityRemoved(const QString &id) +{ + removeSource(id); + KActivities::Info *activity = m_activities.take(id); + if (activity) { + delete activity; + } + m_runningActivities.removeAll(id); + setData(QStringLiteral("Status"), QStringLiteral("Running"), m_runningActivities); +} + +void ActivityEngine::currentActivityChanged(const QString &id) +{ + setData(m_currentActivity, QStringLiteral("Current"), false); + m_currentActivity = id; + setData(id, QStringLiteral("Current"), true); + setData(QStringLiteral("Status"), QStringLiteral("Current"), id); +} + +void ActivityEngine::activityDataChanged() +{ + KActivities::Info *activity = qobject_cast(sender()); + if (!activity) { + return; + } + setData(activity->id(), QStringLiteral("Name"), activity->name()); + setData(activity->id(), QStringLiteral("Icon"), activity->icon()); + setData(activity->id(), QStringLiteral("Current"), m_currentActivity == activity->id()); + setData(activity->id(), QStringLiteral("Score"), m_activityScores.value(activity->id())); +} + +void ActivityEngine::activityStateChanged() +{ + KActivities::Info *activity = qobject_cast(sender()); + const QString id = activity->id(); + if (!activity) { + return; + } + QString state; + switch (activity->state()) { + case KActivities::Info::Running: + state = QLatin1String("Running"); + break; + case KActivities::Info::Starting: + state = QLatin1String("Starting"); + break; + case KActivities::Info::Stopping: + state = QLatin1String("Stopping"); + break; + case KActivities::Info::Stopped: + state = QLatin1String("Stopped"); + break; + case KActivities::Info::Invalid: + default: + state = QLatin1String("Invalid"); + } + setData(id, QStringLiteral("State"), state); + + if (activity->state() == KActivities::Info::Running) { + if (!m_runningActivities.contains(id)) { + m_runningActivities << id; + } + } else { + m_runningActivities.removeAll(id); + } + + setData(QStringLiteral("Status"), QStringLiteral("Running"), m_runningActivities); +} + +Plasma::Service *ActivityEngine::serviceForSource(const QString &source) +{ + // FIXME validate the name + ActivityService *service = new ActivityService(m_activityController, source); + service->setParent(this); + return service; +} + +K_PLUGIN_CLASS_WITH_JSON(ActivityEngine, "plasma-dataengine-activities.json") + +#include "activityengine.moc" diff --git a/plasma/workspace/dataengines/activities/activityengine.h b/plasma/workspace/dataengines/activities/activityengine.h new file mode 100644 index 0000000000..75ce21486c --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityengine.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2010 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +#include "ActivityData.h" +#include "ActivityRankingInterface.h" + +class QDBusServiceWatcher; + +class ActivityService; + +namespace KActivities +{ +class Controller; +class Info; +} + +class ActivityEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + ActivityEngine(QObject *parent, const QVariantList &args); + Plasma::Service *serviceForSource(const QString &source) override; + void init(); + +public Q_SLOTS: + void activityAdded(const QString &id); + void activityRemoved(const QString &id); + void currentActivityChanged(const QString &id); + + void activityDataChanged(); + void activityStateChanged(); + + void disableRanking(); + void enableRanking(); + void rankingChanged(const QStringList &topActivities, const ActivityDataList &activities); + void activityScoresReply(QDBusPendingCallWatcher *watcher); + +private: + void insertActivity(const QString &id); + void setActivityScores(const ActivityDataList &activities); + + KActivities::Controller *m_activityController; + QHash m_activities; + QStringList m_runningActivities; + QString m_currentActivity; + + org::kde::ActivityManager::ActivityRanking *m_activityRankingClient; + QDBusServiceWatcher *m_watcher; + QHash m_activityScores; + + friend class ActivityService; +}; diff --git a/plasma/workspace/dataengines/activities/activityjob.cpp b/plasma/workspace/dataengines/activities/activityjob.cpp new file mode 100644 index 0000000000..db4f9f17a3 --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityjob.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "activityjob.h" + +#include +#include +#include + +#include +#include + +ActivityJob::ActivityJob(KActivities::Controller *controller, const QString &id, const QString &operation, QMap ¶meters, QObject *parent) + : ServiceJob(parent->objectName(), operation, parameters, parent) + , m_activityController(controller) + , m_id(id) +{ +} + +ActivityJob::~ActivityJob() +{ +} + +void ActivityJob::start() +{ + const QString operation = operationName(); + if (operation == QLatin1String("add")) { + // I wonder how well plasma will handle this... + QString name = parameters()[QStringLiteral("Name")].toString(); + if (name.isEmpty()) { + name = i18n("unnamed"); + } + const QString activityId = m_activityController->addActivity(name); + setResult(activityId); + return; + } + if (operation == QLatin1String("remove")) { + QString id = parameters()[QStringLiteral("Id")].toString(); + m_activityController->removeActivity(id); + setResult(true); + return; + } + + // m_id is needed for the rest + if (m_id.isEmpty()) { + setResult(false); + return; + } + if (operation == QLatin1String("setCurrent")) { + m_activityController->setCurrentActivity(m_id); + setResult(true); + return; + } + if (operation == QLatin1String("stop")) { + m_activityController->stopActivity(m_id); + setResult(true); + return; + } + if (operation == QLatin1String("start")) { + m_activityController->startActivity(m_id); + setResult(true); + return; + } + if (operation == QLatin1String("setName")) { + m_activityController->setActivityName(m_id, parameters()[QStringLiteral("Name")].toString()); + setResult(true); + return; + } + if (operation == QLatin1String("setIcon")) { + m_activityController->setActivityIcon(m_id, parameters()[QStringLiteral("Icon")].toString()); + setResult(true); + return; + } + if (operation == QLatin1String("toggleActivityManager")) { + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/PlasmaShell"), + QStringLiteral("org.kde.PlasmaShell"), + QStringLiteral("toggleActivityManager")); + QDBusConnection::sessionBus().call(message, QDBus::NoBlock); + setResult(true); + return; + } + setResult(false); +} diff --git a/plasma/workspace/dataengines/activities/activityjob.h b/plasma/workspace/dataengines/activities/activityjob.h new file mode 100644 index 0000000000..dbb9bf3c4f --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityjob.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include + +namespace KActivities +{ +class Controller; +} // namespace KActivities + +class ActivityJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + ActivityJob(KActivities::Controller *controller, + const QString &id, + const QString &operation, + QMap ¶meters, + QObject *parent = nullptr); + ~ActivityJob() override; + +protected: + void start() override; + +private: + KActivities::Controller *m_activityController; + QString m_id; +}; diff --git a/plasma/workspace/dataengines/activities/activityservice.cpp b/plasma/workspace/dataengines/activities/activityservice.cpp new file mode 100644 index 0000000000..eadbc3e3f8 --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityservice.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2010 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "activityservice.h" +#include "activityjob.h" + +ActivityService::ActivityService(KActivities::Controller *controller, const QString &source) + : m_activityController(controller) + , m_id(source) +{ + setName(QStringLiteral("activities")); +} + +ServiceJob *ActivityService::createJob(const QString &operation, QMap ¶meters) +{ + return new ActivityJob(m_activityController, m_id, operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/activities/activityservice.h b/plasma/workspace/dataengines/activities/activityservice.h new file mode 100644 index 0000000000..05bec05fef --- /dev/null +++ b/plasma/workspace/dataengines/activities/activityservice.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2010 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "activityengine.h" + +#include +#include + +using namespace Plasma; + +namespace KActivities +{ +class Controller; +} // namespace KActivities + +class ActivityService : public Plasma::Service +{ + Q_OBJECT + +public: + ActivityService(KActivities::Controller *controller, const QString &source); + ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + KActivities::Controller *m_activityController; + QString m_id; +}; diff --git a/plasma/workspace/dataengines/activities/org.kde.ActivityManager.ActivityRanking.xml b/plasma/workspace/dataengines/activities/org.kde.ActivityManager.ActivityRanking.xml new file mode 100644 index 0000000000..164a89d9ed --- /dev/null +++ b/plasma/workspace/dataengines/activities/org.kde.ActivityManager.ActivityRanking.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/activities/plasma-dataengine-activities.json b/plasma/workspace/dataengines/activities/plasma-dataengine-activities.json new file mode 100644 index 0000000000..f98258504e --- /dev/null +++ b/plasma/workspace/dataengines/activities/plasma-dataengine-activities.json @@ -0,0 +1,143 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani Armitage", + "Name[ar]": "Chani Armitage", + "Name[az]": "Chani Armitage", + "Name[ca]": "Chani Armitage", + "Name[cs]": "Chani Armitage", + "Name[de]": "Chani Armitage", + "Name[en_GB]": "Chani Armitage", + "Name[es]": "Chani Armitage", + "Name[eu]": "Chani Armitage", + "Name[fi]": "Chani Armitage", + "Name[fr]": "Chani Armitage", + "Name[hu]": "Chani Armitage", + "Name[ia]": "Chani Armitage", + "Name[it]": "Chani Armitage", + "Name[ko]": "Chani Armitage", + "Name[lt]": "Chani Armitage", + "Name[nl]": "Chani Armitage", + "Name[nn]": "Chani Armitage", + "Name[pl]": "Chani Armitage", + "Name[pt_BR]": "Chani Armitage", + "Name[ro]": "Chani Armitage", + "Name[ru]": "Chani Armitage", + "Name[sk]": "Chani Armitage", + "Name[sl]": "Chani Armitage", + "Name[sv]": "Chani Armitage", + "Name[tr]": "Chani Armitage", + "Name[uk]": "Chani Armitage", + "Name[vi]": "Chani Armitage", + "Name[x-test]": "xxChani Armitagexx", + "Name[zh_CN]": "Chani Armitage" + } + ], + "Category": "", + "Description": "Information on Plasma Activities", + "Description[ar]": "معلومات أنشطة بلازما", + "Description[az]": "Plasma İş Otaqları məlumatları", + "Description[ca]": "Informació quant a activitats del Plasma", + "Description[cs]": "Informace o Plasma aktivitách.", + "Description[de]": "Informationen über Plasma-Aktivitäten", + "Description[en_GB]": "Information on Plasma Activities", + "Description[es]": "Información sobre las actividades de Plasma", + "Description[eu]": "Plasmaren jarduerei buruzko informazioa", + "Description[fi]": "Tietoa Plasma-aktiviteeteista", + "Description[fr]": "Informations sur les activités de Plasma", + "Description[hu]": "Információk a Plasma aktivitásokról", + "Description[ia]": "Information re activitates de Plasma", + "Description[it]": "Informazioni sulle attività di Plasma", + "Description[ko]": "Plasma 활동 정보", + "Description[lt]": "Informacija apie Plasma veiklas", + "Description[nl]": "Informatie over Plasma activiteiten", + "Description[nn]": "Informasjon om Plasma-aktivitetar", + "Description[pa]": "ਪਲਾਜ਼ਮਾ ਸਰਗਰਮੀਆਂ ਲਈ ਜਾਣਕਾਰੀ", + "Description[pl]": "Wyświetla informacje o aktywnościach Plazmy", + "Description[pt_BR]": "Informações sobre as atividades do Plasma", + "Description[ro]": "Informații despre activitățile Plasma", + "Description[ru]": "Сведения о комнатах Plasma", + "Description[sk]": "Informácie o Plasma aktivitách", + "Description[sl]": "Informacije o dejavnostih za Plasmo", + "Description[sv]": "Information om aktiviteter i Plasma", + "Description[ta]": "பிளாஸ்மா செயல்பாடுகளை பற்றிய விவரங்கள்", + "Description[tr]": "Plasma Etkinlikleri Üzerine Bilgiler", + "Description[uk]": "Інформація про простори дій Плазми", + "Description[vi]": "Thông tin về chức năng Hoạt động của Plasma", + "Description[x-test]": "xxInformation on Plasma Activitiesxx", + "Description[zh_CN]": "关于 Plasma 活动的信息", + "EnabledByDefault": true, + "Icon": "preferences-desktop-activities", + "Id": "org.kde.activities", + "License": "LGPL", + "Name": "Activities Engine", + "Name[ar]": "محرّك الأنشطة", + "Name[ast]": "Motor d'actividaes", + "Name[az]": "İş Otağı üçün məlumat mənbəyi", + "Name[bg]": "Ядро за дейности", + "Name[bs]": "Motor aktivnosti", + "Name[ca@valencia]": "Motor d'activitats", + "Name[ca]": "Motor d'activitats", + "Name[cs]": "Stroj aktivit", + "Name[da]": "Aktivitetsmotor", + "Name[de]": "Aktivitätenverwaltung", + "Name[el]": "Μηχανή δραστηριοτήτων", + "Name[en_GB]": "Activities Engine", + "Name[es]": "Motor de actividades", + "Name[et]": "Tegevuste mootor", + "Name[eu]": "Jarduera-motorra", + "Name[fi]": "Aktiviteettimoottori", + "Name[fr]": "Moteur d'activités", + "Name[ga]": "Inneall Gníomhaíochta", + "Name[gl]": "Motor de actividades", + "Name[he]": "מנוע הפעילויות", + "Name[hi]": "गतिविधियाँ इंजन", + "Name[hr]": "Mehanizam aktivnosti", + "Name[hu]": "Aktivitások modul", + "Name[ia]": "Motor de activitate", + "Name[id]": "Mesin Activities", + "Name[is]": "Virknivél", + "Name[it]": "Motore delle attività", + "Name[ja]": "アクティビティエンジン", + "Name[kk]": "Белсенділік тетігі", + "Name[km]": "ម៉ាស៊ីន​សកម្មភាព", + "Name[kn]": "ಚಟುವಟಿಕೆ ಎಂಜಿನ್", + "Name[ko]": "활동 엔진", + "Name[lt]": "Veiklų variklis", + "Name[lv]": "Aktivitāšu dzinējs", + "Name[ml]": "ആക്ടിവിറ്റീസ് എന്‍ജിന്‍", + "Name[mr]": "कार्यपध्दती इंजिन", + "Name[nb]": "Aktivitetsmotor", + "Name[nds]": "Aktivitetenpleger", + "Name[nl]": "Activiteiten-engine", + "Name[nn]": "Aktivitetsmotor", + "Name[pa]": "ਸਰਗਰਮੀ ਇੰਜਣ", + "Name[pl]": "Silnik aktywności", + "Name[pt]": "Motor de Actividades", + "Name[pt_BR]": "Mecanismo de atividades", + "Name[ro]": "Motor de activități", + "Name[ru]": "Источник данных для комнат", + "Name[sk]": "Nástroj aktivít", + "Name[sl]": "Pogon za dejavnosti", + "Name[sr@ijekavian]": "мотор активности", + "Name[sr@ijekavianlatin]": "motor aktivnosti", + "Name[sr@latin]": "motor aktivnosti", + "Name[sr]": "мотор активности", + "Name[sv]": "Aktivitetsgränssnitt", + "Name[ta]": "செயல்பாட்டு பின்னணி சேவை", + "Name[tg]": "Низоми фаъолиятҳо", + "Name[th]": "กลไกจัดการกิจกรรม", + "Name[tr]": "Etkinlik Motoru", + "Name[ug]": "پائالىيەت ماتورى", + "Name[uk]": "Рушій просторів дій", + "Name[vi]": "Dụng cụ \"Hoạt động\"", + "Name[wa]": "Moteur d' activités", + "Name[x-test]": "xxActivities Enginexx", + "Name[zh_CN]": "活动引擎", + "Name[zh_TW]": "活動引擎", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-Plasma-EngineName": "activities" +} diff --git a/plasma/workspace/dataengines/applicationjobs/CMakeLists.txt b/plasma/workspace/dataengines/applicationjobs/CMakeLists.txt new file mode 100644 index 0000000000..55cbd73972 --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/CMakeLists.txt @@ -0,0 +1,21 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_applicationjobs\") + +set(kuiserver_engine_SRCS + kuiserverengine.cpp + jobcontrol.cpp + jobaction.cpp +) + +add_library(plasma_engine_applicationjobs MODULE ${kuiserver_engine_SRCS}) +target_link_libraries(plasma_engine_applicationjobs + Qt::DBus + KF5::CoreAddons + KF5::I18n + KF5::KIOCore + KF5::Plasma + KF5::Service + PW::LibNotificationManager +) + +install(TARGETS plasma_engine_applicationjobs DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) +install(FILES applicationjobs.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/applicationjobs/Messages.sh b/plasma/workspace/dataengines/applicationjobs/Messages.sh new file mode 100644 index 0000000000..3bcfd09fd7 --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_applicationjobs.pot diff --git a/plasma/workspace/dataengines/applicationjobs/applicationjobs.operations b/plasma/workspace/dataengines/applicationjobs/applicationjobs.operations new file mode 100644 index 0000000000..5b8f5e615c --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/applicationjobs.operations @@ -0,0 +1,8 @@ + + + + + + + diff --git a/plasma/workspace/dataengines/applicationjobs/jobaction.cpp b/plasma/workspace/dataengines/applicationjobs/jobaction.cpp new file mode 100644 index 0000000000..291c8d62be --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/jobaction.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "jobaction.h" + +#include +#include +#include + +void JobAction::start() +{ + qDebug() << "Trying to perform the action" << operationName(); + + if (!m_job) { + setErrorText(i18nc("%1 is the subject (can be anything) upon which the job is performed", "The JobView for %1 cannot be found", destination())); + setError(-1); + emitResult(); + return; + } + + // TODO: check with capabilities before performing actions. + if (operationName() == QLatin1String("resume")) { + m_job->resume(); + } else if (operationName() == QLatin1String("suspend")) { + m_job->suspend(); + } else if (operationName() == QLatin1String("stop")) { + m_job->kill(); + } + + emitResult(); +} diff --git a/plasma/workspace/dataengines/applicationjobs/jobaction.h b/plasma/workspace/dataengines/applicationjobs/jobaction.h new file mode 100644 index 0000000000..6b1b338a7e --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/jobaction.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "kuiserverengine.h" + +#include + +#include + +#include "job.h" + +namespace NotificationManager +{ +class Job; +} + +class JobAction : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + JobAction(NotificationManager::Job *job, const QString &operation, QMap ¶meters, QObject *parent = nullptr) + : ServiceJob(KuiserverEngine::sourceName(job), operation, parameters, parent) + , m_job(job) + { + } + + void start() override; + +private: + QPointer m_job; +}; diff --git a/plasma/workspace/dataengines/applicationjobs/jobcontrol.cpp b/plasma/workspace/dataengines/applicationjobs/jobcontrol.cpp new file mode 100644 index 0000000000..fabbe2fdd6 --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/jobcontrol.cpp @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "jobcontrol.h" +#include "jobaction.h" +#include "kuiserverengine.h" + +using namespace NotificationManager; + +JobControl::JobControl(QObject *parent, Job *job) + : Plasma::Service(parent) + , m_job(job) +{ + setName(QStringLiteral("applicationjobs")); + setDestination(KuiserverEngine::sourceName(job)); +} + +Plasma::ServiceJob *JobControl::createJob(const QString &operation, QMap ¶meters) +{ + return new JobAction(m_job, operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/applicationjobs/jobcontrol.h b/plasma/workspace/dataengines/applicationjobs/jobcontrol.h new file mode 100644 index 0000000000..7505894d3c --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/jobcontrol.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include + +#include "job.h" + +class JobControl : public Plasma::Service +{ + Q_OBJECT + +public: + JobControl(QObject *parent, NotificationManager::Job *job); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + QPointer m_job; +}; diff --git a/plasma/workspace/dataengines/applicationjobs/kuiserverengine.cpp b/plasma/workspace/dataengines/applicationjobs/kuiserverengine.cpp new file mode 100644 index 0000000000..a6edbc10ab --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/kuiserverengine.cpp @@ -0,0 +1,262 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "kuiserverengine.h" +#include "jobcontrol.h" + +#include + +#include +#include +#include + +#include "notifications.h" + +#include + +using namespace NotificationManager; + +KuiserverEngine::KuiserverEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + init(); +} + +KuiserverEngine::~KuiserverEngine() +{ +} + +QString KuiserverEngine::sourceName(Job *job) +{ + return QStringLiteral("Job %1").arg(job->id()); +} + +uint KuiserverEngine::jobId(const QString &sourceName) +{ + return sourceName.midRef(4 /*length of Job + space*/).toUInt(); +} + +Plasma::Service *KuiserverEngine::serviceForSource(const QString &source) +{ + const uint id = jobId(source); + if (!id) { + return DataEngine::serviceForSource(source); + } + + auto it = std::find_if(m_jobs.constBegin(), m_jobs.constBegin(), [&id](Job *job) { + return job->id() == id; + }); + + if (it == m_jobs.constEnd()) { + return DataEngine::serviceForSource(source); + } + + return new JobControl(this, *it); +} + +void KuiserverEngine::init() +{ + m_jobsModel = JobsModel::createJobsModel(); + // TODO see if this causes any issues when/if other processes are using applicationjobs engine, e.g. Latte Dock + m_jobsModel->init(); + + connect(m_jobsModel.data(), &Notifications::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { + for (int i = first; i <= last; ++i) { + const QModelIndex idx = m_jobsModel->index(first, 0, parent); + Job *job = idx.data(Notifications::JobDetailsRole).value(); + registerJob(job); + } + }); + + connect(m_jobsModel.data(), &Notifications::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) { + for (int i = first; i <= last; ++i) { + const QModelIndex idx = m_jobsModel->index(first, 0, parent); + Job *job = idx.data(Notifications::JobDetailsRole).value(); + removeJob(job); + } + }); +} + +void KuiserverEngine::updateDescriptionField(Job *job, int number, QString (Job::*labelGetter)() const, QString (Job::*valueGetter)() const) +{ + const QString source = sourceName(job); + const QString labelString = QStringLiteral("label%1").arg(number); + const QString labelNameString = QStringLiteral("labelName%1").arg(number); + const QString labelFileNameString = QStringLiteral("labelFileName%1").arg(number); + + const QString label = ((job)->*labelGetter)(); + const QString value = ((job)->*valueGetter)(); + + if (label.isEmpty() && value.isEmpty()) { + setData(source, labelString, QVariant()); + setData(source, labelNameString, QVariant()); + setData(source, labelFileNameString, QVariant()); + } else { + setData(source, labelNameString, label); + setData(source, labelString, value); + + const QUrl url = QUrl::fromUserInput(value, QString(), QUrl::AssumeLocalFile); + setData(source, labelFileNameString, url.toString(QUrl::PreferLocalFile | QUrl::RemoveFragment | QUrl::RemoveQuery)); + } + setData(source, labelString); +} + +void KuiserverEngine::updateUnit(Job *job, + int number, + const QString &unit, + qulonglong (NotificationManager::Job::*processedGetter)() const, + qulonglong (NotificationManager::Job::*totalGetter)() const) +{ + const QString source = sourceName(job); + + setData(source, QStringLiteral("totalUnit%1").arg(number), unit); + setData(source, QStringLiteral("totalAmount%1").arg(number), ((job)->*totalGetter)()); + setData(source, QStringLiteral("processedUnit%1").arg(number), unit); + setData(source, QStringLiteral("processedAmount%1").arg(number), ((job)->*processedGetter)()); +} + +void KuiserverEngine::registerJob(Job *job) +{ + if (m_jobs.contains(job)) { // shouldn't really happen + return; + } + + const QString source = sourceName(job); + + setData(source, QStringLiteral("appName"), job->desktopEntry()); // job->applicationName()); + setData(source, QStringLiteral("appIconName"), job->applicationIconName()); + setData(source, QStringLiteral("suspendable"), job->suspendable()); + setData(source, QStringLiteral("killable"), job->killable()); + updateState(job); + + connect(job, &Job::stateChanged, this, [this, job] { + updateState(job); + }); + connect(job, &Job::speedChanged, this, [this, job] { + updateEta(job); + }); + + connectJobField(job, &Job::summary, &Job::summaryChanged, QStringLiteral("infoMessage")); + connectJobField(job, &Job::percentage, &Job::percentageChanged, QStringLiteral("percentage")); + connectJobField(job, &Job::error, &Job::errorChanged, QStringLiteral("error")); + connectJobField(job, &Job::errorText, &Job::errorTextChanged, QStringLiteral("errorText")); + connectJobField(job, &Job::destUrl, &Job::destUrlChanged, QStringLiteral("destUrl")); + + static const struct { + int number; + QString (Job::*labelGetter)() const; + void (Job::*labelSignal)(); + QString (Job::*valueGetter)() const; + void (Job::*valueSignal)(); + } s_descriptionFields[] = { + {0, &Job::descriptionLabel1, &Job::descriptionLabel1Changed, &Job::descriptionValue1, &Job::descriptionValue1Changed}, + {1, &Job::descriptionLabel2, &Job::descriptionLabel2Changed, &Job::descriptionValue2, &Job::descriptionValue2Changed}, + }; + + for (auto fields : s_descriptionFields) { + updateDescriptionField(job, fields.number, fields.labelGetter, fields.valueGetter); + connect(job, fields.labelSignal, this, [=] { + updateDescriptionField(job, fields.number, fields.labelGetter, fields.valueGetter); + }); + connect(job, fields.valueSignal, this, [=] { + updateDescriptionField(job, fields.number, fields.labelGetter, fields.valueGetter); + }); + } + + static const struct { + // Previously the dataengine counted units up but for simplicity a fixed number is assigned to each unit + int number; + QString unit; + qulonglong (Job::*processedGetter)() const; + void (Job::*processedSignal)(); + qulonglong (Job::*totalGetter)() const; + void (Job::*totalSignal)(); + } s_unitsFields[] = { + {0, QStringLiteral("bytes"), &Job::processedBytes, &Job::processedBytesChanged, &Job::totalBytes, &Job::totalBytesChanged}, + {1, QStringLiteral("files"), &Job::processedFiles, &Job::processedFilesChanged, &Job::totalFiles, &Job::totalFilesChanged}, + {2, QStringLiteral("dirs"), &Job::processedDirectories, &Job::processedDirectoriesChanged, &Job::totalDirectories, &Job::totalDirectoriesChanged}}; + + for (auto fields : s_unitsFields) { + updateUnit(job, fields.number, fields.unit, fields.processedGetter, fields.totalGetter); + connect(job, fields.processedSignal, this, [=] { + updateUnit(job, fields.number, fields.unit, fields.processedGetter, fields.totalGetter); + }); + connect(job, fields.totalSignal, this, [=] { + updateUnit(job, fields.number, fields.unit, fields.processedGetter, fields.totalGetter); + }); + } + + m_jobs.append(job); +} + +void KuiserverEngine::removeJob(Job *job) +{ + if (!job || !m_jobs.contains(job)) { + return; + } + + m_jobs.removeOne(job); + + const QString source = sourceName(job); + removeSource(source); +} + +QString KuiserverEngine::speedString(qulonglong speed) +{ + return i18nc("Bytes per second", "%1/s", KFormat().formatByteSize(speed)); +} + +void KuiserverEngine::updateState(Job *job) +{ + const QString source = sourceName(job); + + QString stateString; + switch (job->state()) { + case Notifications::JobStateRunning: + stateString = QStringLiteral("running"); + updateSpeed(job); + break; + case Notifications::JobStateSuspended: + stateString = QStringLiteral("suspended"); + setData(source, QStringLiteral("speed"), QVariant()); + setData(source, QStringLiteral("numericSpeed"), QVariant()); + break; + case Notifications::JobStateStopped: + stateString = QStringLiteral("stopped"); + break; + } + + setData(source, QStringLiteral("state"), stateString); + + if (job->state() == Notifications::JobStateStopped) { + removeJob(job); + } +} + +void KuiserverEngine::updateSpeed(Job *job) +{ + const QString source = sourceName(job); + setData(source, QStringLiteral("speed"), speedString(job->speed())); + setData(source, QStringLiteral("numericSpeed"), job->speed()); + updateEta(job); +} + +void KuiserverEngine::updateEta(Job *job) +{ + const QString source = sourceName(job); + + if (job->speed() < 1 || job->totalBytes() < 1) { + setData(source, QStringLiteral("eta"), 0); + return; + } + + const qlonglong remaining = 1000 * (job->totalBytes() - job->processedBytes()); + setData(source, QStringLiteral("eta"), remaining / job->speed()); +} + +K_PLUGIN_CLASS_WITH_JSON(KuiserverEngine, "plasma-dataengine-applicationjobs.json") + +#include "kuiserverengine.moc" diff --git a/plasma/workspace/dataengines/applicationjobs/kuiserverengine.h b/plasma/workspace/dataengines/applicationjobs/kuiserverengine.h new file mode 100644 index 0000000000..30191aa4ec --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/kuiserverengine.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include +#include + +#include "jobsmodel.h" + +namespace NotificationManager +{ +class Job; +} + +namespace Plasma +{ +class Service; +} // namespace Plasma + +class KuiserverEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + KuiserverEngine(QObject *parent, const QVariantList &args); + ~KuiserverEngine() override; + + void init(); + + Plasma::Service *serviceForSource(const QString &source) override; + + static QString sourceName(NotificationManager::Job *job); + static uint jobId(const QString &sourceName); + +private: + template + void connectJobField(NotificationManager::Job *job, T (NotificationManager::Job::*getter)() const, signal changeSignal, const QString &targetFieldName) + { + // Set value initially in case we missed the first change + const QString source = sourceName(job); + setData(source, targetFieldName, ((job)->*getter)()); + // and then listen for changes + connect(job, changeSignal, this, [=] { + setData(source, targetFieldName, ((job)->*getter)()); + }); + } + + void updateDescriptionField(NotificationManager::Job *job, + int number, + QString (NotificationManager::Job::*labelGetter)() const, + QString (NotificationManager::Job::*valueGetter)() const); + + void updateUnit(NotificationManager::Job *job, + int number, + const QString &unit, + qulonglong (NotificationManager::Job::*processedGetter)() const, + qulonglong (NotificationManager::Job::*totalGetter)() const); + + void registerJob(NotificationManager::Job *job); + void removeJob(NotificationManager::Job *job); + + static QString speedString(qulonglong speed); + + void updateState(NotificationManager::Job *job); + void updateSpeed(NotificationManager::Job *job); + void updateEta(NotificationManager::Job *job); + + NotificationManager::JobsModel::Ptr m_jobsModel; + + QVector m_jobs; +}; diff --git a/plasma/workspace/dataengines/applicationjobs/plasma-dataengine-applicationjobs.json b/plasma/workspace/dataengines/applicationjobs/plasma-dataengine-applicationjobs.json new file mode 100644 index 0000000000..feebb2585b --- /dev/null +++ b/plasma/workspace/dataengines/applicationjobs/plasma-dataengine-applicationjobs.json @@ -0,0 +1,117 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Application job updates (via kuiserver)", + "Description[ar]": "تحديثات مهمّة التطبيق (عبر kuiserver)", + "Description[az]": "Tətbiq Tapşırığı yenilənmələri (kuiserver ilə)", + "Description[ca]": "Actualització de treballs d'aplicacions (via kuiserver)", + "Description[cs]": "Aktualizace úloh aplikací (pomocí kuiserver)", + "Description[de]": "Aktualisierung von Anwendungs-Aktionen (via kuiserver)", + "Description[en_GB]": "Application job updates (via kuiserver)", + "Description[es]": "Actualizaciones de la aplicación (vía kuiserver)", + "Description[eu]": "Aplikazio-lanen eguneratzea (kuiserver bidez)", + "Description[fi]": "Sovellustyöpäivitykset (kuiserverin kautta)", + "Description[fr]": "Mises à jours des tâches d'applications (grâce à « kuiserver »)", + "Description[hu]": "Feladatfrissítő (a kuiserveren keresztül)", + "Description[ia]": "Actualisationes de travalio de application (via kuiserver)", + "Description[it]": "Aggiornamenti sui processi delle applicazioni (con kuiserver)", + "Description[ko]": "프로그램 작업 업데이트 (kuiserver를 통하여)", + "Description[lt]": "Programų darbų atnaujinimai (per kuiserver)", + "Description[nl]": "Applicatiejob bijwerken (via kuiserver)", + "Description[nn]": "Oppdateringar for programjobbar (gjennom kuiserver)", + "Description[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਜਾਬ ਅੱਪਡੇਟ (kuiserver ਰਾਹੀਂ)", + "Description[pl]": "Wyświetla uaktualnienia zadania programu (przez kuiserver)", + "Description[pt_BR]": "Atualizações de tarefas de aplicativos (via kuiserver)", + "Description[ro]": "Actualizări pentru sarcina aplicației (via kuiserver)", + "Description[ru]": "Обновления о заданиях в приложениях (данные от kuiserver)", + "Description[sk]": "Aktualizácie úloh aplikácií (pomocou kuiserver)", + "Description[sl]": "Osvežitve poslov (prek programa kuiserver)", + "Description[sv]": "Uppdateringar av information om programjobb (via kuiserver)", + "Description[ta]": "செயலி பணி தகவல்கள் (kuiserver மூலம்)", + "Description[tr]": "Uygulama görevi güncellemeleri (kuiserver ile)", + "Description[uk]": "Оновлення завдань програми (через kuiserver)", + "Description[vi]": "Các cập nhật về công việc của ứng dụng (thông qua kuiserver)", + "Description[x-test]": "xxApplication job updates (via kuiserver)xx", + "Description[zh_CN]": "程序任务更新信息 (通过 kuiserver)", + "Icon": "tool-animator", + "Id": "applicationjobs", + "Name": "Application Job Information", + "Name[ar]": "معلومات مهمّة التطبيق", + "Name[az]": "Tətbiq Tapşırığı Məlumatları", + "Name[be@latin]": "Źviestki pra zadańnie aplikacyi", + "Name[bs]": "Podaci o poslovima programâ", + "Name[ca@valencia]": "Informació de les tasques de les aplicacions", + "Name[ca]": "Informació de les tasques de les aplicacions", + "Name[cs]": "Informace o práci aplikace", + "Name[csb]": "Wëdowiédzô ò robòce aplikacëjów", + "Name[da]": "Information om programjob", + "Name[de]": "Anwendungsaufgaben-Informationen", + "Name[el]": "Πληροφορίες εργασιών εφαρμογών", + "Name[en_GB]": "Application Job Information", + "Name[eo]": "Aplikaĵaj Taskaj Informoj", + "Name[es]": "Información de tarea de aplicación", + "Name[et]": "Rakenduste tööde teave", + "Name[eu]": "Aplikazioko lanei buruzko informazioa", + "Name[fi]": "Sovellustyötiedot", + "Name[fr]": "Informations sur les tâches d'applications en cours d'exécution", + "Name[fy]": "Programma taak ynformaasje", + "Name[ga]": "Eolas Faoi Jabanna Feidhmchláir", + "Name[gl]": "Información de tarefas de aplicación", + "Name[gu]": "કાર્યક્રમ કાર્ય માહિતી", + "Name[he]": "מידע אודות עבודת יישום", + "Name[hi]": "अनुप्रयोग कार्य जानकारी", + "Name[hne]": "अनुपरयोग काम सूचना", + "Name[hr]": "Informacije o poslovima aplikacija", + "Name[hu]": "Az alkalmazások jellemzői", + "Name[ia]": "Information de travalio de application", + "Name[id]": "Informasi Job Aplikasi", + "Name[is]": "Tilkynningar um verkefni forrita", + "Name[it]": "Informazioni sui processi delle applicazioni", + "Name[ja]": "アプリケーションのジョブ情報", + "Name[kk]": "Қолданба тапсырмасының ақпараты", + "Name[km]": "ព័ត៌មាន​ការងារ​របស់​កម្មវិធី", + "Name[kn]": "ಅನ್ವಯ ಕಾರ್ಯ ಮಾಹಿತಿ", + "Name[ko]": "프로그램 작업 정보", + "Name[ku]": "Agahiyên Karê Sepanê", + "Name[lt]": "Programos darbo informacija", + "Name[lv]": "Programmas darba informācija", + "Name[ml]": "പ്രയോഗ ജോലി അറിയിപ്പുകള്‍", + "Name[mr]": "अनुप्रयोग कार्यविषयी माहिती", + "Name[nb]": "Informasjon om programjobb", + "Name[nds]": "Programm-Opgaavinformatschoon", + "Name[nl]": "Programmataakmeldingen", + "Name[nn]": "Informasjon om programjobb", + "Name[or]": "ପ୍ରୟୋଗ କାର୍ଯ୍ୟ ସୂଚନା", + "Name[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਜਾਬ ਜਾਣਕਾਰੀ", + "Name[pl]": "Informacje o zadaniu programu", + "Name[pt]": "Informação da Tarefa da Aplicação", + "Name[pt_BR]": "Informações da tarefa do aplicativo", + "Name[ro]": "Informație despre sarcina aplicației", + "Name[ru]": "Задания в приложениях", + "Name[si]": "යෙදුම් කාර්‍යය තොරතුරු", + "Name[sk]": "Informácie o aplikácii", + "Name[sl]": "Informacije o poslih", + "Name[sr@ijekavian]": "подаци о пословима програма̂", + "Name[sr@ijekavianlatin]": "podaci o poslovima programâ̂", + "Name[sr@latin]": "podaci o poslovima programâ̂", + "Name[sr]": "подаци о пословима програма̂", + "Name[sv]": "Information om programjobb", + "Name[ta]": "செயலி பணி விவரம்", + "Name[th]": "ข้อมูลงานของโปรแกรม", + "Name[tr]": "Uygulama Görev Bilgileri", + "Name[ug]": "پروگرامما خىزمەت ئۇچۇرى", + "Name[uk]": "Відомості про завдання програми", + "Name[vi]": "Thông tin về công việc của ứng dụng", + "Name[wa]": "Pondants et djondants so les bouyes des programes", + "Name[x-test]": "xxApplication Job Informationxx", + "Name[zh_CN]": "程序任务信息", + "Name[zh_TW]": "應用程式工作資訊", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/apps/CMakeLists.txt b/plasma/workspace/dataengines/apps/CMakeLists.txt new file mode 100644 index 0000000000..655b6ee747 --- /dev/null +++ b/plasma/workspace/dataengines/apps/CMakeLists.txt @@ -0,0 +1,18 @@ +set(apps_engine_SRCS + appsengine.cpp + appsource.cpp + appservice.cpp + appjob.cpp +) + +add_library(plasma_engine_apps MODULE ${apps_engine_SRCS}) + +target_link_libraries(plasma_engine_apps + KF5::Plasma + KF5::Service + KF5::KIOCore + KF5::KIOWidgets +) + +install(TARGETS plasma_engine_apps DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) +install(FILES apps.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/apps/appjob.cpp b/plasma/workspace/dataengines/apps/appjob.cpp new file mode 100644 index 0000000000..9ab78387e4 --- /dev/null +++ b/plasma/workspace/dataengines/apps/appjob.cpp @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "appjob.h" + +#include + +AppJob::AppJob(AppSource *source, const QString &operation, QMap ¶meters, QObject *parent) + : ServiceJob(source->objectName(), operation, parameters, parent) + , m_source(source) +{ +} + +AppJob::~AppJob() +{ +} + +void AppJob::start() +{ + const QString operation = operationName(); + if (operation == QLatin1String("launch")) { + auto job = new KIO::ApplicationLauncherJob(m_source->getApp()); + job->start(); + setResult(true); + return; + } + setResult(false); +} diff --git a/plasma/workspace/dataengines/apps/appjob.h b/plasma/workspace/dataengines/apps/appjob.h new file mode 100644 index 0000000000..1640f4b98c --- /dev/null +++ b/plasma/workspace/dataengines/apps/appjob.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include + +// own +#include "appsource.h" + +class AppJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + AppJob(AppSource *source, const QString &operation, QMap ¶meters, QObject *parent = nullptr); + ~AppJob() override; + +protected: + void start() override; + +private: + AppSource *m_source; +}; diff --git a/plasma/workspace/dataengines/apps/apps.operations b/plasma/workspace/dataengines/apps/apps.operations new file mode 100644 index 0000000000..837549f89d --- /dev/null +++ b/plasma/workspace/dataengines/apps/apps.operations @@ -0,0 +1,5 @@ + + + + + diff --git a/plasma/workspace/dataengines/apps/appsengine.cpp b/plasma/workspace/dataengines/apps/appsengine.cpp new file mode 100644 index 0000000000..0fc3fa8312 --- /dev/null +++ b/plasma/workspace/dataengines/apps/appsengine.cpp @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "appsengine.h" +#include "appsource.h" + +#include + +AppsEngine::AppsEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + Q_UNUSED(args); + init(); +} + +AppsEngine::~AppsEngine() +{ +} + +void AppsEngine::init() +{ + addGroup(KServiceGroup::root()); + connect(KSycoca::self(), &KSycoca::databaseChanged, this, [this]() { + removeAllSources(); + addGroup(KServiceGroup::root()); + }); +} + +Plasma::Service *AppsEngine::serviceForSource(const QString &name) +{ + AppSource *source = dynamic_cast(containerForSource(name)); + // if source does not exist, return null service + if (!source) { + return Plasma::DataEngine::serviceForSource(name); + } + + // if source represents a group or something, return null service + if (!source->isApp()) { + return Plasma::DataEngine::serviceForSource(name); + } + // if source represent a proper app, return real service + Plasma::Service *service = source->createService(); + service->setParent(this); + return service; +} + +void AppsEngine::addGroup(KServiceGroup::Ptr group) +{ + if (!(group && group->isValid())) { + return; + } + AppSource *appSource = new AppSource(group, this); + // TODO listen for changes + addSource(appSource); + // do children + foreach (const KServiceGroup::Ptr &subGroup, group->groupEntries(KServiceGroup::NoOptions)) { + addGroup(subGroup); + } + foreach (const KService::Ptr &app, group->serviceEntries(KServiceGroup::NoOptions)) { + addApp(app); + } +} + +void AppsEngine::addApp(KService::Ptr app) +{ + AppSource *appSource = new AppSource(app, this); + // TODO listen for changes + addSource(appSource); +} + +K_PLUGIN_CLASS_WITH_JSON(AppsEngine, "plasma-dataengine-apps.json") + +#include "appsengine.moc" diff --git a/plasma/workspace/dataengines/apps/appsengine.h b/plasma/workspace/dataengines/apps/appsengine.h new file mode 100644 index 0000000000..2687eced8d --- /dev/null +++ b/plasma/workspace/dataengines/apps/appsengine.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include +#include + +#include +#include + +/** + * Apps Data Engine + * + * FIXME + * This engine provides information regarding tasks (windows that are currently open) + * as well as startup tasks (windows that are about to open). + * Each task and startup is represented by a unique source. Sources are added and removed + * as windows are opened and closed. You cannot request a customized source. + * + * A service is also provided for each task. It exposes some operations that can be + * performed on the windows (ex: maximize, minimize, activate). + * + * The data and operations are provided and handled by the taskmanager library. + * It should be noted that only a subset of data and operations are exposed. + */ +class AppsEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + AppsEngine(QObject *parent, const QVariantList &args); + ~AppsEngine() override; + Plasma::Service *serviceForSource(const QString &name) override; + +protected: + virtual void init(); + +private: + friend class AppSource; + void addGroup(KServiceGroup::Ptr group); + void addApp(KService::Ptr app); +}; diff --git a/plasma/workspace/dataengines/apps/appservice.cpp b/plasma/workspace/dataengines/apps/appservice.cpp new file mode 100644 index 0000000000..39f0165e3c --- /dev/null +++ b/plasma/workspace/dataengines/apps/appservice.cpp @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "appservice.h" + +// own +#include "appjob.h" + +AppService::AppService(AppSource *source) + : Plasma::Service(source) + , m_source(source) +{ + setName(QStringLiteral("apps")); +} + +AppService::~AppService() +{ +} + +Plasma::ServiceJob *AppService::createJob(const QString &operation, QMap ¶meters) +{ + return new AppJob(m_source, operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/apps/appservice.h b/plasma/workspace/dataengines/apps/appservice.h new file mode 100644 index 0000000000..c3f3790893 --- /dev/null +++ b/plasma/workspace/dataengines/apps/appservice.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include +#include + +// own +#include "appsource.h" + +/** + * App Service + */ +class AppService : public Plasma::Service +{ + Q_OBJECT + +public: + explicit AppService(AppSource *source); + ~AppService() override; + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + AppSource *m_source; +}; diff --git a/plasma/workspace/dataengines/apps/appsource.cpp b/plasma/workspace/dataengines/apps/appsource.cpp new file mode 100644 index 0000000000..e6297b98d2 --- /dev/null +++ b/plasma/workspace/dataengines/apps/appsource.cpp @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "appsource.h" +#include "appsengine.h" +#include "appservice.h" + +#include + +AppSource::AppSource(KServiceGroup::Ptr group, QObject *parent) + : Plasma::DataContainer(parent) + , m_group(group) + , m_app() + , m_isApp(false) +{ + setObjectName(m_group->entryPath()); + setData(QStringLiteral("isApp"), false); + updateGroup(); +} + +AppSource::AppSource(KService::Ptr app, QObject *parent) + : Plasma::DataContainer(parent) + , m_group() + , m_app(app) + , m_isApp(true) +{ + setObjectName(m_app->storageId()); + setData(QStringLiteral("isApp"), true); + updateApp(); +} + +AppSource::~AppSource() +{ +} + +Plasma::Service *AppSource::createService() +{ + return new AppService(this); +} + +KService::Ptr AppSource::getApp() +{ + return m_app; +} + +bool AppSource::isApp() const +{ + return m_isApp; +} + +void AppSource::updateGroup() +{ + setData(QStringLiteral("iconName"), m_group->icon()); + setData(QStringLiteral("name"), m_group->caption()); + setData(QStringLiteral("comment"), m_group->comment()); + setData(QStringLiteral("display"), !m_group->noDisplay()); + + QStringList entries; + const auto groupEntries = m_group->entries(true, false, true); + for (const KSycocaEntry::Ptr &p : groupEntries) { + if (p->isType(KST_KService)) { + const KService::Ptr service(static_cast(p.data())); + entries << service->storageId(); + } else if (p->isType(KST_KServiceGroup)) { + const KServiceGroup::Ptr serviceGroup(static_cast(p.data())); + entries << serviceGroup->entryPath(); + } else if (p->isType(KST_KServiceSeparator)) { + entries << QStringLiteral("---"); + } else { + qDebug() << "unexpected object in entry list"; + } + } + setData(QStringLiteral("entries"), entries); + + checkForUpdate(); +} + +void AppSource::updateApp() +{ + setData(QStringLiteral("iconName"), m_app->icon()); + setData(QStringLiteral("name"), m_app->name()); + setData(QStringLiteral("genericName"), m_app->genericName()); + setData(QStringLiteral("menuId"), m_app->menuId()); + setData(QStringLiteral("entryPath"), m_app->entryPath()); + setData(QStringLiteral("comment"), m_app->comment()); + setData(QStringLiteral("keywords"), m_app->keywords()); + setData(QStringLiteral("categories"), m_app->categories()); + setData(QStringLiteral("display"), !m_app->noDisplay()); + checkForUpdate(); +} diff --git a/plasma/workspace/dataengines/apps/appsource.h b/plasma/workspace/dataengines/apps/appsource.h new file mode 100644 index 0000000000..4dfcbe6283 --- /dev/null +++ b/plasma/workspace/dataengines/apps/appsource.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2009 Chani Armitage + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include + +#include +#include + +/** + * App Source + */ +class AppSource : public Plasma::DataContainer +{ + Q_OBJECT + +public: + AppSource(KServiceGroup::Ptr startup, QObject *parent); + AppSource(KService::Ptr app, QObject *parent); + ~AppSource() override; + +protected: + Plasma::Service *createService(); + KService::Ptr getApp(); + bool isApp() const; + +private Q_SLOTS: + void updateGroup(); + void updateApp(); + +private: + friend class AppsEngine; + friend class AppJob; + KServiceGroup::Ptr m_group; + KService::Ptr m_app; + bool m_isApp; +}; diff --git a/plasma/workspace/dataengines/apps/plasma-dataengine-apps.json b/plasma/workspace/dataengines/apps/plasma-dataengine-apps.json new file mode 100644 index 0000000000..799aa2bcaf --- /dev/null +++ b/plasma/workspace/dataengines/apps/plasma-dataengine-apps.json @@ -0,0 +1,147 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "chani@kde.org", + "Name": "Chani", + "Name[ar]": "تشاني", + "Name[az]": "Chani", + "Name[ca]": "Chani", + "Name[cs]": "Chani", + "Name[de]": "Chani", + "Name[en_GB]": "Chani", + "Name[es]": "Chani", + "Name[eu]": "Chani", + "Name[fi]": "Chani", + "Name[fr]": "Chani", + "Name[hu]": "Chani", + "Name[ia]": "Chani", + "Name[it]": "Chani", + "Name[ko]": "Chani", + "Name[lt]": "Chani", + "Name[nl]": "Chani", + "Name[nn]": "Chani", + "Name[pl]": "Chani", + "Name[pt_BR]": "Chani", + "Name[ro]": "Chani", + "Name[ru]": "Chani", + "Name[sk]": "Chani", + "Name[sl]": "Chani", + "Name[sv]": "Chani", + "Name[tr]": "Chani", + "Name[uk]": "Chani", + "Name[vi]": "Chani", + "Name[x-test]": "xxChanixx", + "Name[zh_CN]": "Chani" + } + ], + "Description": "Information and launching of all applications in the app menu.", + "Description[ar]": "معلومات وبدء تشغيل جميع التطبيقات في قائمة التطبيق.", + "Description[az]": "Tətbiq menyusundan bütün tətbiqlər haqqında məlumat və onların başladılması.", + "Description[ca]": "Informació i llançament de totes les aplicacions en el menú d'aplicacions.", + "Description[cs]": "Informace a spouštění všech aplikací v nabídce aplikací.", + "Description[de]": "Informationen und Starten aller Anwendungen im Programmmenü.", + "Description[en_GB]": "Information and launching of all applications in the app menu.", + "Description[es]": "Información y ejecución de todas las aplicaciones en el menú de aplicaciones.", + "Description[eu]": "Aplikazio-menuko aplikazio guztiei buruzko informazioa eta haiek abiaraztea.", + "Description[fi]": "Sovellustietoa ja kaikkien sovellusten käynnistys sovellusvalikosta.", + "Description[fr]": "Informations et démarrage de toutes les applications du menu des applications.", + "Description[hu]": "Az alkalmazásmenü alkalmazásainak indítása és jellemzőinek megjelenítése.", + "Description[ia]": "Information e lanceamento de omne applicationes in le menu de app", + "Description[it]": "Informazioni e avvio di tutte le applicazioni nel loro menu.", + "Description[ko]": "프로그램 메뉴의 정보를 보고 실행할 수 있도록 합니다.", + "Description[lt]": "Visų programų informacija ir paleidimas programų meniu.", + "Description[nl]": "Informatie en starten van alle programma's in het progmenu.", + "Description[nn]": "Informasjon og køyring av alle programma i programmenyen.", + "Description[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਮੇਨ ਵਿੱਚ ਸਭ ਐਪਲੀਕੇਸ਼ਨਾਂ ਲਈ ਜਾਣਕਾਰੀ ਅਤੇ ਚਲਾਉਣਾ।", + "Description[pl]": "Uruchamia dowolne programy z menu i wraz z informacjami o nich.", + "Description[pt_BR]": "Informação e inicialização de todos os aplicativos no menu de aplicativos.", + "Description[ro]": "Informații și lansare de aplicații în meniul aplicațiilor.", + "Description[ru]": "Информация и запуск всех приложений из меню приложений.", + "Description[sk]": "Informácie a spúšťanie všetkých aplikácií v ponuke aplikácie.", + "Description[sl]": "Informacije in zagon vseh programov v programskem meniju.", + "Description[sv]": "Information om och start av alla program i programmenyn.", + "Description[ta]": "செயலி பட்டியில் உள்ள செயலிகளை பற்றிய விவரங்களை வழங்கி அவற்றை இயக்கும்.", + "Description[tr]": "Uygulama menüsündeki uygulamalar bilgisi ve çalıştırma.", + "Description[uk]": "Відомості і запуск всіх програма з меню програм.", + "Description[vi]": "Thông tin và việc khởi chạy của tất cả các ứng dụng trong trình đơn ứng dụng.", + "Description[x-test]": "xxInformation and launching of all applications in the app menu.xx", + "Description[zh_CN]": "显示信息或启动菜单中的所有应用程序。", + "EnabledByDefault": true, + "Icon": "user-desktop", + "Id": "apps", + "License": "LGPL", + "Name": "Application Information", + "Name[ar]": "معلومات التطبيقات", + "Name[az]": "Tətbiq Məlumatları", + "Name[bn]": "অ্যাপলিকেশন তথ্য", + "Name[bs]": "Podaci o programima", + "Name[ca@valencia]": "Informació de les aplicacions", + "Name[ca]": "Informació de les aplicacions", + "Name[cs]": "Informace o aplikaci", + "Name[csb]": "Wëdowiédzô ò aplikacëji", + "Name[da]": "Programinformation", + "Name[de]": "Anwendungsinformationen", + "Name[el]": "Πληροφορίες εφαρμογών", + "Name[en_GB]": "Application Information", + "Name[eo]": "Aplikaĵaj Informoj", + "Name[es]": "Información de la aplicación", + "Name[et]": "Rakenduse teave", + "Name[eu]": "Aplikazioari buruzko informazioa", + "Name[fi]": "Sovellustiedot", + "Name[fr]": "Informations sur l'application", + "Name[fy]": "Applikaasje ynformaasje", + "Name[ga]": "Eolas faoi Fheidhmchláir", + "Name[gl]": "Información da aplicación", + "Name[gu]": "કાર્યક્રમ માહિતી", + "Name[he]": "מידע אודות יישום", + "Name[hi]": "अनुप्रयोग जानकारी", + "Name[hr]": "Informacije o aplikacijama", + "Name[hsb]": "Informacija wo aplikaciji", + "Name[hu]": "Az alkalmazások jellemzői", + "Name[ia]": "Information de application", + "Name[id]": "Informasi Aplikasi", + "Name[is]": "Upplýsingar um forrit", + "Name[it]": "Informazioni sull'applicazione", + "Name[ja]": "アプリケーションの情報", + "Name[kk]": "Қолданбаның ақпараты", + "Name[km]": "ព័ត៌មាន​កម្មវិធី", + "Name[kn]": "ಅನ್ವಯ ಮಾಹಿತಿ", + "Name[ko]": "프로그램 정보", + "Name[lt]": "Programos informacija", + "Name[lv]": "Programmas informācija", + "Name[mk]": "Информации за апликации", + "Name[ml]": "പ്രയോഗ വിവരം", + "Name[mr]": "अनुप्रयोग माहिती", + "Name[nb]": "Informasjon om program", + "Name[nds]": "Programm-Informatschoon", + "Name[nl]": "Programmainformatie", + "Name[nn]": "Programinformasjon", + "Name[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਜਾਣਕਾਰੀ", + "Name[pl]": "Informacje o programie", + "Name[pt]": "Informação da Aplicação", + "Name[pt_BR]": "Informações do aplicativo", + "Name[ro]": "Informații despre aplicații", + "Name[ru]": "Информация о приложениях", + "Name[si]": "යෙදුම් තොරතුරු", + "Name[sk]": "Informácie o aplikácii", + "Name[sl]": "Informacije o programih", + "Name[sr@ijekavian]": "подаци о програмима", + "Name[sr@ijekavianlatin]": "podaci o programima", + "Name[sr@latin]": "podaci o programima", + "Name[sr]": "подаци о програмима", + "Name[sv]": "Information om program", + "Name[ta]": "செயலி விவரம்", + "Name[tg]": "Иттилооти барнома", + "Name[th]": "ข้อมูลของโปรแกรม", + "Name[tr]": "Uygulama Bilgileri", + "Name[ug]": "پروگرامما ئۇچۇرى", + "Name[uk]": "Відомості щодо програм", + "Name[vi]": "Thông tin ứng dụng", + "Name[wa]": "Pondants et djondants do programe", + "Name[x-test]": "xxApplication Informationxx", + "Name[zh_CN]": "应用程序信息", + "Name[zh_TW]": "應用程式資訊", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/devicenotifications/CMakeLists.txt b/plasma/workspace/dataengines/devicenotifications/CMakeLists.txt new file mode 100644 index 0000000000..b7ff97de29 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/CMakeLists.txt @@ -0,0 +1,20 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_devicenotifications\") + +set(device_notifications_engine_SRCS + devicenotificationsengine.cpp + ksolidnotify.cpp +) + +add_library(plasma_engine_devicenotifications MODULE ${device_notifications_engine_SRCS}) + +target_link_libraries(plasma_engine_devicenotifications + KF5::Service + KF5::Plasma + KF5::Solid + KF5::I18n + KSysGuard::ProcessCore + KF5::Notifications +) # todo: add kworkspace once ported + +install(TARGETS plasma_engine_devicenotifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) +install(FILES devicenotifications.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR}) diff --git a/plasma/workspace/dataengines/devicenotifications/Messages.sh b/plasma/workspace/dataengines/devicenotifications/Messages.sh new file mode 100644 index 0000000000..23aa817608 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_devicenotifications.pot diff --git a/plasma/workspace/dataengines/devicenotifications/devicenotifications.notifyrc b/plasma/workspace/dataengines/devicenotifications/devicenotifications.notifyrc new file mode 100644 index 0000000000..32909aa5c7 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/devicenotifications.notifyrc @@ -0,0 +1,121 @@ +[Global] +Name=Device Notifier +Name[ar]=مُخطِر الأجهزة +Name[ast]=Avisador de preseos +Name[az]=Cihaz qoşulmaları bildirişləri +Name[be@latin]=Infarmavańnie pra novyja pryłady +Name[bg]=Уведомяване за нови устройства +Name[bn]=ডিভাইস নোটিফায়ার +Name[bn_IN]=ডিভাইসের সূচনা ব্যবস্থা +Name[bs]=Izveštavač o uređajima +Name[ca]=Notificador de dispositius +Name[ca@valencia]=Notificador de dispositius +Name[cs]=Upozornění na zařízení +Name[csb]=Dôwôcz wiédzë ò ùrządzeniach +Name[da]=Enhedsbekendtgørelse +Name[de]=Geräteüberwachung +Name[el]=Ειδοποίηση νέας συσκευής +Name[en_GB]=Device Notifier +Name[eo]=Atentigilo pri nova aparato +Name[es]=Notificador de dispositivos +Name[et]=Seadmete teadustaja +Name[eu]=Gailu-jakinarazlea +Name[fa]=اخطار دهنده‌ی دستگاه +Name[fi]=Laiteilmoitin +Name[fr]=Notification de nouveau périphérique +Name[fy]=Apparaat meidieler +Name[ga]=Fógróir Gléis +Name[gl]=Notificador de dispositivos +Name[gu]=ઉપકરણ નોંધ કરનાર +Name[he]=מודיע על התקנים +Name[hi]=उपकरण सूचक +Name[hne]=उपकरन बतइया +Name[hr]=Glasnik uređaja +Name[hsb]=Zdźělenki wo gratach +Name[hu]=Eszközértesítő +Name[ia]=Notificator de dispositivo +Name[id]=Penotifikasi Perangkat +Name[is]=Tilkynningar um ný tæki +Name[it]=Notificatore dei dispositivi +Name[ja]=デバイスの通知 +Name[kk]=Құрылғы құлақтандырғышы +Name[km]=កម្មវិធី​ជូនដំណឹង​ឧបករណ៍​ +Name[kn]=ಹೊಸ ಸಾಧನ ಸೂಚಕ +Name[ko]=장치 알림이 +Name[ku]=Hişyarkerê Cîhaz +Name[lt]=Pranešimai apie įrenginius +Name[lv]=Ierīču paziņotājs +Name[mk]=Известувач за уреди +Name[ml]=പുതിയ ഡിവൈസ് അറിയിക്കുന്നതിനുള്ള സംവിധാനം +Name[mr]=साधन निदर्शक +Name[nb]=Enhetsvarsler +Name[nds]=Bescheden över Reedschappen +Name[nl]=Apparaatmelder +Name[nn]=Einingsvarslar +Name[pa]=ਜੰਤਰ ਨੋਟੀਫਾਇਰ +Name[pl]=Powiadomienia o urządzeniach +Name[pt]=Notificações do Dispositivo +Name[pt_BR]=Notificação de dispositivos +Name[ro]=Notificare dispozitive +Name[ru]=Подключаемые устройства +Name[si]=උපාංග දැනුම් දීම +Name[sk]=Monitor zariadení +Name[sl]=Obvestila o napravah +Name[sr]=извештавач о уређајима +Name[sr@ijekavian]=извјештавач о уређајима +Name[sr@ijekavianlatin]=izvještavač o uređajima +Name[sr@latin]=izveštavač o uređajima +Name[sv]=Underrättelse om enheter +Name[ta]=சாதன அறிவிப்பான் +Name[th]=แจ้งให้ทราบถึงอุปกรณ์ +Name[tr]=Aygıt Bildirici +Name[ug]=ئۈسكۈنە ئۇقتۇرۇشى +Name[uk]=Сповіщення про пристрої +Name[vi]=Trình thông báo thiết bị +Name[wa]=Notifiaedje d' éndjin +Name[x-test]=xxDevice Notifierxx +Name[zh_CN]=设备通知器 +Name[zh_TW]=裝置通知 +IconName=device-notifier + +[Event/safelyRemovable] +Name=Device Status +Name[ar]=حالة الأجهزة +Name[az]=Qurğuların statusu +Name[ca]=Estat del dispositiu +Name[cs]=Stav zařízení +Name[da]=Enhedsstatus +Name[de]=Gerätestatus +Name[en_GB]=Device Status +Name[es]=Estado del dispositivo +Name[eu]=Gailuaren egoera +Name[fi]=Laitteen tila +Name[fr]=État du périphérique +Name[hi]=उपकरण स्थिति +Name[hsb]=Staw grata +Name[hu]=Eszközállapot +Name[ia]=Stato de dispositivo +Name[id]=Status Perangkat +Name[it]=Stato dei dispositivi +Name[ko]=장치 상태 +Name[lt]=Įrenginio būsena +Name[ml]=ഉപകരണ നില +Name[nl]=Apparaatstatus +Name[nn]=Einingsstatus +Name[pa]=ਡਿਵਾਈਸ ਹਾਲਤ +Name[pl]=Stan urządzenia +Name[pt]=Estado do Dispositivo +Name[pt_BR]=Status do dispositivo +Name[ro]=Stare dispozitiv +Name[ru]=Состояние устройства +Name[sk]=Stav zariadení +Name[sl]=Stanje naprave +Name[sv]=Enhetsstatus +Name[ta]=சாதன நிலை +Name[tr]=Aygıt Durumu +Name[uk]=Стан пристрою +Name[vi]=Trạng thái thiết bị +Name[x-test]=xxDevice Statusxx +Name[zh_CN]=设备状态 +Sound=Oxygen-Sys-App-Message.ogg +Action=Sound diff --git a/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.cpp b/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.cpp new file mode 100644 index 0000000000..9231765829 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.cpp @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2010 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 Lukáš Tinkl + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "devicenotificationsengine.h" + +#include + +DeviceNotificationsEngine::DeviceNotificationsEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_solidNotify(new KSolidNotify(this)) +{ + connect(m_solidNotify, &KSolidNotify::notify, this, &DeviceNotificationsEngine::notify); + connect(m_solidNotify, &KSolidNotify::clearNotification, this, &DeviceNotificationsEngine::clearNotification); +} + +DeviceNotificationsEngine::~DeviceNotificationsEngine() +{ +} + +void DeviceNotificationsEngine::notify(Solid::ErrorType solidError, const QString &error, const QString &errorDetails, const QString &udi) +{ + const QString source = QStringLiteral("%1 notification").arg(udi); + + Plasma::DataEngine::Data notificationData; + notificationData.insert(QStringLiteral("solidError"), solidError); + notificationData.insert(QStringLiteral("error"), error); + notificationData.insert(QStringLiteral("errorDetails"), errorDetails); + notificationData.insert(QStringLiteral("udi"), udi); + + setData(source, notificationData); +} + +void DeviceNotificationsEngine::clearNotification(const QString &udi) +{ + removeSource(QStringLiteral("%1 notification").arg(udi)); +} + +K_PLUGIN_CLASS_WITH_JSON(DeviceNotificationsEngine, "plasma-dataengine-devicenotifications.json") + +#include "devicenotificationsengine.moc" diff --git a/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.h b/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.h new file mode 100644 index 0000000000..dee5f82966 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/devicenotificationsengine.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2010 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 Lukáš Tinkl + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ksolidnotify.h" + +/** + * Engine which provides data sources for device notifications. + * Each notification is represented by one source. + */ +class DeviceNotificationsEngine : public Plasma::DataEngine +{ + Q_OBJECT +public: + DeviceNotificationsEngine(QObject *parent, const QVariantList &args); + ~DeviceNotificationsEngine() override; + +private Q_SLOTS: + void notify(Solid::ErrorType solidError, const QString &error, const QString &errorDetails, const QString &udi); + void clearNotification(const QString &udi); + +private: + KSolidNotify *m_solidNotify; +}; diff --git a/plasma/workspace/dataengines/devicenotifications/ksolidnotify.cpp b/plasma/workspace/dataengines/devicenotifications/ksolidnotify.cpp new file mode 100644 index 0000000000..0a886e6803 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/ksolidnotify.cpp @@ -0,0 +1,238 @@ +/* + SPDX-FileCopyrightText: 2010 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 Lukáš Tinkl + SPDX-FileCopyrightText: 2016 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "ksolidnotify.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +KSolidNotify::KSolidNotify(QObject *parent) + : QObject(parent) +{ + Solid::Predicate p(Solid::DeviceInterface::StorageAccess); + p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive); + p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer); + const QList &devices = Solid::Device::listFromQuery(p); + for (const Solid::Device &dev : devices) { + m_devices.insert(dev.udi(), dev); + connectSignals(&m_devices[dev.udi()]); + } + + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &KSolidNotify::onDeviceAdded); + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &KSolidNotify::onDeviceRemoved); +} + +void KSolidNotify::onDeviceAdded(const QString &udi) +{ + // Clear any stale message from a previous instance + Q_EMIT clearNotification(udi); + Solid::Device device(udi); + m_devices.insert(udi, device); + connectSignals(&m_devices[udi]); +} + +void KSolidNotify::onDeviceRemoved(const QString &udi) +{ + if (m_devices[udi].is()) { + Solid::StorageAccess *access = m_devices[udi].as(); + if (access) { + disconnect(access, nullptr, this, nullptr); + } + } + m_devices.remove(udi); +} + +bool KSolidNotify::isSafelyRemovable(const QString &udi) const +{ + Solid::Device parent = m_devices[udi].parent(); + if (parent.is()) { + Solid::StorageDrive *drive = parent.as(); + return (!drive->isInUse() && (drive->isHotpluggable() || drive->isRemovable())); + } + + const Solid::StorageAccess *access = m_devices[udi].as(); + if (access) { + return !m_devices[udi].as()->isAccessible(); + } else { + // If this check fails, the device has been already physically + // ejected, so no need to say that it is safe to remove it + return false; + } +} + +void KSolidNotify::connectSignals(Solid::Device *device) +{ + Solid::StorageAccess *access = device->as(); + if (access) { + connect(access, &Solid::StorageAccess::teardownDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) { + onSolidReply(SolidReplyType::Teardown, error, errorData, udi); + }); + + connect(access, &Solid::StorageAccess::setupDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) { + onSolidReply(SolidReplyType::Setup, error, errorData, udi); + }); + } + if (device->is()) { + Solid::OpticalDrive *drive = device->parent().as(); + connect(drive, &Solid::OpticalDrive::ejectDone, this, [=](Solid::ErrorType error, const QVariant &errorData, const QString &udi) { + onSolidReply(SolidReplyType::Eject, error, errorData, udi); + }); + } +} + +void KSolidNotify::queryBlockingApps(const QString &devicePath) +{ + QProcess *p = new QProcess; + connect(p, static_cast(&QProcess::errorOccurred), [=](QProcess::ProcessError) { + Q_EMIT blockingAppsReady({}); + p->deleteLater(); + }); + connect(p, static_cast(&QProcess::finished), [=](int, QProcess::ExitStatus) { + QStringList blockApps; + QString out(p->readAll()); + const QVector pidList = out.splitRef(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); + KSysGuard::Processes procs; + for (const QStringRef &pidStr : pidList) { + int pid = pidStr.toInt(); + if (!pid) { + continue; + } + procs.updateOrAddProcess(pid); + KSysGuard::Process *proc = procs.getProcess(pid); + if (!blockApps.contains(proc->name())) { + blockApps << proc->name(); + } + } + blockApps.removeDuplicates(); + Q_EMIT blockingAppsReady(blockApps); + p->deleteLater(); + }); + p->start(QStringLiteral("lsof"), {QStringLiteral("-t"), devicePath}); + // p.start(QStringLiteral("fuser"), {QStringLiteral("-m"), devicePath}); +} + +void KSolidNotify::onSolidReply(SolidReplyType type, Solid::ErrorType error, const QVariant &errorData, const QString &udi) +{ + if ((error == Solid::ErrorType::NoError) && (type == SolidReplyType::Setup)) { + Q_EMIT clearNotification(udi); + return; + } + + QString errorMsg; + + switch (error) { + case Solid::ErrorType::NoError: + if (type != SolidReplyType::Setup && isSafelyRemovable(udi)) { + KNotification::event(QStringLiteral("safelyRemovable"), i18n("Device Status"), i18n("A device can now be safely removed")); + errorMsg = i18n("This device can now be safely removed."); + } + break; + + case Solid::ErrorType::UnauthorizedOperation: + switch (type) { + case SolidReplyType::Setup: + errorMsg = i18n("You are not authorized to mount this device."); + break; + case SolidReplyType::Teardown: + errorMsg = i18nc("Remove is less technical for unmount", "You are not authorized to remove this device."); + break; + case SolidReplyType::Eject: + errorMsg = i18n("You are not authorized to eject this disc."); + break; + } + + break; + case Solid::ErrorType::DeviceBusy: { + if (type == SolidReplyType::Setup) { // can this even happen? + errorMsg = i18n("Could not mount this device as it is busy."); + } else { + Solid::Device device; + + if (type == SolidReplyType::Eject) { + QString discUdi; + for (Solid::Device device : qAsConst(m_devices)) { + if (device.parentUdi() == udi) { + discUdi = device.udi(); + } + } + + if (discUdi.isNull()) { + // This should not happen, bail out + return; + } + + device = Solid::Device(discUdi); + } else { + device = Solid::Device(udi); + } + + Solid::StorageAccess *access = device.as(); + + // Without that, our lambda function would capture an uninitialized object, resulting in UB + // and random crashes + QMetaObject::Connection *c = new QMetaObject::Connection(); + *c = connect(this, &KSolidNotify::blockingAppsReady, [=](const QStringList &blockApps) { + QString errorMessage; + if (blockApps.isEmpty()) { + errorMessage = i18n("One or more files on this device are open within an application."); + } else { + errorMessage = i18np("One or more files on this device are opened in application \"%2\".", + "One or more files on this device are opened in following applications: %2.", + blockApps.count(), + blockApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); + } + Q_EMIT notify(error, errorMessage, errorData.toString(), udi); + disconnect(*c); + delete c; + }); + queryBlockingApps(access->filePath()); + } + + break; + } + case Solid::ErrorType::UserCanceled: + // don't point out the obvious to the user, do nothing here + break; + default: + switch (type) { + case SolidReplyType::Setup: + errorMsg = i18n("Could not mount this device."); + break; + case SolidReplyType::Teardown: + errorMsg = i18nc("Remove is less technical for unmount", "Could not remove this device."); + break; + case SolidReplyType::Eject: + errorMsg = i18n("Could not eject this disc."); + break; + } + + break; + } + + if (!errorMsg.isEmpty()) { + Q_EMIT notify(error, errorMsg, errorData.toString(), udi); + } +} diff --git a/plasma/workspace/dataengines/devicenotifications/ksolidnotify.h b/plasma/workspace/dataengines/devicenotifications/ksolidnotify.h new file mode 100644 index 0000000000..cd187e0964 --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/ksolidnotify.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2010 Jacopo De Simoi + SPDX-FileCopyrightText: 2014 Lukáš Tinkl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +/** + * @brief Class which triggers solid notifications + * + * This is an internal class which listens to solid errors and route them via dbus to an + * appropriate visualization (e.g. the plasma device notifier applet); if such visualization is not available + * errors are shown via regular notifications + * + * @author Jacopo De Simoi + */ + +class KSolidNotify : public QObject +{ + Q_OBJECT + +public: + explicit KSolidNotify(QObject *parent); + +Q_SIGNALS: + void notify(Solid::ErrorType solidError, const QString &error, const QString &errorDetails, const QString &udi); + void blockingAppsReady(const QStringList &apps); + void clearNotification(const QString &udi); + +protected Q_SLOTS: + void onDeviceAdded(const QString &udi); + void onDeviceRemoved(const QString &udi); + +private: + enum class SolidReplyType { + Setup, + Teardown, + Eject, + }; + + void onSolidReply(SolidReplyType type, Solid::ErrorType error, const QVariant &errorData, const QString &udi); + + void connectSignals(Solid::Device *device); + bool isSafelyRemovable(const QString &udi) const; + void queryBlockingApps(const QString &devicePath); + + QHash m_devices; +}; \ No newline at end of file diff --git a/plasma/workspace/dataengines/devicenotifications/plasma-dataengine-devicenotifications.json b/plasma/workspace/dataengines/devicenotifications/plasma-dataengine-devicenotifications.json new file mode 100644 index 0000000000..542a5f1eef --- /dev/null +++ b/plasma/workspace/dataengines/devicenotifications/plasma-dataengine-devicenotifications.json @@ -0,0 +1,142 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "wilderkde@gmail.com", + "Name": "Jacopo De Simoi", + "Name[ar]": "Jacopo De Simoi", + "Name[az]": "Jacopo De Simoi", + "Name[ca]": "Jacopo De Simoi", + "Name[cs]": "Jacopo De Simoi", + "Name[de]": "Jacopo De Simoi", + "Name[en_GB]": "Jacopo De Simoi", + "Name[es]": "Jacopo De Simoi", + "Name[eu]": "Jacopo De Simoi", + "Name[fi]": "Jacopo De Simoi", + "Name[fr]": "Jacopo De Simoi", + "Name[hu]": "Jacopo De Simoi", + "Name[ia]": "Jacopo De Simoi", + "Name[it]": "Jacopo De Simoi", + "Name[ko]": "Jacopo De Simoi", + "Name[lt]": "Jacopo De Simoi", + "Name[nl]": "Jacopo De Simoi", + "Name[nn]": "Jacopo De Simoi", + "Name[pl]": "Jacopo De Simoi", + "Name[pt_BR]": "Jacopo De Simoi", + "Name[ro]": "Jacopo De Simoi", + "Name[ru]": "Jacopo De Simoi", + "Name[sk]": "Jacopo De Simoi", + "Name[sl]": "Jacopo De Simoi", + "Name[sv]": "Jacopo De Simoi", + "Name[tr]": "Jacopo De Simoi", + "Name[uk]": "Jacopo De Simoi", + "Name[vi]": "Jacopo De Simoi", + "Name[x-test]": "xxJacopo De Simoixx", + "Name[zh_CN]": "Jacopo De Simoi" + } + ], + "Category": "", + "Description": "Passive device notifications for the user.", + "Description[ar]": "إخطارات الجهاز السلبية للمستخدم.", + "Description[az]": "İstifadəçi üçün qurğuların passiv bildirişləri.", + "Description[ca]": "Notificacions passives de dispositius per a l'usuari.", + "Description[cs]": "Pasivní upozornění zařízení pro uživatele.", + "Description[de]": "Passive Geräte-Benachrichtigungen für den Anwender.", + "Description[en_GB]": "Passive device notifications for the user.", + "Description[es]": "Notificaciones pasivas de dispositivos para el usuario.", + "Description[eu]": "Erabiltzailearentzako gailu-jakinarazpen pasiboak.", + "Description[fi]": "Passiiviset laiteilmoitukset käyttäjälle.", + "Description[fr]": "Notifications passives de périphériques pour l'utilisateur.", + "Description[hu]": "Passzív eszközértesítések a felhasználónak.", + "Description[ia]": "Notificationes de dispositivo passive pro le usator.", + "Description[it]": "Notifiche dei dispositivi passive per l'utente.", + "Description[ko]": "사용자에게 보이는 수동적인 장치 알림입니다.", + "Description[lt]": "Pasyvūs pranešimai naudotojui apie įrenginius.", + "Description[nl]": "Passieve meldingen van apparaten voor de gebruiker.", + "Description[nn]": "Passive einingsvarslingar for brukaren.", + "Description[pa]": "ਵਰਤੋਂਕਾਰ ਲਈ ਪੈਸਿਵ ਡਿਵਾਈਸ ਨੋਟੀਫਿਕੇਸ਼ਨ।", + "Description[pl]": "Powiadamia biernie użytkownika o urządzeniach.", + "Description[pt_BR]": "Notificações passivas de dispositivos para o usuário.", + "Description[ro]": "Notificări de dispozitiv pasive pentru utilizator.", + "Description[ru]": "Пассивные уведомления об оборудовании для пользователя.", + "Description[sk]": "Pasívne upozornenia zariadení pre užívateľa.", + "Description[sl]": "Pasivno obvestilo naprave za uporabnika.", + "Description[sv]": "Passiva enhetsunderrättelser för användaren.", + "Description[ta]": "பயனருக்கான சாதன அறிவிப்புகள்.", + "Description[tr]": "Kullanıcı için pasif aygıt bildirimleri.", + "Description[uk]": "Пасивні сповіщення користувача щодо пристроїв.", + "Description[vi]": "Thông báo thiết bị thụ động dành cho người dùng.", + "Description[x-test]": "xxPassive device notifications for the user.xx", + "Description[zh_CN]": "为用户提供被动出现的设备通知。", + "Icon": "device-notifier", + "Id": "devicenotifications", + "Name": "Device Notifications", + "Name[ar]": "إخطارات الأجهزة", + "Name[az]": "Qurğular haqqında məlumatlar", + "Name[bg]": "Уведомления от устройства", + "Name[bn]": "ডিভাইস বিজ্ঞপ্তি", + "Name[bs]": "Obavještenja o uređajima", + "Name[ca@valencia]": "Notificacions dels dispositius", + "Name[ca]": "Notificacions dels dispositius", + "Name[cs]": "Upozornění zařízení", + "Name[da]": "Enhedsbekendtgørelser", + "Name[de]": "Geräte-Benachrichtigungen", + "Name[el]": "Ειδοποιήσεις συσκευών", + "Name[en_GB]": "Device Notifications", + "Name[es]": "Notificaciones de dispositivo", + "Name[et]": "Seadmete märguanded", + "Name[eu]": "Gailu-jakinarazpenak", + "Name[fi]": "Laiteilmoitukset", + "Name[fr]": "Notifications des périphériques", + "Name[ga]": "Fógraí Gléasanna", + "Name[gl]": "Notificacións dos dispositivos", + "Name[he]": "הודעות של התקנים", + "Name[hi]": "औज़ार सूचनाएँ", + "Name[hr]": "Obavijesti uređaja", + "Name[hsb]": "Zdźělenki grata", + "Name[hu]": "Eszközértesítések", + "Name[ia]": "Notificationes de dispositivo", + "Name[id]": "Notifikasi Perangkat", + "Name[is]": "Tilkynningar um tæki", + "Name[it]": "Notifiche dei dispositivi", + "Name[ja]": "デバイスの通知", + "Name[kk]": "Құрылғы құлақтандырулары", + "Name[km]": "ការ​​ជូន​ដំណឹង​​ឧបករណ៍", + "Name[kn]": "ಸಾಧನ ಸೂಚನೆಗಳು", + "Name[ko]": "장치 알림", + "Name[lt]": "Pranešimai apie įrenginius", + "Name[lv]": "Iekārtu paziņojumi", + "Name[ml]": "ഡിവൈസിനെപറ്റിയുള്ള അറിയിപ്പുകള്‍", + "Name[mr]": "साधन सूचना", + "Name[nb]": "Enhetsvarslinger", + "Name[nds]": "Reedschap-Bescheden", + "Name[nl]": "Meldingen van apparaten", + "Name[nn]": "Einingssvarslingar", + "Name[pa]": "ਜੰਤਰ ਨੋਟੀਫਿਕੇਸ਼ਨ", + "Name[pl]": "Powiadomienia urządzeń", + "Name[pt]": "Notificações de Dispositivos", + "Name[pt_BR]": "Notificações de dispositivos", + "Name[ro]": "Notificări dispozitive", + "Name[ru]": "Уведомления об устройствах", + "Name[si]": "මෙවලම් දැනුම් දීම්", + "Name[sk]": "Upozornenia zariadení", + "Name[sl]": "Obvestila o napravah", + "Name[sr@ijekavian]": "Обавјештења о уређајима", + "Name[sr@ijekavianlatin]": "Obavještenja o uređajima", + "Name[sr@latin]": "Obaveštenja o uređajima", + "Name[sr]": "Обавештења о уређајима", + "Name[sv]": "Enhetsunderrättelser", + "Name[ta]": "சாதன அறிவிப்புகள்", + "Name[tg]": "Огоҳиҳои дастгоҳ", + "Name[th]": "การแจ้งเกี่ยวกับอุปกรณ์", + "Name[tr]": "Aygıt Bildirimleri", + "Name[ug]": "ئۈسكۈنە ئۇقتۇرۇشلىرى", + "Name[uk]": "Сповіщення про пристрої", + "Name[vi]": "Thông báo của thiết bị", + "Name[wa]": "Notifiaedjes des éndjins", + "Name[x-test]": "xxDevice Notificationsxx", + "Name[zh_CN]": "设备通知", + "Name[zh_TW]": "裝置通知", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/dict/CMakeLists.txt b/plasma/workspace/dataengines/dict/CMakeLists.txt new file mode 100644 index 0000000000..893b8a5ef1 --- /dev/null +++ b/plasma/workspace/dataengines/dict/CMakeLists.txt @@ -0,0 +1,16 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_dict\") + +set(dict_engine_SRCS + dictengine.cpp +) + +add_library(plasma_engine_dict MODULE ${dict_engine_SRCS}) + +target_link_libraries (plasma_engine_dict + KF5::Plasma + KF5::Service + KF5::I18n + Qt::Network +) + +install(TARGETS plasma_engine_dict DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) diff --git a/plasma/workspace/dataengines/dict/Messages.sh b/plasma/workspace/dataengines/dict/Messages.sh new file mode 100644 index 0000000000..29218d33e1 --- /dev/null +++ b/plasma/workspace/dataengines/dict/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_dict.pot diff --git a/plasma/workspace/dataengines/dict/buggywords b/plasma/workspace/dataengines/dict/buggywords new file mode 100644 index 0000000000..711fb3c0e1 --- /dev/null +++ b/plasma/workspace/dataengines/dict/buggywords @@ -0,0 +1 @@ +which diff --git a/plasma/workspace/dataengines/dict/dictengine.cpp b/plasma/workspace/dataengines/dict/dictengine.cpp new file mode 100644 index 0000000000..ef9403cbcf --- /dev/null +++ b/plasma/workspace/dataengines/dict/dictengine.cpp @@ -0,0 +1,250 @@ +/* + SPDX-FileCopyrightText: 2007 Jeff Cooper + SPDX-FileCopyrightText: 2007 Thomas Georgiou + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "dictengine.h" +#include + +#include +#include +#include +#include +#include + +#include + +DictEngine::DictEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_tcpSocket(nullptr) +{ + Q_UNUSED(args) + m_serverName = QLatin1String("dict.org"); // In case we need to switch it later + m_dictName = QLatin1String("wn"); // Default, good dictionary +} + +DictEngine::~DictEngine() +{ +} + +void DictEngine::setDict(const QString &dict) +{ + m_dictName = dict; +} + +void DictEngine::setServer(const QString &server) +{ + m_serverName = server; +} + +static QString wnToHtml(const QString &word, QByteArray &text) +{ + QList splitText = text.split('\n'); + QString def; + def += QLatin1String("
\n"); + static QRegularExpression linkRx(QStringLiteral("{(.*?)}")); + + bool isFirst = true; + while (!splitText.empty()) { + // 150 n definitions retrieved - definitions follow + // 151 word database name - text follows + // 250 ok (optional timing information here) + // 552 No match + QString currentLine = splitText.takeFirst(); + if (currentLine.startsWith(QLatin1String("151"))) { + isFirst = true; + continue; + } + + if (currentLine.startsWith('.')) { + def += QLatin1String(""); + continue; + } + + if (currentLine.startsWith("552")) { + return i18n("No match found for %1", word); + } + + if (!(currentLine.startsWith(QLatin1String("150")) || currentLine.startsWith(QLatin1String("151")) || currentLine.startsWith(QLatin1String("250")))) { + // Handle links + int offset = 0; + QRegularExpressionMatchIterator it = linkRx.globalMatch(currentLine); + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + QUrl url; + url.setScheme("dict"); + url.setPath(match.captured(1)); + const QString linkText = QStringLiteral("%2").arg(url.toString(), match.captured(1)); + currentLine.replace(match.capturedStart(0) + offset, match.capturedLength(0), linkText); + offset += linkText.length() - match.capturedLength(0); + } + + if (isFirst) { + def += "
" + currentLine + "
\n
"; + isFirst = false; + continue; + } else { + static QRegularExpression newLineRx(QStringLiteral("([1-9]{1,2}:)")); + if (currentLine.contains(newLineRx)) { + def += QLatin1String("\n
\n"); + } + static QRegularExpression makeMeBoldRx(QStringLiteral("^([\\s\\S]*[1-9]{1,2}:)")); + currentLine.replace(makeMeBoldRx, QLatin1String("\\1")); + def += currentLine; + continue; + } + } + } + + def += QLatin1String("
"); + return def; +} + +void DictEngine::getDefinition() +{ + m_tcpSocket->readAll(); + QByteArray ret; + + const QByteArray command = QByteArray("DEFINE ") + m_dictName.toLatin1() + " \"" + m_currentWord.toUtf8() + "\"\n"; + // qDebug() << command; + m_tcpSocket->write(command); + m_tcpSocket->flush(); + + while (!ret.contains("250") && !ret.contains("552") && !ret.contains("550")) { + m_tcpSocket->waitForReadyRead(); + ret += m_tcpSocket->readAll(); + } + + m_tcpSocket->disconnectFromHost(); + const QString html = wnToHtml(m_currentWord, ret); + // setData(m_currentQuery, m_dictName, html); + setData(m_currentQuery, QStringLiteral("text"), html); +} + +void DictEngine::getDicts() +{ + m_tcpSocket->readAll(); + QByteArray ret; + + m_tcpSocket->write(QByteArray("SHOW DB\n")); + m_tcpSocket->flush(); + + m_tcpSocket->waitForReadyRead(); + while (!ret.contains("250")) { + m_tcpSocket->waitForReadyRead(); + ret += m_tcpSocket->readAll(); + } + + QVariantMap *availableDicts = new QVariantMap; + const QList retLines = ret.split('\n'); + for (const QByteArray &curr : retLines) { + if (curr.startsWith("554")) { + // TODO: What happens if no DB available? + // TODO: Eventually there will be functionality to change the server... + break; + } + + // ignore status code and empty lines + if (curr.startsWith("250") || curr.startsWith("110") || curr.isEmpty()) { + continue; + } + + if (!curr.startsWith('-') && !curr.startsWith('.')) { + const QString line = QString::fromUtf8(curr).trimmed(); + const QString id = line.section(' ', 0, 0); + QString description = line.section(' ', 1); + if (description.startsWith('"') && description.endsWith('"')) { + description.remove(0, 1); + description.chop(1); + } + setData(QStringLiteral("list-dictionaries"), id, description); // this is additive + availableDicts->insert(id, description); + } + } + m_availableDictsCache.insert(m_serverName, availableDicts); + + m_tcpSocket->disconnectFromHost(); +} + +void DictEngine::socketClosed() +{ + if (m_tcpSocket) { + m_tcpSocket->deleteLater(); + } + m_tcpSocket = nullptr; +} + +bool DictEngine::sourceRequestEvent(const QString &query) +{ + // FIXME: this is COMPLETELY broken .. it can only look up one query at a time! + // a DataContainer subclass that does the look up should probably be made + if (m_tcpSocket) { + m_tcpSocket->abort(); // stop if lookup is in progress and new query is requested + m_tcpSocket->deleteLater(); + m_tcpSocket = nullptr; + } + + QStringList queryParts = query.split(':', Qt::SkipEmptyParts); + if (queryParts.isEmpty()) { + return false; + } + + m_currentWord = queryParts.last(); + m_currentQuery = query; + + // asked for a dictionary? + if (queryParts.count() > 1) { + setDict(queryParts[queryParts.count() - 2]); + // default to wordnet + } else { + setDict(QStringLiteral("wn")); + } + + // asked for a server? + if (queryParts.count() > 2) { + setServer(queryParts[queryParts.count() - 3]); + // default to wordnet + } else { + setServer(QStringLiteral("dict.org")); + } + + if (m_currentWord.simplified().isEmpty()) { + setData(m_currentQuery, m_dictName, QString()); + } else { + if (m_currentWord == QLatin1String("list-dictionaries")) { + // Use cache if available + QVariantMap *dicts = m_availableDictsCache.object(m_serverName); + if (dicts) { + for (auto it = dicts->constBegin(); it != dicts->constEnd(); ++it) { + setData(m_currentQuery, it.key(), it.value()); + } + return true; + } + } + + // We need to do this in order to create the DataContainer immediately in DataEngine + // so it can connect to updates. Not sure why DataEnginePrivate::requestSource + // doesn't create the DataContainer when sourceRequestEvent returns true, by doing + // source(sourceName) instead of source(sourceName, false), but well, I'm too scared to change that. + setData(m_currentQuery, QVariant()); + + m_tcpSocket = new QTcpSocket(this); + connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed); + + if (m_currentWord == QLatin1String("list-dictionaries")) { + connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDicts); + } else { + connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDefinition); + } + + m_tcpSocket->connectToHost(m_serverName, 2628); + } + + return true; +} + +K_PLUGIN_CLASS_WITH_JSON(DictEngine, "plasma-dataengine-dict.json") + +#include "dictengine.moc" diff --git a/plasma/workspace/dataengines/dict/dictengine.h b/plasma/workspace/dataengines/dict/dictengine.h new file mode 100644 index 0000000000..9f7b09754a --- /dev/null +++ b/plasma/workspace/dataengines/dict/dictengine.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2007 Jeff Cooper + SPDX-FileCopyrightText: 2007 Thomas Georgiou + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once +#include +#include +#include +#include +class QTcpSocket; + +/** + * This class evaluates the basic expressions given in the interface. + */ + +class DictEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + DictEngine(QObject *parent, const QVariantList &args); + ~DictEngine() override; + +protected: + bool sourceRequestEvent(const QString &word) override; + +private Q_SLOTS: + void getDefinition(); + void socketClosed(); + void getDicts(); + +private: + void setDict(const QString &dict); + void setServer(const QString &server); + + QHash m_dictNameToDictCode; + QTcpSocket *m_tcpSocket; + QString m_currentWord; + QString m_currentQuery; + QString m_dictName; + QString m_serverName; + QCache m_availableDictsCache; +}; diff --git a/plasma/workspace/dataengines/dict/plasma-dataengine-dict.json b/plasma/workspace/dataengines/dict/plasma-dataengine-dict.json new file mode 100644 index 0000000000..70789d8781 --- /dev/null +++ b/plasma/workspace/dataengines/dict/plasma-dataengine-dict.json @@ -0,0 +1,128 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Look up word meanings", + "Description[ar]": "ابحث عن معاني الكلمات", + "Description[az]": "Söz və tərcümələrin mənalarını göstərmək", + "Description[ca]": "Cerca els significats de paraules", + "Description[cs]": "Vyhledávání významu slov", + "Description[de]": "Nachschlagen von Wortbedeutungen", + "Description[en_GB]": "Look up word meanings", + "Description[es]": "Buscar significado de las palabras", + "Description[eu]": "Bilatu hitzen esanahiak", + "Description[fi]": "Etsi sanojen merkityksiä", + "Description[fr]": "Rechercher les sens des mots", + "Description[hu]": "Értelmező szótár", + "Description[ia]": "Cerca significato de parola", + "Description[it]": "Cerca i significati delle parole", + "Description[ko]": "단어의 뜻 찾기", + "Description[lt]": "Žodžių reikšmių paieška", + "Description[nl]": "Zoek de betekenis van woorden op", + "Description[nn]": "Slå opp tydinga til ord", + "Description[pa]": "ਸ਼ਬਦਾਂ ਦੇ ਅਰਥ ਖੋਜ", + "Description[pl]": "Wyszukuje znaczenia słów", + "Description[pt_BR]": "Procurar os significados das palavras", + "Description[ro]": "Caută înțelesul cuvintelor", + "Description[ru]": "Показ значений слов или перевода", + "Description[sk]": "Vyhľadávanie významov slov", + "Description[sl]": "Poišči pomene besed", + "Description[sv]": "Slå upp ords betydelse", + "Description[ta]": "சொற்களின் பொருட்களை கண்டறிய உதவும்", + "Description[tr]": "Sözcük anlamlarına bak", + "Description[uk]": "Пошук значень слів", + "Description[vi]": "Tra nghĩa của từ", + "Description[x-test]": "xxLook up word meaningsxx", + "Description[zh_CN]": "查询单词含义", + "Icon": "accessories-dictionary", + "Id": "dict", + "License": "", + "Name": "Dictionary", + "Name[ar]": "قاموس", + "Name[as]": "অভিধান", + "Name[ast]": "Diccionariu", + "Name[az]": "Lüğət", + "Name[be@latin]": "Słoŭnik", + "Name[bg]": "Речник", + "Name[bn]": "অভিধান", + "Name[bn_IN]": "অভিধান", + "Name[bs]": "Rječnik", + "Name[ca@valencia]": "Diccionari", + "Name[ca]": "Diccionari", + "Name[cs]": "Slovník", + "Name[csb]": "Słowôrz", + "Name[da]": "Ordbog", + "Name[de]": "Wörterbuch", + "Name[el]": "Λεξικό", + "Name[en_GB]": "Dictionary", + "Name[eo]": "Vortaro", + "Name[es]": "Diccionario", + "Name[et]": "Sõnaraamat", + "Name[eu]": "Hiztegia", + "Name[fa]": "واژه‌نامه", + "Name[fi]": "Sanakirja", + "Name[fr]": "Dictionnaire", + "Name[fy]": "Wurdboek", + "Name[ga]": "Foclóir", + "Name[gl]": "Dicionario", + "Name[gu]": "ડિક્શનરી", + "Name[he]": "מילון", + "Name[hi]": "शब्दकोश", + "Name[hne]": "सब्दकोस", + "Name[hr]": "Rječnik", + "Name[hsb]": "Słownik", + "Name[hu]": "Szótár", + "Name[ia]": "Dictionario", + "Name[id]": "Kamus", + "Name[is]": "Orðabók", + "Name[it]": "Dizionario", + "Name[ja]": "辞書", + "Name[kk]": "Сөздік", + "Name[km]": "វចនានុក្រម", + "Name[kn]": "ಶಬ್ದಕೋಶ ", + "Name[ko]": "사전", + "Name[ku]": "Ferheng", + "Name[lt]": "Žodynas", + "Name[lv]": "Vārdnīca", + "Name[mai]": "शब्दकोश", + "Name[mk]": "Речник", + "Name[ml]": "നിഘണ്ടു", + "Name[mr]": "माहितीकोष", + "Name[nb]": "Ordbok", + "Name[nds]": "Wöörbook", + "Name[nl]": "Woordenboek", + "Name[nn]": "Ordbok", + "Name[or]": "ଅଭିଧାନ", + "Name[pa]": "ਡਿਕਸ਼ਨਰੀ", + "Name[pl]": "Słownik", + "Name[pt]": "Dicionário", + "Name[pt_BR]": "Dicionário", + "Name[ro]": "Dicționar", + "Name[ru]": "Словарь", + "Name[si]": "ශබ්දකෝෂය", + "Name[sk]": "Slovník", + "Name[sl]": "Slovar", + "Name[sr@ijekavian]": "рјечник", + "Name[sr@ijekavianlatin]": "rječnik", + "Name[sr@latin]": "rečnik", + "Name[sr]": "речник", + "Name[sv]": "Ordlista", + "Name[ta]": "அகராதி", + "Name[tg]": "Луғат", + "Name[th]": "พจนานุกรม", + "Name[tr]": "Sözlük", + "Name[ug]": "لۇغەت", + "Name[uk]": "Словник", + "Name[vi]": "Từ điển", + "Name[wa]": "Motî", + "Name[x-test]": "xxDictionaryxx", + "Name[zh_CN]": "词典", + "Name[zh_TW]": "字典", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/executable/CMakeLists.txt b/plasma/workspace/dataengines/executable/CMakeLists.txt new file mode 100644 index 0000000000..953317ede2 --- /dev/null +++ b/plasma/workspace/dataengines/executable/CMakeLists.txt @@ -0,0 +1,11 @@ +set(executable_engine_SRCS + executable.cpp +) + +kcoreaddons_add_plugin(plasma_engine_executable SOURCES ${executable_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_executable + KF5::Plasma + KF5::Service + KF5::CoreAddons +) diff --git a/plasma/workspace/dataengines/executable/executable.cpp b/plasma/workspace/dataengines/executable/executable.cpp new file mode 100644 index 0000000000..395de15baa --- /dev/null +++ b/plasma/workspace/dataengines/executable/executable.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2007, 2008 Petri Damsten + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "executable.h" +#include +ExecutableContainer::ExecutableContainer(const QString &command, QObject *parent) + : Plasma::DataContainer(parent) + , m_process(nullptr) +{ + setObjectName(command); + connect(this, &Plasma::DataContainer::updateRequested, this, &ExecutableContainer::exec); + exec(); +} + +ExecutableContainer::~ExecutableContainer() +{ + if (m_process) { + disconnect(m_process, nullptr, this, nullptr); + } + delete m_process; +} + +void ExecutableContainer::finished(int exitCode, QProcess::ExitStatus exitStatus) +{ + setData(QStringLiteral("exit code"), exitCode); + setData(QStringLiteral("exit status"), exitStatus); + setData(QStringLiteral("stdout"), QString::fromLocal8Bit(m_process->readAllStandardOutput())); + setData(QStringLiteral("stderr"), QString::fromLocal8Bit(m_process->readAllStandardError())); + checkForUpdate(); +} + +void ExecutableContainer::exec() +{ + if (!m_process) { + m_process = new KProcess(); + connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); + m_process->setOutputChannelMode(KProcess::SeparateChannels); + m_process->setShellCommand(objectName()); + } + + if (m_process->state() == QProcess::NotRunning) { + m_process->start(); + } else { + qDebug() << "Process" << objectName() << "already running. Pid:" << m_process->processId(); + } +} + +ExecutableEngine::ExecutableEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + setMinimumPollingInterval(1000); +} + +bool ExecutableEngine::sourceRequestEvent(const QString &source) +{ + addSource(new ExecutableContainer(source, this)); + return true; +} + +K_PLUGIN_CLASS_WITH_JSON(ExecutableEngine, "plasma-dataengine-executable.json") + +#include "executable.moc" diff --git a/plasma/workspace/dataengines/executable/executable.h b/plasma/workspace/dataengines/executable/executable.h new file mode 100644 index 0000000000..53c8472936 --- /dev/null +++ b/plasma/workspace/dataengines/executable/executable.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2007, 2008 Petri Damsten + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include + +class ExecutableContainer : public Plasma::DataContainer +{ + Q_OBJECT +public: + explicit ExecutableContainer(const QString &command, QObject *parent = nullptr); + ~ExecutableContainer() override; + +protected Q_SLOTS: + void finished(int exitCode, QProcess::ExitStatus exitStatus); + void exec(); + +private: + KProcess *m_process; +}; + +class ExecutableEngine : public Plasma::DataEngine +{ + Q_OBJECT +public: + ExecutableEngine(QObject *parent, const QVariantList &args); + +protected: + bool sourceRequestEvent(const QString &source) override; +}; diff --git a/plasma/workspace/dataengines/executable/plasma-dataengine-executable.json b/plasma/workspace/dataengines/executable/plasma-dataengine-executable.json new file mode 100644 index 0000000000..b351f66e5f --- /dev/null +++ b/plasma/workspace/dataengines/executable/plasma-dataengine-executable.json @@ -0,0 +1,123 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Run Executable Data Engine", + "Description[ar]": "محرك بيانات لتشغل برامج الأوامر", + "Description[az]": "İcra oluna bilən verilənlər mexanizmini işə salmaq", + "Description[ca]": "Motor de dades d'ordres executables", + "Description[cs]": "Spustit rozhraní pro spouštění programů", + "Description[de]": "Datentreiber für ausführbare Objekte", + "Description[en_GB]": "Run Executable Data Engine", + "Description[es]": "Ejecutar motor de datos ejecutables", + "Description[eu]": "Exekutatu exekutagarrien datu-motorra", + "Description[fi]": "Tietomoottori ohjelmien suorittamiseen", + "Description[fr]": "Lancer le moteur de données d'exécutables", + "Description[hu]": "Adatkezelő futtatása", + "Description[ia]": "Motor per exequer datos executabile", + "Description[it]": "Motore di dati per esecuzione di comandi", + "Description[ko]": "명령 실행 데이터 엔진", + "Description[lt]": "Paleisti vykdomąjį duomenų variklį", + "Description[nl]": "Uitvoerbare gegevensengine starten", + "Description[nn]": "Datamotor for kommandokøyrar", + "Description[pa]": "ਚੱਲਣਯੋਗ ਡਾਟਾ ਇੰਜਣ ਚਲਾਓ", + "Description[pl]": "Silnik wykonywania danych wykonywalnych", + "Description[pt_BR]": "Mecanismo de dados da execução de comandos", + "Description[ro]": "Motor de date pentru lansarea executabilelor", + "Description[ru]": "Источник данных исполняемых объектов", + "Description[sk]": "Dátový nástroj na spustenie programov", + "Description[sl]": "Zaženi podatkovni vir o izvajalnih programih", + "Description[sv]": "Datagränssnitt för att köra program", + "Description[ta]": "இயக்க வல்லவற்றை இயக்கும் நிரல்", + "Description[tr]": "Çalıştırılabilir Veri Motorunu Başlat", + "Description[uk]": "Виконати програму рушія даних", + "Description[vi]": "Chạy dụng cụ \"Dữ liệu\" thực thi được", + "Description[x-test]": "xxRun Executable Data Enginexx", + "Description[zh_CN]": "运行程序数据引擎", + "Icon": "application-x-executable-script", + "Id": "executable", + "Name": "Run Commands", + "Name[ar]": "شغّل أوامر", + "Name[az]": "Əmrləri başlatmaq", + "Name[be@latin]": "Vykonvaj zahady", + "Name[bg]": "Изпълнение на команди", + "Name[bn]": "কমান্ড চালাও", + "Name[bn_IN]": "কমান্ড সঞ্চালনা", + "Name[bs]": "Izvršavanje naredbi", + "Name[ca@valencia]": "Execució d'ordres", + "Name[ca]": "Execució d'ordres", + "Name[cs]": "Spustit příkazy", + "Name[csb]": "Zrëszanié pòlétów", + "Name[da]": "Kør kommandoer", + "Name[de]": "Befehle ausführen", + "Name[el]": "Εκτέλεση εντολών", + "Name[en_GB]": "Run Commands", + "Name[eo]": "Lanĉi komandon", + "Name[es]": "Ejecutar órdenes", + "Name[et]": "Käsu käivitamine", + "Name[eu]": "Exekutatu komandoak", + "Name[fi]": "Suorita komentoja", + "Name[fr]": "Exécuter des commandes", + "Name[fy]": "Rin kommando's", + "Name[ga]": "Rith Orduithe", + "Name[gl]": "Executar ordes", + "Name[gu]": "આદેશ ચલાવો", + "Name[he]": "הרצת פקודות", + "Name[hi]": "कमांड चलाएँ", + "Name[hne]": "कमांड चलाव", + "Name[hr]": "Pokreni naredbe", + "Name[hsb]": "Přikazy wuwjesć", + "Name[hu]": "Parancsvégrehajtó", + "Name[ia]": "Exeque commandos", + "Name[id]": "Jalankan Perintah", + "Name[is]": "Keyra skipanir", + "Name[it]": "Esegui comandi", + "Name[ja]": "コマンドを実行", + "Name[kk]": "Команданы орындау", + "Name[km]": "រត់​ពាក្យ​បញ្ជា", + "Name[kn]": "ಆದೇಶಗಳನ್ನು ಚಲಾಯಿಸು", + "Name[ko]": "명령 실행", + "Name[ku]": "Fermanan Bixebitîne", + "Name[lt]": "Vykdyti komandas", + "Name[lv]": "Palaist komandas", + "Name[mk]": "Изврши наредби", + "Name[ml]": "ആജ്ഞകള്‍ പ്രവര്‍ത്തിപ്പിയ്ക്കുക", + "Name[mr]": "आदेश चालवा", + "Name[nb]": "Kjør kommandoer", + "Name[nds]": "Befehlen utföhren", + "Name[nl]": "Commando's uitvoeren", + "Name[nn]": "Køyr kommandoar", + "Name[or]": "ନିର୍ଦ୍ଦେଶଗୁଡ଼ିକୁ ଚଲାନ୍ତୁ", + "Name[pa]": "ਕਮਾਂਡਾਂ ਚਲਾਓ", + "Name[pl]": "Wykonywanie poleceń", + "Name[pt]": "Execução de Comandos", + "Name[pt_BR]": "Executar comandos", + "Name[ro]": "Execută comenzi", + "Name[ru]": "Выполнить команду", + "Name[si]": "විධාන ක්‍රියාත්මක කරන්න", + "Name[sk]": "Spustiť príkazy", + "Name[sl]": "Zaganjanje ukazov", + "Name[sr@ijekavian]": "извршавање наредби", + "Name[sr@ijekavianlatin]": "izvršavanje naredbi", + "Name[sr@latin]": "izvršavanje naredbi", + "Name[sr]": "извршавање наредби", + "Name[sv]": "Kör kommandon", + "Name[ta]": "கட்டளைகளை இயக்கு", + "Name[tg]": "Иҷрои фармонҳо", + "Name[th]": "ประมวลผลคำสั่ง", + "Name[tr]": "Komut Çalıştır", + "Name[ug]": "بۇيرۇقلارنى ئىجرا قىل", + "Name[uk]": "Виконання команд", + "Name[vi]": "Chạy lệnh", + "Name[wa]": "Enonder des cmandes", + "Name[x-test]": "xxRun Commandsxx", + "Name[zh_CN]": "运行命令", + "Name[zh_TW]": "執行指令", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/favicons/CMakeLists.txt b/plasma/workspace/dataengines/favicons/CMakeLists.txt new file mode 100644 index 0000000000..f21163e4d3 --- /dev/null +++ b/plasma/workspace/dataengines/favicons/CMakeLists.txt @@ -0,0 +1,11 @@ +set(favicons_engine_SRCS + favicons.cpp + faviconprovider.cpp +) + +kcoreaddons_add_plugin(plasma_engine_favicons SOURCES ${favicons_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_favicons + KF5::Plasma + KF5::KIOCore + Qt::Gui +) diff --git a/plasma/workspace/dataengines/favicons/faviconprovider.cpp b/plasma/workspace/dataengines/favicons/faviconprovider.cpp new file mode 100644 index 0000000000..4bdbeeeebf --- /dev/null +++ b/plasma/workspace/dataengines/favicons/faviconprovider.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + SPDX-FileCopyrightText: 2008 Marco Martin + SPDX-FileCopyrightText: 2013 Andrea Scarpino + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "faviconprovider.h" + +#include +#include +#include + +#include +#include +#include + +class FaviconProvider::Private +{ +public: + Private(FaviconProvider *parent) + : q(parent) + { + } + + void imageRequestFinished(KIO::StoredTransferJob *job); + + FaviconProvider *q; + QImage image; + QString cachePath; +}; + +void FaviconProvider::Private::imageRequestFinished(KIO::StoredTransferJob *job) +{ + if (job->error()) { + Q_EMIT q->error(q); + return; + } + + image = QImage::fromData(job->data()); + if (!image.isNull()) { + image.save(cachePath, "PNG"); + } + Q_EMIT q->finished(q); +} + +FaviconProvider::FaviconProvider(QObject *parent, const QString &url) + : QObject(parent) + , m_url(url) + , d(new Private(this)) +{ + QUrl faviconUrl = QUrl::fromUserInput(url); + const QString fileName = KIO::favIconForUrl(faviconUrl); + + if (!fileName.isEmpty()) { + d->cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + '/' + fileName + ".png"; + d->image.load(d->cachePath, "PNG"); + } else { + d->cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/favicons/" + faviconUrl.host() + ".png"; + faviconUrl.setPath(QStringLiteral("/favicon.ico")); + + if (faviconUrl.isValid()) { + KIO::StoredTransferJob *job = KIO::storedGet(faviconUrl, KIO::NoReload, KIO::HideProgressInfo); + // job->setProperty("uid", id); + connect(job, &KJob::result, this, [this, job]() { + d->imageRequestFinished(job); + }); + } + } +} + +FaviconProvider::~FaviconProvider() +{ + delete d; +} + +QImage FaviconProvider::image() const +{ + return d->image; +} + +QString FaviconProvider::identifier() const +{ + return m_url; +} + +#include "moc_faviconprovider.cpp" diff --git a/plasma/workspace/dataengines/favicons/faviconprovider.h b/plasma/workspace/dataengines/favicons/faviconprovider.h new file mode 100644 index 0000000000..c1d96938af --- /dev/null +++ b/plasma/workspace/dataengines/favicons/faviconprovider.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + SPDX-FileCopyrightText: 2008 Marco Martin + SPDX-FileCopyrightText: 2013 Andrea Scarpino + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class QImage; + +/** + * This class provides a favicon for a given url + */ +class FaviconProvider : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new favicon provider. + * + * @param parent The parent object. + * @param url The provider URL. + */ + FaviconProvider(QObject *parent, const QString &url); + + /** + * Destroys the favicon provider. + */ + ~FaviconProvider() override; + + /** + * Returns the requested image. + * + * @note This method returns only a valid image after the + * finished() signal has been emitted. + */ + QImage image() const; + + /** + * Returns the identifier of the comic request (name + date). + */ + QString identifier() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever a request has been finished + * successfully. + * + * @param provider The provider which emitted the signal. + */ + void finished(FaviconProvider *provider); + + /** + * This signal is emitted whenever an error has occurred. + * + * @param provider The provider which emitted the signal. + */ + void error(FaviconProvider *provider); + +private: + QString m_url; + + class Private; + Private *const d; +}; diff --git a/plasma/workspace/dataengines/favicons/favicons.cpp b/plasma/workspace/dataengines/favicons/favicons.cpp new file mode 100644 index 0000000000..2528068202 --- /dev/null +++ b/plasma/workspace/dataengines/favicons/favicons.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2007 Marco Martin + SPDX-FileCopyrightText: 2013 Andrea Scarpino + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "favicons.h" + +#include +#include + +#include "faviconprovider.h" + +FaviconsEngine::FaviconsEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ +} + +FaviconsEngine::~FaviconsEngine() +{ +} + +bool FaviconsEngine::updateSourceEvent(const QString &identifier) +{ + FaviconProvider *provider = new FaviconProvider(this, identifier); + + connect(provider, &FaviconProvider::finished, this, &FaviconsEngine::finished); + connect(provider, &FaviconProvider::error, this, &FaviconsEngine::error); + + if (!provider->image().isNull()) { + setData(provider->identifier(), QStringLiteral("Icon"), provider->image()); + } + + return true; +} + +bool FaviconsEngine::sourceRequestEvent(const QString &identifier) +{ + setData(identifier, QPixmap()); + return updateSourceEvent(identifier); +} + +void FaviconsEngine::finished(FaviconProvider *provider) +{ + setData(provider->identifier(), QStringLiteral("Icon"), provider->image()); + provider->deleteLater(); +} + +void FaviconsEngine::error(FaviconProvider *provider) +{ + setData(provider->identifier(), QImage()); + provider->deleteLater(); +} + +K_PLUGIN_CLASS_WITH_JSON(FaviconsEngine, "plasma-dataengine-favicons.json") + +#include "favicons.moc" diff --git a/plasma/workspace/dataengines/favicons/favicons.h b/plasma/workspace/dataengines/favicons/favicons.h new file mode 100644 index 0000000000..901a7b4ac9 --- /dev/null +++ b/plasma/workspace/dataengines/favicons/favicons.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + SPDX-FileCopyrightText: 2008 Marco Martin + SPDX-FileCopyrightText: 2013 Andrea Scarpino + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class FaviconProvider; + +/** + * This class provides favicons for websites + * + * the queries are just the url of websites we want to fetch an icon + */ +class FaviconsEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + FaviconsEngine(QObject *parent, const QVariantList &args); + ~FaviconsEngine() override; + +protected: + bool sourceRequestEvent(const QString &identifier) override; + +protected Q_SLOTS: + bool updateSourceEvent(const QString &identifier) override; + +private Q_SLOTS: + void finished(FaviconProvider *); + void error(FaviconProvider *); +}; diff --git a/plasma/workspace/dataengines/favicons/plasma-dataengine-favicons.json b/plasma/workspace/dataengines/favicons/plasma-dataengine-favicons.json new file mode 100644 index 0000000000..0cadec55f6 --- /dev/null +++ b/plasma/workspace/dataengines/favicons/plasma-dataengine-favicons.json @@ -0,0 +1,121 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Data Engine for getting favicons of web sites", + "Description[ar]": "محرّك بيانات للحصول على favicons من مواقع الوِب", + "Description[az]": "Veb səhifələrin işarələrinin əldə olunması", + "Description[ca]": "Motor de dades per a obtenir les icones de web dels llocs web", + "Description[cs]": "Mechanismus pro získávání ikon webových stránek", + "Description[de]": "Daten-Treiber zum Holen von Webseitensymbolen von Webseiten", + "Description[en_GB]": "Data Engine for getting favicons of web sites", + "Description[es]": "Motor de datos para obtener favicons de los sitios web", + "Description[eu]": "Webgunetako web-ikonoak eskuratzeko datu-motorra", + "Description[fi]": "Tietomoottori verkkosivustokuvakkeiden noutoon", + "Description[fr]": "Moteur de données permettant d'obtenir les émoticônes de sites Internet", + "Description[hu]": "Weboldalak favikonjainak letöltésére szolgáló adatmotor", + "Description[ia]": "Motor de datos per obtener favicons del sitos web", + "Description[it]": "Motore di dati per ottenere le iconcine dei siti Web", + "Description[ko]": "웹 사이트의 파비콘을 가져오는 데이터 엔진", + "Description[lt]": "Duomenų variklis, skirtas internetinių svetainių piktogramų gavimui", + "Description[nl]": "Gegevensengine voor het ophalen van favicons van websites", + "Description[nn]": "Datamotor for henting av bokmerkeikon til nettsider", + "Description[pl]": "Silnik danych do pobierania ikon stron internetowych", + "Description[pt_BR]": "Mecanismo de dados para busca dos favicons de páginas Web", + "Description[ro]": "Motor de date pentru preluarea pictogramelor saiturilor web", + "Description[ru]": "Источник данных значков веб-сайтов", + "Description[sk]": "Dátový nástroj na získavanie favikon z webových stránok", + "Description[sl]": "Podatkovni vir za pridobivanje ikon spletnih strani", + "Description[sv]": "Datagränssnitt för att hämta favoritikoner för webbplatser", + "Description[ta]": "இணையதளங்களின் சின்னங்களைப் பெறும் நிரல்", + "Description[tr]": "Web sitelerinden site simgelerini almak için bir Veri Motoru", + "Description[uk]": "Рушій даних для отримання піктограм для вебсайтів", + "Description[vi]": "Dụng cụ dữ liệu để lấy các biểu tượng địa điểm web", + "Description[x-test]": "xxData Engine for getting favicons of web sitesxx", + "Description[zh_CN]": "用于获取网站图标的数据引擎", + "Icon": "view-web-browser-dom-tree", + "Id": "favicons", + "Name": "Favicons", + "Name[ar]": "الأيقونات", + "Name[az]": "Veb səhifələrin işarələri", + "Name[be@latin]": "Ikony sajtaŭ", + "Name[bg]": "Уеб-икони", + "Name[bn]": "ফ্যাভ-আইকন", + "Name[bn_IN]": "Favicons", + "Name[bs]": "Favikone", + "Name[ca@valencia]": "Icones de web", + "Name[ca]": "Icones de web", + "Name[cs]": "Oblíbené ikony", + "Name[csb]": "Faviconë (ikònczi)", + "Name[da]": "Favicons", + "Name[de]": "Webseitensymbole", + "Name[el]": "Αγαπημένα εικονίδια", + "Name[en_GB]": "Favicons", + "Name[eo]": "Favorikonoj", + "Name[es]": "Favicons", + "Name[et]": "Lemmikikoonid", + "Name[eu]": "web-ikonoak", + "Name[fi]": "Webbisivukuvakkeet", + "Name[fr]": "Émoticônes", + "Name[fy]": "Favicon-ôfbyldings", + "Name[ga]": "Deilbhíní Ceanán", + "Name[gl]": "Iconas de páxina web", + "Name[gu]": "ફેવિકોન્સ", + "Name[he]": "סמלי מועדפים", + "Name[hi]": "फेविकॉन", + "Name[hne]": "फेविकान", + "Name[hr]": "Omiljene ikone", + "Name[hu]": "Favikonok", + "Name[ia]": "Favicons", + "Name[id]": "Favicon", + "Name[is]": "Veftáknmyndir (favicons)", + "Name[it]": "Iconcine", + "Name[ja]": "ファビコン", + "Name[kk]": "Favicon таңбашалары", + "Name[km]": "រូប​តំណាង​សំណព្វ", + "Name[kn]": "ಫೆವಿಕಾನ್‌ಗಳು", + "Name[ko]": "파비콘", + "Name[ku]": "Nîşanên Malperan", + "Name[lt]": "Svetainių piktogramos", + "Name[lv]": "TīmekļaIkonas", + "Name[mai]": "फेविकान", + "Name[mk]": "Омилени икони", + "Name[ml]": "ഇഷ്ടചിഹ്നങ്ങള്‍", + "Name[mr]": "Favicons", + "Name[nb]": "Ikoner for favorittsteder", + "Name[nds]": "Nettsieden-Lüttbiller", + "Name[nl]": "Favicons", + "Name[nn]": "Bokmerkeikon", + "Name[or]": "Favicons", + "Name[pa]": "ਫੇਵੀਕਾਨ", + "Name[pl]": "Ikony stron internetowych", + "Name[pt]": "'Favicons'", + "Name[pt_BR]": "Favicons", + "Name[ro]": "Pictograme saituri", + "Name[ru]": "Значки веб-сайтов", + "Name[si]": "Favicons", + "Name[sk]": "Favikony", + "Name[sl]": "Ikone spletnih strani", + "Name[sr@ijekavian]": "фавиконе", + "Name[sr@ijekavianlatin]": "favikone", + "Name[sr@latin]": "favikone", + "Name[sr]": "фавиконе", + "Name[sv]": "Favoritikoner", + "Name[ta]": "சின்னங்கள்", + "Name[th]": "ไอคอนเว็บ", + "Name[tr]": "Site Simgeleri", + "Name[ug]": "تور بېكەت سىنبەلگە", + "Name[uk]": "Піктограми «Вибраного»", + "Name[vi]": "Biểu tượng trang", + "Name[wa]": "Pititès imådjetes favicons", + "Name[x-test]": "xxFaviconsxx", + "Name[zh_CN]": "网站图标", + "Name[zh_TW]": "網站圖示", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/filebrowser/CMakeLists.txt b/plasma/workspace/dataengines/filebrowser/CMakeLists.txt new file mode 100644 index 0000000000..c31d91053f --- /dev/null +++ b/plasma/workspace/dataengines/filebrowser/CMakeLists.txt @@ -0,0 +1,10 @@ +set(filebrowser_engine_SRCS + filebrowserengine.cpp +) + +kcoreaddons_add_plugin(plasma_engine_filebrowser SOURCES ${filebrowser_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_filebrowser + KF5::Plasma + KF5::Service + KF5::KIOCore +) diff --git a/plasma/workspace/dataengines/filebrowser/filebrowserengine.cpp b/plasma/workspace/dataengines/filebrowser/filebrowserengine.cpp new file mode 100644 index 0000000000..3dc1ab131e --- /dev/null +++ b/plasma/workspace/dataengines/filebrowser/filebrowserengine.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "filebrowserengine.h" + +#include + +#include +#include +#include + +#define InvalidIfEmpty(A) ((A.isEmpty()) ? (QVariant()) : (QVariant(A))) +#define forMatchingSources \ + for (DataEngine::SourceDict::iterator it = sources.begin(); it != sources.end(); ++it) \ + if (dir == QDir(it.key())) + +FileBrowserEngine::FileBrowserEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_dirWatch(nullptr) +{ + Q_UNUSED(args) + + m_dirWatch = new KDirWatch(this); + connect(m_dirWatch, &KDirWatch::created, this, &FileBrowserEngine::dirCreated); + connect(m_dirWatch, &KDirWatch::deleted, this, &FileBrowserEngine::dirDeleted); + connect(m_dirWatch, &KDirWatch::dirty, this, &FileBrowserEngine::dirDirty); +} + +FileBrowserEngine::~FileBrowserEngine() +{ + delete m_dirWatch; +} + +void FileBrowserEngine::init() +{ + qDebug() << "init() called"; +} + +bool FileBrowserEngine::sourceRequestEvent(const QString &path) +{ + qDebug() << "source requested() called: " << path; + m_dirWatch->addDir(path); + setData(path, QStringLiteral("type"), QVariant("unknown")); + updateData(path, INIT); + return true; +} + +void FileBrowserEngine::dirDirty(const QString &path) +{ + updateData(path, DIRTY); +} + +void FileBrowserEngine::dirCreated(const QString &path) +{ + updateData(path, CREATED); +} + +void FileBrowserEngine::dirDeleted(const QString &path) +{ + updateData(path, DELETED); +} + +void FileBrowserEngine::updateData(const QString &path, EventType event) +{ + Q_UNUSED(event) + + ObjectType type = NOTHING; + if (QDir(path).exists()) { + type = DIRECTORY; + } else if (QFile::exists(path)) { + type = FILE; + } + + DataEngine::SourceDict sources = containerDict(); + + QDir dir(path); + clearData(path); + + if (type == DIRECTORY) { + qDebug() << "directory info processing: " << path; + if (dir.isReadable()) { + const QStringList visibleFiles = dir.entryList(QDir::Files, QDir::Name); + const QStringList allFiles = dir.entryList(QDir::Files | QDir::Hidden, QDir::Name); + + const QStringList visibleDirectories = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); + const QStringList allDirectories = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden, QDir::Name); + + forMatchingSources + { + qDebug() << "MATCH"; + it.value()->setData(QStringLiteral("item.type"), QVariant("directory")); + + QVariant vdTmp; + if (!visibleDirectories.isEmpty()) + vdTmp = QVariant(visibleDirectories); + it.value()->setData(QStringLiteral("directories.visible"), vdTmp); + + QVariant adTmp; + if (!allDirectories.empty()) + adTmp = QVariant(allDirectories); + it.value()->setData(QStringLiteral("directories.all"), adTmp); + + QVariant vfTmp; + if (!visibleFiles.empty()) + vfTmp = QVariant(visibleFiles); + it.value()->setData(QStringLiteral("files.visible"), vfTmp); + + QVariant afTmp; + if (!allFiles.empty()) + afTmp = QVariant(allFiles); + it.value()->setData(QStringLiteral("files.all"), afTmp); + } + } + } else if (type == FILE) { + qDebug() << "file info processing: " << path; + forMatchingSources + { + it.value()->setData(QStringLiteral("item.type"), QVariant("file")); + } + } else { + forMatchingSources + { + it.value()->setData(QStringLiteral("item.type"), QVariant("imaginary")); + } + }; +} + +void FileBrowserEngine::clearData(const QString &path) +{ + QDir dir(path); + const DataEngine::SourceDict sources = containerDict(); + for (DataEngine::SourceDict::const_iterator it = sources.begin(); it != sources.end(); ++it) { + if (dir == QDir(it.key())) { + qDebug() << "matched: " << path << " " << it.key(); + it.value()->removeAllData(); + + } else { + qDebug() << "didn't match: " << path << " " << it.key(); + } + } +} + +K_PLUGIN_CLASS_WITH_JSON(FileBrowserEngine, "plasma-dataengine-filebrowser.json") + +#include "filebrowserengine.moc" diff --git a/plasma/workspace/dataengines/filebrowser/filebrowserengine.h b/plasma/workspace/dataengines/filebrowser/filebrowserengine.h new file mode 100644 index 0000000000..6f3db73e7c --- /dev/null +++ b/plasma/workspace/dataengines/filebrowser/filebrowserengine.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2007 Ivan Cukic + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class KDirWatch; + +/** + * This class evaluates the basic expressions given in the interface. + */ +class FileBrowserEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + FileBrowserEngine(QObject *parent, const QVariantList &args); + ~FileBrowserEngine() override; + +protected: + bool sourceRequestEvent(const QString &path) override; + void init(); + +protected Q_SLOTS: + void dirDirty(const QString &path); + void dirCreated(const QString &path); + void dirDeleted(const QString &path); + +private: + enum EventType { + INIT, + DIRTY, + CREATED, + DELETED, + }; + enum ObjectType { + NOTHING, + FILE, + DIRECTORY, + }; + + KDirWatch *m_dirWatch; + void updateData(const QString &path, EventType event); + void clearData(const QString &path); + + // QMap < QString, QStringList > m_regiteredListeners; +}; diff --git a/plasma/workspace/dataengines/filebrowser/plasma-dataengine-filebrowser.json b/plasma/workspace/dataengines/filebrowser/plasma-dataengine-filebrowser.json new file mode 100644 index 0000000000..2125fe40b7 --- /dev/null +++ b/plasma/workspace/dataengines/filebrowser/plasma-dataengine-filebrowser.json @@ -0,0 +1,124 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Information about files and directories.", + "Description[ar]": "معلومات حول الملفات والأدلة.", + "Description[az]": "Fayllar və qovluqlar haqqında məlumatlar.", + "Description[ca]": "Informació quant a fitxers i directoris.", + "Description[cs]": "Informace o souborech a adresářích.", + "Description[de]": "Informationen über Dateien und Ordner.", + "Description[en_GB]": "Information about files and directories.", + "Description[es]": "Información sobre los archivos y directorios.", + "Description[eu]": "Fitxategi eta direktorioei buruzko informazioa", + "Description[fi]": "Tietoa tiedostoista ja kansioista.", + "Description[fr]": "Informations sur les fichiers et les dossiers.", + "Description[hu]": "Fájl- és mappajellemzők.", + "Description[ia]": "Information re files e directorios.", + "Description[it]": "Informazioni su file e cartelle.", + "Description[ko]": "파일과 디렉터리 정보입니다.", + "Description[lt]": "Informacija apie failus ir katalogus.", + "Description[nl]": "Informatie over bestanden en mappen.", + "Description[nn]": "Informasjon om filer og mapper.", + "Description[pl]": "Wyświetla informacje o plikach i katalogach.", + "Description[pt_BR]": "Informações sobre pastas e arquivos.", + "Description[ro]": "Informații despre fișiere și dosare.", + "Description[ru]": "Сведения о файлах и папках для виджетов.", + "Description[sk]": "Informácie o súboroch a adresároch.", + "Description[sl]": "Informacije o datotekah in mapah.", + "Description[sv]": "Information om filer och kataloger.", + "Description[ta]": "கோப்புகள் மற்றும் அடைவுகளைப் பற்றிய விவரங்கள்", + "Description[tr]": "Dosyalar ve dizinler hakkında bilgiler.", + "Description[uk]": "Інформація про файли і каталоги.", + "Description[vi]": "Thông tin về các tệp và thư mục.", + "Description[x-test]": "xxInformation about files and directories.xx", + "Description[zh_CN]": "关于文件和目录的信息。", + "Icon": "system-file-manager", + "Id": "filebrowser", + "License": "", + "Name": "Files and Directories", + "Name[ar]": "ملفات وأدلة", + "Name[ast]": "Ficheros y direutorios", + "Name[az]": "Fayllar və Qovluqlar", + "Name[be@latin]": "Fajły j katalohi", + "Name[bg]": "Файлове и папки", + "Name[bn]": "ফাইল এবং ডিরেক্টরি", + "Name[bn_IN]": "ফাইল ও ডিরেক্টরি", + "Name[bs]": "Datoteke i fascikle", + "Name[ca@valencia]": "Fitxers i directoris", + "Name[ca]": "Fitxers i directoris", + "Name[cs]": "Soubory a složky", + "Name[csb]": "Lopczi ë katalodżi", + "Name[da]": "Filer og mapper", + "Name[de]": "Dateien und Ordner", + "Name[el]": "Αρχεία και κατάλογοι", + "Name[en_GB]": "Files and Directories", + "Name[eo]": "Dosieroj kaj Dosierujoj", + "Name[es]": "Archivos y directorios", + "Name[et]": "Failid ja kataloogid", + "Name[eu]": "Fitxategiak eta direktorioak", + "Name[fi]": "Tiedostot ja kansiot", + "Name[fr]": "Fichiers et dossiers", + "Name[fy]": "triemmen en triemtafels", + "Name[ga]": "Comhaid agus Comhadlanna", + "Name[gl]": "Ficheiros e directorios", + "Name[gu]": "ફાઇલો અને ડિરેક્ટરીઓ", + "Name[he]": "קבצים ותיקיות", + "Name[hi]": "फ़ाइलें तथा डिरेक्ट्रियाँ", + "Name[hne]": "फाइल अउ डिरेक्टरी", + "Name[hr]": "Datoteke i direktoriji", + "Name[hsb]": "Dataje a zapiski", + "Name[hu]": "Fájlok és mappák", + "Name[ia]": "Files e directorios", + "Name[id]": "File dan Direktori", + "Name[is]": "Skrár og möppur", + "Name[it]": "File e cartelle", + "Name[ja]": "ファイルとディレクトリ", + "Name[kk]": "Файлдар мен Қапшықтар", + "Name[km]": "ឯកសារ​ និង​ថត", + "Name[kn]": "ಕಡತ ಹಾಗೂ ಕಡಕಕೋಶಗಳು", + "Name[ko]": "파일과 디렉터리", + "Name[lt]": "Failai ir katalogai", + "Name[lv]": "Datnes un mapes", + "Name[mai]": "फाइल आओर निर्देशिका", + "Name[mk]": "Датотеки и папки", + "Name[ml]": "ഫയല്‍, തട്ട് എന്നിവ", + "Name[mr]": "फाईल व संचयीका", + "Name[nb]": "Filer og mapper", + "Name[nds]": "Dateien un Ornern", + "Name[nl]": "Bestanden en mappen", + "Name[nn]": "Filer og mapper", + "Name[or]": "ଫାଇଲ ଏବଂ ଡ଼ିରେକ୍ଟୋରୀଗୁଡ଼ିକ", + "Name[pa]": "ਫਾਈਲਾਂ ਅਤੇ ਡਾਇਰੈਕਟਰੀਆਂ", + "Name[pl]": "Pliki i katalogi", + "Name[pt]": "Ficheiros e Pastas", + "Name[pt_BR]": "Arquivos e pastas", + "Name[ro]": "Fișiere și dosare", + "Name[ru]": "Файлы и папки", + "Name[si]": "ගොනු සහ බහලුම්", + "Name[sk]": "Súbory a adresáre", + "Name[sl]": "Datoteke in mape", + "Name[sr@ijekavian]": "фајлови и фасцикле", + "Name[sr@ijekavianlatin]": "fajlovi i fascikle", + "Name[sr@latin]": "fajlovi i fascikle", + "Name[sr]": "фајлови и фасцикле", + "Name[sv]": "Filer och kataloger", + "Name[ta]": "கோப்புகளும் அடைவுகளும்", + "Name[tg]": "Файлҳо ва ҷузвадонҳо", + "Name[th]": "ดูแฟ้มและไดเรกทอรี", + "Name[tr]": "Dosyalar ve Dizinler", + "Name[ug]": "ھۆججەت ۋە مۇندەرىجەلەر", + "Name[uk]": "Файли і каталоги", + "Name[vi]": "Tệp và thư mục", + "Name[wa]": "Fitchîs eyet ridants", + "Name[x-test]": "xxFiles and Directoriesxx", + "Name[zh_CN]": "文件和目录", + "Name[zh_TW]": "檔案與目錄", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/geolocation/CMakeLists.txt b/plasma/workspace/dataengines/geolocation/CMakeLists.txt new file mode 100644 index 0000000000..175687bd4d --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/CMakeLists.txt @@ -0,0 +1,47 @@ +remove_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) # needed for QNetworkConfigurationManager + +set(plasma_geolocation_interface_SRCS geolocationprovider.cpp) +add_library(plasma-geolocation-interface SHARED ${plasma_geolocation_interface_SRCS}) +target_link_libraries(plasma-geolocation-interface + PUBLIC + Qt::Core + Qt::Network + KF5::Plasma + PRIVATE + KF5::KIOCore +) +set_target_properties(plasma-geolocation-interface PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) +install(TARGETS plasma-geolocation-interface ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +install(FILES geolocationprovider.h ${CMAKE_CURRENT_BINARY_DIR}/geolocation_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/plasma/geolocation + COMPONENT Devel) + +kcoreaddons_add_plugin(plasma_engine_geolocation SOURCES geolocation.cpp INSTALL_NAMESPACE plasma/dataengine) +target_compile_definitions(plasma_engine_geolocation PRIVATE -DQT_NO_KEYWORDS) +generate_export_header(plasma_engine_geolocation EXPORT_FILE_NAME "geolocation_export.h" BASE_NAME "GEOLOCATION") +target_link_libraries(plasma_engine_geolocation + plasma-geolocation-interface + KF5::Plasma + KF5::CoreAddons + KF5::KIOCore + KF5::NetworkManagerQt + KF5::Service + KF5::Solid) + +kcoreaddons_add_plugin(plasma-geolocation-ip SOURCES location_ip.cpp INSTALL_NAMESPACE plasma/geolocationprovider) +ecm_qt_declare_logging_category(plasma-geolocation-ip HEADER geolocdebug.h IDENTIFIER DATAENGINE_GEOLOCATION CATEGORY_NAME org.kde.plasma.dataengine.geolocation) +target_compile_definitions(plasma-geolocation-ip PRIVATE -DQT_NO_KEYWORDS) +target_link_libraries(plasma-geolocation-ip plasma-geolocation-interface KF5::KIOCore KF5::NetworkManagerQt) + +pkg_check_modules(LIBGPS libgps IMPORTED_TARGET) + +if(TARGET PkgConfig::LIBGPS) + kcoreaddons_add_plugin(plasma-geolocation-gps SOURCES location_gps.cpp INSTALL_NAMESPACE plasma/geolocationprovider) + ecm_qt_declare_logging_category(plasma-geolocation-gps HEADER geolocdebug.h IDENTIFIER DATAENGINE_GEOLOCATION CATEGORY_NAME org.kde.plasma.dataengine.geolocation) + target_link_libraries(plasma-geolocation-gps plasma-geolocation-interface PkgConfig::LIBGPS) +endif() diff --git a/plasma/workspace/dataengines/geolocation/geolocation.cpp b/plasma/workspace/dataengines/geolocation/geolocation.cpp new file mode 100644 index 0000000000..642d81b395 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/geolocation.cpp @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damsten + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "geolocation.h" + +#include + +#include +#include +#include +#include + +static const char SOURCE[] = "location"; + +Geolocation::Geolocation(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + setMinimumPollingInterval(500); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::networkingEnabledChanged, this, &Geolocation::networkStatusChanged); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::wirelessEnabledChanged, this, &Geolocation::networkStatusChanged); + m_updateTimer.setInterval(100); + m_updateTimer.setSingleShot(true); + connect(&m_updateTimer, &QTimer::timeout, this, &Geolocation::actuallySetData); + m_networkChangedTimer.setInterval(100); + m_networkChangedTimer.setSingleShot(true); + connect(&m_networkChangedTimer, &QTimer::timeout, this, [this] { + updatePlugins(GeolocationProvider::NetworkConnected); + }); + init(); +} + +void Geolocation::init() +{ + // TODO: should this be delayed even further, e.g. when the source is requested? + const QVector offers = KPluginMetaData::findPlugins("plasma/geolocationprovider"); + for (const auto &metaData : offers) { + auto result = KPluginFactory::instantiatePlugin(metaData, this); + if (result) { + GeolocationProvider *plugin = result.plugin; + m_plugins << plugin; + plugin->init(&m_data, &m_accuracy); + connect(plugin, &GeolocationProvider::updated, this, &Geolocation::pluginUpdated); + connect(plugin, &GeolocationProvider::availabilityChanged, this, &Geolocation::pluginAvailabilityChanged); + } else { + qDebug() << "Failed to load GeolocationProvider:" << metaData.fileName() << result.errorString; + } + } +} + +Geolocation::~Geolocation() +{ + qDeleteAll(m_plugins); +} + +QStringList Geolocation::sources() const +{ + return QStringList() << SOURCE; +} + +bool Geolocation::updateSourceEvent(const QString &name) +{ + // qDebug() << name; + if (name == SOURCE) { + return updatePlugins(GeolocationProvider::SourceEvent); + } + + return false; +} + +bool Geolocation::updatePlugins(GeolocationProvider::UpdateTriggers triggers) +{ + bool changed = false; + + for (GeolocationProvider *plugin : qAsConst(m_plugins)) { + changed = plugin->requestUpdate(triggers) || changed; + } + + if (changed) { + m_updateTimer.start(); + } + + return changed; +} + +bool Geolocation::sourceRequestEvent(const QString &name) +{ + qDebug() << name; + if (name == SOURCE) { + updatePlugins(GeolocationProvider::ForcedUpdate); + setData(SOURCE, m_data); + return true; + } + + return false; +} + +void Geolocation::networkStatusChanged(bool isOnline) +{ + qDebug() << "network status changed"; + if (isOnline) { + m_networkChangedTimer.start(); + } +} + +void Geolocation::pluginAvailabilityChanged(GeolocationProvider *provider) +{ + m_data.clear(); + m_accuracy.clear(); + + provider->requestUpdate(GeolocationProvider::ForcedUpdate); + + bool changed = false; + for (GeolocationProvider *plugin : qAsConst(m_plugins)) { + changed = plugin->populateSharedData() || changed; + } + + if (changed) { + m_updateTimer.start(); + } +} + +void Geolocation::pluginUpdated() +{ + m_updateTimer.start(); +} + +void Geolocation::actuallySetData() +{ + setData(SOURCE, m_data); +} + +K_PLUGIN_CLASS_WITH_JSON(Geolocation, "plasma-dataengine-geolocation.json") + +#include "geolocation.moc" diff --git a/plasma/workspace/dataengines/geolocation/geolocation.h b/plasma/workspace/dataengines/geolocation/geolocation.h new file mode 100644 index 0000000000..452ec4ddeb --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/geolocation.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damstén + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +#include "geolocationprovider.h" + +class GeolocationProvider; + +class Geolocation : public Plasma::DataEngine +{ + Q_OBJECT + +public: + Geolocation(QObject *parent, const QVariantList &args); + ~Geolocation() override; + virtual void init(); + QStringList sources() const override; + +protected: + bool sourceRequestEvent(const QString &name) override; + bool updateSourceEvent(const QString &name) override; + bool updatePlugins(GeolocationProvider::UpdateTriggers triggers); + +protected Q_SLOTS: + void networkStatusChanged(bool isOnline); + void pluginAvailabilityChanged(GeolocationProvider *provider); + void pluginUpdated(); + void actuallySetData(); + +private: + Data m_data; + EntryAccuracy m_accuracy; + QList m_plugins; + QTimer m_updateTimer; + QTimer m_networkChangedTimer; +}; diff --git a/plasma/workspace/dataengines/geolocation/geolocationprovider.cpp b/plasma/workspace/dataengines/geolocation/geolocationprovider.cpp new file mode 100644 index 0000000000..cc69da23e1 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/geolocationprovider.cpp @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "geolocationprovider.h" + +GeolocationProvider::GeolocationProvider(QObject *parent, const QVariantList &args) + : QObject(parent) + , m_sharedData(nullptr) + , m_sharedAccuracies(nullptr) + , m_accuracy(1000) + , m_updateTriggers(SourceEvent) + , m_available(true) + , m_updating(false) +{ + Q_UNUSED(args) + m_updateTimer.setSingleShot(true); + m_updateTimer.setInterval(0); + qRegisterMetaType("Plasma::DataEngine::Data"); + connect(&m_updateTimer, &QTimer::timeout, this, &GeolocationProvider::updated); +} + +void GeolocationProvider::init(Plasma::DataEngine::Data *data, EntryAccuracy *accuracies) +{ + m_sharedData = data; + m_sharedAccuracies = accuracies; + init(); +} + +int GeolocationProvider::accuracy() const +{ + return m_accuracy; +} + +bool GeolocationProvider::isAvailable() const +{ + return m_available; +} + +bool GeolocationProvider::requestUpdate(GeolocationProvider::UpdateTriggers triggers) +{ + if (m_available && !m_updating && (triggers == ForcedUpdate || triggers & m_updateTriggers)) { + m_updating = true; + update(); + return true; + } + + return false; +} + +GeolocationProvider::UpdateTriggers GeolocationProvider::updateTriggers() const +{ + return m_updateTriggers; +} + +bool GeolocationProvider::populateSharedData() +{ + Plasma::DataEngine::Data::const_iterator it = m_data.constBegin(); + bool changed = false; + + while (it != m_data.constEnd()) { + if (!m_sharedData->contains(it.key()) || m_sharedAccuracies->value(it.key()) < m_accuracy) { + m_sharedData->insert(it.key(), it.value()); + m_sharedAccuracies->insert(it.key(), m_accuracy); + changed = true; + } + + ++it; + } + + return changed; +} + +void GeolocationProvider::setAccuracy(int accuracy) +{ + m_accuracy = accuracy; +} + +void GeolocationProvider::setIsAvailable(bool available) +{ + if (m_available == available) { + return; + } + + m_available = available; + Q_EMIT availabilityChanged(this); +} + +void GeolocationProvider::setData(const Plasma::DataEngine::Data &data) +{ + m_updating = false; + m_data = data; + if (populateSharedData()) { + m_updateTimer.start(); + } +} + +void GeolocationProvider::setData(const QString &key, const QVariant &value) +{ + m_updating = false; + m_data.insert(key, value); + + if (!m_sharedData->contains(key) || m_sharedAccuracies->value(QStringLiteral("key")) < m_accuracy) { + m_sharedData->insert(key, value); + m_sharedAccuracies->insert(key, accuracy()); + m_updateTimer.start(); + } +} + +void GeolocationProvider::setUpdateTriggers(UpdateTriggers triggers) +{ + m_updateTriggers = triggers; +} + +void GeolocationProvider::init() +{ +} + +void GeolocationProvider::update() +{ +} diff --git a/plasma/workspace/dataengines/geolocation/geolocationprovider.h b/plasma/workspace/dataengines/geolocation/geolocationprovider.h new file mode 100644 index 0000000000..2a8cea201b --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/geolocationprovider.h @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "geolocation_export.h" + +typedef QHash EntryAccuracy; + +class GEOLOCATION_EXPORT GeolocationProvider : public QObject +{ + Q_OBJECT + +public: + enum UpdateTrigger { + ForcedUpdate = 0, + SourceEvent = 1, + NetworkConnected = 2, + }; + Q_DECLARE_FLAGS(UpdateTriggers, UpdateTrigger) + + explicit GeolocationProvider(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + void init(Plasma::DataEngine::Data *data, EntryAccuracy *accuracies); + + UpdateTriggers updateTriggers() const; + int accuracy() const; + bool isAvailable() const; + bool requestUpdate(UpdateTriggers trigger); + bool populateSharedData(); + +Q_SIGNALS: + void updated(); + void availabilityChanged(GeolocationProvider *provider); + +protected: + void setAccuracy(int accuracy); + void setIsAvailable(bool available); + void setUpdateTriggers(UpdateTriggers triggers); + virtual void init(); + virtual void update(); + +protected Q_SLOTS: + void setData(const Plasma::DataEngine::Data &data); + void setData(const QString &key, const QVariant &value); + +private: + Plasma::DataEngine::Data *m_sharedData; + EntryAccuracy *m_sharedAccuracies; + Plasma::DataEngine::Data m_data; + QTimer m_updateTimer; + int m_accuracy; + UpdateTriggers m_updateTriggers; + bool m_available : 1; + bool m_updating : 1; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(GeolocationProvider::UpdateTriggers) diff --git a/plasma/workspace/dataengines/geolocation/location_gps.cpp b/plasma/workspace/dataengines/geolocation/location_gps.cpp new file mode 100644 index 0000000000..ab69c2dcb3 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/location_gps.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damstén + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "location_gps.h" +#include "geolocdebug.h" + +Gpsd::Gpsd(gps_data_t *gpsdata) + : m_gpsdata(gpsdata) + , m_abort(false) +{ +} + +Gpsd::~Gpsd() +{ + m_abort = true; + m_condition.wakeOne(); + wait(); +} + +void Gpsd::update() +{ + if (!isRunning()) { + start(); + } else { + m_condition.wakeOne(); + } +} + +void Gpsd::run() +{ +#if defined(GPSD_API_MAJOR_VERSION) && (GPSD_API_MAJOR_VERSION >= 3) && defined(WATCH_ENABLE) + gps_stream(m_gpsdata, WATCH_ENABLE, nullptr); +#else + gps_query(m_gpsdata, "w+x\n"); +#endif + + while (!m_abort) { + Plasma::DataEngine::Data d; + +#if GPSD_API_MAJOR_VERSION >= 7 + if (gps_read(m_gpsdata, NULL, 0) != -1) { +#elif GPSD_API_MAJOR_VERSION >= 5 + if (gps_read(m_gpsdata) != -1) { +#else + if (gps_poll(m_gpsdata) != -1) { +#endif +#if GPSD_API_MAJOR_VERSION >= 9 + if (m_gpsdata->online.tv_sec || m_gpsdata->online.tv_nsec) { +#else + if (m_gpsdata->online) { +#endif +#if defined(STATUS_UNK) // STATUS_NO_FIX was renamed to STATUS_UNK without bumping API version + if (m_gpsdata->fix.status != STATUS_UNK) { +#elif GPSD_API_MAJOR_VERSION >= 10 + if (m_gpsdata->fix.status != STATUS_NO_FIX) { +#else + if (m_gpsdata->status != STATUS_NO_FIX) { +#endif + d["accuracy"] = 30; + d["latitude"] = QString::number(m_gpsdata->fix.latitude); + d["longitude"] = QString::number(m_gpsdata->fix.longitude); + } + } + } + + Q_EMIT dataReady(d); + + m_condition.wait(&m_mutex); + } +} + +Gps::Gps(QObject *parent, const QVariantList &args) + : GeolocationProvider(parent, args) + , m_gpsd(nullptr) +#if GPSD_API_MAJOR_VERSION >= 5 + , m_gpsdata(nullptr) +#endif +{ +#if GPSD_API_MAJOR_VERSION >= 5 + m_gpsdata = new gps_data_t; + if (gps_open("localhost", DEFAULT_GPSD_PORT, m_gpsdata) != -1) { +#else + gps_data_t *m_gpsdata = gps_open("localhost", DEFAULT_GPSD_PORT); + if (m_gpsdata) { +#endif + qCDebug(DATAENGINE_GEOLOCATION) << "gpsd found."; + m_gpsd = new Gpsd(m_gpsdata); + connect(m_gpsd, SIGNAL(dataReady(Plasma::DataEngine::Data)), this, SLOT(setData(Plasma::DataEngine::Data))); + } else { + qCWarning(DATAENGINE_GEOLOCATION) << "gpsd not found"; + } + + setIsAvailable(m_gpsd); +} + +Gps::~Gps() +{ + delete m_gpsd; +#if GPSD_API_MAJOR_VERSION >= 5 + delete m_gpsdata; +#endif +} + +void Gps::update() +{ + if (m_gpsd) { + m_gpsd->update(); + } +} + +K_PLUGIN_CLASS_WITH_JSON(Gps, "plasma-geolocation-gps.json") + +#include "location_gps.moc" diff --git a/plasma/workspace/dataengines/geolocation/location_gps.h b/plasma/workspace/dataengines/geolocation/location_gps.h new file mode 100644 index 0000000000..c208260452 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/location_gps.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damstén + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include "geolocationprovider.h" + +class Gpsd : public QThread +{ + Q_OBJECT +public: + explicit Gpsd(gps_data_t *gpsdata); + ~Gpsd() override; + + void update(); + +Q_SIGNALS: + void dataReady(const Plasma::DataEngine::Data &data); + +protected: + void run() override; + +private: + gps_data_t *m_gpsdata; + QMutex m_mutex; + QWaitCondition m_condition; + bool m_abort; +}; + +class Gps : public GeolocationProvider +{ + Q_OBJECT +public: + explicit Gps(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + ~Gps() override; + + void update() override; + +private: + Gpsd *m_gpsd; +#if GPSD_API_MAJOR_VERSION >= 5 + gps_data_t *m_gpsdata; +#endif +}; diff --git a/plasma/workspace/dataengines/geolocation/location_ip.cpp b/plasma/workspace/dataengines/geolocation/location_ip.cpp new file mode 100644 index 0000000000..27b530810c --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/location_ip.cpp @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damstén + + Original Implementation: + SPDX-FileCopyrightText: 2009 Andrew Coles + + Extension to iplocationtools engine: + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "location_ip.h" +#include "geolocdebug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Ip::Private : public QObject +{ + Q_OBJECT +public: + Private(Ip *q) + : q(q) + { + } + + void readGeoLocation(KJob *job) + { + m_geoLocationResolved = true; + if (job && job->error()) { + qCCritical(DATAENGINE_GEOLOCATION) << "error: " << job->errorString(); + m_geoLocationPayload.clear(); + checkUpdateData(); + return; + } + const QJsonObject json = QJsonDocument::fromJson(m_geoLocationPayload).object(); + m_geoLocationPayload.clear(); + + auto accuracyIt = json.find(QStringLiteral("accuracy")); + if (accuracyIt != json.end()) { + m_data[QStringLiteral("accuracy")] = (*accuracyIt).toDouble(); + } else { + m_data[QStringLiteral("accuracy")] = 40000; + } + + auto locationIt = json.find(QStringLiteral("location")); + if (locationIt != json.end()) { + QJsonObject location = (*locationIt).toObject(); + m_data[QStringLiteral("latitude")] = location.value(QStringLiteral("lat")).toDouble(); + m_data[QStringLiteral("longitude")] = location.value(QStringLiteral("lng")).toDouble(); + } + checkUpdateData(); + } + + void clear() + { + m_geoLocationPayload.clear(); + m_countryPayload.clear(); + m_countryResolved = false; + m_geoLocationResolved = false; + m_data.clear(); + } + + void geoLocationData(KIO::Job *job, const QByteArray &data) + { + Q_UNUSED(job) + + if (data.isEmpty()) { + return; + } + m_geoLocationPayload.append(data); + } + + void countryData(KIO::Job *job, const QByteArray &data) + { + Q_UNUSED(job) + + if (data.isEmpty()) { + return; + } + m_countryPayload.append(data); + } + + void readCountry(KJob *job) + { + m_countryResolved = true; + if (job && job->error()) { + qCCritical(DATAENGINE_GEOLOCATION) << "error: " << job->errorString(); + m_countryPayload.clear(); + checkUpdateData(); + return; + } + + const QJsonObject json = QJsonDocument::fromJson(m_countryPayload).object(); + m_countryPayload.clear(); + + m_data[QStringLiteral("country")] = json.value(QStringLiteral("country_name")).toString(); + m_data[QStringLiteral("country code")] = json.value(QStringLiteral("country_code")).toString(); + checkUpdateData(); + } + +private: + void checkUpdateData() + { + if (!m_countryResolved || !m_geoLocationResolved) { + return; + } + q->setData(m_data); + } + + Ip *q; + QByteArray m_geoLocationPayload; + QByteArray m_countryPayload; + bool m_countryResolved = false; + bool m_geoLocationResolved = false; + Plasma::DataEngine::Data m_data; +}; + +Ip::Ip(QObject *parent, const QVariantList &args) + : GeolocationProvider(parent, args) + , d(new Private(this)) +{ + setUpdateTriggers(SourceEvent | NetworkConnected); +} + +Ip::~Ip() +{ + delete d; +} + +static QJsonArray accessPoints() +{ + QJsonArray wifiAccessPoints; + const KConfigGroup config = KSharedConfig::openConfig()->group(QStringLiteral("org.kde.plasma.geolocation.ip")); + if (!NetworkManager::isWirelessEnabled() || !config.readEntry("Wifi", false)) { + return wifiAccessPoints; + } + for (const auto &device : NetworkManager::networkInterfaces()) { + QSharedPointer wifi = qSharedPointerDynamicCast(device); + if (!wifi) { + continue; + } + for (const auto &network : wifi->networks()) { + const QString &ssid = network->ssid(); + if (ssid.isEmpty() || ssid.endsWith(QLatin1String("_nomap"))) { + // skip hidden SSID and networks with "_nomap" + continue; + } + for (const auto &accessPoint : network->accessPoints()) { + wifiAccessPoints.append(QJsonObject{{QStringLiteral("macAddress"), accessPoint->hardwareAddress()}}); + } + } + } + return wifiAccessPoints; +} + +void Ip::update() +{ + d->clear(); + if (!NetworkManager::isNetworkingEnabled()) { + setData(Plasma::DataEngine::Data()); + return; + } + const QJsonArray wifiAccessPoints = accessPoints(); + QJsonObject request; + if (wifiAccessPoints.count() >= 2) { + request.insert(QStringLiteral("wifiAccessPoints"), wifiAccessPoints); + } + const QByteArray postData = QJsonDocument(request).toJson(QJsonDocument::Compact); + const QString apiKey = QStringLiteral("60e8eae6-3988-4ada-ad48-2cfddddf216b"); + KIO::TransferJob *datajob = + KIO::http_post(QUrl(QStringLiteral("https://location.services.mozilla.com/v1/geolocate?key=%1").arg(apiKey)), postData, KIO::HideProgressInfo); + datajob->addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); + + qCDebug(DATAENGINE_GEOLOCATION) << "Fetching https://location.services.mozilla.com/v1/geolocate"; + connect(datajob, &KIO::TransferJob::data, d, &Ip::Private::geoLocationData); + connect(datajob, &KIO::TransferJob::result, d, &Ip::Private::readGeoLocation); + + datajob = KIO::http_post(QUrl(QStringLiteral("https://location.services.mozilla.com/v1/country?key=%1").arg(apiKey)), postData, KIO::HideProgressInfo); + datajob->addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); + connect(datajob, &KIO::TransferJob::data, d, &Ip::Private::countryData); + connect(datajob, &KIO::TransferJob::result, d, &Ip::Private::readCountry); +} + +K_PLUGIN_CLASS_WITH_JSON(Ip, "plasma-geolocation-ip.json") + +#include "location_ip.moc" diff --git a/plasma/workspace/dataengines/geolocation/location_ip.h b/plasma/workspace/dataengines/geolocation/location_ip.h new file mode 100644 index 0000000000..757808dbe1 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/location_ip.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damstén + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "geolocationprovider.h" + +class Ip : public GeolocationProvider +{ + Q_OBJECT +public: + explicit Ip(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + ~Ip() override; + + void update() override; + +private: + class Private; + Private *const d; +}; diff --git a/plasma/workspace/dataengines/geolocation/plasma-dataengine-geolocation.json b/plasma/workspace/dataengines/geolocation/plasma-dataengine-geolocation.json new file mode 100644 index 0000000000..022d0eda88 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/plasma-dataengine-geolocation.json @@ -0,0 +1,145 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "damu@iki.fi", + "Name": "Petri Damstén", + "Name[ar]": "Petri Damstén", + "Name[az]": "Petri Damstén", + "Name[ca]": "Petri Damstén", + "Name[cs]": "Petri Damstén", + "Name[de]": "Petri Damstén", + "Name[en_GB]": "Petri Damstén", + "Name[es]": "Petri Damstén", + "Name[eu]": "Petri Damstén", + "Name[fi]": "Petri Damstén", + "Name[fr]": "Petri Damstén", + "Name[hu]": "Petri Damstén", + "Name[ia]": "Petri Damstén", + "Name[it]": "Petri Damstén", + "Name[ko]": "Petri Damstén", + "Name[lt]": "Petri Damstén", + "Name[nl]": "Petri Damstén", + "Name[nn]": "Petri Damstén", + "Name[pl]": "Petri Damstén", + "Name[pt_BR]": "Petri Damstén", + "Name[ro]": "Petri Damstén", + "Name[ru]": "Petri Damstén", + "Name[sk]": "Petri Damstén", + "Name[sl]": "Petri Damstén", + "Name[sv]": "Petri Damstén", + "Name[tr]": "Petri Damstén", + "Name[uk]": "Petri Damstén", + "Name[vi]": "Petri Damstén", + "Name[x-test]": "xxPetri Damsténxx", + "Name[zh_CN]": "Petri Damstén" + } + ], + "Category": "", + "Description": "Geolocation Data Engine", + "Description[ar]": "محرّك بيانات الموقع الجغرافي", + "Description[az]": "Coğrafi məkan haqqında məlumat mənbəyi", + "Description[ca]": "Motor de dades de geolocalització", + "Description[cs]": "Datový nástroj geolokace", + "Description[de]": "Datentreiber für Geolokalisierung", + "Description[en_GB]": "Geolocation Data Engine", + "Description[es]": "Motor de datos de geolocalización", + "Description[eu]": "Geokokapen datuen motorra", + "Description[fi]": "Sijaintitietomoottori", + "Description[fr]": "Moteur de données de géo-localisation", + "Description[hu]": "Kezelőmodul a földrajzi helyzet kiírásához", + "Description[ia]": "Motor de datos de geolocation", + "Description[it]": "Motore di dati per la geolocalizzazione", + "Description[ko]": "위치 데이터 엔진", + "Description[lt]": "Geografinės vietos nustatymo duomenų variklis", + "Description[nl]": "Geolocatie gegevensengine", + "Description[nn]": "Geolocation-datamotor", + "Description[pa]": "ਭੂਗੋਲਿਕ-ਟਿਕਾਣਾ ਡਾਟਾ ਇੰਜਣ", + "Description[pl]": "Silnik danych geolokalizacji", + "Description[pt_BR]": "Mecanismo de dados de localização geográfica", + "Description[ro]": "Motor de date Geolocalizare", + "Description[ru]": "Источник данных о местоположении", + "Description[sk]": "Dátový nástroj geolokalizácie", + "Description[sl]": "Podatkovni pogon s podatki o geolokaciji", + "Description[sv]": "Datagränssnitt för geografisk lokalisering", + "Description[ta]": "புவியிருப்பிடத்தை வழங்கும் நிரல்", + "Description[tr]": "Coğrafi Konum Veri Motoru", + "Description[uk]": "Рушій даних геопозиціювання", + "Description[vi]": "Dụng cụ dữ liệu định vị địa lí", + "Description[x-test]": "xxGeolocation Data Enginexx", + "Description[zh_CN]": "地理位置数据引擎", + "Icon": "applications-internet", + "Id": "geolocation", + "Name": "Geolocation", + "Name[ar]": "الموقع الجغرافي", + "Name[az]": "Coğrafi məkan", + "Name[bg]": "Геолокация", + "Name[bs]": "Geolokacija", + "Name[ca@valencia]": "Geolocalització", + "Name[ca]": "Geolocalització", + "Name[cs]": "Geolokace", + "Name[csb]": "Geògrafnô lokalizacëjô", + "Name[da]": "Geo-lokalisering", + "Name[de]": "Geolokalisierung", + "Name[el]": "Γεωτοποθέτηση", + "Name[en_GB]": "Geolocation", + "Name[eo]": "GeoLokado", + "Name[es]": "Geolocalización", + "Name[et]": "Geolokatsioon", + "Name[eu]": "Geokokapena", + "Name[fi]": "Paikkasijainti", + "Name[fr]": "Géo-localisation", + "Name[fy]": "Geolokaasje", + "Name[ga]": "Geolocation", + "Name[gl]": "Xeolocalización", + "Name[he]": "מציאת מיקום גאוגרפי", + "Name[hi]": "भौगोलिक स्थान", + "Name[hr]": "Geolociranje", + "Name[hsb]": "Geolokacija", + "Name[hu]": "Földrajzi helyzet", + "Name[ia]": "Geolocation", + "Name[id]": "Geolokasi", + "Name[is]": "Hnattstaðsetningar", + "Name[it]": "Geolocalizzazione", + "Name[ja]": "ジオロケーション", + "Name[kk]": "Жердегі орны", + "Name[km]": "ទីតាំង​ភូមិសាស្ដ្រ", + "Name[kn]": "ಭೂಪ್ರದೇಶ", + "Name[ko]": "위치", + "Name[lt]": "Geografinės vietos nustatymas", + "Name[lv]": "Ģeolokācija", + "Name[mk]": "Геолоцирање", + "Name[ml]": "ഭൂസ്ഥാനങ്ങള്‍", + "Name[mr]": "जिओलोकेशन", + "Name[nb]": "Geografisk plassering", + "Name[nds]": "Eersteden", + "Name[nl]": "Geolocatie", + "Name[nn]": "Geolocation", + "Name[pa]": "ਭੂਗੋਲਿਕ ਟਿਕਾਣਾ", + "Name[pl]": "Geolokalizacja", + "Name[pt]": "Geo-Localização", + "Name[pt_BR]": "Localização geográfica", + "Name[ro]": "Geolocalizare", + "Name[ru]": "Местоположение", + "Name[si]": "පිහිටුම", + "Name[sk]": "Geolokalizácia", + "Name[sl]": "Geolokacija", + "Name[sr@ijekavian]": "геолокација", + "Name[sr@ijekavianlatin]": "geolokacija", + "Name[sr@latin]": "geolokacija", + "Name[sr]": "геолокација", + "Name[sv]": "Geografisk lokalisering", + "Name[ta]": "புவியிருப்பிடம்", + "Name[tg]": "Ҷойгиршавии ҷуғрофӣ", + "Name[th]": "การระบุพิกัดตำแหน่ง", + "Name[tr]": "Geolocation", + "Name[ug]": "جۇغراپىيىلىك ئورۇن", + "Name[uk]": "Геопозиціювання", + "Name[vi]": "Định vị địa lí", + "Name[wa]": "Djeyoplaeçmint", + "Name[x-test]": "xxGeolocationxx", + "Name[zh_CN]": "地理位置", + "Name[zh_TW]": "Geolocation", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/geolocation/plasma-geolocation-gps.json b/plasma/workspace/dataengines/geolocation/plasma-geolocation-gps.json new file mode 100644 index 0000000000..54d83664ae --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/plasma-geolocation-gps.json @@ -0,0 +1,110 @@ +{ + "KPlugin": { + "Description": "Geolocation from GPS address.", + "Description[ar]": "تحديد الموقع الجغرافي من عنوان جي.بي.اس", + "Description[az]": "GPS ünvanına görə coğrafi məkan.", + "Description[ca]": "Geolocalització des d'una adreça GPS.", + "Description[cs]": "Geolokace z GPS adresy.", + "Description[de]": "Geolokalisierung mittels GPS-Koordinate.", + "Description[en_GB]": "Geolocation from GPS address.", + "Description[es]": "Geolocalización desde una dirección GPS.", + "Description[eu]": "Geokokapena GPS helbidetik.", + "Description[fi]": "Sijaintitieto GPS-osoitteesta.", + "Description[fr]": "Géo-localisation depuis une adresse GPS.", + "Description[hu]": "A földrajzi helyzet meghatározása GPS segítségével.", + "Description[ia]": "Geolocation ex adresse GPS.", + "Description[it]": "Geolocalizzazione dalla posizione GPS.", + "Description[ko]": "GPS 위치에 따른 주소입니다.", + "Description[lt]": "Geografinės vietos nustatymas pagal GPS adresą.", + "Description[nl]": "Geolocatie uit GPS-adres.", + "Description[nn]": "Geolocation med GPS-adresse.", + "Description[pa]": "GPS ਐਡਰੈੱਸ ਤੋਂ ਭੂਗੋਲਿਕ ਟਿਕਾਣਾ।", + "Description[pl]": "Geolokalizuje z adresu GPS.", + "Description[pt_BR]": "Localização geográfica de endereços de GPS.", + "Description[ro]": "Geolocalizare din adresă GPS.", + "Description[ru]": "Геолокация по адресу GPS.", + "Description[sk]": "Geolokalizácia z GPS adresy.", + "Description[sl]": "Geolokacija iz podatkov GPS.", + "Description[sv]": "Geografisk lokalisering från GPS-adress.", + "Description[ta]": "GPS முகவரியிலிருந்து இருப்பிடத்தைக் கண்டுபிடிக்கும்", + "Description[tr]": "GPS adresinden coğrafi konum.", + "Description[uk]": "Геопозиціювання за GPS-адресою", + "Description[vi]": "Định vị địa lí từ địa chỉ GPS.", + "Description[x-test]": "xxGeolocation from GPS address.xx", + "Description[zh_CN]": "通过 GPS 地址确定的地理位置。", + "Icon": "applications-internet", + "Id": "gps", + "Name": "Geolocation GPS", + "Name[ar]": "تحديد الموقع الجغرافي جي.بي.اس", + "Name[az]": "GPS üzrə coğrafi məkan", + "Name[bg]": "Геолокация (GPS)", + "Name[bs]": "Geolokacija GPS", + "Name[ca@valencia]": "Geolocalització GPS", + "Name[ca]": "Geolocalització GPS", + "Name[cs]": "Geolokace GPS", + "Name[csb]": "Geògrafnô lokalizacëjô GPS", + "Name[da]": "GPS til geo-lokalisering", + "Name[de]": "Geolokalisierung GPS", + "Name[el]": "GPS Γεωτοποθέτηση", + "Name[en_GB]": "Geolocation GPS", + "Name[eo]": "GPS loktrovado", + "Name[es]": "Geolocalización mediante GPS", + "Name[et]": "Geolokatsioon GPS-ist", + "Name[eu]": "GPS geokokapena", + "Name[fi]": "Paikkasijainti GPS", + "Name[fr]": "Géo-localisation par GPS", + "Name[fy]": "Geolokaasje GPS", + "Name[ga]": "Córas Suite Domhanda Geolocation", + "Name[gl]": "Xeolocalización con GPS", + "Name[he]": "מציאת מיקום גאוגרפי GPS", + "Name[hi]": "भौगोलिक स्थान जीपीएस", + "Name[hr]": "GPS geolociranje", + "Name[hsb]": "Geolokacija GPS", + "Name[hu]": "GPS-alapú helyzetjelző", + "Name[ia]": "Geolocation GPS", + "Name[id]": "Geolokasi GPS", + "Name[is]": "GPS hnattstaðsetning", + "Name[it]": "Geolocalizzazione GPS", + "Name[ja]": "ジオロケーション GPS", + "Name[kk]": "GPS-тен орнын табу", + "Name[km]": "ទីតាំង​ភូមិសាស្ត្រ GPS", + "Name[kn]": "ಭೂಪ್ರದೇಶದ GPS", + "Name[ko]": "GPS 위치", + "Name[lt]": "Geografinės vietos nustatymas pagal GPS", + "Name[lv]": "Ģeolokācija GPS", + "Name[mai]": "भूअवस्थिति जीपीएस", + "Name[mk]": "ГПС за геолоцирање", + "Name[ml]": "ജിപിഎസ് ഭൂസ്ഥാനങ്ങള്‍", + "Name[mr]": "GPS जिओलोकेशन", + "Name[nb]": "Geografisk plassering GPS", + "Name[nds]": "GPS-Eersteed", + "Name[nl]": "Geolocatie GPS", + "Name[nn]": "Geolocation GPS", + "Name[pa]": "ਭੂਗੋਲਿਕ-ਟਿਕਾਣਾ GPS", + "Name[pl]": "Geolokalizacja: GPS", + "Name[pt]": "Geo-Localização por GPS", + "Name[pt_BR]": "Localização geográfica GPS", + "Name[ro]": "GPS geolocalizare", + "Name[ru]": "Геолокация по GPS", + "Name[si]": "පිහිටුම් GPS", + "Name[sk]": "Geolokalizácia GPS", + "Name[sl]": "Geolokacija GPS", + "Name[sr@ijekavian]": "геолокација ГПС", + "Name[sr@ijekavianlatin]": "geolokacija GPS", + "Name[sr@latin]": "geolokacija GPS", + "Name[sr]": "геолокација ГПС", + "Name[sv]": "Lokalisera geografiskt med GPS", + "Name[ta]": "GPS மூலம் புவியிருப்பிடம்", + "Name[tg]": "Ҷойгиршавии ҷуғрофии GPS", + "Name[th]": "การระบุพิกัดตำแหน่งด้วย GPS", + "Name[tr]": "Geolocation GPS", + "Name[ug]": "جۇغراپىيىلىك ئورۇن GPS", + "Name[uk]": "Геопозиціювання за GPS", + "Name[vi]": "Định vị địa lí GPS", + "Name[wa]": "GPS di djeyoplaeçmint", + "Name[x-test]": "xxGeolocation GPSxx", + "Name[zh_CN]": "GPS 地理位置", + "Name[zh_TW]": "Geolocation GPS" + }, + "X-KDE-ParentApp": "geolocation" +} diff --git a/plasma/workspace/dataengines/geolocation/plasma-geolocation-ip.json b/plasma/workspace/dataengines/geolocation/plasma-geolocation-ip.json new file mode 100644 index 0000000000..16a7319e48 --- /dev/null +++ b/plasma/workspace/dataengines/geolocation/plasma-geolocation-ip.json @@ -0,0 +1,110 @@ +{ + "KPlugin": { + "Description": "Geolocation from IP address.", + "Description[ar]": "تحديد الموقع الجغرافي من عنوان الإنترنت", + "Description[az]": "İP ünvanına görə coğrafi məkan", + "Description[ca]": "Geolocalització des d'una adreça IP.", + "Description[cs]": "Geolokace z IP adresy.", + "Description[de]": "Geolokalisierung mittels IP-Adresse.", + "Description[en_GB]": "Geolocation from IP address.", + "Description[es]": "Geolocalización desde una dirección IP.", + "Description[eu]": "Geokokapena IP helbidetik.", + "Description[fi]": "Sijaintitieto IP-osoitteesta.", + "Description[fr]": "Géo-localisation depuis une adresse IP.", + "Description[hu]": "Meghatározza a földrajzi helyzetet az IP cím alapján.", + "Description[ia]": "Geolocation ex adresse IP", + "Description[it]": "Geolocalizzazione dall'indirizzo IP.", + "Description[ko]": "IP에서 얻어낸 주소입니다.", + "Description[lt]": "Geografinės vietos nustatymas pagal IP adresą.", + "Description[nl]": "Geolocatie uit IP-adres.", + "Description[nn]": "Geolocation med IP-adresse.", + "Description[pa]": "IP ਐਡਰੈੱਸ ਤੋਂ ਭੂਗੋਲਿਕ-ਟਿਕਾਣਾ", + "Description[pl]": "Geolokalizuje za pomocą adresu IP.", + "Description[pt_BR]": "Localização geográfica de endereços IP.", + "Description[ro]": "Geolocalizare din adresă IP.", + "Description[ru]": "Геолокация по IP-адресу.", + "Description[sk]": "Geolokalizácia z IP adresy.", + "Description[sl]": "Geolokacija iz naslova IP.", + "Description[sv]": "Geografisk lokalisering från IP-adress.", + "Description[ta]": "IP முகவரியிலிருந்து இருப்பிடத்தைக் கண்டுபிடிக்கும்", + "Description[tr]": "IP adresinden coğrafi konum.", + "Description[uk]": "Геопозиціювання за IP-адресою", + "Description[vi]": "Định vị địa lí từ địa chỉ IP.", + "Description[x-test]": "xxGeolocation from IP address.xx", + "Description[zh_CN]": "通过 IP 地址确定的地理位置。", + "Icon": "applications-internet", + "Id": "ip", + "Name": "Geolocation IP", + "Name[ar]": "تحديد الموقع الجغرافي عنوان الإنترنت", + "Name[az]": "İP üzrə coğrafi məkan", + "Name[bg]": "Геолокация (IP)", + "Name[bs]": "Geolokacija IP", + "Name[ca@valencia]": "Geolocalització IP", + "Name[ca]": "Geolocalització IP", + "Name[cs]": "Geolokace IP", + "Name[csb]": "IP geògrafny lokalizacëji", + "Name[da]": "IP-adresse til geo-lokalisering", + "Name[de]": "Geolokalisierung IP", + "Name[el]": "Γεωτοποθεσίες IP", + "Name[en_GB]": "Geolocation IP", + "Name[eo]": "GeoLokado IP", + "Name[es]": "Geolocalización mediante IP", + "Name[et]": "Geolokatsioon IP-st", + "Name[eu]": "IP geokokapen", + "Name[fi]": "Paikkasijainti-ip-osoite", + "Name[fr]": "Géo-localisation par IP", + "Name[fy]": "Geolokaasje IP", + "Name[ga]": "Geolocation IP", + "Name[gl]": "Xeolocalización pola IP", + "Name[he]": "מציאת מיקום גיאוגרפי IP", + "Name[hi]": "भौगोलिक स्थान आइपी", + "Name[hr]": "IP geolociranje", + "Name[hsb]": "Geolokacija IP", + "Name[hu]": "Földrajzi helyzet IP címmel", + "Name[ia]": "Geolocation IP", + "Name[id]": "Geolokasi IP", + "Name[is]": "IP vistfang hnattstaðsetningar", + "Name[it]": "Geolocalizzazione IP", + "Name[ja]": "ジオロケーション IP", + "Name[kk]": "IP-ден орнын табу", + "Name[km]": "IP ទីតាំង​ភូមិសាស្ត្រ", + "Name[kn]": "ಭೂಪ್ರದೇಶದ IP", + "Name[ko]": "IP 위치", + "Name[lt]": "Geografinės vietos nustatymas pagal IP", + "Name[lv]": "Ģeolokācija IP", + "Name[mai]": "भूअवस्थिति IP", + "Name[mk]": "ИП за геолоцирање", + "Name[ml]": "ഭൂസ്ഥാന ഐപി", + "Name[mr]": "जिओलोकेशन IP", + "Name[nb]": "Geografisk plassering IP", + "Name[nds]": "IP-Eersteed", + "Name[nl]": "Geolocatie-IP", + "Name[nn]": "Geolocation IP", + "Name[pa]": "ਭੂਗੋਲਿਕ-ਟਿਕਾਣਾ IP", + "Name[pl]": "Geolokalizacja: IP", + "Name[pt]": "Geo-Localização por IP", + "Name[pt_BR]": "Localização geográfica IP", + "Name[ro]": "IP geolocalizare", + "Name[ru]": "Геолокация по IP-адресу", + "Name[si]": "පිහිටුම් IP", + "Name[sk]": "Geolokalizácia IP", + "Name[sl]": "Geolokacija IP", + "Name[sr@ijekavian]": "геолокација ИП", + "Name[sr@ijekavianlatin]": "geolokacija IP", + "Name[sr@latin]": "geolokacija IP", + "Name[sr]": "геолокација ИП", + "Name[sv]": "Lokalisera geografiskt med IP-adress", + "Name[ta]": "IP மூலம் புவியிருப்பிடம்", + "Name[tg]": "Ҷойгиршавии ҷуғрофии IP", + "Name[th]": "การระบุพิกัดตำแหน่งจากไอพี", + "Name[tr]": "Geolocation IP", + "Name[ug]": "جۇغراپىيىلىك ئورۇن IP", + "Name[uk]": "Геопозиціювання за IP", + "Name[vi]": "Định vị địa lí IP", + "Name[wa]": "Gjeyoplaeçmint di l' IP", + "Name[x-test]": "xxGeolocation IPxx", + "Name[zh_CN]": "IP 地理位置", + "Name[zh_TW]": "Geolocation IP" + }, + "X-KDE-ParentApp": "geolocation" +} diff --git a/plasma/workspace/dataengines/hotplug/CMakeLists.txt b/plasma/workspace/dataengines/hotplug/CMakeLists.txt new file mode 100644 index 0000000000..b8ab5bb6fa --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/CMakeLists.txt @@ -0,0 +1,23 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_hotplug\") + +set(hotplug_engine_SRCS + deviceaction.cpp + deviceserviceaction.cpp + hotplugengine.cpp + hotplugservice.cpp + hotplugjob.cpp +) + +kcoreaddons_add_plugin(plasma_engine_hotplug SOURCES ${hotplug_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_hotplug + KF5::CoreAddons + KF5::Plasma + KF5::Solid + KF5::Service + KF5::KIOCore + KF5::KIOWidgets # KDesktopFileActions + KF5::Notifications + KF5::I18n +) + +install(FILES hotplug.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/hotplug/Messages.sh b/plasma/workspace/dataengines/hotplug/Messages.sh new file mode 100644 index 0000000000..86a6bcb747 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_hotplug.pot diff --git a/plasma/workspace/dataengines/hotplug/deviceaction.cpp b/plasma/workspace/dataengines/hotplug/deviceaction.cpp new file mode 100644 index 0000000000..a0ef92f5a4 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/deviceaction.cpp @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "deviceaction.h" + +DeviceAction::DeviceAction() +{ +} + +DeviceAction::~DeviceAction() +{ +} + +void DeviceAction::setIconName(const QString &iconName) +{ + m_iconName = iconName; +} + +void DeviceAction::setLabel(const QString &label) +{ + m_label = label; +} + +QString DeviceAction::iconName() const +{ + return m_iconName; +} + +QString DeviceAction::label() const +{ + return m_label; +} diff --git a/plasma/workspace/dataengines/hotplug/deviceaction.h b/plasma/workspace/dataengines/hotplug/deviceaction.h new file mode 100644 index 0000000000..995bea1c7f --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/deviceaction.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +class DeviceAction +{ +public: + DeviceAction(); + virtual ~DeviceAction(); + + QString label() const; + QString iconName() const; + + virtual QString id() const = 0; + virtual void execute(Solid::Device &device) = 0; + +protected: + void setLabel(const QString &label); + void setIconName(const QString &icon); + +private: + QString m_label; + QString m_iconName; +}; diff --git a/plasma/workspace/dataengines/hotplug/deviceserviceaction.cpp b/plasma/workspace/dataengines/hotplug/deviceserviceaction.cpp new file mode 100644 index 0000000000..13d55ffae3 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/deviceserviceaction.cpp @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "deviceserviceaction.h" + +#include + +#include +#include +#include +#include +#include +#include + +class MacroExpander : public KMacroExpanderBase +{ +public: + MacroExpander(const Solid::Device &device) + : KMacroExpanderBase('%') + , m_device(device) + { + } + +protected: + int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; + +private: + Solid::Device m_device; +}; + +class DelayedExecutor : public QObject +{ + Q_OBJECT +public: + DelayedExecutor(const KServiceAction &service, Solid::Device &device); + +private Q_SLOTS: + void _k_storageSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi); + +private: + void delayedExecute(const QString &udi); + + KServiceAction m_service; +}; + +DeviceServiceAction::DeviceServiceAction() + : DeviceAction() +{ + DeviceAction::setIconName(QStringLiteral("dialog-cancel")); + DeviceAction::setLabel(i18nc("A default name for an action without proper label", "Unknown")); +} + +QString DeviceServiceAction::id() const +{ + if (m_service.name().isEmpty() && m_service.exec().isEmpty()) { + return QString(); + } else { + return "#Service:" + m_service.name() + m_service.exec(); + } +} + +void DeviceServiceAction::execute(Solid::Device &device) +{ + new DelayedExecutor(m_service, device); +} + +void DelayedExecutor::_k_storageSetupDone(Solid::ErrorType error, QVariant errorData, const QString &udi) +{ + Q_UNUSED(errorData); + + if (!error) { + delayedExecute(udi); + } +} + +void DeviceServiceAction::setService(const KServiceAction &service) +{ + DeviceAction::setIconName(service.icon()); + DeviceAction::setLabel(service.text()); + + m_service = service; +} + +KServiceAction DeviceServiceAction::service() const +{ + return m_service; +} + +int MacroExpander::expandEscapedMacro(const QString &str, int pos, QStringList &ret) +{ + ushort option = str[pos + 1].unicode(); + + switch (option) { + case 'f': // Filepath + case 'F': // case insensitive + if (m_device.is()) { + ret << m_device.as()->filePath(); + } else { + qWarning() << "DeviceServiceAction::execute: " << m_device.udi() << " is not a StorageAccess device"; + } + break; + case 'd': // Device node + case 'D': // case insensitive + if (m_device.is()) { + ret << m_device.as()->device(); + } else { + qWarning() << "DeviceServiceAction::execute: " << m_device.udi() << " is not a Block device"; + } + break; + case 'i': // UDI + case 'I': // case insensitive + ret << m_device.udi(); + break; + case '%': + ret = QStringList(QLatin1String("%")); + break; + default: + return -2; // subst with same and skip + } + return 2; +} + +DelayedExecutor::DelayedExecutor(const KServiceAction &service, Solid::Device &device) + : m_service(service) +{ + if (device.is() && !device.as()->isAccessible()) { + Solid::StorageAccess *access = device.as(); + + connect(access, &Solid::StorageAccess::setupDone, this, &DelayedExecutor::_k_storageSetupDone); + + access->setup(); + } else { + delayedExecute(device.udi()); + } +} + +void DelayedExecutor::delayedExecute(const QString &udi) +{ + Solid::Device device(udi); + + QString exec = m_service.exec(); + MacroExpander mx(device); + mx.expandMacrosShellQuote(exec); + + KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(exec); + job->setIcon(m_service.icon()); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); + job->start(); + + deleteLater(); +} + +#include "deviceserviceaction.moc" diff --git a/plasma/workspace/dataengines/hotplug/deviceserviceaction.h b/plasma/workspace/dataengines/hotplug/deviceserviceaction.h new file mode 100644 index 0000000000..e7e8492ad8 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/deviceserviceaction.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "deviceaction.h" + +#include +#include + +class DeviceServiceAction : public DeviceAction +{ +public: + DeviceServiceAction(); + QString id() const override; + void execute(Solid::Device &device) override; + + void setService(const KServiceAction &service); + KServiceAction service() const; + +private: + KServiceAction m_service; +}; diff --git a/plasma/workspace/dataengines/hotplug/hotplug.operations b/plasma/workspace/dataengines/hotplug/hotplug.operations new file mode 100644 index 0000000000..c58d0dc30b --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplug.operations @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/plasma/workspace/dataengines/hotplug/hotplugengine.cpp b/plasma/workspace/dataengines/hotplug/hotplugengine.cpp new file mode 100644 index 0000000000..925c630e34 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugengine.cpp @@ -0,0 +1,272 @@ +/* + SPDX-FileCopyrightText: 2007 Menard Alexis + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "hotplugengine.h" +#include "hotplugservice.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// solid specific includes +#include +#include +#include +#include +#include +#include + +//#define HOTPLUGENGINE_TIMING + +HotplugEngine::HotplugEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_dirWatch(new KDirWatch(this)) +{ + const QStringList folders = + QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("solid/actions"), QStandardPaths::LocateDirectory); + + for (const QString &folder : folders) { + m_dirWatch->addDir(folder, KDirWatch::WatchFiles); + } + connect(m_dirWatch, &KDirWatch::created, this, &HotplugEngine::updatePredicates); + connect(m_dirWatch, &KDirWatch::deleted, this, &HotplugEngine::updatePredicates); + connect(m_dirWatch, &KDirWatch::dirty, this, &HotplugEngine::updatePredicates); + init(); +} + +HotplugEngine::~HotplugEngine() +{ +} + +void HotplugEngine::init() +{ + findPredicates(); + + Solid::Predicate p(Solid::DeviceInterface::StorageAccess); + p |= Solid::Predicate(Solid::DeviceInterface::StorageDrive); + p |= Solid::Predicate(Solid::DeviceInterface::StorageVolume); + p |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive); + p |= Solid::Predicate(Solid::DeviceInterface::OpticalDisc); + p |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer); + p |= Solid::Predicate(Solid::DeviceInterface::Camera); + const QList devices = Solid::Device::listFromQuery(p); + for (const Solid::Device &dev : devices) { + m_startList.insert(dev.udi(), dev); + } + + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &HotplugEngine::onDeviceAdded); + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &HotplugEngine::onDeviceRemoved); + + m_encryptedPredicate = Solid::Predicate(QStringLiteral("StorageVolume"), QStringLiteral("usage"), "Encrypted"); + + processNextStartupDevice(); +} + +Plasma::Service *HotplugEngine::serviceForSource(const QString &source) +{ + return new HotplugService(this, source); +} + +void HotplugEngine::processNextStartupDevice() +{ + if (!m_startList.isEmpty()) { + QHash::iterator it = m_startList.begin(); + // Solid::Device dev = const_cast(m_startList.takeFirst()); + handleDeviceAdded(it.value(), false); + m_startList.erase(it); + } + + if (m_startList.isEmpty()) { + m_predicates.clear(); + } else { + QTimer::singleShot(0, this, &HotplugEngine::processNextStartupDevice); + } +} + +void HotplugEngine::findPredicates() +{ + m_predicates.clear(); + QStringList files; + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("solid/actions"), QStandardPaths::LocateDirectory); + for (const QString &dir : dirs) { + QDirIterator it(dir, QStringList() << QStringLiteral("*.desktop")); + while (it.hasNext()) { + files.prepend(it.next()); + } + } + // qDebug() << files; + for (const QString &path : qAsConst(files)) { + KDesktopFile cfg(path); + const QString string_predicate = cfg.desktopGroup().readEntry("X-KDE-Solid-Predicate"); + // qDebug() << path << string_predicate; + m_predicates.insert(QUrl(path).fileName(), Solid::Predicate::fromString(string_predicate)); + } + + if (m_predicates.isEmpty()) { + m_predicates.insert(QString(), Solid::Predicate::fromString(QString())); + } +} + +void HotplugEngine::updatePredicates(const QString &path) +{ + Q_UNUSED(path) + + findPredicates(); + + QHashIterator it(m_devices); + while (it.hasNext()) { + it.next(); + Solid::Device device(it.value()); + QString udi(it.key()); + + const QStringList predicates = predicatesForDevice(device); + if (!predicates.isEmpty()) { + if (sources().contains(udi)) { + Plasma::DataEngine::Data data; + data.insert(QStringLiteral("predicateFiles"), predicates); + data.insert(QStringLiteral("actions"), actionsForPredicates(predicates)); + setData(udi, data); + } else { + handleDeviceAdded(device, false); + } + } else if (!m_encryptedPredicate.matches(device) && sources().contains(udi)) { + removeSource(udi); + } + } +} + +QStringList HotplugEngine::predicatesForDevice(Solid::Device &device) const +{ + QStringList interestingDesktopFiles; + // search in all desktop configuration file if the device inserted is a correct device + QHashIterator it(m_predicates); + // qDebug() << "=================" << udi; + while (it.hasNext()) { + it.next(); + if (it.value().matches(device)) { + // qDebug() << " hit" << it.key(); + interestingDesktopFiles << it.key(); + } + } + + return interestingDesktopFiles; +} + +QVariantList HotplugEngine::actionsForPredicates(const QStringList &predicates) const +{ + QVariantList actions; + actions.reserve(predicates.count()); + + for (const QString &desktop : predicates) { + const QString actionUrl = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "solid/actions/" + desktop); + QList services = KDesktopFileActions::userDefinedServices(KService(actionUrl), true); + if (!services.isEmpty()) { + Plasma::DataEngine::Data action; + action.insert(QStringLiteral("predicate"), desktop); + action.insert(QStringLiteral("text"), services[0].text()); + action.insert(QStringLiteral("icon"), services[0].icon()); + actions << action; + } + } + + return actions; +} + +void HotplugEngine::onDeviceAdded(const QString &udi) +{ + Solid::Device device(udi); + handleDeviceAdded(device); +} + +void HotplugEngine::handleDeviceAdded(Solid::Device &device, bool added) +{ + // qDebug() << "adding" << device.udi(); +#ifdef HOTPLUGENGINE_TIMING + QTime t; + t.start(); +#endif + // Skip things we know we don't care about + if (device.is()) { + Solid::StorageDrive *drive = device.as(); + if (!drive->isHotpluggable()) { +#ifdef HOTPLUGENGINE_TIMING + qDebug() << "storage, but not pluggable, returning" << t.restart(); +#endif + return; + } + } else if (device.is()) { + Solid::StorageVolume *volume = device.as(); + Solid::StorageVolume::UsageType type = volume->usage(); + if ((type == Solid::StorageVolume::Unused || type == Solid::StorageVolume::PartitionTable) && !device.is()) { +#ifdef HOTPLUGENGINE_TIMING + qDebug() << "storage volume, but not of interest" << t.restart(); +#endif + return; + } + } + + m_devices.insert(device.udi(), device); + + if (m_predicates.isEmpty()) { + findPredicates(); + } + + const QStringList interestingDesktopFiles = predicatesForDevice(device); + const bool isEncryptedContainer = m_encryptedPredicate.matches(device); + + if (!interestingDesktopFiles.isEmpty() || isEncryptedContainer) { + // qDebug() << device.product(); + // qDebug() << device.vendor(); + // qDebug() << "number of interesting desktop file : " << interestingDesktopFiles.size(); + Plasma::DataEngine::Data data; + data.insert(QStringLiteral("added"), added); + data.insert(QStringLiteral("udi"), device.udi()); + + if (!device.description().isEmpty()) { + data.insert(QStringLiteral("text"), device.description()); + } else { + data.insert(QStringLiteral("text"), QString(device.vendor() + QLatin1Char(' ') + device.product())); + } + data.insert(QStringLiteral("icon"), device.icon()); + data.insert(QStringLiteral("emblems"), device.emblems()); + data.insert(QStringLiteral("predicateFiles"), interestingDesktopFiles); + data.insert(QStringLiteral("actions"), actionsForPredicates(interestingDesktopFiles)); + + data.insert(QStringLiteral("isEncryptedContainer"), isEncryptedContainer); + + setData(device.udi(), data); + // qDebug() << "add hardware solid : " << udi; + } + +#ifdef HOTPLUGENGINE_TIMING + qDebug() << "total time" << t.restart(); +#endif +} + +void HotplugEngine::onDeviceRemoved(const QString &udi) +{ + // qDebug() << "remove hardware:" << udi; + + if (m_startList.contains(udi)) { + m_startList.remove(udi); + return; + } + + m_devices.remove(udi); + removeSource(udi); +} + +K_PLUGIN_CLASS_WITH_JSON(HotplugEngine, "plasma-dataengine-hotplug.json") + +#include "hotplugengine.moc" diff --git a/plasma/workspace/dataengines/hotplug/hotplugengine.h b/plasma/workspace/dataengines/hotplug/hotplugengine.h new file mode 100644 index 0000000000..ca5a5198f4 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugengine.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2007 Menard Alexis + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +#include + +class KDirWatch; + +/** + * This class is connected with solid, filter devices and provide signal with source for applet in Plasma + */ +class HotplugEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + HotplugEngine(QObject *parent, const QVariantList &args); + ~HotplugEngine() override; + void init(); + Plasma::Service *serviceForSource(const QString &source) override; + +protected Q_SLOTS: + void onDeviceAdded(const QString &udi); + void onDeviceRemoved(const QString &udi); + +private: + void handleDeviceAdded(Solid::Device &dev, bool added = true); + void findPredicates(); + QStringList predicatesForDevice(Solid::Device &device) const; + QVariantList actionsForPredicates(const QStringList &predicates) const; + +private Q_SLOTS: + void processNextStartupDevice(); + void updatePredicates(const QString &path); + +private: + QHash m_predicates; + QHash m_startList; + QHash m_devices; + Solid::Predicate m_encryptedPredicate; + KDirWatch *m_dirWatch; +}; diff --git a/plasma/workspace/dataengines/hotplug/hotplugjob.cpp b/plasma/workspace/dataengines/hotplug/hotplugjob.cpp new file mode 100644 index 0000000000..d4e69bb9ea --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugjob.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + SPDX-FileCopyrightText: 2019 Harald Sitter + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "hotplugjob.h" + +#include "deviceserviceaction.h" + +#include +#include +#include +#include + +void HotplugJob::start() +{ + if (operationName() == QLatin1String("invokeAction")) { + const QString desktopFile = parameters()[QStringLiteral("predicate")].toString(); + const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "solid/actions/" + desktopFile); + + QList services = KDesktopFileActions::userDefinedServices(KService(filePath), true); + if (services.size() < 1) { + qWarning() << "Failed to resolve hotplugjob action" << desktopFile << filePath; + setError(KJob::UserDefinedError); + setErrorText(i18nc("error; %1 is the desktop file name of the service", "Failed to resolve service action for %1.", desktopFile)); + setResult(false); // calls emitResult internally. + return; + } + // Cannot be > 1, we only have one filePath, and < 1 was handled as error. + Q_ASSERT(services.size() == 1); + + DeviceServiceAction action; + action.setService(services.takeFirst()); + + Solid::Device device(m_dest); + action.execute(device); + } + + emitResult(); +} diff --git a/plasma/workspace/dataengines/hotplug/hotplugjob.h b/plasma/workspace/dataengines/hotplug/hotplugjob.h new file mode 100644 index 0000000000..5db3916a39 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugjob.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "hotplugengine.h" + +#include + +class HotplugJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + HotplugJob(HotplugEngine *engine, const QString &destination, const QString &operation, QMap ¶meters, QObject *parent = nullptr) + : ServiceJob(destination, operation, parameters, parent) + , m_engine(engine) + , m_dest(destination) + { + } + + void start() override; + +private: + HotplugEngine *m_engine; + QString m_dest; +}; diff --git a/plasma/workspace/dataengines/hotplug/hotplugservice.cpp b/plasma/workspace/dataengines/hotplug/hotplugservice.cpp new file mode 100644 index 0000000000..d0de60e48d --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugservice.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "hotplugservice.h" +#include "hotplugengine.h" +#include "hotplugjob.h" + +HotplugService::HotplugService(HotplugEngine *parent, const QString &source) + : Plasma::Service(parent) + , m_engine(parent) +{ + setName(QStringLiteral("hotplug")); + setDestination(source); +} + +Plasma::ServiceJob *HotplugService::createJob(const QString &operation, QMap ¶meters) +{ + return new HotplugJob(m_engine, destination(), operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/hotplug/hotplugservice.h b/plasma/workspace/dataengines/hotplug/hotplugservice.h new file mode 100644 index 0000000000..5ff5658c3e --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/hotplugservice.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +class HotplugEngine; + +class HotplugService : public Plasma::Service +{ + Q_OBJECT + +public: + HotplugService(HotplugEngine *parent, const QString &source); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + HotplugEngine *m_engine; + QString m_dest; +}; diff --git a/plasma/workspace/dataengines/hotplug/plasma-dataengine-hotplug.json b/plasma/workspace/dataengines/hotplug/plasma-dataengine-hotplug.json new file mode 100644 index 0000000000..ae7377d987 --- /dev/null +++ b/plasma/workspace/dataengines/hotplug/plasma-dataengine-hotplug.json @@ -0,0 +1,149 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "wilderkde@gmail.com", + "Name": "The Plasma Team", + "Name[ar]": "فريق بلازما", + "Name[az]": "Plasma komandası", + "Name[ca]": "L'equip del Plasma", + "Name[cs]": "Team Plasma", + "Name[de]": "Das Plasma-Team", + "Name[en_GB]": "The Plasma Team", + "Name[es]": "El equipo de Plasma", + "Name[eu]": "Plasma taldea", + "Name[fi]": "Plasma-työryhmä", + "Name[fr]": "L'équipe de Plasma", + "Name[hu]": "A Plasma fejlesztői", + "Name[ia]": "Le equipa de Plasma", + "Name[it]": "La squadra di Plasma", + "Name[ko]": "Plasma 팀", + "Name[lt]": "Plasma komanda", + "Name[nl]": "Het team van Plasma", + "Name[nn]": "Utviklingslaget for Plasma", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਟੀਮ", + "Name[pl]": "Zespół Plazmy", + "Name[pt_BR]": "Temas do Plasma", + "Name[ro]": "Echipa Plasma", + "Name[ru]": "Команда разработчиков Plasma", + "Name[sk]": "Plasma Tím", + "Name[sl]": "Ekipa Plasme", + "Name[sv]": "Plasma-gruppen", + "Name[ta]": "பிளாஸ்மா குழு", + "Name[tr]": "Plazma Takımı", + "Name[uk]": "Команда розробників Плазми", + "Name[vi]": "Đội Plasma", + "Name[x-test]": "xxThe Plasma Teamxx", + "Name[zh_CN]": "Plasma 开发团队" + } + ], + "Category": "", + "Description": "Tracks hot-pluggable devices as they appear and disappear.", + "Description[ar]": "يتتبع الأجهزة القابلة للتوصيل أثناء ظهورها واختفائها.", + "Description[az]": "Qoşulduğunda və ayrıldığında qoşulabilən cihazların izlənməsi", + "Description[ca]": "Segueix els dispositius de connexió en calent quan apareixen i desapareixen.", + "Description[cs]": "Oznamovat připojení a odpojení za běhu připojitelných zařízení", + "Description[de]": "Überwacht Hotplug-Geräte auf Aktivierung und Deaktivierung.", + "Description[en_GB]": "Tracks hot-pluggable devices as they appear and disappear.", + "Description[es]": "Registra los dispositivos extraíbles cuando aparecen y desaparecen.", + "Description[eu]": "Beroan konekta daitezkeen gailuen jarraipena egiten du, haiek agertu eta desagertu ahala.", + "Description[fi]": "Tarkkailee ajonaikaisesti kytkettäviä laitteita.", + "Description[fr]": "Surveille l'apparition ou la disparition de périphériques pouvant être connectés à chaud.", + "Description[hu]": "Követi a menet közben csatolható (hot-pluggable) eszközöket.", + "Description[ia]": "Il tracia dispositivos hot-pluggable (insertabile a calide) assi como illos appare e disappare", + "Description[it]": "Sorveglia i dispositivi rimuovibili a caldo quando appaiono o scompaiono.", + "Description[ko]": "장치가 연결되고 해제될 때 기록합니다.", + "Description[lt]": "Seka greitai prijungiamų įrenginių atsiradimą ir išnykimą.", + "Description[nl]": "Volgt inplugbare apparaten als deze verschijnen en verdwijnen.", + "Description[nn]": "Sporar hotplug-einingar når dei dukkar opp og forsvinn.", + "Description[pa]": "ਹਾਟ-ਪਲੱਗਯੋਗ ਡਿਵਾਈਸ ਜਾਣਕਾਰੀ, ਜਿਵੇਂ ਉਹ ਲਗਾਏ ਤੇ ਹਟਾਏ ਜਾਣਗੇ।", + "Description[pl]": "Śledzi pojawianie i znikanie urządzeń podłączanych \"na gorąco\".", + "Description[pt_BR]": "Segue os dispositivos removíveis, à medida que aparecem e desaparecem.", + "Description[ro]": "Urmărește dispozitivele detașabile după cum apar sau dispar.", + "Description[ru]": "Следит за появлением и исчезновением подключаемых на лету устройств.", + "Description[sk]": "Sleduje hot-plug zariadenia pri ich zobrazení a zmiznutí.", + "Description[sl]": "Sledi hitro priključljivim napravam, ki se pojavljajo in izginjajo.", + "Description[sv]": "Följer enheter som kan kopplas in under spänning när de dyker upp och försvinner.", + "Description[ta]": "எப்போதேனும் செருகி கழற்றக்கூடிய சாதனங்களை கவனிக்கும்.", + "Description[tr]": "Takıldıklarında ve çıkarıldıklarında tak çalıştır aygıtlarını izler.", + "Description[uk]": "Стежить за з’єднанням і від’єднанням портативних пристроїв.", + "Description[vi]": "Theo dõi các thiết bị cắm nóng được khi chúng xuất hiện và biến mất.", + "Description[x-test]": "xxTracks hot-pluggable devices as they appear and disappear.xx", + "Description[zh_CN]": "跟踪热插拔设备的出现与消失。", + "Icon": "drive-removable-media-usb", + "Id": "hotplug", + "License": "", + "Name": "Hotplug Events", + "Name[ar]": "أحدات التوصيل المباشر", + "Name[az]": "Cihaz qoşulmaları bildirişləri", + "Name[be@latin]": "Źjaŭleńnie novych pryładaŭ", + "Name[bn]": "হট-প্লাগ ঘটনাবলী", + "Name[bs]": "Događaji vrućeg uključivanja", + "Name[ca@valencia]": "Esdeveniments de connexió en calent", + "Name[ca]": "Esdeveniments de connexió en calent", + "Name[cs]": "Události hotplugu", + "Name[csb]": "Hotplug Event", + "Name[da]": "Hotplug-begivenheder", + "Name[de]": "Hotplug-Ereignisse", + "Name[el]": "Γεγονότα Hotplug", + "Name[en_GB]": "Hotplug Events", + "Name[eo]": "Dumkura permuteblaj eventoj", + "Name[es]": "Eventos en caliente", + "Name[et]": "Hotplug-sündmused", + "Name[eu]": "Hotplug gertaerak", + "Name[fi]": "Hotplug-tapahtumat", + "Name[fr]": "Événements de branchement à chaud", + "Name[fy]": "Hotplug barren", + "Name[ga]": "Imeachtaí Hotplug", + "Name[gl]": "Eventos de Hotplug", + "Name[gu]": "હોટપ્લગ ઇવેન્ટ્સ", + "Name[he]": "אירועי חיבור התקנים נשלפים", + "Name[hi]": "हाटप्लग कार्यक्रम", + "Name[hne]": "हाटप्लग घटना", + "Name[hr]": "Događaji brzog uštekavanja", + "Name[hsb]": "Hotplug-podawki", + "Name[hu]": "Eszközcsatolást kezelő plazmoid", + "Name[ia]": "Eventos de hotplug", + "Name[id]": "Peristiwa Hotplug", + "Name[is]": "Hraðtengingaatburðir", + "Name[it]": "Eventi di collegamento a caldo", + "Name[ja]": "Hotplug イベント", + "Name[kk]": "Істеп турғанда қосу оқиғалар", + "Name[km]": "ព្រឹត្តិការណ៍​ដោត​ដើរ", + "Name[kn]": "Hotplug ಘಟನೆಗಳು (ಈವೆಂಟ್)", + "Name[ko]": "핫플러그 이벤트", + "Name[lt]": "Greitojo prijungimo įvykiai", + "Name[lv]": "Hotplug notikumi", + "Name[ml]": "ഹോട്ട്പ്ലഗ് സംഭവങ്ങള്‍", + "Name[mr]": "हॉटप्लग घटना", + "Name[nb]": "Tilkoblingshendelser", + "Name[nds]": "Tokoppel-Begeefnissen", + "Name[nl]": "Hotplug-gebeurtenissen", + "Name[nn]": "Hotplug-hendingar", + "Name[pa]": "ਹਾਟਪਲੱਗ ਈਵੈਂਟ", + "Name[pl]": "Zdarzenia podłączenia \"na gorąco\"", + "Name[pt]": "Eventos do Hotplug", + "Name[pt_BR]": "Eventos do hotplug", + "Name[ro]": "Evenimente de atașare", + "Name[ru]": "Уведомление о подключении устройств", + "Name[si]": "හොට් ප්ලග් අවස්ථා", + "Name[sk]": "Hotplug udalosti", + "Name[sl]": "Dogodki ob priključitvah", + "Name[sr@ijekavian]": "догађаји врућег укључивања", + "Name[sr@ijekavianlatin]": "događaji vrućeg uključivanja", + "Name[sr@latin]": "događaji vrućeg uključivanja", + "Name[sr]": "догађаји врућег укључивања", + "Name[sv]": "Inkopplingshändelser", + "Name[ta]": "செருக‍க்கூடியவை குறித்த நிகழ்வுகள்", + "Name[th]": "เหตุการณ์อุปกรณ์ Hot Plug", + "Name[tr]": "Tak Kullan Olayları", + "Name[ug]": "قىزىق قىستۇرۇش ھادىسىسى", + "Name[uk]": "Події гарячого підключення", + "Name[vi]": "Các sự kiện cắm nóng", + "Name[wa]": "Evenmints tchôd-tchôkîs", + "Name[x-test]": "xxHotplug Eventsxx", + "Name[zh_CN]": "热插拔事件", + "Name[zh_TW]": "熱插拔事件", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/keystate/CMakeLists.txt b/plasma/workspace/dataengines/keystate/CMakeLists.txt new file mode 100644 index 0000000000..b78f53fcef --- /dev/null +++ b/plasma/workspace/dataengines/keystate/CMakeLists.txt @@ -0,0 +1,18 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_keystate\") + +set(keystate_engine_SRCS + keystate.cpp + keyservice.cpp +) + +kcoreaddons_add_plugin(plasma_engine_keystate SOURCES ${keystate_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_keystate + KF5::GuiAddons + KF5::Plasma + KF5::Service + KF5::KIOCore + KF5::I18n +) + +install(FILES modifierkeystate.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/keystate/Messages.sh b/plasma/workspace/dataengines/keystate/Messages.sh new file mode 100644 index 0000000000..0c9356c32e --- /dev/null +++ b/plasma/workspace/dataengines/keystate/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/plasma_engine_keystate.pot diff --git a/plasma/workspace/dataengines/keystate/keyservice.cpp b/plasma/workspace/dataengines/keystate/keyservice.cpp new file mode 100644 index 0000000000..4f1b1cba7c --- /dev/null +++ b/plasma/workspace/dataengines/keystate/keyservice.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "keyservice.h" + +#include + +KeyService::KeyService(QObject *parent, KModifierKeyInfo *keyInfo, Qt::Key key) + : Plasma::Service(parent) + , m_keyInfo(keyInfo) + , m_key(key) +{ + setName(QStringLiteral("modifierkeystate")); + setDestination(QStringLiteral("keys")); +} + +Plasma::ServiceJob *KeyService::createJob(const QString &operation, QMap ¶meters) +{ + if (operation == QLatin1String("Lock")) { + return new LockKeyJob(this, parameters); + } else if (operation == QLatin1String("Latch")) { + return new LatchKeyJob(this, parameters); + } + + return nullptr; +} + +void KeyService::lock(bool lock) +{ + m_keyInfo->setKeyLocked(m_key, lock); +} + +void KeyService::latch(bool lock) +{ + m_keyInfo->setKeyLatched(m_key, lock); +} + +LockKeyJob::LockKeyJob(KeyService *service, const QMap ¶meters) + : Plasma::ServiceJob(service->destination(), QStringLiteral("Lock"), parameters, service) + , m_service(service) +{ +} + +void LockKeyJob::start() +{ + m_service->lock(parameters().value(QStringLiteral("Lock")).toBool()); + setResult(true); +} + +LatchKeyJob::LatchKeyJob(KeyService *service, const QMap ¶meters) + : Plasma::ServiceJob(service->destination(), QStringLiteral("Lock"), parameters, service) + , m_service(service) +{ +} + +void LatchKeyJob::start() +{ + m_service->latch(parameters().value(QStringLiteral("Lock")).toBool()); + setResult(true); +} + +// vim: sw=4 sts=4 et tw=100 diff --git a/plasma/workspace/dataengines/keystate/keyservice.h b/plasma/workspace/dataengines/keystate/keyservice.h new file mode 100644 index 0000000000..500cd8080f --- /dev/null +++ b/plasma/workspace/dataengines/keystate/keyservice.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +class KModifierKeyInfo; + +class KeyService : public Plasma::Service +{ + Q_OBJECT + +public: + KeyService(QObject *parent, KModifierKeyInfo *keyInfo, Qt::Key key); + void lock(bool lock); + void latch(bool lock); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + KModifierKeyInfo *m_keyInfo; + Qt::Key m_key; +}; + +class LockKeyJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + LockKeyJob(KeyService *service, const QMap ¶meters); + void start() override; + +private: + KeyService *m_service; +}; + +class LatchKeyJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + LatchKeyJob(KeyService *service, const QMap ¶meters); + void start() override; + +private: + KeyService *m_service; +}; diff --git a/plasma/workspace/dataengines/keystate/keystate.cpp b/plasma/workspace/dataengines/keystate/keystate.cpp new file mode 100644 index 0000000000..d139dbd482 --- /dev/null +++ b/plasma/workspace/dataengines/keystate/keystate.cpp @@ -0,0 +1,129 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "keystate.h" + +#include "keyservice.h" +#include + +KeyStatesEngine::KeyStatesEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + m_mods.insert(Qt::Key_Shift, I18N_NOOP("Shift")); + m_mods.insert(Qt::Key_Control, I18N_NOOP("Ctrl")); + m_mods.insert(Qt::Key_Alt, I18N_NOOP("Alt")); + m_mods.insert(Qt::Key_Meta, I18N_NOOP("Meta")); + m_mods.insert(Qt::Key_Super_L, I18N_NOOP("Super")); + m_mods.insert(Qt::Key_Hyper_L, I18N_NOOP("Hyper")); + m_mods.insert(Qt::Key_AltGr, I18N_NOOP("AltGr")); + m_mods.insert(Qt::Key_NumLock, I18N_NOOP("Num Lock")); + m_mods.insert(Qt::Key_CapsLock, I18N_NOOP("Caps Lock")); + m_mods.insert(Qt::Key_ScrollLock, I18N_NOOP("Scroll Lock")); + + m_buttons.insert(Qt::LeftButton, I18N_NOOP("Left Button")); + m_buttons.insert(Qt::RightButton, I18N_NOOP("Right Button")); + m_buttons.insert(Qt::MiddleButton, I18N_NOOP("Middle Button")); + m_buttons.insert(Qt::XButton1, I18N_NOOP("First X Button")); + m_buttons.insert(Qt::XButton2, I18N_NOOP("Second X Button")); + init(); +} + +KeyStatesEngine::~KeyStatesEngine() +{ +} + +void KeyStatesEngine::init() +{ + QMap::const_iterator it; + QMap::const_iterator end = m_mods.constEnd(); + for (it = m_mods.constBegin(); it != end; ++it) { + if (m_keyInfo.knowsKey(it.key())) { + Data data; + data.insert(I18N_NOOP("Pressed"), m_keyInfo.isKeyPressed(it.key())); + data.insert(I18N_NOOP("Latched"), m_keyInfo.isKeyLatched(it.key())); + data.insert(I18N_NOOP("Locked"), m_keyInfo.isKeyLocked(it.key())); + setData(it.value(), data); + } + } + + QMap::const_iterator it2; + QMap::const_iterator end2 = m_buttons.constEnd(); + for (it2 = m_buttons.constBegin(); it2 != end2; ++it2) { + Data data; + data.insert(I18N_NOOP("Pressed"), m_keyInfo.isButtonPressed(it2.key())); + setData(it2.value(), data); + } + + connect(&m_keyInfo, &KModifierKeyInfo::keyPressed, this, &KeyStatesEngine::keyPressed); + connect(&m_keyInfo, &KModifierKeyInfo::keyLatched, this, &KeyStatesEngine::keyLatched); + connect(&m_keyInfo, &KModifierKeyInfo::keyLocked, this, &KeyStatesEngine::keyLocked); + connect(&m_keyInfo, &KModifierKeyInfo::buttonPressed, this, &KeyStatesEngine::mouseButtonPressed); + connect(&m_keyInfo, &KModifierKeyInfo::keyAdded, this, &KeyStatesEngine::keyAdded); + connect(&m_keyInfo, &KModifierKeyInfo::keyRemoved, this, &KeyStatesEngine::keyRemoved); +} + +Plasma::Service *KeyStatesEngine::serviceForSource(const QString &source) +{ + QMap::const_iterator it; + QMap::const_iterator end = m_mods.constEnd(); + for (it = m_mods.constBegin(); it != end; ++it) { + if (it.value() == source) { + return new KeyService(this, &m_keyInfo, it.key()); + } + } + + return Plasma::DataEngine::serviceForSource(source); +} + +void KeyStatesEngine::keyPressed(Qt::Key key, bool state) +{ + if (m_mods.contains(key)) { + setData(m_mods.value(key), I18N_NOOP("Pressed"), state); + } +} + +void KeyStatesEngine::keyLatched(Qt::Key key, bool state) +{ + if (m_mods.contains(key)) { + setData(m_mods.value(key), I18N_NOOP("Latched"), state); + } +} + +void KeyStatesEngine::keyLocked(Qt::Key key, bool state) +{ + if (m_mods.contains(key)) { + setData(m_mods.value(key), I18N_NOOP("Locked"), state); + } +} + +void KeyStatesEngine::mouseButtonPressed(Qt::MouseButton button, bool state) +{ + if (m_buttons.contains(button)) { + setData(m_buttons.value(button), I18N_NOOP("Pressed"), state); + } +} + +void KeyStatesEngine::keyAdded(Qt::Key key) +{ + if (m_mods.contains(key)) { + Data data; + data.insert(I18N_NOOP("Pressed"), m_keyInfo.isKeyPressed(key)); + data.insert(I18N_NOOP("Latched"), m_keyInfo.isKeyLatched(key)); + data.insert(I18N_NOOP("Locked"), m_keyInfo.isKeyLocked(key)); + setData(m_mods.value(key), data); + } +} + +void KeyStatesEngine::keyRemoved(Qt::Key key) +{ + if (m_mods.contains(key)) { + removeSource(m_mods.value(key)); + } +} + +K_PLUGIN_CLASS_WITH_JSON(KeyStatesEngine, "plasma-dataengine-keystate.json") + +#include "keystate.moc" diff --git a/plasma/workspace/dataengines/keystate/keystate.h b/plasma/workspace/dataengines/keystate/keystate.h new file mode 100644 index 0000000000..65a844d07b --- /dev/null +++ b/plasma/workspace/dataengines/keystate/keystate.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +/** + * This engine provides the current state of the keyboard modifiers + * and mouse buttons, primarily useful for accessibility feature support. + */ +class KeyStatesEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + KeyStatesEngine(QObject *parent, const QVariantList &args); + ~KeyStatesEngine() override; + + void init(); + Plasma::Service *serviceForSource(const QString &source) override; + +protected: + // bool sourceRequestEvent(const QString &name); + // bool updateSourceEvent(const QString &source); + +protected Q_SLOTS: + void keyPressed(Qt::Key key, bool state); + void keyLatched(Qt::Key key, bool state); + void keyLocked(Qt::Key key, bool state); + void mouseButtonPressed(Qt::MouseButton button, bool state); + void keyAdded(Qt::Key key); + void keyRemoved(Qt::Key key); + +private: + KModifierKeyInfo m_keyInfo; + QMap m_mods; + QMap m_buttons; +}; diff --git a/plasma/workspace/dataengines/keystate/modifierkeystate.operations b/plasma/workspace/dataengines/keystate/modifierkeystate.operations new file mode 100644 index 0000000000..492ef4e0ae --- /dev/null +++ b/plasma/workspace/dataengines/keystate/modifierkeystate.operations @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/keystate/plasma-dataengine-keystate.json b/plasma/workspace/dataengines/keystate/plasma-dataengine-keystate.json new file mode 100644 index 0000000000..e5f715ebeb --- /dev/null +++ b/plasma/workspace/dataengines/keystate/plasma-dataengine-keystate.json @@ -0,0 +1,150 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "aseigo@kde.org", + "Name": "Aaron Seigo", + "Name[ar]": "Aaron Seigo", + "Name[az]": "Aaron Seigo", + "Name[ca]": "Aaron Seigo", + "Name[cs]": "Aaron Seigo", + "Name[de]": "Aaron Seigo", + "Name[en_GB]": "Aaron Seigo", + "Name[es]": "Aaron Seigo", + "Name[eu]": "Aaron Seigo", + "Name[fi]": "Aaron Seigo", + "Name[fr]": "Aaron Seigo", + "Name[hu]": "Aaron Seigo", + "Name[ia]": "Aaron Seigo", + "Name[it]": "Aaron Seigo", + "Name[ko]": "Aaron Seigo", + "Name[lt]": "Aaron Seigo", + "Name[nl]": "Aaron Seigo", + "Name[nn]": "Aaron Seigo", + "Name[pl]": "Aaron Seigo", + "Name[pt_BR]": "Aaron Seigo", + "Name[ro]": "Aaron Seigo", + "Name[ru]": "Aaron Seigo", + "Name[sk]": "Aaron Seigo", + "Name[sl]": "Aaron Seigo", + "Name[sv]": "Aaron Seigo", + "Name[tr]": "Aaron Seigo", + "Name[uk]": "Aaron Seigo", + "Name[vi]": "Aaron Seigo", + "Name[x-test]": "xxAaron Seigoxx", + "Name[zh_CN]": "Aaron Seigo" + } + ], + "Category": "Accessibility", + "Description": "Keyboard modifier and mouse buttons states", + "Description[ar]": "حالات مُعدِّل لوحة المفاتيح وأزرار الفأرة", + "Description[az]": "Dəyişdirici klaviatura düyməsi və siçan düymələri vəziyyəti", + "Description[ca]": "Modificador de l'estat del teclat i dels botons del ratolí", + "Description[cs]": "Stavy modifikátorů klávesnice a tlačítek myši", + "Description[de]": "Status von Modifizierer- und Maustasten", + "Description[en_GB]": "Keyboard modifier and mouse buttons states", + "Description[es]": "Estado del modificador de teclado y botones del ratón", + "Description[eu]": "Teklatuaren aldatzailearen eta saguaren botoien egoerak", + "Description[fi]": "Muuntonäppäinten ja hiiripainikkeiden tilat", + "Description[fr]": "Modificateur du clavier et des états des boutons de souris.", + "Description[hu]": "Módosítóbillentyűk és egérgombok", + "Description[ia]": "Modificator de claviero e statos de buttones del mus", + "Description[it]": "Stati dei modificatori della tastiera e dei pulsanti del mouse", + "Description[ko]": "키보드 수정자 키 및 마우스 단추 상태", + "Description[lt]": "Klaviatūros modifikatorių ir pelės mygtukų būsenos", + "Description[nl]": "Wijzigt toetsenbord en muisknoptoestanden", + "Description[nn]": "Status for valtastar og museknappar", + "Description[pa]": "ਕੀਬੋਰਡ ਸੋਧਕ ਅਤੇ ਮਾਊਂਸ ਬਟਨ ਹਾਲਤ", + "Description[pl]": "Wyświetla stany modyfikatorów klawiatury i przycisków myszy", + "Description[pt_BR]": "Estados do modificador de teclado e botões do mouse", + "Description[ro]": "Stările modificatorului de tastatură și a butoanelor mausului", + "Description[ru]": "Состояния клавиш-модификаторов на клавиатуре и кнопок мыши", + "Description[sk]": "Stavy modifikátorov klávesnice a tlačidiel myši", + "Description[sl]": "Stanje spremenilnih tipk tipkovnice in tipk miške", + "Description[sv]": "Tillstånd för väljartangenter och musknappar", + "Description[ta]": "விசைப்பலகை மாற்றி விசைகளின் மற்றும் சுட்டி பட்டன்களின் நிலை", + "Description[tr]": "Fare ve klavye değiştiricileri durumu", + "Description[uk]": "Стан модифікаторів клавіатури і кнопок миші", + "Description[vi]": "Trạng thái phím bổ trợ và nút chuột", + "Description[x-test]": "xxKeyboard modifier and mouse buttons statesxx", + "Description[zh_CN]": "显示键盘修饰健及鼠标按键状态", + "EnabledByDefault": true, + "Icon": "input-keyboard", + "Id": "keystate", + "License": "LGPL", + "Name": "Keyboard and Mouse State", + "Name[ar]": "حالة الفارة ولوحة المفاتيح", + "Name[ast]": "Estáu del tecláu y mur", + "Name[az]": "Klaviatura və Siçan vəziyyəti", + "Name[bg]": "Състояние на клавиатура и мишка", + "Name[bn]": "কীবোর্ড ও মাউস অবস্থা", + "Name[bs]": "Stanje tastature i miša", + "Name[ca@valencia]": "Estat del teclat i del ratolí", + "Name[ca]": "Estat del teclat i ratolí", + "Name[cs]": "Stav klávesnice a myši", + "Name[csb]": "Stón klawiaturë ë mëszë", + "Name[da]": "Tilstand for tastatur og mus", + "Name[de]": "Status von Tastatur und Maus", + "Name[el]": "Κατάσταση πληκτρολογίου και ποντικιού", + "Name[en_GB]": "Keyboard and Mouse State", + "Name[eo]": "Stato de klavaro kaj muso", + "Name[es]": "Estado del teclado y del ratón", + "Name[et]": "Klaviatuuri ja hiire olek", + "Name[eu]": "Teklatuaren eta saguaren egoera", + "Name[fi]": "Näppäimistön ja hiiren tila", + "Name[fr]": "États du clavier et de la souris", + "Name[fy]": "Toetseboerd en mûs tastân", + "Name[ga]": "Staid an Mhéarchláir agus na Luiche", + "Name[gl]": "Estado do teclado e do rato", + "Name[gu]": "કીબોર્ડ અને માઉસ સ્થિતિ", + "Name[he]": "מצב מקלדת ועכבר", + "Name[hi]": "कुंजीपटल व माउस स्थिति", + "Name[hr]": "Stanje tipkovnice i miša", + "Name[hsb]": "Staw tastatury a myše", + "Name[hu]": "Egér- és billentyűzetállapot-jelző", + "Name[ia]": "Stato del mus e del claviero", + "Name[id]": "Keadaan Mouse dan Keyboard", + "Name[is]": "Staða lyklaborðs og músar", + "Name[it]": "Stato di tastiera e mouse", + "Name[ja]": "キーボードとマウスの状態", + "Name[kk]": "Перенетақта мен тышқанның күй-жайы", + "Name[km]": "ស្ថានភាព​ក្ដារចុច និង​កណ្ដុល", + "Name[kn]": "ಕೀಲಿಮಣೆ ಮತ್ತು ಮೌಸ್‌ನ ಸ್ಥಿತಿ", + "Name[ko]": "키보드와 마우스 상태", + "Name[lt]": "Klaviatūros ir pelės būsena", + "Name[lv]": "Tastatūra un peles stāvoklis", + "Name[mk]": "Состојба на тастатура и глушец", + "Name[ml]": "കീബോര്‍ഡിന്റേയും മൌസിന്റേയും അവസ്ഥ", + "Name[mr]": "कळफलक व माऊस स्थिती", + "Name[nb]": "Tilstand for tastatur og mus", + "Name[nds]": "Tastatuur- un Muus-Status", + "Name[nl]": "Toetsenbord en muisstatus", + "Name[nn]": "Status for tastatur og mus", + "Name[pa]": "ਕੀ-ਬੋਰਡ ਅਤੇ ਮਾਊਂਸ ਹਾਲਤ", + "Name[pl]": "Stan klawiatury i myszy", + "Name[pt]": "Estado do Teclado e Rato", + "Name[pt_BR]": "Estado do teclado e mouse", + "Name[ro]": "Starea tastaturii și mausului", + "Name[ru]": "Состояние клавиатуры и мыши", + "Name[si]": "යතුරුපුවරුව සහ මවුස තත්වය", + "Name[sk]": "Stav klávesnice a myši", + "Name[sl]": "Stanje tipkovnice in miške", + "Name[sr@ijekavian]": "стање тастатуре и миша", + "Name[sr@ijekavianlatin]": "stanje tastature i miša", + "Name[sr@latin]": "stanje tastature i miša", + "Name[sr]": "стање тастатуре и миша", + "Name[sv]": "Tillstånd för tangentbord och mus", + "Name[ta]": "விசைப்பலகை மற்றும் சுட்டியின் நிலை", + "Name[tg]": "Клавиатура ва муш", + "Name[th]": "สถานะของแป้นพิมพ์และเมาส์", + "Name[tr]": "Klavye ve Fare Durumu", + "Name[ug]": "ھەرپتاختا ۋە چاشقىنەك ھالىتى", + "Name[uk]": "Стан клавіатури і миші", + "Name[vi]": "Trạng thái bàn phím và chuột", + "Name[wa]": "Estat del taprece et del sori", + "Name[x-test]": "xxKeyboard and Mouse Statexx", + "Name[zh_CN]": "键盘和鼠标状态", + "Name[zh_TW]": "鍵盤與滑鼠狀態", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/mouse/CMakeLists.txt b/plasma/workspace/dataengines/mouse/CMakeLists.txt new file mode 100644 index 0000000000..1796f997d9 --- /dev/null +++ b/plasma/workspace/dataengines/mouse/CMakeLists.txt @@ -0,0 +1,22 @@ +include_directories( ${CMAKE_CURRENT_BINARY_DIR}/../../) + +set(mouse_engine_SRCS + mouseengine.cpp +) + +if (X11_Xfixes_FOUND) + set(mouse_engine_SRCS ${mouse_engine_SRCS} cursornotificationhandler.cpp) +endif () + +kcoreaddons_add_plugin(plasma_engine_mouse SOURCES ${mouse_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_mouse + Qt::Widgets + Qt::X11Extras + KF5::Plasma + KF5::WindowSystem + X11::X11 +) + +if (X11_Xfixes_FOUND) + target_link_libraries(plasma_engine_mouse X11::Xfixes) +endif () diff --git a/plasma/workspace/dataengines/mouse/cursornotificationhandler.cpp b/plasma/workspace/dataengines/mouse/cursornotificationhandler.cpp new file mode 100644 index 0000000000..95f8a51d94 --- /dev/null +++ b/plasma/workspace/dataengines/mouse/cursornotificationhandler.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2007 Fredrik Höglund + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "cursornotificationhandler.h" + +#include + +#include + +/* + * This class is a QWidget because we need an X window to + * be able to receive XFixes events. We don't actually map + * the widget. + */ + +CursorNotificationHandler::CursorNotificationHandler() + : QWidget() + , currentName(0) +{ + Display *dpy = QX11Info::display(); + int errorBase; + haveXfixes = false; + + // Request cursor change notification events + if (XFixesQueryExtension(dpy, &fixesEventBase, &errorBase)) { + int major, minor; + XFixesQueryVersion(dpy, &major, &minor); + + if (major >= 2) { + XFixesSelectCursorInput(dpy, winId(), XFixesDisplayCursorNotifyMask); + haveXfixes = true; + } + } +} + +CursorNotificationHandler::~CursorNotificationHandler() +{ +} + +QString CursorNotificationHandler::cursorName() +{ + if (!haveXfixes) + return QString(); + + if (!currentName) { + // Xfixes doesn't have a request for getting the current cursor name, + // but it's included in the XFixesCursorImage struct. + XFixesCursorImage *image = XFixesGetCursorImage(QX11Info::display()); + currentName = image->atom; + XFree(image); + } + + return cursorName(currentName); +} + +QString CursorNotificationHandler::cursorName(Atom cursor) +{ + QString name; + + // XGetAtomName() is a synchronous call, so we cache the name + // in an atom<->string map the first time we see a name + // to keep the X server round trips down. + if (names.contains(cursor)) + name = names[cursor]; + else { + char *data = XGetAtomName(QX11Info::display(), cursor); + name = QString::fromUtf8(data); + XFree(data); + + names.insert(cursor, name); + } + + return name; +} + +bool CursorNotificationHandler::x11Event(XEvent *event) +{ + if (event->type != fixesEventBase + XFixesCursorNotify) + return false; + + XFixesCursorNotifyEvent *xfe = reinterpret_cast(event); + currentName = xfe->cursor_name; + + Q_EMIT cursorNameChanged(cursorName(currentName)); + + return false; +} diff --git a/plasma/workspace/dataengines/mouse/cursornotificationhandler.h b/plasma/workspace/dataengines/mouse/cursornotificationhandler.h new file mode 100644 index 0000000000..9101a3d7b7 --- /dev/null +++ b/plasma/workspace/dataengines/mouse/cursornotificationhandler.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2007 Fredrik Höglund + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include +#include + +class CursorNotificationHandler : public QWidget +{ + Q_OBJECT + +public: + CursorNotificationHandler(); + ~CursorNotificationHandler() override; + + QString cursorName(); + +Q_SIGNALS: + void cursorNameChanged(const QString &name); + +protected: + bool x11Event(XEvent *); + +private: + QString cursorName(Atom cursor); + +private: + bool haveXfixes; + int fixesEventBase; + Atom currentName; + QMap names; +}; diff --git a/plasma/workspace/dataengines/mouse/mouseengine.cpp b/plasma/workspace/dataengines/mouse/mouseengine.cpp new file mode 100644 index 0000000000..5155ef7812 --- /dev/null +++ b/plasma/workspace/dataengines/mouse/mouseengine.cpp @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2007 Fredrik Höglund + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "mouseengine.h" + +#include + +#ifdef HAVE_XFIXES +#include "cursornotificationhandler.h" +#endif + +MouseEngine::MouseEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , timerId(0) +#ifdef HAVE_XFIXES + , handler(nullptr) +#endif +{ + Q_UNUSED(args) + init(); +} + +MouseEngine::~MouseEngine() +{ + if (timerId) + killTimer(timerId); +#ifdef HAVE_XFIXES + delete handler; +#endif +} + +QStringList MouseEngine::sources() const +{ + QStringList list; + + list << QLatin1String("Position"); +#ifdef HAVE_XFIXES + list << QLatin1String("Name"); +#endif + + return list; +} + +void MouseEngine::init() +{ + if (!timerId) + timerId = startTimer(40); + + // Init cursor position + QPoint pos = QCursor::pos(); + setData(QLatin1String("Position"), QVariant(pos)); + lastPosition = pos; + +#ifdef HAVE_XFIXES + handler = new CursorNotificationHandler; + connect(handler, &CursorNotificationHandler::cursorNameChanged, this, &MouseEngine::updateCursorName); + + setData(QLatin1String("Name"), QVariant(handler->cursorName())); +#endif +} + +void MouseEngine::timerEvent(QTimerEvent *) +{ + QPoint pos = QCursor::pos(); + + if (pos != lastPosition) { + setData(QLatin1String("Position"), QVariant(pos)); + lastPosition = pos; + } +} + +void MouseEngine::updateCursorName(const QString &name) +{ + setData(QLatin1String("Name"), QVariant(name)); +} + +K_PLUGIN_CLASS_WITH_JSON(MouseEngine, "plasma-dataengine-mouse.json") + +#include "mouseengine.moc" diff --git a/plasma/workspace/dataengines/mouse/mouseengine.h b/plasma/workspace/dataengines/mouse/mouseengine.h new file mode 100644 index 0000000000..aaaacc0fb4 --- /dev/null +++ b/plasma/workspace/dataengines/mouse/mouseengine.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2007 Fredrik Höglund + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include + +#include + +#ifdef HAVE_XFIXES +class CursorNotificationHandler; +#endif + +class MouseEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + MouseEngine(QObject *parent, const QVariantList &args); + ~MouseEngine() override; + + QStringList sources() const override; + +protected: + void init(); + void timerEvent(QTimerEvent *) override; + +private Q_SLOTS: + void updateCursorName(const QString &name); + +private: + QPoint lastPosition; + int timerId; +#ifdef HAVE_XFIXES + CursorNotificationHandler *handler; +#endif +}; diff --git a/plasma/workspace/dataengines/mouse/plasma-dataengine-mouse.json b/plasma/workspace/dataengines/mouse/plasma-dataengine-mouse.json new file mode 100644 index 0000000000..76a96eb1fa --- /dev/null +++ b/plasma/workspace/dataengines/mouse/plasma-dataengine-mouse.json @@ -0,0 +1,120 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Mouse position and cursor", + "Description[ar]": "موقع ومؤشّر الفأرة", + "Description[az]": "Siçan və kursorun yeri", + "Description[ca]": "Posició del ratolí i del cursor", + "Description[cs]": "Pozice myši a kurzoru", + "Description[de]": "Mausposition und -zeiger", + "Description[en_GB]": "Mouse position and cursor", + "Description[es]": "Posición del ratón y cursor", + "Description[eu]": "Saguaren kokalekua eta kurtsorea", + "Description[fi]": "Hiiren sijainti ja osoitin", + "Description[fr]": "Position et curseur de la souris", + "Description[hu]": "Egérmutató-pozíció", + "Description[ia]": "Position e cursor del mus", + "Description[it]": "Posizione e puntatore del mouse", + "Description[ko]": "마우스 위치와 커서", + "Description[lt]": "Pelės pozicija ir žymeklis", + "Description[nl]": "Muispositie en cursor", + "Description[nn]": "Plassering av musa og peikaren", + "Description[pa]": "ਮਾਊਸ ਸਥਿਤੀ ਅਤੇ ਕਰਸਰ", + "Description[pl]": "Wyświetla położenie wskaźnika myszy", + "Description[pt_BR]": "Posição do mouse e cursor", + "Description[ro]": "Poziția și cursorul mausului", + "Description[ru]": "Положение мыши и вид указателя", + "Description[sk]": "Pozícia myši a kurzora", + "Description[sl]": "Položaj miške in njene kazalke", + "Description[sv]": "Musposition och pekare", + "Description[ta]": "சுட்டியின் இருப்பிடம் மற்றும் சுட்டிக்குறி", + "Description[tr]": "Fare konumu ve imleci", + "Description[uk]": "Позиція вказівника миші і курсора", + "Description[vi]": "Vị trí và con trỏ chuột", + "Description[x-test]": "xxMouse position and cursorxx", + "Description[zh_CN]": "鼠标位置和光标", + "Icon": "input-mouse", + "Id": "mouse", + "Name": "Pointer Position", + "Name[ar]": "موقع المؤشّر", + "Name[ast]": "Posición del punteru", + "Name[az]": "Kursorun yeri", + "Name[be@latin]": "Miesca kursora", + "Name[bg]": "Разположение на курсора", + "Name[bs]": "Položaj pokazivača", + "Name[ca@valencia]": "Posició del punter", + "Name[ca]": "Posició de l'apuntador", + "Name[cs]": "Pozice ukazatele", + "Name[da]": "Markørposition", + "Name[de]": "Mausposition", + "Name[el]": "Θέση δρομέα", + "Name[en_GB]": "Pointer Position", + "Name[eo]": "Pozicio de la Montrilo", + "Name[es]": "Posición del puntero", + "Name[et]": "Kursori asukoht", + "Name[eu]": "Erakuslearen kokalekua", + "Name[fi]": "Osoittimen sijainti", + "Name[fr]": "Position du pointeur", + "Name[fy]": "Mûsoanwizer posysje", + "Name[ga]": "Ionad na Luiche", + "Name[gl]": "Posición do rato", + "Name[gu]": "દર્શક સ્થિતિ", + "Name[he]": "מיקום סמן", + "Name[hi]": "संकेतक स्थान", + "Name[hne]": "पाइन्टर स्थिति", + "Name[hr]": "Pozicija pokazivača miša", + "Name[hsb]": "Pozicija pokazowaka", + "Name[hu]": "Egérpozíció", + "Name[ia]": "Position de punctator", + "Name[id]": "Posisi Pointer", + "Name[is]": "Staðsetning bendils", + "Name[it]": "Posizione del puntatore", + "Name[ja]": "マウスポインタの位置", + "Name[kk]": "Көрсеткіш орны", + "Name[km]": "ទីតាំង​ទស្សន៍​ទ្រនិច", + "Name[kn]": "ಸೂಚಿಯ ಸ್ಥಳ", + "Name[ko]": "포인터 위치", + "Name[lt]": "Rodyklės pozicija", + "Name[lv]": "Peles pozīcija", + "Name[mk]": "Позиција на покажувач", + "Name[ml]": "സൂചികയുടെ സ്ഥാനം", + "Name[mr]": "पॉइन्टर स्थान", + "Name[nb]": "Pekerposisjon", + "Name[nds]": "Muuswieser-Steed", + "Name[nl]": "Muisaanwijzerpositie", + "Name[nn]": "Peikarplassering", + "Name[or]": "ସୂଚକ ସ୍ଥାନ", + "Name[pa]": "ਪੁਆਇੰਟਰ ਸਥਿਤੀ", + "Name[pl]": "Położenie wskaźnika myszy", + "Name[pt]": "Posição do Cursor", + "Name[pt_BR]": "Posição do ponteiro", + "Name[ro]": "Poziție indicator", + "Name[ru]": "Положение указателя мыши", + "Name[si]": "ලක්‍ෂක ස්ථානය", + "Name[sk]": "Pozícia kurzora", + "Name[sl]": "Položaj kazalke", + "Name[sr@ijekavian]": "положај показивача", + "Name[sr@ijekavianlatin]": "položaj pokazivača", + "Name[sr@latin]": "položaj pokazivača", + "Name[sr]": "положај показивача", + "Name[sv]": "Pekarposition", + "Name[ta]": "சுட்டிக்குறியின் இருப்பிடம்", + "Name[te]": "సూచకి స్థానము", + "Name[th]": "ตำแหน่งของตัวชี้", + "Name[tr]": "İşaretçi Konumu", + "Name[ug]": "نۇربەلگە ئورنى", + "Name[uk]": "Позиція вказівника", + "Name[vi]": "Vị trí con trỏ", + "Name[wa]": "Eplaeçmint do pwinteu", + "Name[x-test]": "xxPointer Positionxx", + "Name[zh_CN]": "指针位置", + "Name[zh_TW]": "指標位置", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/mpris2/CMakeLists.txt b/plasma/workspace/dataengines/mpris2/CMakeLists.txt new file mode 100644 index 0000000000..7b133dd2da --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/CMakeLists.txt @@ -0,0 +1,39 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_mpris2\") +add_definitions(-DQT_USE_FAST_OPERATOR_PLUS) + +set(mpris2_engine_SRCS + mpris2engine.cpp + multiplexer.cpp + multiplexedservice.cpp + playercontrol.cpp + playeractionjob.cpp + playercontainer.cpp +) + +ecm_qt_declare_logging_category(mpris2_engine_SRCS HEADER debug.h + IDENTIFIER MPRIS2 + CATEGORY_NAME kde.dataengine.mpris + DEFAULT_SEVERITY Info) + +set_source_files_properties( + org.freedesktop.DBus.Properties.xml + org.mpris.MediaPlayer2.Player.xml + org.mpris.MediaPlayer2.xml + PROPERTIES + NO_NAMESPACE ON) +qt_add_dbus_interface(mpris2_engine_SRCS org.freedesktop.DBus.Properties.xml dbusproperties) +qt_add_dbus_interface(mpris2_engine_SRCS org.mpris.MediaPlayer2.Player.xml mprisplayer) +qt_add_dbus_interface(mpris2_engine_SRCS org.mpris.MediaPlayer2.xml mprisroot) + +kcoreaddons_add_plugin(plasma_engine_mpris2 SOURCES ${mpris2_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_mpris2 + Qt::DBus + KF5::ConfigCore + KF5::GlobalAccel + KF5::I18n + KF5::Service + KF5::Plasma + KF5::XmlGui +) + +install(FILES mpris2.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/mpris2/Messages.sh b/plasma/workspace/dataengines/mpris2/Messages.sh new file mode 100644 index 0000000000..0e393ff37e --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_mpris2.pot diff --git a/plasma/workspace/dataengines/mpris2/TODO b/plasma/workspace/dataengines/mpris2/TODO new file mode 100644 index 0000000000..1805732034 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/TODO @@ -0,0 +1,2 @@ +* add tracklist support (secondary source? eg: amarok:tracklist) +* add playlists support (secondary source? eg: amarok:playlists) diff --git a/plasma/workspace/dataengines/mpris2/mpris2.operations b/plasma/workspace/dataengines/mpris2/mpris2.operations new file mode 100644 index 0000000000..5e5205c53d --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/mpris2.operations @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/mpris2/mpris2engine.cpp b/plasma/workspace/dataengines/mpris2/mpris2engine.cpp new file mode 100644 index 0000000000..e8116d8abd --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/mpris2engine.cpp @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2007-2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "mpris2engine.h" + +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "multiplexedservice.h" +#include "multiplexer.h" +#include "playercontainer.h" +#include "playercontrol.h" + +Mpris2Engine::Mpris2Engine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + auto watcher = + new QDBusServiceWatcher(QStringLiteral("org.mpris.MediaPlayer2*"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &Mpris2Engine::serviceOwnerChanged); + + QDBusPendingCall async = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, &Mpris2Engine::serviceNameFetchFinished); +} + +Plasma::Service *Mpris2Engine::serviceForSource(const QString &source) +{ + if (source == Multiplexer::sourceName) { + if (!m_multiplexer) { + createMultiplexer(); + } + return new MultiplexedService(m_multiplexer.data(), this); + } else { + PlayerContainer *container = qobject_cast(containerForSource(source)); + if (container) { + return new PlayerControl(container, this); + } else { + return DataEngine::serviceForSource(source); + } + } +} + +QStringList Mpris2Engine::sources() const +{ + if (m_multiplexer) + return DataEngine::sources(); + else + return DataEngine::sources() << Multiplexer::sourceName; +} + +void Mpris2Engine::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) +{ + if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) + return; + + QString sourceName = serviceName.mid(23); + + if (!oldOwner.isEmpty()) { + qCDebug(MPRIS2) << "MPRIS service" << serviceName << "just went offline"; + if (m_multiplexer) { + m_multiplexer.data()->removePlayer(sourceName); + } + removeSource(sourceName); + } + + if (!newOwner.isEmpty()) { + qCDebug(MPRIS2) << "MPRIS service" << serviceName << "just came online"; + addMediaPlayer(serviceName, sourceName); + } +} + +bool Mpris2Engine::updateSourceEvent(const QString &source) +{ + if (source == Multiplexer::sourceName) { + return false; + } else { + PlayerContainer *container = qobject_cast(containerForSource(source)); + if (container) { + container->refresh(); + return true; + } else { + return false; + } + } +} + +bool Mpris2Engine::sourceRequestEvent(const QString &source) +{ + if (source == Multiplexer::sourceName) { + createMultiplexer(); + return true; + } + return false; +} + +void Mpris2Engine::initialFetchFinished(PlayerContainer *container) +{ + qCDebug(MPRIS2) << "Props fetch for" << container->objectName() << "finished; adding"; + + // don't let future refreshes trigger this + disconnect(container, &PlayerContainer::initialFetchFinished, this, &Mpris2Engine::initialFetchFinished); + disconnect(container, &PlayerContainer::initialFetchFailed, this, &Mpris2Engine::initialFetchFailed); + + // Check if the player follows the specification dutifully. + const auto data = container->data(); + if (data.value(QStringLiteral("Identity")).toString().isEmpty() || !data.value(QStringLiteral("SupportedUriSchemes")).isValid() + || !data.value(QStringLiteral("SupportedMimeTypes")).isValid()) { + qCDebug(MPRIS2) << "MPRIS2 service" << container->objectName() << "isn't standard-compliant, ignoring"; + return; + } + + addSource(container); + if (m_multiplexer) { + m_multiplexer.data()->addPlayer(container); + } +} + +void Mpris2Engine::initialFetchFailed(PlayerContainer *container) +{ + qCWarning(MPRIS2) << "Failed to find working MPRIS2 interface for" << container->dbusAddress(); + container->deleteLater(); +} + +void Mpris2Engine::serviceNameFetchFinished(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply propsReply = *watcher; + watcher->deleteLater(); + + if (propsReply.isError()) { + qCWarning(MPRIS2) << "Could not get list of available D-Bus services"; + } else { + foreach (const QString &serviceName, propsReply.value()) { + if (serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) { + qCDebug(MPRIS2) << "Found MPRIS2 service" << serviceName; + // watch out for race conditions; the media player could + // have appeared between starting the service watcher and + // this call being dealt with + // NB: _disappearing_ between sending this call and doing + // this processing is fine + QString sourceName = serviceName.mid(23); + PlayerContainer *container = qobject_cast(containerForSource(sourceName)); + if (!container) { + qCDebug(MPRIS2) << "Haven't already seen" << serviceName; + addMediaPlayer(serviceName, sourceName); + } + } + } + } +} + +void Mpris2Engine::addMediaPlayer(const QString &serviceName, const QString &sourceName) +{ + PlayerContainer *container = new PlayerContainer(serviceName, this); + container->setObjectName(sourceName); + connect(container, &PlayerContainer::initialFetchFinished, this, &Mpris2Engine::initialFetchFinished); + connect(container, &PlayerContainer::initialFetchFailed, this, &Mpris2Engine::initialFetchFailed); +} + +void Mpris2Engine::createMultiplexer() +{ + Q_ASSERT(!m_multiplexer); + m_multiplexer = new Multiplexer(this); + + SourceDict dict = containerDict(); + SourceDict::const_iterator i = dict.constBegin(); + while (i != dict.constEnd()) { + PlayerContainer *container = qobject_cast(i.value()); + m_multiplexer.data()->addPlayer(container); + ++i; + } + addSource(m_multiplexer.data()); + // Don't delete sourceName because currentData refers to it + connect(m_multiplexer, &Multiplexer::playerListEmptied, m_multiplexer, &Multiplexer::deleteLater, Qt::UniqueConnection); +} + +K_PLUGIN_CLASS_WITH_JSON(Mpris2Engine, "plasma-dataengine-mpris2.json") + +#include "mpris2engine.moc" diff --git a/plasma/workspace/dataengines/mpris2/mpris2engine.h b/plasma/workspace/dataengines/mpris2/mpris2engine.h new file mode 100644 index 0000000000..09cfc5d327 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/mpris2engine.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2007-2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include + +class QDBusPendingCallWatcher; +class PlayerContainer; +class Multiplexer; + +/** + * The MPRIS2 data engine. + */ +class Mpris2Engine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + Mpris2Engine(QObject *parent, const QVariantList &args); + + Plasma::Service *serviceForSource(const QString &source) override; + QStringList sources() const override; + +protected: + bool sourceRequestEvent(const QString &source) override; + bool updateSourceEvent(const QString &source) override; + +private Q_SLOTS: + void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner); + void initialFetchFinished(PlayerContainer *container); + void initialFetchFailed(PlayerContainer *container); + void serviceNameFetchFinished(QDBusPendingCallWatcher *watcher); + +private: + void addMediaPlayer(const QString &serviceName, const QString &sourceName); + void createMultiplexer(); + + QPointer m_multiplexer; +}; diff --git a/plasma/workspace/dataengines/mpris2/multiplexedservice.cpp b/plasma/workspace/dataengines/mpris2/multiplexedservice.cpp new file mode 100644 index 0000000000..1429576f16 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/multiplexedservice.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "multiplexedservice.h" + +#include "multiplexer.h" +#include + +#include +#include +#include + +MultiplexedService::MultiplexedService(Multiplexer *multiplexer, QObject *parent) + : Plasma::Service(parent) +{ + setObjectName(Multiplexer::sourceName + QLatin1String(" controller")); + setName(QStringLiteral("mpris2")); + setDestination(Multiplexer::sourceName); + + connect(multiplexer, &Multiplexer::activePlayerChanged, this, &MultiplexedService::activePlayerChanged); + + activePlayerChanged(multiplexer->activePlayer()); +} + +Plasma::ServiceJob *MultiplexedService::createJob(const QString &operation, QMap ¶meters) +{ + if (m_control) { + return m_control.data()->createJob(operation, parameters); + } + return nullptr; +} + +void MultiplexedService::updateEnabledOperations() +{ + if (m_control) { + foreach (const QString &op, operationNames()) { + setOperationEnabled(op, m_control.data()->isOperationEnabled(op)); + } + } else { + foreach (const QString &op, operationNames()) { + setOperationEnabled(op, false); + } + } +} + +void MultiplexedService::activePlayerChanged(PlayerContainer *container) +{ + delete m_control.data(); + + if (container) { + m_control = new PlayerControl(container, container->getDataEngine()); + connect(m_control.data(), &PlayerControl::enabledOperationsChanged, this, &MultiplexedService::updateEnabledOperations); + } + + updateEnabledOperations(); +} + +void MultiplexedService::enableGlobalShortcuts() +{ + if (m_actionCollection) { + return; + } + + m_actionCollection = new KActionCollection(this, QStringLiteral("mediacontrol")); + m_actionCollection->setComponentDisplayName(i18nc("Name for global shortcuts category", "Media Controller")); + QAction *playPauseAction = m_actionCollection->addAction(QStringLiteral("playpausemedia")); + playPauseAction->setText(i18n("Play/Pause media playback")); + KGlobalAccel::setGlobalShortcut(playPauseAction, Qt::Key_MediaPlay); + connect(playPauseAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & PlayerContainer::CanControl) { + const QString playbackStatus = m_control->rawData().value(QStringLiteral("PlaybackStatus")).toString(); + if (playbackStatus == QLatin1String("Playing")) { + if (m_control->capabilities() & PlayerContainer::CanPause) { + m_control->playerInterface()->Pause(); + } + } else { + if (m_control->capabilities() & PlayerContainer::CanPlay) { + m_control->playerInterface()->Play(); + } + } + } + }); + + QAction *nextAction = m_actionCollection->addAction(QStringLiteral("nextmedia")); + nextAction->setText(i18n("Media playback next")); + KGlobalAccel::setGlobalShortcut(nextAction, Qt::Key_MediaNext); + connect(nextAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & (PlayerContainer::CanControl | PlayerContainer::CanGoNext)) { + m_control->playerInterface()->Next(); + } + }); + + QAction *previousAction = m_actionCollection->addAction(QStringLiteral("previousmedia")); + previousAction->setText(i18n("Media playback previous")); + KGlobalAccel::setGlobalShortcut(previousAction, Qt::Key_MediaPrevious); + connect(previousAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & (PlayerContainer::CanControl | PlayerContainer::CanGoPrevious)) { + m_control->playerInterface()->Previous(); + } + }); + + QAction *stopAction = m_actionCollection->addAction(QStringLiteral("stopmedia")); + stopAction->setText(i18n("Stop media playback")); + KGlobalAccel::setGlobalShortcut(stopAction, Qt::Key_MediaStop); + connect(stopAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & PlayerContainer::CanStop) { + m_control->playerInterface()->Stop(); + } + }); + + QAction *pauseAction = m_actionCollection->addAction(QStringLiteral("pausemedia")); + pauseAction->setText(i18n("Pause media playback")); + KGlobalAccel::setGlobalShortcut(pauseAction, Qt::Key_MediaPause); + connect(pauseAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & PlayerContainer::CanPause) { + m_control->playerInterface()->Pause(); + } + }); + + QAction *playAction = m_actionCollection->addAction(QStringLiteral("playmedia")); + playAction->setText(i18n("Play media playback")); + KGlobalAccel::setGlobalShortcut(playAction, QKeySequence()); + connect(playAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & PlayerContainer::CanPlay) { + m_control->playerInterface()->Play(); + } + }); + + QAction *volumeupAction = m_actionCollection->addAction(QStringLiteral("mediavolumeup")); + volumeupAction->setText(i18n("Media volume up")); + KGlobalAccel::setGlobalShortcut(volumeupAction, QKeySequence()); + connect(volumeupAction, &QAction::triggered, this, [this] { + if (m_control && m_control->capabilities() & PlayerContainer::CanControl) { + m_control->changeVolume(0.05, true); + } + }); + + QAction *volumedownAction = m_actionCollection->addAction(QStringLiteral("mediavolumedown")); + volumedownAction->setText(i18n("Media volume down")); + KGlobalAccel::setGlobalShortcut(volumedownAction, QKeySequence()); + connect(volumedownAction, &QAction::triggered, this, [this] { + if (m_control && m_control->playerInterface()->canControl()) { + m_control->changeVolume(-0.05, true); + } + }); +} diff --git a/plasma/workspace/dataengines/mpris2/multiplexedservice.h b/plasma/workspace/dataengines/mpris2/multiplexedservice.h new file mode 100644 index 0000000000..bfd231c4c7 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/multiplexedservice.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +#include + +#include "playercontrol.h" +#include + +class Multiplexer; +class PlayerControl; +class KActionCollection; + +class MultiplexedService : public Plasma::Service +{ + Q_OBJECT + +public: + explicit MultiplexedService(Multiplexer *multiplexer, QObject *parent = nullptr); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +public Q_SLOTS: + void enableGlobalShortcuts(); + +private Q_SLOTS: + void updateEnabledOperations(); + void activePlayerChanged(PlayerContainer *container); + +private: + QPointer m_control; + KActionCollection *m_actionCollection = nullptr; +}; diff --git a/plasma/workspace/dataengines/mpris2/multiplexer.cpp b/plasma/workspace/dataengines/mpris2/multiplexer.cpp new file mode 100644 index 0000000000..9a166cb673 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/multiplexer.cpp @@ -0,0 +1,234 @@ +/* + SPDX-FileCopyrightText: 2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "multiplexer.h" +#include + +#include + +#include +#include // for Q_ASSERT + +#include + +#include "debug.h" + +// the '@' at the start is not valid for D-Bus names, so it will +// never interfere with an actual MPRIS2 player +const QLatin1String Multiplexer::sourceName = QLatin1String("@multiplex"); + +Multiplexer::Multiplexer(QObject *parent) + : DataContainer(parent) +{ + setObjectName(sourceName); +} + +void Multiplexer::evaluatePlayer(PlayerContainer *container) +{ + bool makeActive = m_activeName.isEmpty(); + + QString name = container->objectName(); + const QString containerPlaybackStatus = container->data().value(QStringLiteral("PlaybackStatus")).toString(); + const QString multiplexerPlaybackStatus = data().value(QStringLiteral("PlaybackStatus")).toString(); + + m_playing.remove(name); + m_paused.remove(name); + m_stopped.remove(name); + + // Ensure the actual player is always in the correct category + if (containerPlaybackStatus == QLatin1String("Playing")) { + m_playing.insert(name, container); + } else if (containerPlaybackStatus == QLatin1String("Paused")) { + m_paused.insert(name, container); + } else { + m_stopped.insert(name, container); + } + + const auto proxyPid = container->data().value(QStringLiteral("Metadata")).toMap().value(QStringLiteral("kde:pid")).toUInt(); + if (proxyPid) { + auto it = m_proxies.find(proxyPid); + if (it == m_proxies.end()) { + m_proxies.insert(proxyPid, container); + } + } + + const auto containerPid = container->data().value(QStringLiteral("InstancePid")).toUInt(); + PlayerContainer *proxy = m_proxies.value(containerPid); + if (proxy) { + // Operate on the proxy from now on + container = proxy; + name = container->objectName(); + } + + if (!makeActive) { + // If this player has higher status than the current multiplexer player, switch over to it + if (m_playing.value(name) && multiplexerPlaybackStatus != QLatin1String("Playing")) { + qCDebug(MPRIS2) << "Player" << name << "is now playing but current was not"; + makeActive = true; + } else if (m_paused.value(name) && multiplexerPlaybackStatus != QLatin1String("Playing") && multiplexerPlaybackStatus != QLatin1String("Paused")) { + qCDebug(MPRIS2) << "Player" << name << "is now paused but current was stopped"; + makeActive = true; + } + } + + if (m_activeName == name) { + // If we are the current player and move to a lower status, switch to another one, if necessary + if (m_paused.value(name) && !m_playing.isEmpty()) { + qCDebug(MPRIS2) << "Current player" << m_activeName << "is now paused but there is another playing one, switching players"; + setBestActive(); + makeActive = false; + } else if (m_stopped.value(name) && (!m_playing.isEmpty() || !m_paused.isEmpty())) { + qCDebug(MPRIS2) << "Current player" << m_activeName << "is now stopped but there is another playing or paused one, switching players"; + setBestActive(); + makeActive = false; + } else { + makeActive = true; + } + } + + if (makeActive) { + if (m_activeName != name) { + qCDebug(MPRIS2) << "Switching from" << m_activeName << "to" << name; + m_activeName = name; + } + replaceData(container->data()); + checkForUpdate(); + Q_EMIT activePlayerChanged(container); + } +} + +void Multiplexer::addPlayer(PlayerContainer *container) +{ + evaluatePlayer(container); + + connect(container, &Plasma::DataContainer::dataUpdated, this, &Multiplexer::playerUpdated); +} + +void Multiplexer::removePlayer(const QString &name) +{ + PlayerContainer *container = m_playing.take(name); + if (!container) + container = m_paused.take(name); + if (!container) + container = m_stopped.take(name); + if (container) + container->disconnect(this); + + // Remove proxy by value (container), not key (pid), which could have changed + const auto pid = m_proxies.key(container); + if (pid) { + m_proxies.remove(pid); + } + + if (name == m_activeName) { + setBestActive(); + } + + // When there is no player opened + if (m_paused.empty() && m_stopped.empty() && m_playing.empty()) { + Q_EMIT playerListEmptied(); + } +} + +PlayerContainer *Multiplexer::activePlayer() const +{ + if (m_activeName.isEmpty()) { + return nullptr; + } + + PlayerContainer *container = m_playing.value(m_activeName); + if (!container) + container = m_paused.value(m_activeName); + if (!container) + container = m_stopped.value(m_activeName); + Q_ASSERT(container); + return container; +} + +void Multiplexer::playerUpdated(const QString &name, const Plasma::DataEngine::Data &newData) +{ + Q_UNUSED(name); + Q_UNUSED(newData); + evaluatePlayer(qobject_cast(sender())); +} + +PlayerContainer *Multiplexer::firstPlayerFromHash(const QHash &hash, PlayerContainer **proxyCandidate) const +{ + if (proxyCandidate) { + *proxyCandidate = nullptr; + } + + auto it = hash.begin(); + if (it == hash.end()) { + return nullptr; + } + + PlayerContainer *container = it.value(); + const auto containerPid = container->data().value(QStringLiteral("InstancePid")).toUInt(); + + // Check if this player is being proxied by someone else and prefer the proxy + // but only if it is in the same hash (same state) + if (PlayerContainer *proxy = m_proxies.value(containerPid)) { + if (std::find(hash.begin(), hash.end(), proxy) == hash.end()) { + if (proxyCandidate) { + *proxyCandidate = proxy; + } + return nullptr; + // continue; + } + return proxy; + } + + return container; +} + +void Multiplexer::setBestActive() +{ + qCDebug(MPRIS2) << "Activating best player"; + PlayerContainer *proxyCandidate = nullptr; + + PlayerContainer *container = firstPlayerFromHash(m_playing, &proxyCandidate); + if (!container) { + // If we found a proxy earlier, prefer it over a random other player in that category + if (proxyCandidate && std::find(m_paused.constBegin(), m_paused.constEnd(), proxyCandidate) != m_paused.constEnd()) { + container = proxyCandidate; + } else { + container = firstPlayerFromHash(m_paused, &proxyCandidate); + } + } + if (!container) { + if (proxyCandidate && std::find(m_stopped.constBegin(), m_stopped.constEnd(), proxyCandidate) != m_stopped.constEnd()) { + container = proxyCandidate; + } else { + container = firstPlayerFromHash(m_stopped, &proxyCandidate); + } + } + + if (!container) { + qCDebug(MPRIS2) << "There is currently no player"; + m_activeName.clear(); + removeAllData(); + } else { + m_activeName = container->objectName(); + qCDebug(MPRIS2) << "Determined" << m_activeName << "to be the best player"; + replaceData(container->data()); + checkForUpdate(); + } + + Q_EMIT activePlayerChanged(container); +} + +void Multiplexer::replaceData(const Plasma::DataEngine::Data &data) +{ + removeAllData(); + + Plasma::DataEngine::Data::const_iterator it = data.constBegin(); + while (it != data.constEnd()) { + setData(it.key(), it.value()); + ++it; + } + setData(QStringLiteral("Source Name"), m_activeName); +} diff --git a/plasma/workspace/dataengines/mpris2/multiplexer.h b/plasma/workspace/dataengines/mpris2/multiplexer.h new file mode 100644 index 0000000000..b4bcde696e --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/multiplexer.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +#include "playercontainer.h" + +#include + +class Multiplexer : public Plasma::DataContainer +{ + Q_OBJECT + +public: + static const QLatin1String sourceName; + + explicit Multiplexer(QObject *parent = nullptr); + + void addPlayer(PlayerContainer *container); + void removePlayer(const QString &name); + PlayerContainer *activePlayer() const; + +Q_SIGNALS: + void activePlayerChanged(PlayerContainer *container); + + /** + * There is no player opened. + * + * @since 5.24 + */ + void playerListEmptied(); + +private Q_SLOTS: + void playerUpdated(const QString &name, const Plasma::DataEngine::Data &data); + +private: + void evaluatePlayer(PlayerContainer *container); + void setBestActive(); + void replaceData(const Plasma::DataEngine::Data &data); + PlayerContainer *firstPlayerFromHash(const QHash &hash, PlayerContainer **proxyCandidate) const; + + QString m_activeName; + QHash m_playing; + QHash m_paused; + QHash m_stopped; + + QHash m_proxies; +}; diff --git a/plasma/workspace/dataengines/mpris2/org.freedesktop.DBus.Properties.xml b/plasma/workspace/dataengines/mpris2/org.freedesktop.DBus.Properties.xml new file mode 100644 index 0000000000..3bbf8268d6 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.Player.xml b/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 0000000000..5f9d0d1d1e --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.xml b/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.xml new file mode 100644 index 0000000000..bd9df3ea34 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/org.mpris.MediaPlayer2.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/mpris2/plasma-dataengine-mpris2.json b/plasma/workspace/dataengines/mpris2/plasma-dataengine-mpris2.json new file mode 100644 index 0000000000..0c9282ad42 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/plasma-dataengine-mpris2.json @@ -0,0 +1,103 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Provides information from and control over media players via MPRIS2", + "Description[ar]": "يوفر معلومات من مشغلات الوسائط والتحكم فيها عبر MPRIS2", + "Description[az]": "MPRIS2 ilə media pleyerin idarəsini və onun haqqında məlumatları təmin edir", + "Description[ca]": "Proporciona informació i controla els reproductors de suports via el MPRIS2", + "Description[cs]": "Poskytuje informace a ovládání přehrávačů médií přes MPRIS2", + "Description[de]": "Stellt Informationen über Medienspieler unter Verwendung von MPRIS2 bereit und ermöglicht deren Steuerung", + "Description[en_GB]": "Provides information from and control over media players via MPRIS2", + "Description[es]": "Proporciona información y control sobre reproductores multimedia vía MPRIS2", + "Description[eu]": "Multimedia-jotzaileei buruzko informazioa ematen du eta haiek kontrolatzen ditu MPRIS2 bidez", + "Description[fi]": "Tarjoaa mediasoitintiedot ja mediasoitinten hallinnan MPRIS2:n avulla", + "Description[fr]": "Fournit des informations sur les lecteurs de média et les contrôle avec « MPRIS2 ».", + "Description[hu]": "Információkat szolgáltat és vezérli a médialejátszókat az MPRIS2-n keresztül", + "Description[ia]": "Il provide information ex e control super media players via MPRIS2", + "Description[it]": "Fornisce le informazioni dai lettori multimediali e li controlla con MPRIS2", + "Description[ko]": "MPRIS2를 통하여 미디어 재생기를 제어하고 정보 가져오기", + "Description[lt]": "Suteikia informaciją iš MPRIS2 ir per jį valdo medijos leistuves", + "Description[nl]": "Levert informatie uit en besturing aan mediaspelers via MPRIS2", + "Description[nn]": "Gjev informasjon frå, og kontroll over, mediespelarar via MPRIS2-grensesnittet", + "Description[pl]": "Dostarcza informacji z odtwarzaczy multimedialnych, a także steruje nimi przez MPRIS2", + "Description[pt_BR]": "Fornece informações e controla leitores multimídia através de MPRIS2", + "Description[ro]": "Furnizează informații de la și controlează lectorii multimedia prin MPRIS2", + "Description[ru]": "Предоставление информации и управление медиапроигрывателем с помощью MPRIS2", + "Description[sk]": "Poskytuje informácie z, a ovláda prehrávače médií cez MPRIS2", + "Description[sl]": "Prikazuje informacije in omogoča nadzor predstavnostnih predvajalnikov prek MPRIS2", + "Description[sv]": "Tillhandahåller information från och styrning av mediaspelare via MPRIS2", + "Description[ta]": "MPRIS2 மூலம் ஊடக இயக்கிகளிலிருந்து விவரங்களை பெற்று அவற்றை கட்டுப்படுத்தும்", + "Description[tr]": "MPRIS2 kullanarak çoklu ortam oynatıcılarından bilgi sağlar ve onları denetler", + "Description[uk]": "Надає дані з мультимедійних програвачів та керує ними за допомогою MPRIS2", + "Description[vi]": "Cung cấp thông tin của và cách điều khiển với các trình phát phương tiện thông qua MPRIS2", + "Description[x-test]": "xxProvides information from and control over media players via MPRIS2xx", + "Description[zh_CN]": "通过 MPRIS2 对媒体播放器进行信息获取和控制", + "Icon": "applications-multimedia", + "Id": "mpris2", + "License": "", + "Name": "MPRIS2", + "Name[ar]": "MPRIS2", + "Name[ast]": "MPRIS2", + "Name[az]": "MPRIS2", + "Name[bs]": "MPRIS2", + "Name[ca@valencia]": "MPRIS2", + "Name[ca]": "MPRIS2", + "Name[cs]": "MPRIS2", + "Name[da]": "MPRIS2", + "Name[de]": "MPRIS2", + "Name[el]": "MPRIS2", + "Name[en_GB]": "MPRIS2", + "Name[es]": "MPRIS2", + "Name[et]": "MPRIS2", + "Name[eu]": "MPRIS2", + "Name[fi]": "MPRIS2", + "Name[fr]": "MPRIS2", + "Name[gl]": "MPRIS2", + "Name[he]": "MPRIS2", + "Name[hi]": "एमप्रिस२", + "Name[hu]": "MPRIS2", + "Name[ia]": "MPRIS2", + "Name[id]": "MPRIS2", + "Name[is]": "MPRIS2", + "Name[it]": "MPRIS2", + "Name[ja]": "MPRIS2", + "Name[kk]": "MPRIS2", + "Name[km]": "MPRIS2", + "Name[ko]": "MPRIS2", + "Name[lt]": "MPRIS2", + "Name[ml]": "MPRIS2", + "Name[mr]": "MPRIS2", + "Name[nb]": "MPRIS2", + "Name[nds]": "MPRIS2", + "Name[nl]": "MPRIS2", + "Name[nn]": "MPRIS2", + "Name[pa]": "MPRIS2", + "Name[pl]": "MPRIS2", + "Name[pt]": "MPRIS2", + "Name[pt_BR]": "MPRIS2", + "Name[ro]": "MPRIS2", + "Name[ru]": "MPRIS2", + "Name[sk]": "MPRIS2", + "Name[sl]": "MPRIS2", + "Name[sr@ijekavian]": "МПРИС 2", + "Name[sr@ijekavianlatin]": "MPRIS 2", + "Name[sr@latin]": "MPRIS 2", + "Name[sr]": "МПРИС 2", + "Name[sv]": "MPRIS2", + "Name[ta]": "MPRIS2", + "Name[tg]": "MPRIS2", + "Name[tr]": "MPRIS2", + "Name[uk]": "MPRIS2", + "Name[vi]": "MPRIS2", + "Name[x-test]": "xxMPRIS2xx", + "Name[zh_CN]": "MPRIS2", + "Name[zh_TW]": "MPRIS2", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/mpris2/playeractionjob.cpp b/plasma/workspace/dataengines/mpris2/playeractionjob.cpp new file mode 100644 index 0000000000..dc84a8ee75 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playeractionjob.cpp @@ -0,0 +1,180 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "playeractionjob.h" + +#include +#include +#include + +#define TRANSLATION_DOMAIN "plasma_engine_mpris2" +#include + +#include +#include +#include + +#include "debug.h" + +PlayerActionJob::PlayerActionJob(const QString &operation, QMap ¶meters, PlayerControl *parent) + : ServiceJob(parent->name() + ": " + operation, operation, parameters, parent) + , m_controller(parent) +{ +} + +void PlayerActionJob::start() +{ + const QString operation(operationName()); + + if (!m_controller) { + setError(Failed); + emitResult(); + return; + } + + qCDebug(MPRIS2) << "Trying to perform the action" << operationName(); + if (!m_controller->isOperationEnabled(operation)) { + setError(Denied); + emitResult(); + return; + } + + if (operation == QLatin1String("Quit") || operation == QLatin1String("Raise") || operation == QLatin1String("SetFullscreen")) { + listenToCall(m_controller->rootInterface()->asyncCall(operation)); + } else if (operation == QLatin1String("Play") || operation == QLatin1String("Pause") || operation == QLatin1String("PlayPause") + || operation == QLatin1String("Stop") || operation == QLatin1String("Next") || operation == QLatin1String("Previous")) { + listenToCall(m_controller->playerInterface()->asyncCall(operation)); + } else if (operation == QLatin1String("Seek")) { + if (parameters().value(QStringLiteral("microseconds")).canConvert()) { + listenToCall(m_controller->playerInterface()->Seek(parameters()[QStringLiteral("microseconds")].toLongLong())); + } else { + setErrorText(QStringLiteral("microseconds")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("SetPosition")) { + if (parameters().value(QStringLiteral("microseconds")).canConvert()) { + listenToCall(m_controller->playerInterface()->SetPosition(m_controller->trackId(), parameters()[QStringLiteral("microseconds")].toLongLong())); + } else { + setErrorText(QStringLiteral("microseconds")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("OpenUri")) { + if (parameters().value(QStringLiteral("uri")).canConvert()) { + listenToCall(m_controller->playerInterface()->OpenUri(QString::fromLatin1(parameters()[QStringLiteral("uri")].toUrl().toEncoded()))); + } else { + qCDebug(MPRIS2) << "uri was of type" << parameters().value(QStringLiteral("uri")).userType(); + setErrorText(QStringLiteral("uri")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("SetLoopStatus")) { + if (parameters().value(QStringLiteral("status")).type() == QVariant::String) { + setDBusProperty(m_controller->playerInterface()->interface(), QStringLiteral("LoopStatus"), QDBusVariant(parameters()[QStringLiteral("status")])); + } else { + setErrorText(QStringLiteral("status")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("SetShuffle")) { + if (parameters().value(QStringLiteral("on")).type() == QVariant::Bool) { + setDBusProperty(m_controller->playerInterface()->interface(), QStringLiteral("Shuffle"), QDBusVariant(parameters()[QStringLiteral("on")])); + } else { + setErrorText(QStringLiteral("on")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("SetRate")) { + if (parameters().value(QStringLiteral("rate")).type() == QVariant::Double) { + setDBusProperty(m_controller->playerInterface()->interface(), QStringLiteral("Rate"), QDBusVariant(parameters()[QStringLiteral("rate")])); + } else { + setErrorText(QStringLiteral("rate")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("SetVolume")) { + if (parameters().value(QStringLiteral("level")).type() == QVariant::Double) { + setDBusProperty(m_controller->playerInterface()->interface(), QStringLiteral("Volume"), QDBusVariant(parameters()[QStringLiteral("level")])); + } else { + setErrorText(QStringLiteral("level")); + setError(MissingArgument); + emitResult(); + } + } else if (operation == QLatin1String("ChangeVolume")) { + if (parameters().value(QStringLiteral("delta")).type() != QVariant::Double) { + setErrorText(QStringLiteral("delta")); + setError(MissingArgument); + emitResult(); + return; + } + if (parameters().value(QStringLiteral("showOSD")).type() != QVariant::Bool) { + setErrorText(QStringLiteral("showOSD")); + setError(MissingArgument); + emitResult(); + return; + } + + m_controller->changeVolume(parameters()[QStringLiteral("delta")].toDouble(), parameters()[QStringLiteral("showOSD")].toBool()); + setError(NoError); + emitResult(); + } else if (operation == QLatin1String("GetPosition")) { + m_controller->updatePosition(); + } else { + setError(UnknownOperation); + emitResult(); + } +} + +void PlayerActionJob::listenToCall(const QDBusPendingCall &call) +{ + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &PlayerActionJob::callFinished); +} + +void PlayerActionJob::callFinished(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply result = *watcher; + watcher->deleteLater(); + + if (result.isError()) { + // FIXME: try to be a bit cleverer with the error message? + setError(Failed); + setErrorText(result.error().message()); + } else { + setError(NoError); + } + + emitResult(); +} + +void PlayerActionJob::setDBusProperty(const QString &iface, const QString &propName, const QDBusVariant &value) +{ + if (!m_controller) { + setError(Failed); + emitResult(); + return; + } + + listenToCall(m_controller->propertiesInterface()->Set(iface, propName, value)); +} + +QString PlayerActionJob::errorString() const +{ + if (error() == Denied) { + const QString name = m_controller ? m_controller->name() : QString(); + return i18n("The media player '%1' cannot perform the action '%2'.", name, operationName()); + } else if (error() == Failed) { + return i18n("Attempting to perform the action '%1' failed with the message '%2'.", operationName(), errorText()); + } else if (error() == MissingArgument) { + return i18n("The argument '%1' for the action '%2' is missing or of the wrong type.", operationName(), errorText()); + } else if (error() == UnknownOperation) { + return i18n("The operation '%1' is unknown.", operationName()); + } + return i18n("Unknown error."); +} + +// vim: sw=4 sts=4 et tw=100 diff --git a/plasma/workspace/dataengines/mpris2/playeractionjob.h b/plasma/workspace/dataengines/mpris2/playeractionjob.h new file mode 100644 index 0000000000..2a96b2d270 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playeractionjob.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +#include "playercontrol.h" + +class QDBusPendingCallWatcher; +class QDBusPendingCall; +class QDBusVariant; + +class PlayerActionJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + PlayerActionJob(const QString &operation, QMap ¶meters, PlayerControl *parent); + + enum { + /** + * The media player reports that the operation is not possible + */ + Denied = UserDefinedError, + /** + * Calling the media player resulted in an error + */ + Failed, + /** + * An argument is missing or of wrong type + * + * errorText is argument name + */ + MissingArgument, + /** + * The operation name is unknown + */ + UnknownOperation, + }; + + void start() override; + + QString errorString() const override; + +private Q_SLOTS: + void callFinished(QDBusPendingCallWatcher *); + void setDBusProperty(const QString &iface, const QString &propName, const QDBusVariant &value); + +private: + void listenToCall(const QDBusPendingCall &call); + + QPointer m_controller; +}; diff --git a/plasma/workspace/dataengines/mpris2/playercontainer.cpp b/plasma/workspace/dataengines/mpris2/playercontainer.cpp new file mode 100644 index 0000000000..913cbca2e0 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playercontainer.cpp @@ -0,0 +1,374 @@ +/* + SPDX-FileCopyrightText: 2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "playercontainer.h" + +#include +#include +#include + +#define MPRIS2_PATH "/org/mpris/MediaPlayer2" +#define POS_UPD_STRING "Position last updated (UTC)" + +#include + +#include +#include + +#include "debug.h" + +static QVariant::Type expPropType(const QString &propName) +{ + if (propName == QLatin1String("Identity")) + return QVariant::String; + else if (propName == QLatin1String("DesktopEntry")) + return QVariant::String; + else if (propName == QLatin1String("SupportedUriSchemes")) + return QVariant::StringList; + else if (propName == QLatin1String("SupportedMimeTypes")) + return QVariant::StringList; + else if (propName == QLatin1String("Fullscreen")) + return QVariant::Bool; + else if (propName == QLatin1String("PlaybackStatus")) + return QVariant::String; + else if (propName == QLatin1String("LoopStatus")) + return QVariant::String; + else if (propName == QLatin1String("Shuffle")) + return QVariant::Bool; + else if (propName == QLatin1String("Rate")) + return QVariant::Double; + else if (propName == QLatin1String("MinimumRate")) + return QVariant::Double; + else if (propName == QLatin1String("MaximumRate")) + return QVariant::Double; + else if (propName == QLatin1String("Volume")) + return QVariant::Double; + else if (propName == QLatin1String("Position")) + return QVariant::LongLong; + else if (propName == QLatin1String("Metadata")) + return QVariant::Map; + // we give out CanControl, as this may completely + // change the UI of the widget + else if (propName == QLatin1String("CanControl")) + return QVariant::Bool; + else if (propName == QLatin1String("CanSeek")) + return QVariant::Bool; + else if (propName == QLatin1String("CanGoNext")) + return QVariant::Bool; + else if (propName == QLatin1String("CanGoPrevious")) + return QVariant::Bool; + else if (propName == QLatin1String("CanRaise")) + return QVariant::Bool; + else if (propName == QLatin1String("CanQuit")) + return QVariant::Bool; + else if (propName == QLatin1String("CanPlay")) + return QVariant::Bool; + else if (propName == QLatin1String("CanPause")) + return QVariant::Bool; + return QVariant::Invalid; +} + +static PlayerContainer::Cap capFromName(const QString &capName) +{ + if (capName == QLatin1String("CanQuit")) + return PlayerContainer::CanQuit; + else if (capName == QLatin1String("CanRaise")) + return PlayerContainer::CanRaise; + else if (capName == QLatin1String("CanSetFullscreen")) + return PlayerContainer::CanSetFullscreen; + else if (capName == QLatin1String("CanControl")) + return PlayerContainer::CanControl; + else if (capName == QLatin1String("CanPlay")) + return PlayerContainer::CanPlay; + else if (capName == QLatin1String("CanPause")) + return PlayerContainer::CanPause; + else if (capName == QLatin1String("CanSeek")) + return PlayerContainer::CanSeek; + else if (capName == QLatin1String("CanGoNext")) + return PlayerContainer::CanGoNext; + else if (capName == QLatin1String("CanGoPrevious")) + return PlayerContainer::CanGoPrevious; + return PlayerContainer::NoCaps; +} + +PlayerContainer::PlayerContainer(const QString &busAddress, QObject *parent) + : DataContainer(parent) + , m_caps(NoCaps) + , m_fetchesPending(0) + , m_dbusAddress(busAddress) + , m_currentRate(0.0) +{ + Q_ASSERT(!busAddress.isEmpty()); + Q_ASSERT(busAddress.startsWith(QLatin1String("org.mpris.MediaPlayer2."))); + + // MPRIS specifies, that in case a player supports several instances, each additional + // instance after the first one is supposed to append ".instance" at the end of + // its dbus address. So instances of media players, which implement this correctly + // can have one D-Bus connection per instance and can be identified by their pids. + QDBusReply pidReply = QDBusConnection::sessionBus().interface()->servicePid(busAddress); + if (pidReply.isValid()) { + setData("InstancePid", pidReply.value()); + } + + m_propsIface = new OrgFreedesktopDBusPropertiesInterface(busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); + + m_playerIface = new OrgMprisMediaPlayer2PlayerInterface(busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); + + m_rootIface = new OrgMprisMediaPlayer2Interface(busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); + + connect(m_propsIface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &PlayerContainer::propertiesChanged); + + connect(m_playerIface, &OrgMprisMediaPlayer2PlayerInterface::Seeked, this, &PlayerContainer::seeked); + + refresh(); +} + +void PlayerContainer::refresh() +{ + // despite these calls being async, we should never update values in the + // wrong order (eg: a stale GetAll response overwriting a more recent value + // from a PropertiesChanged signal) due to D-Bus message ordering guarantees. + + QDBusPendingCall async = m_propsIface->GetAll(OrgMprisMediaPlayer2Interface::staticInterfaceName()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &PlayerContainer::getPropsFinished); + ++m_fetchesPending; + + async = m_propsIface->GetAll(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName()); + watcher = new QDBusPendingCallWatcher(async, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &PlayerContainer::getPropsFinished); + ++m_fetchesPending; +} + +static bool decodeUri(QVariantMap &map, const QString &entry) +{ + if (map.contains(entry)) { + QString urlString = map.value(entry).toString(); + QUrl url = QUrl::fromEncoded(urlString.toUtf8()); + if (!url.isValid()) { + // try to be lenient + url = QUrl(urlString); + } + if (url.isValid()) { + map.insert(entry, QVariant(url)); + return true; + } else { + map.remove(entry); + return false; + } + } + // count it as a success if it doesn't exist + return true; +} + +void PlayerContainer::copyProperty(const QString &propName, const QVariant &_value, QVariant::Type expType, UpdateType updType) +{ + QVariant value = _value; + // we protect our users from bogus values + if (value.userType() == qMetaTypeId()) { + if (expType == QVariant::Map) { + QDBusArgument arg = value.value(); + // Bug 374531: MapType fits all kinds of maps but we crash when we try to stream the arg into a + // QVariantMap below but get a wrong signature, e.g. a{ss} instead of the expected a{sv} + if (arg.currentType() != QDBusArgument::MapType || arg.currentSignature() != QLatin1String("a{sv}")) { + qCWarning(MPRIS2) << m_dbusAddress << "exports" << propName << "with the wrong type; it should be D-Bus type \"a{sv}\" instead of " + << arg.currentSignature(); + return; + } + QVariantMap map; + arg >> map; + if (propName == QLatin1String("Metadata")) { + if (!decodeUri(map, QLatin1String("mpris:artUrl"))) { + qCWarning(MPRIS2) << m_dbusAddress << "has an invalid URL for the mpris:artUrl entry of the \"Metadata\" property"; + } + if (!decodeUri(map, QLatin1String("xesam:url"))) { + qCWarning(MPRIS2) << m_dbusAddress << "has an invalid URL for the xesam:url entry of the \"Metadata\" property"; + } + } + value = QVariant(map); + } + } + if (value.type() != expType) { + const char *gotTypeCh = QDBusMetaType::typeToSignature(value.userType()); + QString gotType = gotTypeCh ? QString::fromLatin1(gotTypeCh) : QStringLiteral(""); + const char *expTypeCh = QDBusMetaType::typeToSignature(expType); + QString expType = expTypeCh ? QString::fromLatin1(expTypeCh) : QStringLiteral(""); + + qCWarning(MPRIS2) << m_dbusAddress << "exports" << propName << "as D-Bus type" << gotType << "but it should be D-Bus type" << expType; + } + if (value.convert(expType)) { + if (propName == QLatin1String("Position")) { + setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); + + } else if (propName == QLatin1String("Metadata")) { + if (updType == UpdatedSignal) { + const QString oldTrackId = data().value(QStringLiteral("Metadata")).toMap().value(QStringLiteral("mpris:trackid")).toString(); + const QString newTrackId = value.toMap().value(QStringLiteral("mpris:trackid")).toString(); + if (oldTrackId != newTrackId) { + setData(QStringLiteral("Position"), static_cast(0)); + setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); + } + } + + if (value.toMap().value(QStringLiteral("mpris:length")).toLongLong() <= 0) { + QMap metadataMap = value.toMap(); + metadataMap.remove(QStringLiteral("mpris:length")); + value = QVariant(metadataMap); + } + + } else if (propName == QLatin1String("Rate") && data().value(QStringLiteral("PlaybackStatus")).toString() == QLatin1String("Playing")) { + if (data().contains(QLatin1String("Position"))) + recalculatePosition(); + m_currentRate = value.toDouble(); + + } else if (propName == QLatin1String("PlaybackStatus")) { + if (data().contains(QLatin1String("Position")) && data().contains(QLatin1String("PlaybackStatus"))) { + updatePosition(); + } + + // update the effective rate + if (data().contains(QLatin1String("Rate"))) { + if (value.toString() == QLatin1String("Playing")) + m_currentRate = data().value(QStringLiteral("Rate")).toDouble(); + else + m_currentRate = 0.0; + } + if (value.toString() == QLatin1String("Stopped")) { + // assume the position has reset to 0, since this is really the + // only sensible value for a stopped track + setData(QStringLiteral("Position"), static_cast(0)); + setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); + } + } else if (propName == QLatin1String("DesktopEntry")) { + QString filename = value.toString() + QLatin1String(".desktop"); + KDesktopFile desktopFile(filename); + QString iconName = desktopFile.readIcon(); + if (!iconName.isEmpty()) { + setData(QStringLiteral("Desktop Icon Name"), iconName); + } + } + setData(propName, value); + } +} + +void PlayerContainer::updateFromMap(const QVariantMap &map, UpdateType updType) +{ + QMap::const_iterator i = map.constBegin(); + while (i != map.constEnd()) { + QVariant::Type type = expPropType(i.key()); + if (type != QVariant::Invalid) { + copyProperty(i.key(), i.value(), type, updType); + } + + Cap cap = capFromName(i.key()); + if (cap != NoCaps) { + if (i.value().type() == QVariant::Bool) { + if (i.value().toBool()) { + m_caps |= cap; + } else { + m_caps &= ~cap; + } + } else { + const char *gotTypeCh = QDBusMetaType::typeToSignature(i.value().userType()); + QString gotType = gotTypeCh ? QString::fromLatin1(gotTypeCh) : QStringLiteral(""); + + qCWarning(MPRIS2) << m_dbusAddress << "exports" << i.key() << "as D-Bus type" << gotType << "but it should be D-Bus type \"b\""; + } + } + // fake the CanStop capability + if (cap == CanControl || i.key() == QLatin1String("PlaybackStatus")) { + if ((m_caps & CanControl) && i.value().toString() != QLatin1String("Stopped")) { + qCDebug(MPRIS2) << "Enabling stop action"; + m_caps |= CanStop; + } else { + qCDebug(MPRIS2) << "Disabling stop action"; + m_caps &= ~CanStop; + } + } + ++i; + } +} + +void PlayerContainer::getPropsFinished(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply propsReply = *watcher; + watcher->deleteLater(); + + if (m_fetchesPending < 1) { + // we already failed + return; + } + + if (propsReply.isError()) { + qCWarning(MPRIS2) << m_dbusAddress << "does not implement" << OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() << "correctly" + << "Error message was" << propsReply.error().name() << propsReply.error().message(); + m_fetchesPending = 0; + Q_EMIT initialFetchFailed(this); + return; + } + + updateFromMap(propsReply.value(), FetchAll); + checkForUpdate(); + + --m_fetchesPending; + if (m_fetchesPending == 0) { + Q_EMIT initialFetchFinished(this); + } +} + +void PlayerContainer::updatePosition() +{ + QDBusPendingCall async = m_propsIface->Get(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName(), QStringLiteral("Position")); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &PlayerContainer::getPositionFinished); +} + +void PlayerContainer::getPositionFinished(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply propsReply = *watcher; + watcher->deleteLater(); + + if (propsReply.isError()) { + qCWarning(MPRIS2) << m_dbusAddress << "does not implement" << OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() << "correctly"; + qCDebug(MPRIS2) << "Error message was" << propsReply.error().name() << propsReply.error().message(); + return; + } + + setData(QStringLiteral("Position"), propsReply.value().toLongLong()); + setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); + checkForUpdate(); +} + +void PlayerContainer::propertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + Q_UNUSED(interface) + + updateFromMap(changedProperties, UpdatedSignal); + if (!invalidatedProperties.isEmpty()) { + refresh(); + } + checkForUpdate(); +} + +void PlayerContainer::seeked(qlonglong position) +{ + setData(QStringLiteral("Position"), position); + setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); + checkForUpdate(); +} + +void PlayerContainer::recalculatePosition() +{ + Q_ASSERT(data().contains("Position")); + + qint64 pos = data().value(QStringLiteral("Position")).toLongLong(); + QDateTime lastUpdated = data().value(POS_UPD_STRING).toDateTime(); + QDateTime now = QDateTime::currentDateTimeUtc(); + qint64 diff = lastUpdated.msecsTo(now) * 1000; + qint64 newPos = pos + static_cast(diff * m_currentRate); + setData(QStringLiteral("Position"), newPos); + setData(POS_UPD_STRING, now); +} diff --git a/plasma/workspace/dataengines/mpris2/playercontainer.h b/plasma/workspace/dataengines/mpris2/playercontainer.h new file mode 100644 index 0000000000..ef6e45f70a --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playercontainer.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2008-2012 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +class OrgFreedesktopDBusPropertiesInterface; +class OrgMprisMediaPlayer2Interface; +class OrgMprisMediaPlayer2PlayerInterface; +class QDBusPendingCallWatcher; + +class PlayerContainer : public Plasma::DataContainer +{ + Q_OBJECT + +public: + explicit PlayerContainer(const QString &busAddress, QObject *parent = nullptr); + + QString dbusAddress() const + { + return m_dbusAddress; + } + OrgFreedesktopDBusPropertiesInterface *propertiesInterface() const + { + return m_propsIface; + } + OrgMprisMediaPlayer2Interface *rootInterface() const + { + return m_rootIface; + } + OrgMprisMediaPlayer2PlayerInterface *playerInterface() const + { + return m_playerIface; + } + + enum Cap { + NoCaps = 0, + CanQuit = 1 << 0, + CanRaise = 1 << 1, + CanSetFullscreen = 1 << 2, + CanControl = 1 << 3, + CanPlay = 1 << 4, + CanPause = 1 << 5, + CanSeek = 1 << 6, + CanGoNext = 1 << 7, + CanGoPrevious = 1 << 8, + // CanStop is not directly provided by the spec, + // but we infer it from PlaybackStatus and CanControl + CanStop = 1 << 9, + }; + Q_DECLARE_FLAGS(Caps, Cap) + Caps capabilities() const + { + return m_caps; + } + + enum UpdateType { + FetchAll, + UpdatedSignal, + }; + + void refresh(); + void updatePosition(); + +Q_SIGNALS: + void initialFetchFinished(PlayerContainer *self); + void initialFetchFailed(PlayerContainer *self); + void capsChanged(Caps newCaps); + +private Q_SLOTS: + void getPropsFinished(QDBusPendingCallWatcher *watcher); + void getPositionFinished(QDBusPendingCallWatcher *watcher); + void propertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + void seeked(qlonglong position); + +private: + void copyProperty(const QString &propName, const QVariant &value, QVariant::Type expType, UpdateType updType); + void updateFromMap(const QVariantMap &map, UpdateType updType); + void recalculatePosition(); + + Caps m_caps; + int m_fetchesPending; + QString m_dbusAddress; + OrgFreedesktopDBusPropertiesInterface *m_propsIface; + OrgMprisMediaPlayer2Interface *m_rootIface; + OrgMprisMediaPlayer2PlayerInterface *m_playerIface; + double m_currentRate; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(PlayerContainer::Caps) diff --git a/plasma/workspace/dataengines/mpris2/playercontrol.cpp b/plasma/workspace/dataengines/mpris2/playercontrol.cpp new file mode 100644 index 0000000000..595e50667c --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playercontrol.cpp @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "playercontrol.h" + +#include "playeractionjob.h" + +#include +#include +#include + +#include +#include +#include + +PlayerControl::PlayerControl(PlayerContainer *container, QObject *parent) + : Plasma::Service(parent) + , m_container(container) +{ + setObjectName(container->objectName() + QLatin1String(" controller")); + setName(QStringLiteral("mpris2")); + setDestination(container->objectName()); + + connect(container, &Plasma::DataContainer::dataUpdated, this, &PlayerControl::updateEnabledOperations); + connect(container, &QObject::destroyed, this, &PlayerControl::containerDestroyed); + updateEnabledOperations(); +} + +void PlayerControl::updateEnabledOperations() +{ + PlayerContainer::Caps caps = PlayerContainer::NoCaps; + if (m_container) + caps = m_container->capabilities(); + + setOperationEnabled(QStringLiteral("Quit"), caps & PlayerContainer::CanQuit); + setOperationEnabled(QStringLiteral("Raise"), caps & PlayerContainer::CanRaise); + setOperationEnabled(QStringLiteral("SetFullscreen"), caps & PlayerContainer::CanSetFullscreen); + + setOperationEnabled(QStringLiteral("Play"), caps & PlayerContainer::CanPlay); + setOperationEnabled(QStringLiteral("Pause"), caps & PlayerContainer::CanPause); + setOperationEnabled(QStringLiteral("PlayPause"), caps & (PlayerContainer::CanPlay | PlayerContainer::CanPause)); + setOperationEnabled(QStringLiteral("Stop"), caps & PlayerContainer::CanStop); + setOperationEnabled(QStringLiteral("Next"), caps & PlayerContainer::CanGoNext); + setOperationEnabled(QStringLiteral("Previous"), caps & PlayerContainer::CanGoPrevious); + setOperationEnabled(QStringLiteral("Seek"), caps & PlayerContainer::CanSeek); + setOperationEnabled(QStringLiteral("SetPosition"), caps & PlayerContainer::CanSeek); + setOperationEnabled(QStringLiteral("OpenUri"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("SetVolume"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("ChangeVolume"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("SetLoopStatus"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("SetRate"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("SetShuffle"), caps & PlayerContainer::CanControl); + setOperationEnabled(QStringLiteral("GetPosition"), true); + + Q_EMIT enabledOperationsChanged(); +} + +QDBusObjectPath PlayerControl::trackId() const +{ + QVariant mprisTrackId = m_container->data().value(QStringLiteral("Metadata")).toMap().value(QStringLiteral("mpris:trackid")); + if (mprisTrackId.canConvert()) { + return mprisTrackId.value(); + } + QString mprisTrackIdString = mprisTrackId.toString(); + if (!mprisTrackIdString.isEmpty()) { + return QDBusObjectPath(mprisTrackIdString); + } + return QDBusObjectPath(); +} + +void PlayerControl::containerDestroyed() +{ + m_container = nullptr; +} + +void PlayerControl::changeVolume(double delta, bool showOSD) +{ + // Not relying on property/setProperty to avoid doing blocking DBus calls + + const double volume = m_container->data().value(QStringLiteral("Volume")).toDouble(); + const double newVolume = qBound(0.0, volume + delta, qMax(volume, 1.0)); + + QDBusPendingCall reply = propertiesInterface()->Set(m_container->playerInterface()->interface(), QStringLiteral("Volume"), QDBusVariant(newVolume)); + + // Update the container value right away so when calling this method in quick succession + // (mouse wheeling the tray icon) next call already gets the new value + m_container->setData(QStringLiteral("Volume"), newVolume); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, [this, showOSD](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + } + + if (showOSD) { + const auto &data = m_container->data(); + + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("mediaPlayerVolumeChanged")); + + msg.setArguments({qRound(data.value(QStringLiteral("Volume")).toDouble() * 100), data.value("Identity", ""), data.value("Desktop Icon Name", "")}); + + QDBusConnection::sessionBus().asyncCall(msg); + } + }); +} + +Plasma::ServiceJob *PlayerControl::createJob(const QString &operation, QMap ¶meters) +{ + if (!m_container) + return nullptr; + return new PlayerActionJob(operation, parameters, this); +} + +// vim: sw=4 sts=4 et tw=100 diff --git a/plasma/workspace/dataengines/mpris2/playercontrol.h b/plasma/workspace/dataengines/mpris2/playercontrol.h new file mode 100644 index 0000000000..2218bfef92 --- /dev/null +++ b/plasma/workspace/dataengines/mpris2/playercontrol.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include "playercontainer.h" + +#include +#include + +class OrgFreedesktopDBusPropertiesInterface; +class OrgMprisMediaPlayer2Interface; +class OrgMprisMediaPlayer2PlayerInterface; + +class PlayerControl : public Plasma::Service +{ + Q_OBJECT + +public: + PlayerControl(PlayerContainer *container, QObject *parent); + + OrgMprisMediaPlayer2Interface *rootInterface() const + { + return m_container->rootInterface(); + } + OrgMprisMediaPlayer2PlayerInterface *playerInterface() const + { + return m_container->playerInterface(); + } + OrgFreedesktopDBusPropertiesInterface *propertiesInterface() const + { + return m_container->propertiesInterface(); + } + void updatePosition() const + { + m_container->updatePosition(); + } + PlayerContainer::Caps capabilities() const + { + return m_container->capabilities(); + } + const QMap /*DataEngine::Data*/ rawData() const + { + return m_container->data(); + } + + QDBusObjectPath trackId() const; + + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + + void changeVolume(double delta, bool showOSD); + +Q_SIGNALS: + void enabledOperationsChanged(); + +private Q_SLOTS: + void updateEnabledOperations(); + void containerDestroyed(); + +private: + PlayerContainer *m_container; +}; diff --git a/plasma/workspace/dataengines/notifications/CMakeLists.txt b/plasma/workspace/dataengines/notifications/CMakeLists.txt new file mode 100644 index 0000000000..24e3f86778 --- /dev/null +++ b/plasma/workspace/dataengines/notifications/CMakeLists.txt @@ -0,0 +1,28 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_notifications\") + +set(notifications_engine_SRCS + notificationsengine.cpp + notificationservice.cpp + notificationaction.cpp +) + +ecm_qt_declare_logging_category(notifications_engine_SRCS HEADER debug.h + IDENTIFIER NOTIFICATIONS + CATEGORY_NAME kde.dataengine.notifications` + DEFAULT_SEVERITY Info) + +kcoreaddons_add_plugin(plasma_engine_notifications SOURCES ${notifications_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_notifications + Qt::DBus + KF5::I18n + KF5::IconThemes + KF5::KIOCore + KF5::Notifications + KF5::Plasma + KF5::Service + KF5::NotifyConfig + PW::LibNotificationManager +) + +install(FILES notifications.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/notifications/Messages.sh b/plasma/workspace/dataengines/notifications/Messages.sh new file mode 100644 index 0000000000..5d52b61d8d --- /dev/null +++ b/plasma/workspace/dataengines/notifications/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_notifications.pot diff --git a/plasma/workspace/dataengines/notifications/notificationaction.cpp b/plasma/workspace/dataengines/notifications/notificationaction.cpp new file mode 100644 index 0000000000..5cdec2af90 --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationaction.cpp @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "notificationaction.h" + +#include "server.h" + +#include + +#include "debug.h" + +using namespace NotificationManager; + +void NotificationAction::start() +{ + qCDebug(NOTIFICATIONS) << "Trying to perform the action " << operationName() << " on " << destination(); + qCDebug(NOTIFICATIONS) << "actionId: " << parameters()["actionId"].toString(); + qCDebug(NOTIFICATIONS) << "params: " << parameters(); + + if (!m_engine) { + setErrorText(i18n("The notification dataEngine is not set.")); + setError(-1); + emitResult(); + return; + } + + const QStringList dest = destination().split(' '); + + uint id = 0; + if (dest.count() > 1 && !dest[1].toInt()) { + setErrorText(i18n("Invalid destination: %1", destination())); + setError(-2); + emitResult(); + return; + } else if (dest.count() > 1) { + id = dest[1].toUInt(); + } + + if (operationName() == QLatin1String("invokeAction")) { + qCDebug(NOTIFICATIONS) << "invoking action on " << id; + Server::self().invokeAction(id, parameters()[QStringLiteral("actionId")].toString(), {}, Notifications::None); + } else if (operationName() == QLatin1String("userClosed")) { + // userClosedNotification deletes the job, so we have to invoke it queued, in this case emitResult() can be called + m_engine->metaObject()->invokeMethod(m_engine, "removeNotification", Qt::QueuedConnection, Q_ARG(uint, id), Q_ARG(uint, 2)); + } else if (operationName() == QLatin1String("expireNotification")) { + // expireNotification deletes the job, so we have to invoke it queued, in this case emitResult() can be called + m_engine->metaObject()->invokeMethod(m_engine, "removeNotification", Qt::QueuedConnection, Q_ARG(uint, id), Q_ARG(uint, 1)); + } else if (operationName() == QLatin1String("createNotification")) { + int expireTimeout = parameters().value(QStringLiteral("expireTimeout")).toInt(); + bool isPersistent = parameters().value(QStringLiteral("isPersistent")).toBool(); + + QVariantMap hints; + if (parameters().value(QStringLiteral("skipGrouping")).toBool()) { + hints.insert(QStringLiteral("x-kde-skipGrouping"), true); + } + + int rv = m_engine->createNotification(parameters().value(QStringLiteral("appName")).toString(), + parameters().value(QStringLiteral("appIcon")).toString(), + parameters().value(QStringLiteral("summary")).toString(), + parameters().value(QStringLiteral("body")).toString(), + isPersistent ? 0 : expireTimeout, + parameters().value(QStringLiteral("actions")).toStringList(), + hints); + setResult(rv); + return; + } else if (operationName() == QLatin1String("configureNotification")) { + m_engine->configureNotification(parameters()[QStringLiteral("appRealName")].toString(), parameters()[QStringLiteral("eventId")].toString()); + } else if (operationName() == QLatin1String("inhibit")) { + const QString hint = parameters()[QStringLiteral("hint")].toString(); + const QString value = parameters()[QStringLiteral("value")].toString(); + auto t = m_engine->createInhibition(hint, value); + setResult(QVariant::fromValue(t)); + return; + } + + emitResult(); +} diff --git a/plasma/workspace/dataengines/notifications/notificationaction.h b/plasma/workspace/dataengines/notifications/notificationaction.h new file mode 100644 index 0000000000..e1c1466e7e --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationaction.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "notificationsengine.h" + +#include + +class NotificationAction : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + NotificationAction(NotificationsEngine *engine, + const QString &destination, + const QString &operation, + QMap ¶meters, + QObject *parent = nullptr) + : ServiceJob(destination, operation, parameters, parent) + , m_engine(engine) + { + } + + void start() override; + +private: + NotificationsEngine *m_engine; +}; diff --git a/plasma/workspace/dataengines/notifications/notifications.operations b/plasma/workspace/dataengines/notifications/notifications.operations new file mode 100644 index 0000000000..63e1e6705d --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notifications.operations @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/notifications/notificationsengine.cpp b/plasma/workspace/dataengines/notifications/notificationsengine.cpp new file mode 100644 index 0000000000..371a14008f --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationsengine.cpp @@ -0,0 +1,238 @@ +/* + SPDX-FileCopyrightText: 2008 Dmitry Suzdalev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationsengine.h" +#include "notificationservice.h" + +#include "notification.h" +#include "server.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "debug.h" + +using namespace NotificationManager; + +NotificationsEngine::NotificationsEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + init(); +} + +NotificationsEngine::~NotificationsEngine() +{ +} + +void NotificationsEngine::init() +{ + connect(&Server::self(), &Server::notificationAdded, this, [this](const Notification ¬ification) { + notificationAdded(notification); + }); + + connect(&Server::self(), &Server::notificationReplaced, this, [this](uint replacedId, const Notification ¬ification) { + // Notification will already have the correct identical ID + Q_UNUSED(replacedId); + notificationAdded(notification); + }); + + connect(&Server::self(), &Server::notificationRemoved, this, [this](uint id, Server::CloseReason reason) { + Q_UNUSED(reason); + const QString source = QStringLiteral("notification %1").arg(id); + // if we don't have that notification in our local list, + // it has already been closed so don't notify a second time + if (m_activeNotifications.remove(source) > 0) { + removeSource(source); + } + }); + + Server::self().init(); +} + +void NotificationsEngine::notificationAdded(const Notification ¬ification) +{ + const QString app_name = notification.applicationName(); + const QString appRealName = notification.notifyRcName(); + const QString eventId = notification.eventId(); // FIXME = hints[QStringLiteral("x-kde-eventId")].toString(); + const QStringList urls = QUrl::toStringList(notification.urls()); + const QString desktopEntry = notification.desktopEntry(); + const QString summary = notification.summary(); + + QString bodyFinal = notification.body(); // is already sanitized by NotificationManager + QString summaryFinal = notification.summary(); + int timeout = notification.timeout(); + + if (bodyFinal.isEmpty()) { + // some ridiculous apps will send just a title (#372112), in that case, treat it as though there's only a body + bodyFinal = summary; + summaryFinal = app_name; + } + + uint id = notification.id(); // replaces_id ? replaces_id : m_nextId++; + + QString appname_str = app_name; + if (appname_str.isEmpty()) { + appname_str = i18n("Unknown Application"); + } + + bool isPersistent = (timeout == 0); + + const int AVERAGE_WORD_LENGTH = 6; + const int WORD_PER_MINUTE = 250; + int count = notification.summary().length() + notification.body().length() - strlen(""); + + // -1 is "server default", 0 is persistent with "server default" display time, + // anything more should honor the setting + if (timeout <= 0) { + timeout = 60000 * count / AVERAGE_WORD_LENGTH / WORD_PER_MINUTE; + + // Add two seconds for the user to notice the notification, and ensure + // it last at least five seconds, otherwise all the user see is a + // flash + timeout = 2000 + qMax(timeout, 3000); + } + + const QString source = QStringLiteral("notification %1").arg(id); + + Plasma::DataEngine::Data notificationData; + notificationData.insert(QStringLiteral("id"), QString::number(id)); + notificationData.insert(QStringLiteral("eventId"), eventId); + notificationData.insert(QStringLiteral("appName"), notification.applicationName()); + // TODO should be proper passed in icon? + notificationData.insert(QStringLiteral("appIcon"), notification.applicationIconName()); + notificationData.insert(QStringLiteral("summary"), summaryFinal); + notificationData.insert(QStringLiteral("body"), bodyFinal); + + QStringList actions; + for (int i = 0; i < notification.actionNames().count(); ++i) { + actions << notification.actionNames().at(i) << notification.actionLabels().at(i); + } + // NotificationManager hides the configure and default stuff from us but we need to re-add them + // to the actions list for compatibility + if (!notification.configureActionLabel().isEmpty()) { + actions << QStringLiteral("settings") << notification.configureActionLabel(); + } + if (notification.hasDefaultAction()) { + actions << QStringLiteral("default") << QString(); + } + + notificationData.insert(QStringLiteral("actions"), actions); + notificationData.insert(QStringLiteral("isPersistent"), isPersistent); + notificationData.insert(QStringLiteral("expireTimeout"), timeout); + + notificationData.insert(QStringLiteral("desktopEntry"), desktopEntry); + + KService::Ptr service = KService::serviceByStorageId(desktopEntry); + if (service) { + notificationData.insert(QStringLiteral("appServiceName"), service->name()); + notificationData.insert(QStringLiteral("appServiceIcon"), service->icon()); + } + + notificationData.insert(QStringLiteral("appRealName"), appRealName); + // NotificationManager configurable is anything that has a notifyrc or desktop entry + // but the old stuff assumes only stuff with notifyrc to be configurable + notificationData.insert(QStringLiteral("configurable"), !notification.notifyRcName().isEmpty()); + + QImage image = notification.image(); + notificationData.insert(QStringLiteral("image"), image.isNull() ? QVariant() : image); + + int urgency = -1; + switch (notification.urgency()) { + case Notifications::LowUrgency: + urgency = 0; + break; + case Notifications::NormalUrgency: + urgency = 1; + break; + case Notifications::CriticalUrgency: + urgency = 2; + break; + } + + if (urgency > -1) { + notificationData.insert(QStringLiteral("urgency"), urgency); + } + + notificationData.insert(QStringLiteral("urls"), urls); + + setData(source, notificationData); + + m_activeNotifications.insert(source, notification.applicationName() + notification.summary()); +} + +void NotificationsEngine::removeNotification(uint id, uint closeReason) +{ + const QString source = QStringLiteral("notification %1").arg(id); + // if we don't have that notification in our local list, + // it has already been closed so don't notify a second time + if (m_activeNotifications.remove(source) > 0) { + removeSource(source); + Server::self().closeNotification(id, static_cast(closeReason)); + } +} + +Plasma::Service *NotificationsEngine::serviceForSource(const QString &source) +{ + return new NotificationService(this, source); +} + +int NotificationsEngine::createNotification(const QString &appName, + const QString &appIcon, + const QString &summary, + const QString &body, + int timeout, + const QStringList &actions, + const QVariantMap &hints) +{ + Notification notification; + notification.setApplicationName(appName); + notification.setApplicationIconName(appIcon); + notification.setSummary(summary); + notification.setBody(body); // sanitizes + notification.setActions(actions); + notification.setTimeout(timeout); + notification.processHints(hints); + Server::self().add(notification); + return 0; +} + +void NotificationsEngine::configureNotification(const QString &appName, const QString &eventId) +{ + KNotifyConfigWidget *widget = KNotifyConfigWidget::configure(nullptr, appName); + if (!eventId.isEmpty()) { + widget->selectEvent(eventId); + } +} + +QSharedPointer NotificationsEngine::createInhibition(const QString &hint, const QString &value) +{ + auto ni = new NotificationInhibiton; + ni->hint = hint; + ni->value = value; + + QPointer guard(this); + QSharedPointer rc(ni, [this, guard](NotificationInhibiton *ni) { + if (guard) { + m_inhibitions.removeOne(ni); + } + delete ni; + }); + m_inhibitions.append(ni); + return rc; +} + +K_PLUGIN_CLASS_WITH_JSON(NotificationsEngine, "plasma-dataengine-notifications.json") + +#include "notificationsengine.moc" diff --git a/plasma/workspace/dataengines/notifications/notificationsengine.h b/plasma/workspace/dataengines/notifications/notificationsengine.h new file mode 100644 index 0000000000..7d1d453d9c --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationsengine.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2008 Dmitry Suzdalev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace NotificationManager +{ +class Notification; +} + +struct NotificationInhibiton { + QString hint; + QString value; +}; + +typedef QSharedPointer NotificationInhibitonPtr; + +/** + * Engine which provides data sources for notifications. + * Each notification is represented by one source. + */ +class NotificationsEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + NotificationsEngine(QObject *parent, const QVariantList &args); + ~NotificationsEngine() override; + + virtual void init(); + + /** + * This function implements part of Notifications DBus interface. + * Once called, will add notification source to the engine + */ + uint Notify(const QString &app_name, + uint replaces_id, + const QString &app_icon, + const QString &summary, + const QString &body, + const QStringList &actions, + const QVariantMap &hints, + int timeout); + + Plasma::Service *serviceForSource(const QString &source) override; + + int createNotification(const QString &appName, + const QString &appIcon, + const QString &summary, + const QString &body, + int timeout, + const QStringList &actions, + const QVariantMap &hints); + + void configureNotification(const QString &appName, const QString &eventId = QString()); + + /* + * Block all notification where a given notification hint matches a given value. + * + * Inhibition is dropped when dereferenced. + */ + NotificationInhibitonPtr createInhibition(const QString &hint, const QString &value); + +public Q_SLOTS: + void removeNotification(uint id, uint closeReason); + +private: + void notificationAdded(const NotificationManager::Notification ¬ification); + + QHash m_activeNotifications; + + QList m_inhibitions; + + friend class NotificationAction; +}; + +Q_DECLARE_METATYPE(NotificationInhibitonPtr); diff --git a/plasma/workspace/dataengines/notifications/notificationservice.cpp b/plasma/workspace/dataengines/notifications/notificationservice.cpp new file mode 100644 index 0000000000..ab2addfa1b --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationservice.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "notificationservice.h" +#include "notificationaction.h" +#include "notificationsengine.h" + +NotificationService::NotificationService(NotificationsEngine *parent, const QString &source) + : Plasma::Service(parent) + , m_notificationEngine(parent) +{ + setName(QStringLiteral("notifications")); + setDestination(source); +} + +Plasma::ServiceJob *NotificationService::createJob(const QString &operation, QMap ¶meters) +{ + return new NotificationAction(m_notificationEngine, destination(), operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/notifications/notificationservice.h b/plasma/workspace/dataengines/notifications/notificationservice.h new file mode 100644 index 0000000000..5449e36f58 --- /dev/null +++ b/plasma/workspace/dataengines/notifications/notificationservice.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2008 Rob Scheepmaker + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +class NotificationsEngine; + +class NotificationService : public Plasma::Service +{ + Q_OBJECT + +public: + NotificationService(NotificationsEngine *parent, const QString &source); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + NotificationsEngine *m_notificationEngine; +}; diff --git a/plasma/workspace/dataengines/notifications/plasma-dataengine-notifications.json b/plasma/workspace/dataengines/notifications/plasma-dataengine-notifications.json new file mode 100644 index 0000000000..cfa122825d --- /dev/null +++ b/plasma/workspace/dataengines/notifications/plasma-dataengine-notifications.json @@ -0,0 +1,123 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Passive visual notifications for the user.", + "Description[ar]": "إخطارات الجهاز المرئية للمستخدم.", + "Description[az]": "Bu istifadəçi üçün qurğuların passiv vizual bildirişləri.", + "Description[ca]": "Notificacions visuals passives per a l'usuari.", + "Description[cs]": "Pasivní vizuální upozornění pro uživatele.", + "Description[de]": "Passive sichtbare Benachrichtigungen für den Anwender.", + "Description[en_GB]": "Passive visual notifications for the user.", + "Description[es]": "Notificaciones visuales pasivas para el usuario.", + "Description[eu]": "Erabiltzailearentzako ikusizko jakinarazpen pasiboak.", + "Description[fi]": "Näkyvä passiivinen ilmoitus käyttäjälle.", + "Description[fr]": "Notifications visuelles passives pour l'utilisateur.", + "Description[hu]": "Passzív értesítő üzeneteket tud küldeni a felhasználónak.", + "Description[ia]": "Notificationes visual passive pro le usator.", + "Description[it]": "Notifiche visuali passive per l'utente.", + "Description[ko]": "사용자에게 보이는 수동적인 알림입니다.", + "Description[lt]": "Pasyvūs vaizdiniai pranešimai naudotojui.", + "Description[nl]": "Passieve visuele meldingen voor de gebruiker.", + "Description[nn]": "Passive visuelle varsel for brukaren.", + "Description[pa]": "ਵਰਤੋਂਕਾਰ ਲਈ ਪੈਸਿਵ ਦਿੱਖ ਨੋਟੀਫਿਕੇਸ਼ਨ।", + "Description[pl]": "Bierne i graficznie powiadamia użytkownika.", + "Description[pt_BR]": "Notificações visuais passivas para o usuário.", + "Description[ro]": "Notificări vizuale pasive pentru utilizator.", + "Description[ru]": "Пассивные визуальные уведомления для пользователя.", + "Description[sk]": "Pasívne vizuálne upozornenia pre užívateľa.", + "Description[sl]": "Pasivna vidna obvestila za uporabnika.", + "Description[sv]": "Passiva visuella underrättelser för användaren.", + "Description[ta]": "பயனருக்கான பார்வைமுறை அறிவிப்புகள்", + "Description[tr]": "Kullanıcı için pasif görsel bildirimler.", + "Description[uk]": "Пасивні візуальні сповіщення для користувача.", + "Description[vi]": "Thông báo trực quan thụ động dành cho người dùng.", + "Description[x-test]": "xxPassive visual notifications for the user.xx", + "Description[zh_CN]": "为用户提供被动出现的视觉通知。", + "Icon": "preferences-desktop-notification-bell", + "Id": "notifications", + "Name": "Application Notifications", + "Name[ar]": "إخطارات التطبيقات", + "Name[az]": "Tətbiq bildirişləri", + "Name[be@latin]": "Infarmavańni aplikacyj", + "Name[bg]": "Програмни съобщения", + "Name[bn]": "অ্যাপলিকেশন বিজ্ঞপ্তি", + "Name[bn_IN]": "অ্যাপ্লিকেশনের সূচনাবার্তা", + "Name[bs]": "Obavještenja programa", + "Name[ca@valencia]": "Notificacions de les aplicacions", + "Name[ca]": "Notificacions de les aplicacions", + "Name[cs]": "Upozornění aplikací", + "Name[csb]": "Dôwanié wiédzë ò aplikacëjach", + "Name[da]": "Programbekendtgørelser", + "Name[de]": "Anwendungs-Benachrichtigungen", + "Name[el]": "Ειδοποιήσεις εφαρμογών", + "Name[en_GB]": "Application Notifications", + "Name[eo]": "Aplikaĵaj Atentigoj", + "Name[es]": "Notificaciones de aplicaciones", + "Name[et]": "Rakenduste märguanded", + "Name[eu]": "Aplikazioen jakinarazpenak", + "Name[fi]": "Sovellusilmoitukset", + "Name[fr]": "Notifications des applications", + "Name[fy]": "Applikaasje ntifikaasjes", + "Name[ga]": "Fógairtí Feidhmchláir", + "Name[gl]": "Notificacións das aplicacións", + "Name[gu]": "કાર્યક્રમ નોંધણીઓ", + "Name[he]": "הודעות יישומים", + "Name[hi]": "अनुप्रयोग सूचनाएँ", + "Name[hne]": "अनुपरयोग सूचना", + "Name[hr]": "Obavijesti aplikacija", + "Name[hsb]": "Zdźělenki aplikacije", + "Name[hu]": "Értesítő üzenetek", + "Name[ia]": "Notificationes de application", + "Name[id]": "Notifikasi Aplikasi", + "Name[is]": "Tilkynningar forrita", + "Name[it]": "Notifiche delle applicazioni", + "Name[ja]": "アプリケーションの通知", + "Name[kk]": "Қолданба құлақтандырулары", + "Name[km]": "ការ​ជូនដំណឹង​កម្មវិធី​", + "Name[kn]": "ಅನ್ವಯ ಸೂಚನೆಗಳು", + "Name[ko]": "프로그램 알림", + "Name[ku]": "Hişyariyên Sepanê", + "Name[lt]": "Programų pranešimai", + "Name[lv]": "Programmu paziņojumi", + "Name[mk]": "Известувања за апликации", + "Name[ml]": "പ്രയോഗ അറിയിപ്പുകള്‍", + "Name[mr]": "अनुप्रयोग सूचना", + "Name[nb]": "Programvarslinger", + "Name[nds]": "Programm-Bescheden", + "Name[nl]": "Programmameldingen", + "Name[nn]": "Programvarsel", + "Name[or]": "ପ୍ରୟୋଗ ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ", + "Name[pa]": "ਐਪਲੀਕੇਸ਼ਨ ਨੋਟੀਫਿਕੇਸ਼ਨ", + "Name[pl]": "Powiadomienia programów", + "Name[pt]": "Notificações das Aplicações", + "Name[pt_BR]": "Notificações do aplicativo", + "Name[ro]": "Notificări aplicații", + "Name[ru]": "Уведомления приложений", + "Name[si]": "යෙදුම් දැනුම් දීම්", + "Name[sk]": "Upozornenia aplikácií", + "Name[sl]": "Obvestila programov", + "Name[sr@ijekavian]": "обавјештења програма", + "Name[sr@ijekavianlatin]": "obavještenja programa", + "Name[sr@latin]": "obaveštenja programa", + "Name[sr]": "обавештења програма", + "Name[sv]": "Programunderrättelser", + "Name[ta]": "செயலி அறிவிப்புகள்", + "Name[tg]": "Огоҳиҳои барнома", + "Name[th]": "การแจ้งให้ทราบของโปรแกรม", + "Name[tr]": "Uygulama Bildirimleri", + "Name[ug]": "پروگرامما ئۇقتۇرۇشى", + "Name[uk]": "Сповіщення програм", + "Name[vi]": "Thông báo của ứng dụng", + "Name[wa]": "Notifiaedjes do programe", + "Name[x-test]": "xxApplication Notificationsxx", + "Name[zh_CN]": "应用程序通知", + "Name[zh_TW]": "應用程式通知", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/packagekit/CMakeLists.txt b/plasma/workspace/dataengines/packagekit/CMakeLists.txt new file mode 100644 index 0000000000..5d6cf671e0 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/CMakeLists.txt @@ -0,0 +1,13 @@ +set(packagekit_engine_SRCS + packagekitjob.cpp + packagekitengine.cpp + packagekitservice.cpp +) + +kcoreaddons_add_plugin(plasma_engine_packagekit SOURCES ${packagekit_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_packagekit KF5::Plasma KF5::CoreAddons Qt::DBus ) + +install(FILES packagekit.operations + DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) + diff --git a/plasma/workspace/dataengines/packagekit/packagekit.operations b/plasma/workspace/dataengines/packagekit/packagekit.operations new file mode 100644 index 0000000000..b1d84165e8 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekit.operations @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/plasma/workspace/dataengines/packagekit/packagekitengine.cpp b/plasma/workspace/dataengines/packagekit/packagekitengine.cpp new file mode 100644 index 0000000000..8f9053b492 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitengine.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "packagekitengine.h" +#include "packagekitservice.h" + +#include +#include + +PackagekitEngine::PackagekitEngine(QObject *parent, const QVariantList &args) + : DataEngine(parent, args) + , m_pk_available(false) +{ +} + +void PackagekitEngine::init() +{ + QDBusMessage message; + message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"), + QStringLiteral("/org/freedesktop/DBus"), + QStringLiteral("org.freedesktop.DBus"), + QStringLiteral("ListActivatableNames")); + + QDBusMessage reply = QDBusConnection::sessionBus().call(message); + if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() == 1) { + QStringList list = reply.arguments().first().toStringList(); + if (list.contains(QLatin1String("org.freedesktop.PackageKit"))) { + m_pk_available = true; + } + } + + setData(QStringLiteral("Status"), QStringLiteral("available"), m_pk_available); +} + +Plasma::Service *PackagekitEngine::serviceForSource(const QString &source) +{ + if (m_pk_available) { + return new PackagekitService(this); + } + + // if packagekit not available, return null service + return Plasma::DataEngine::serviceForSource(source); +} + +K_PLUGIN_CLASS_WITH_JSON(PackagekitEngine, "plasma-dataengine-packagekit.json") + +#include "packagekitengine.moc" diff --git a/plasma/workspace/dataengines/packagekit/packagekitengine.h b/plasma/workspace/dataengines/packagekit/packagekitengine.h new file mode 100644 index 0000000000..e23084f5e0 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitengine.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class PackagekitEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + PackagekitEngine(QObject *parent, const QVariantList &args); + void init(); + +protected: + Plasma::Service *serviceForSource(const QString &source) override; + +private: + bool m_pk_available; +}; diff --git a/plasma/workspace/dataengines/packagekit/packagekitjob.cpp b/plasma/workspace/dataengines/packagekit/packagekitjob.cpp new file mode 100644 index 0000000000..7fb6dffae0 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitjob.cpp @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "packagekitjob.h" +#include +#include + +PackagekitJob::PackagekitJob(const QString &destination, const QString &operation, const QMap ¶meters, QObject *parent) + : ServiceJob(destination, operation, parameters, parent) +{ +} + +PackagekitJob::~PackagekitJob() +{ +} + +void PackagekitJob::start() +{ + const QString operation = operationName(); + + if (operation == QLatin1String("uninstallApplication")) { + QStringList files(parameters()[QStringLiteral("Url")].toString()); + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PackageKit"), + QStringLiteral("/org/freedesktop/PackageKit"), + QStringLiteral("org.freedesktop.PackageKit.Modify"), + QStringLiteral("RemovePackageByFiles")); + message << (uint)0; + message << files; + message << QString(); + + QDBusConnection::sessionBus().call(message, QDBus::NoBlock); + setResult(true); + return; + } + + setResult(false); +} diff --git a/plasma/workspace/dataengines/packagekit/packagekitjob.h b/plasma/workspace/dataengines/packagekit/packagekitjob.h new file mode 100644 index 0000000000..d2422a237e --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitjob.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class PackagekitJob : public Plasma::ServiceJob +{ + Q_OBJECT +public: + PackagekitJob(const QString &destination, const QString &operation, const QMap ¶meters, QObject *parent = nullptr); + ~PackagekitJob() override; + +protected: + void start() override; + +private: +}; diff --git a/plasma/workspace/dataengines/packagekit/packagekitservice.cpp b/plasma/workspace/dataengines/packagekit/packagekitservice.cpp new file mode 100644 index 0000000000..5cb4df8f4d --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitservice.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "packagekitservice.h" +#include "packagekitjob.h" + +PackagekitService::PackagekitService(QObject *parent) + : Plasma::Service(parent) +{ + setName(QStringLiteral("packagekit")); +} + +Plasma::ServiceJob *PackagekitService::createJob(const QString &operation, QMap ¶meters) +{ + return new PackagekitJob(destination(), operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/packagekit/packagekitservice.h b/plasma/workspace/dataengines/packagekit/packagekitservice.h new file mode 100644 index 0000000000..3712568492 --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/packagekitservice.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2012 Gregor Taetzner + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +class PackagekitService : public Plasma::Service +{ + Q_OBJECT +public: + explicit PackagekitService(QObject *parent = nullptr); + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; +}; diff --git a/plasma/workspace/dataengines/packagekit/plasma-dataengine-packagekit.json b/plasma/workspace/dataengines/packagekit/plasma-dataengine-packagekit.json new file mode 100644 index 0000000000..e0b2c8fdab --- /dev/null +++ b/plasma/workspace/dataengines/packagekit/plasma-dataengine-packagekit.json @@ -0,0 +1,130 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "plasma-devel@kde.org", + "Name": "Gregor Taetzner", + "Name[ar]": "Gregor Taetzner", + "Name[az]": "Gregor Taetzner", + "Name[ca]": "Gregor Taetzner", + "Name[cs]": "Gregor Taetzner", + "Name[de]": "Gregor Taetzner", + "Name[en_GB]": "Gregor Taetzner", + "Name[es]": "Gregor Taetzner", + "Name[eu]": "Gregor Taetzner", + "Name[fi]": "Gregor Taetzner", + "Name[fr]": "Gregor Taetzner", + "Name[hu]": "Gregor Taetzner", + "Name[ia]": "Gregor Taetzner", + "Name[it]": "Gregor Taetzner", + "Name[ko]": "Gregor Taetzner", + "Name[lt]": "Gregor Taetzner", + "Name[nl]": "Gregor Taetzner", + "Name[nn]": "Gregor Taetzner", + "Name[pl]": "Gregor Taetzner", + "Name[pt_BR]": "Gregor Taetzner", + "Name[ro]": "Gregor Taetzner", + "Name[ru]": "Gregor Taetzner", + "Name[sk]": "Gregor Taetzner", + "Name[sl]": "Gregor Taetzner", + "Name[sv]": "Gregor Taetzner", + "Name[tr]": "Gregor Taetzner", + "Name[uk]": "Gregor Taetzner", + "Name[vi]": "Gregor Taetzner", + "Name[x-test]": "xxGregor Taetznerxx", + "Name[zh_CN]": "Gregor Taetzner" + } + ], + "Category": "", + "Description": "PackageKit Data Engine", + "Description[ar]": "محرّك بيانات عُدّة الحزم", + "Description[az]": "PackageKit verilənlər mənbəyi", + "Description[ca]": "Motor de dades del PackageKit", + "Description[cs]": "Datový nástroj PackageKit", + "Description[de]": "Daten-Treiber für PackageKit", + "Description[en_GB]": "PackageKit Data Engine", + "Description[es]": "Motor de datos de PackageKit", + "Description[eu]": "PackageKit datuen motorra", + "Description[fi]": "PackageKit-tietomoottori", + "Description[fr]": "Moteur de données de « PackageKit ».", + "Description[hu]": "PackageKit adatmotor", + "Description[ia]": "Motor de Datos (Data Engine) de PackageKit", + "Description[it]": "Motore di dati PackageKit", + "Description[ko]": "PackageKit 데이터 엔진", + "Description[lt]": "PackageKit duomenų variklis", + "Description[nl]": "Pakketkit Gegevensengine", + "Description[nn]": "PackageKit-datamotor", + "Description[pa]": "PackageKit ਡਾਟਾ ਇੰਜਣ", + "Description[pl]": "Silnik danych PackageKit", + "Description[pt_BR]": "Mecanismo de dados do PackageKit", + "Description[ro]": "Motor de date PackageKit", + "Description[ru]": "Источник данных PackageKit", + "Description[sk]": "Dátový engine PackageKit", + "Description[sl]": "Podatkovni pogon PackageKit", + "Description[sv]": "PackageKit-datagränssnitt", + "Description[ta]": "PackageKit தரவு நிரல்", + "Description[tr]": "PackageKit Veri Motoru", + "Description[uk]": "Рушій даних PackageKit", + "Description[vi]": "Dụng cụ dữ liệu PackageKit", + "Description[x-test]": "xxPackageKit Data Enginexx", + "Description[zh_CN]": "PackageKit 数据引擎", + "Icon": "package-x-generic", + "Id": "packagekit", + "License": "GPL", + "Name": "PackageKit", + "Name[ar]": "عُدّة الحزم", + "Name[ast]": "PackageKit", + "Name[az]": "PackageKit", + "Name[bs]": "Paket opreme", + "Name[ca@valencia]": "PackageKit", + "Name[ca]": "PackageKit", + "Name[cs]": "PackageKit", + "Name[da]": "PackageKit", + "Name[de]": "PackageKit", + "Name[el]": "PackageKit", + "Name[en_GB]": "PackageKit", + "Name[es]": "PackageKit", + "Name[et]": "PackageKit", + "Name[eu]": "PackageKit", + "Name[fi]": "PackageKit", + "Name[fr]": "PackageKit", + "Name[gl]": "PackageKit", + "Name[he]": "ערכת חבילות", + "Name[hi]": "पैकेजकिट", + "Name[hu]": "PackageKit", + "Name[ia]": "PackageKit (equipamento de pacchetto)", + "Name[id]": "PackageKit", + "Name[is]": "PackageKit", + "Name[it]": "PackageKit", + "Name[ja]": "PackageKit", + "Name[kk]": "PackageKit", + "Name[ko]": "PackageKit", + "Name[lt]": "PackageKit", + "Name[ml]": "പാക്കേജ്കിറ്റ്", + "Name[nb]": "PackageKit", + "Name[nds]": "PackageKit", + "Name[nl]": "PackageKit", + "Name[nn]": "PackageKit", + "Name[pa]": "ਪੈਕੇਜਕਿਟ", + "Name[pl]": "PackageKit", + "Name[pt]": "PackageKit", + "Name[pt_BR]": "PackageKit", + "Name[ro]": "PackageKit", + "Name[ru]": "PackageKit", + "Name[sk]": "PackageKit", + "Name[sl]": "PackageKit", + "Name[sr@ijekavian]": "Пакиџ‑кит", + "Name[sr@ijekavianlatin]": "PackageKit", + "Name[sr@latin]": "PackageKit", + "Name[sr]": "Пакиџ‑кит", + "Name[sv]": "PackageKit", + "Name[ta]": "PackageKit", + "Name[tr]": "PackageKit", + "Name[uk]": "PackageKit", + "Name[vi]": "PackageKit", + "Name[x-test]": "xxPackageKitxx", + "Name[zh_CN]": "PackageKit", + "Name[zh_TW]": "PackageKit", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/places/CMakeLists.txt b/plasma/workspace/dataengines/places/CMakeLists.txt new file mode 100644 index 0000000000..85f831c433 --- /dev/null +++ b/plasma/workspace/dataengines/places/CMakeLists.txt @@ -0,0 +1,22 @@ +set(places_engine_SRCS + placesengine.cpp + placeservice.cpp + placesproxymodel.cpp + setupdevicejob.cpp + modeljob.h +) + +set(CMAKE_AUTOMOC TRUE) + +kcoreaddons_add_plugin(plasma_engine_places SOURCES ${places_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_places + KF5::Plasma + KF5::KIOCore + KF5::KIOFileWidgets + KF5::Solid +) + +install(FILES org.kde.places.operations + DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services ) + diff --git a/plasma/workspace/dataengines/places/TODO b/plasma/workspace/dataengines/places/TODO new file mode 100644 index 0000000000..5eeb94349c --- /dev/null +++ b/plasma/workspace/dataengines/places/TODO @@ -0,0 +1,2 @@ +* periodic refresh? KFilePlacesModel doesn't broadcast + device setups etc. between programs diff --git a/plasma/workspace/dataengines/places/jobs.h b/plasma/workspace/dataengines/places/jobs.h new file mode 100644 index 0000000000..94ac932073 --- /dev/null +++ b/plasma/workspace/dataengines/places/jobs.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +#include + +#include "modeljob.h" + +class AddEditPlaceJob : public ModelJob +{ +public: + AddEditPlaceJob(KFilePlacesModel *model, QModelIndex index, const QVariantMap ¶meters, QObject *parent = nullptr) + : ModelJob(parent, model, index, (index.isValid() ? "Edit" : "Add"), parameters) + , m_text(parameters[QStringLiteral("Name")].toString()) + , m_url(parameters[QStringLiteral("Url")].toUrl()) + , m_icon(parameters[QStringLiteral("Icon")].toString()) + { + } + + void start() override + { + if (m_index.isValid()) { + m_model->editPlace(m_index, m_text, m_url, m_icon); + } else { + m_model->addPlace(m_text, m_url, m_icon); + } + } + +private: + QString m_text; + QUrl m_url; + QString m_icon; +}; + +class RemovePlaceJob : public ModelJob +{ +public: + RemovePlaceJob(KFilePlacesModel *model, const QModelIndex &index, QObject *parent) + : ModelJob(parent, model, index, QStringLiteral("Remove")) + { + } + + void start() override + { + m_model->removePlace(m_index); + } +}; + +class ShowPlaceJob : public ModelJob +{ +public: + ShowPlaceJob(KFilePlacesModel *model, const QModelIndex &index, bool show = true, QObject *parent = nullptr) + : ModelJob(parent, model, index, (show ? "Show" : "Hide")) + , m_show(show) + { + } + + void start() override + { + m_model->setPlaceHidden(m_index, m_show); + } + +private: + bool m_show; +}; + +class TeardownDeviceJob : public ModelJob +{ +public: + TeardownDeviceJob(KFilePlacesModel *model, const QModelIndex &index, QObject *parent = nullptr) + : ModelJob(parent, model, index, QStringLiteral("Teardown Device")) + { + } + + void start() override + { + m_model->requestTeardown(m_index); + } +}; + +#include "setupdevicejob.h" diff --git a/plasma/workspace/dataengines/places/modeljob.h b/plasma/workspace/dataengines/places/modeljob.h new file mode 100644 index 0000000000..037936dbc6 --- /dev/null +++ b/plasma/workspace/dataengines/places/modeljob.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +#include +#include + +class ModelJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + ModelJob(QObject *parent, KFilePlacesModel *model, const QModelIndex &index, const QString &operation, const QVariantMap ¶meters = QVariantMap()) + : ServiceJob(QString::number(index.row()), operation, parameters, parent) + , m_model(model) + , m_index(index) + { + } + +protected: + KFilePlacesModel *m_model; + QModelIndex m_index; +}; diff --git a/plasma/workspace/dataengines/places/org.kde.places.operations b/plasma/workspace/dataengines/places/org.kde.places.operations new file mode 100644 index 0000000000..ee03f88f84 --- /dev/null +++ b/plasma/workspace/dataengines/places/org.kde.places.operations @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/places/placesengine.cpp b/plasma/workspace/dataengines/places/placesengine.cpp new file mode 100644 index 0000000000..12b46ee849 --- /dev/null +++ b/plasma/workspace/dataengines/places/placesengine.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "placesengine.h" + +#include +#include +#include + +#include "placeservice.h" +#include "placesproxymodel.h" + +PlacesEngine::PlacesEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + m_placesModel = new KFilePlacesModel(this); + m_proxyModel = new PlacesProxyModel(this, m_placesModel); + setModel(QStringLiteral("places"), m_proxyModel); +} + +PlacesEngine::~PlacesEngine() +{ +} + +Plasma::Service *PlacesEngine::serviceForSource(const QString &source) +{ + if (source == QLatin1String("places")) { + return new PlaceService(this, m_placesModel); + } + + return DataEngine::serviceForSource(source); +} + +K_PLUGIN_CLASS_WITH_JSON(PlacesEngine, "plasma-dataengine-places.json") + +#include "placesengine.moc" diff --git a/plasma/workspace/dataengines/places/placesengine.h b/plasma/workspace/dataengines/places/placesengine.h new file mode 100644 index 0000000000..d209ce2451 --- /dev/null +++ b/plasma/workspace/dataengines/places/placesengine.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include + +class PlacesProxyModel; + +class PlacesEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + PlacesEngine(QObject *parent, const QVariantList &args); + ~PlacesEngine() override; + + Plasma::Service *serviceForSource(const QString &source) override; + +private: + KFilePlacesModel *m_placesModel; + PlacesProxyModel *m_proxyModel; +}; diff --git a/plasma/workspace/dataengines/places/placeservice.cpp b/plasma/workspace/dataengines/places/placeservice.cpp new file mode 100644 index 0000000000..577ff2a653 --- /dev/null +++ b/plasma/workspace/dataengines/places/placeservice.cpp @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "placeservice.h" +#include "jobs.h" + +#include + +PlaceService::PlaceService(QObject *parent, KFilePlacesModel *model) + : Plasma::Service(parent) + , m_model(model) +{ + setName(QStringLiteral("org.kde.places")); + + setDestination(QStringLiteral("places")); + qDebug() << "Created a place service for" << destination(); +} + +Plasma::ServiceJob *PlaceService::createJob(const QString &operation, QMap ¶meters) +{ + QModelIndex index = m_model->index(parameters.value(QStringLiteral("placeIndex")).toInt(), 0); + + if (!index.isValid()) { + return nullptr; + } + + qDebug() << "Job" << operation << "with arguments" << parameters << "requested"; + if (operation == QLatin1String("Add")) { + return new AddEditPlaceJob(m_model, index, parameters, this); + } else if (operation == QLatin1String("Edit")) { + return new AddEditPlaceJob(m_model, QModelIndex(), parameters, this); + } else if (operation == QLatin1String("Remove")) { + return new RemovePlaceJob(m_model, index, this); + } else if (operation == QLatin1String("Hide")) { + return new ShowPlaceJob(m_model, index, false, this); + } else if (operation == QLatin1String("Show")) { + return new ShowPlaceJob(m_model, index, true, this); + } else if (operation == QLatin1String("Setup Device")) { + return new SetupDeviceJob(m_model, index, this); + } else if (operation == QLatin1String("Teardown Device")) { + return new TeardownDeviceJob(m_model, index, this); + } else { + // FIXME: BAD! No! + return nullptr; + } +} + +// vim: sw=4 sts=4 et tw=100 diff --git a/plasma/workspace/dataengines/places/placeservice.h b/plasma/workspace/dataengines/places/placeservice.h new file mode 100644 index 0000000000..102375ae66 --- /dev/null +++ b/plasma/workspace/dataengines/places/placeservice.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +#include "placesengine.h" + +class PlaceService : public Plasma::Service +{ + Q_OBJECT + +public: + PlaceService(QObject *parent, KFilePlacesModel *model); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + KFilePlacesModel *m_model; + QModelIndex m_index; +}; diff --git a/plasma/workspace/dataengines/places/placesproxymodel.cpp b/plasma/workspace/dataengines/places/placesproxymodel.cpp new file mode 100644 index 0000000000..2044b03e3c --- /dev/null +++ b/plasma/workspace/dataengines/places/placesproxymodel.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "placesproxymodel.h" + +#include +#include +#include + +PlacesProxyModel::PlacesProxyModel(QObject *parent, KFilePlacesModel *model) + : QIdentityProxyModel(parent) + , m_placesModel(model) +{ + setSourceModel(model); +} + +QHash PlacesProxyModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, "display"); + roles.insert(Qt::DecorationRole, "decoration"); + roles.insert(KFilePlacesModel::UrlRole, "url"); + roles.insert(KFilePlacesModel::HiddenRole, "hidden"); + roles.insert(KFilePlacesModel::SetupNeededRole, "setupNeeded"); + roles.insert(KFilePlacesModel::FixedDeviceRole, "fixedDevice"); + roles.insert(KFilePlacesModel::CapacityBarRecommendedRole, "capacityBarRecommended"); + roles.insert(PlaceIndexRole, "placeIndex"); + roles.insert(IsDeviceRole, "isDevice"); + roles.insert(PathRole, "path"); + roles.insert(SizeRole, "size"); + roles.insert(UsedRole, "used"); + roles.insert(AvailableRole, "available"); + return roles; +} + +QVariant PlacesProxyModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case PlaceIndexRole: + return index.row(); + case IsDeviceRole: + return m_placesModel->deviceForIndex(index).isValid(); + case PathRole: + return m_placesModel->url(index).path(); + + case SizeRole: { + const QString path = m_placesModel->url(index).path(); + QStorageInfo info{path}; + return info.isValid() && info.isReady() ? info.bytesTotal() : QVariant{}; + } + case UsedRole: { + const QString path = m_placesModel->url(index).path(); + QStorageInfo info{path}; + return info.isValid() && info.isReady() ? info.bytesTotal() - info.bytesAvailable() : QVariant{}; + } + case AvailableRole: { + const QString path = m_placesModel->url(index).path(); + QStorageInfo info{path}; + return info.isValid() && info.isReady() ? info.bytesAvailable() : QVariant{}; + } + default: + return QIdentityProxyModel::data(index, role); + } +} diff --git a/plasma/workspace/dataengines/places/placesproxymodel.h b/plasma/workspace/dataengines/places/placesproxymodel.h new file mode 100644 index 0000000000..f803846612 --- /dev/null +++ b/plasma/workspace/dataengines/places/placesproxymodel.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +class PlacesProxyModel : public QIdentityProxyModel +{ + Q_OBJECT + +public: + enum Roles { + PlaceIndexRole = KFilePlacesModel::CapacityBarRecommendedRole + 100, + IsDeviceRole, + PathRole, + SizeRole, + UsedRole, + AvailableRole, + }; + + PlacesProxyModel(QObject *parent, KFilePlacesModel *model); + + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role) const override; + +private: + KFilePlacesModel *m_placesModel; +}; diff --git a/plasma/workspace/dataengines/places/plasma-dataengine-places.json b/plasma/workspace/dataengines/places/plasma-dataengine-places.json new file mode 100644 index 0000000000..9a9b9a3e47 --- /dev/null +++ b/plasma/workspace/dataengines/places/plasma-dataengine-places.json @@ -0,0 +1,156 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "alex.merry@kdemail.net", + "Name": "Alex Merry", + "Name[ar]": "Alex Merry", + "Name[az]": "Alex Merry", + "Name[ca]": "Alex Merry", + "Name[cs]": "Alex Merry", + "Name[de]": "Alex Merry", + "Name[en_GB]": "Alex Merry", + "Name[es]": "Alex Merry", + "Name[eu]": "Alex Merry", + "Name[fi]": "Alex Merry", + "Name[fr]": "Alex Merry", + "Name[hu]": "Alex Merry", + "Name[ia]": "Alex Merry", + "Name[it]": "Alex Merry", + "Name[ko]": "Alex Merry", + "Name[lt]": "Alex Merry", + "Name[nl]": "Alex Merry", + "Name[nn]": "Alex Merry", + "Name[pl]": "Alex Merry", + "Name[pt_BR]": "Alex Merry", + "Name[ro]": "Alex Merry", + "Name[ru]": "Alex Merry", + "Name[sk]": "Alex Merry", + "Name[sl]": "Alex Merry", + "Name[sv]": "Alex Merry", + "Name[tr]": "Alex Merry", + "Name[uk]": "Alex Merry", + "Name[vi]": "Alex Merry", + "Name[x-test]": "xxAlex Merryxx", + "Name[zh_CN]": "Alex Merry" + } + ], + "Category": "System", + "Description": "Places, as seen in the file manager and in file dialogs.", + "Description[ar]": "الأماكن، كما في مدير الملفات وحواريات الملفات.", + "Description[az]": "Fayl menecerində və fayl dialoqlarında görünən Giriş Nöqtələri.", + "Description[ca]": "Llocs, com es veuen en el gestor de fitxers i en els diàlegs de fitxer.", + "Description[cs]": "Umístění, jak je vidíte ve správci souborů a souborových dialozích.", + "Description[de]": "Orte, wie sie in der Dateiverwaltung und in Dateiauswahl-Dialogen zu sehen sind.", + "Description[en_GB]": "Places, as seen in the file manager and in file dialogues.", + "Description[es]": "Lugares, como se ven en el gestor de archivos y en los diálogos de archivos.", + "Description[eu]": "Lekuak, fitxategi-kudeatzailean eta fitxategien elkarrizketa-koadroetan ikusi bezala.", + "Description[fi]": "Tiedostonhallinnan ja tiedostovalintaikkunoiden erityissijainnit.", + "Description[fr]": "Emplacements, tel que vus par le gestionnaire de fichiers et les boîtes de dialogue de fichiers.", + "Description[hu]": "Helyek panel (ahogy a fájlkezelőben és a fájlmegnyitó ablakokban látható).", + "Description[ia]": "Placias, tal como vidite in le gerente de file e in le dialogos de file", + "Description[it]": "Risorse, come si vedono nel gestore dei file e nelle finestre di selezione dei file.", + "Description[ko]": "파일 관리자와 파일 대화 상자에 나타나는 위치입니다.", + "Description[lt]": "Vietos, kaip jos yra matomos failų tvarkytuvėje ir failų dialoguose.", + "Description[nl]": "Plaatsen, zoals deze gezien worden in de bestandsbeheerder en in bestandsdialogen.", + "Description[nn]": "Stadar, slik ein kan sjå dei i filhandsamaren og dialogvindauge for filer.", + "Description[pa]": "ਥਾਵਾਂ, ਜਿਵੇਂ ਕਿ ਫਾਈਲ ਮੈਨੇਜਰ ਅਤੇ ਹੋਰ ਫਾਈਲ ਡਾਇਲਾਗ ਵਿੱਚ ਦਿਸਦੀਆਂ ਹਨ।", + "Description[pl]": "Wyświetla miejsca, tak jak w przeglądarce plików i oknach dialogowych plików.", + "Description[pt_BR]": "Locais, como visto no gerenciador de arquivo e nos diálogos de arquivo.", + "Description[ro]": "Locuri, așa cum sunt văzute în gestionarul de fișiere și în dialogurile de fișier.", + "Description[ru]": "Точки входа, показываемые в диспетчере файлов и файловых диалогах.", + "Description[sk]": "Miesta, ako sú vidieť v správcovi súborov a súborových dialógoch.", + "Description[sl]": "Mesta, kot jih je mogoče videti v upravljalniku datotek in pogovornih oknih za datoteke.", + "Description[sv]": "Platser som visas i filhanterare och fildialogrutor.", + "Description[ta]": "கோப்பு உலாவியிலும் கோப்பு சாளரங்களிலும் காட்டப்படும் இடங்கள்", + "Description[tr]": "Dosya yöneticisi ve dosya iletişim kutularında görüldüğü üzere Konumlar.", + "Description[uk]": "Місця, так, як їх показано у менеджері файлів і діалогових вікнах.", + "Description[vi]": "Địa điểm, như trong trình quản lí tệp và các hộp thoại tệp.", + "Description[x-test]": "xxPlaces, as seen in the file manager and in file dialogs.xx", + "Description[zh_CN]": "提供文件管理器和文件对话框中显示的常用位置列表。", + "EnabledByDefault": true, + "Icon": "folder-favorites", + "Id": "places", + "License": "LGPL", + "Name": "Places", + "Name[ar]": "أماكن", + "Name[ast]": "Llugares", + "Name[az]": "Giriş Nöqtələri", + "Name[be@latin]": "Miescy", + "Name[bg]": "Места", + "Name[bn]": "স্থান", + "Name[bs]": "Mjesta", + "Name[ca@valencia]": "Llocs", + "Name[ca]": "Llocs", + "Name[cs]": "Místa", + "Name[csb]": "Place", + "Name[da]": "Steder", + "Name[de]": "Orte", + "Name[el]": "Τοποθεσίες", + "Name[en_GB]": "Places", + "Name[eo]": "Lokoj", + "Name[es]": "Lugares", + "Name[et]": "Asukohad", + "Name[eu]": "Lekuak", + "Name[fi]": "Sijainnit", + "Name[fr]": "Emplacements", + "Name[fy]": "Places", + "Name[ga]": "Áiteanna", + "Name[gl]": "Lugares", + "Name[gu]": "જગ્યાઓ", + "Name[he]": "מקומות", + "Name[hi]": "स्थान", + "Name[hne]": "प्लेसेस", + "Name[hr]": "Mjesta", + "Name[hsb]": "Městna", + "Name[hu]": "Helyek", + "Name[ia]": "Placias", + "Name[id]": "Tempat", + "Name[is]": "Staðir", + "Name[it]": "Risorse", + "Name[ja]": "場所", + "Name[ka]": "ადგილები", + "Name[kk]": "Орындар", + "Name[km]": "កន្លែង", + "Name[kn]": "ಸ್ಥಳಗಳು", + "Name[ko]": "위치", + "Name[ku]": "Cih", + "Name[lt]": "Vietos", + "Name[lv]": "Vietas", + "Name[mai]": "स्थान", + "Name[mk]": "Места", + "Name[ml]": "സ്ഥലങ്ങള്‍", + "Name[mr]": "जागा", + "Name[nb]": "Steder", + "Name[nds]": "Steden", + "Name[nl]": "Locaties", + "Name[nn]": "Stadar", + "Name[or]": "ସ୍ଥାନଗୁଡ଼ିକ", + "Name[pa]": "ਥਾਵਾਂ", + "Name[pl]": "Miejsca", + "Name[pt]": "Locais", + "Name[pt_BR]": "Locais", + "Name[ro]": "Locuri", + "Name[ru]": "Точки входа", + "Name[si]": "ස්ථාන", + "Name[sk]": "Miesta", + "Name[sl]": "Mesta", + "Name[sr@ijekavian]": "мјеста", + "Name[sr@ijekavianlatin]": "mjesta", + "Name[sr@latin]": "mesta", + "Name[sr]": "места", + "Name[sv]": "Platser", + "Name[ta]": "இடங்கள்", + "Name[tg]": "Ҷойҳо", + "Name[th]": "ที่หลัก ๆ", + "Name[tr]": "Konumlar", + "Name[ug]": "ئورۇنلار", + "Name[uk]": "Місця", + "Name[vi]": "Địa điểm", + "Name[wa]": "Plaeces", + "Name[x-test]": "xxPlacesxx", + "Name[zh_CN]": "常用位置", + "Name[zh_TW]": "地方", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/places/setupdevicejob.cpp b/plasma/workspace/dataengines/places/setupdevicejob.cpp new file mode 100644 index 0000000000..282dc239ef --- /dev/null +++ b/plasma/workspace/dataengines/places/setupdevicejob.cpp @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "setupdevicejob.h" + +void SetupDeviceJob::setupDone(const QModelIndex &index, bool success) +{ + if (index == m_index) { + setError(!success); + emitResult(); + } +} + +void SetupDeviceJob::setupError(const QString &message) +{ + if (!error() || errorText().isEmpty()) { + setErrorText(message); + } +} + +// vim: sw=4 sts=4 et tw=100 diff --git a/plasma/workspace/dataengines/places/setupdevicejob.h b/plasma/workspace/dataengines/places/setupdevicejob.h new file mode 100644 index 0000000000..c22b9eee12 --- /dev/null +++ b/plasma/workspace/dataengines/places/setupdevicejob.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +#include "modeljob.h" + +class SetupDeviceJob : public ModelJob +{ + Q_OBJECT + +public: + SetupDeviceJob(KFilePlacesModel *model, QModelIndex index, QObject *parent = nullptr) + : ModelJob(parent, model, index, QStringLiteral("Setup Device")) + { + connect(model, &KFilePlacesModel::setupDone, this, &SetupDeviceJob::setupDone); + connect(model, &KFilePlacesModel::errorMessage, this, &SetupDeviceJob::setupError); + } + + void start() override + { + m_model->requestSetup(m_index); + } + +private Q_SLOTS: + void setupDone(const QModelIndex &index, bool success); + void setupError(const QString &message); +}; diff --git a/plasma/workspace/dataengines/powermanagement/CMakeLists.txt b/plasma/workspace/dataengines/powermanagement/CMakeLists.txt new file mode 100644 index 0000000000..102a626554 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/CMakeLists.txt @@ -0,0 +1,25 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_powermanagement\") + +set(powermanagement_engine_SRCS + powermanagementengine.cpp + powermanagementjob.cpp + powermanagementservice.cpp +) + +set(krunner_xml ${plasma-workspace_SOURCE_DIR}/krunner/dbus/org.kde.krunner.App.xml) +qt_add_dbus_interface(powermanagement_engine_SRCS ${krunner_xml} krunner_interface) + +kcoreaddons_add_plugin(plasma_engine_powermanagement SOURCES ${powermanagement_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_powermanagement + KF5::Solid + KF5::Plasma + KF5::IdleTime + KF5::CoreAddons + KF5::I18n + KF5::Service + Qt::DBus + PW::KWorkspace +) + +install(FILES powermanagementservice.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) diff --git a/plasma/workspace/dataengines/powermanagement/Messages.sh b/plasma/workspace/dataengines/powermanagement/Messages.sh new file mode 100644 index 0000000000..ff4754e1ff --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_powermanagement.pot diff --git a/plasma/workspace/dataengines/powermanagement/README.txt b/plasma/workspace/dataengines/powermanagement/README.txt new file mode 100644 index 0000000000..c657bc9f1e --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/README.txt @@ -0,0 +1,13 @@ +TODO: +====== +- Sleepstates don't match what solidshell reports +- ac plug state does not get updated +- this engine probably shares some functionality with + the solidengine, have a look at that and evaluate + +Notes +====== +There's a battery applet (also in kdebase) which uses this engine + +-- sebas + diff --git a/plasma/workspace/dataengines/powermanagement/plasma-dataengine-powermanagement.json b/plasma/workspace/dataengines/powermanagement/plasma-dataengine-powermanagement.json new file mode 100644 index 0000000000..dc38eb8b13 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/plasma-dataengine-powermanagement.json @@ -0,0 +1,78 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "Battery, AC, sleep and PowerDevil information.", + "Description[ar]": "معلومات عن البطارية والشحن و النوم و عفريت الطاقة", + "Description[az]": "Batareya, elektrik şəbəkəsi, və PowerDevil məlumatları.", + "Description[ca]": "Informació de la bateria, AC, adorm i del PowerDevil.", + "Description[cs]": "Informace o baterii, AC a uspání.", + "Description[de]": "Informationen zu Akku, Netzanschluss, Standby-Modus und PowerDevil.", + "Description[en_GB]": "Battery, AC, sleep and PowerDevil information.", + "Description[es]": "Información de batería, CA, suspensión y PowerDevil.", + "Description[eu]": "Bateria, AC, loa eta PowerDevil-i buruzko informazioa.", + "Description[fi]": "Akku-, virta-, valmiustila- ja PowerDevil-tiedot.", + "Description[fr]": "Batterie, alimentation secteur, veille et informations sur PowerDevil.", + "Description[hu]": "A telepek, a tápellátás és a PowerDevil energiakezelő jellemzői.", + "Description[ia]": "Batteria, AC, reposo e information de PowerDevil.", + "Description[it]": "Informazioni su batteria, alimentazione, sospensione e PowerDevil", + "Description[ko]": "배터리; AC; 절전 모드 및 PowerDevil 정보입니다.", + "Description[lt]": "Informacija apie akumuliatorių, kintamąją srovę, pristabdymą ir PowerDevil.", + "Description[nl]": "Accu-, stroomvoorziening-, slaapstand- en PowerDevil-informatie.", + "Description[nn]": "Informasjon om batteristatus, lading og liknande.", + "Description[pa]": "ਬੈਟਰੀ, ਏਸੀ, ਸਲੀਪ ਅਤੇ ਪਾਵਰਡਿਵੈਲ ਜਾਣਕਾਰੀ ਹੈ।", + "Description[pl]": "Wyświetla informacje o baterii, zasilaczu, usypianiu i z PowerDevil.", + "Description[pt_BR]": "Bateria, adaptador de energia, dormir e informação do PowerDevil.", + "Description[ro]": "Informații despre acumulator, priză, adormire și PowerDevil.", + "Description[ru]": "Информация о батареях, электрической сети и PowerDevil.", + "Description[sk]": "Informácie o batérii, AC a uspaní.", + "Description[sl]": "Informacije o bateriji, omrežnem napajanju, pripravljenosti in PowerDevilu.", + "Description[sv]": "Information om batteri, nätspänning, viloläge och Powerdevil.", + "Description[ta]": "மின்கலம், ஆற்றல், தூக்கம், மற்றும் PowerDevil குறித்த விவரங்கள்", + "Description[tr]": "Pil, Adaptör, uyku kipi ve PowerDevil bilgileri.", + "Description[uk]": "Акумулятор, мережеве живлення, відомості щодо присипляння і PowerDevil.", + "Description[vi]": "Thông tin pin, củ sạc, chế độ ngủ và PowerDevil.", + "Description[x-test]": "xxBattery, AC, sleep and PowerDevil information.xx", + "Description[zh_CN]": "提供电池、交流适配器、系统睡眠及 PowerDevil 信息。", + "Icon": "preferences-system-power-management", + "Id": "powermanagement", + "Name": "Power Management", + "Name[ar]": "إدارة الطاقة", + "Name[az]": "Enerjiyə nəzarət", + "Name[ca]": "Gestió d'energia", + "Name[cs]": "Správa napájení", + "Name[de]": "Energieverwaltung", + "Name[en_GB]": "Power Management", + "Name[es]": "Gestión de energía", + "Name[eu]": "Energia-kudeaketa", + "Name[fi]": "Virranhallinta", + "Name[fr]": "Gestion de l'énergie", + "Name[hu]": "Energiakezelő", + "Name[ia]": "Gestion de energia", + "Name[it]": "Gestione energetica", + "Name[ko]": "전원 관리", + "Name[lt]": "Energijos valdymas", + "Name[nl]": "Energiebeheer", + "Name[nn]": "Straumstyring", + "Name[pa]": "ਪਾਵਰ ਇੰਤਜ਼ਾਮ", + "Name[pl]": "Zarządzanie energią", + "Name[pt_BR]": "Gerenciamento de energia", + "Name[ro]": "Gestiunea alimentării", + "Name[ru]": "Управление питанием", + "Name[sk]": "Správa napájania", + "Name[sl]": "Upravljanje z energijo", + "Name[sv]": "Strömhantering", + "Name[ta]": "ஆற்றல் மேலாண்மை", + "Name[tr]": "Güç Yönetimi", + "Name[uk]": "Керування живленням", + "Name[vi]": "Quản lí nguồn điện", + "Name[x-test]": "xxPower Managementxx", + "Name[zh_CN]": "电源管理", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementengine.cpp b/plasma/workspace/dataengines/powermanagement/powermanagementengine.cpp new file mode 100644 index 0000000000..94351c06a4 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementengine.cpp @@ -0,0 +1,846 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2007-2008 Sebastian Kuegler + SPDX-FileCopyrightText: 2007 Maor Vanmak + SPDX-FileCopyrightText: 2008 Dario Freddi + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "powermanagementengine.h" + +// kde-workspace/libs +#include + +// solid specific includes +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "powermanagementservice.h" +#include + +static const char SOLID_POWERMANAGEMENT_SERVICE[] = "org.kde.Solid.PowerManagement"; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(InhibitionInfo) + +PowermanagementEngine::PowermanagementEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_sources(basicSourceNames()) + , m_session(new SessionManagement(this)) +{ + Q_UNUSED(args) + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + init(); +} + +PowermanagementEngine::~PowermanagementEngine() +{ +} + +void PowermanagementEngine::init() +{ + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &PowermanagementEngine::deviceAdded); + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &PowermanagementEngine::deviceRemoved); + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(SOLID_POWERMANAGEMENT_SERVICE)) { + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + QStringLiteral("brightnessChanged"), + this, + SLOT(screenBrightnessChanged(int)))) { + qDebug() << "error connecting to Brightness changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + QStringLiteral("brightnessMaxChanged"), + this, + SLOT(maximumScreenBrightnessChanged(int)))) { + qDebug() << "error connecting to max brightness changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/KeyboardBrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.KeyboardBrightnessControl"), + QStringLiteral("keyboardBrightnessChanged"), + this, + SLOT(keyboardBrightnessChanged(int)))) { + qDebug() << "error connecting to Keyboard Brightness changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/KeyboardBrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.KeyboardBrightnessControl"), + QStringLiteral("keyboardBrightnessMaxChanged"), + this, + SLOT(maximumKeyboardBrightnessChanged(int)))) { + qDebug() << "error connecting to max keyboard Brightness changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/HandleButtonEvents"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.HandleButtonEvents"), + QStringLiteral("triggersLidActionChanged"), + this, + SLOT(triggersLidActionChanged(bool)))) { + qDebug() << "error connecting to lid action trigger changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/PolicyAgent"), + QStringLiteral("org.kde.Solid.PowerManagement.PolicyAgent"), + QStringLiteral("InhibitionsChanged"), + this, + SLOT(inhibitionsChanged(QList, QStringList)))) { + qDebug() << "error connecting to inhibition changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement"), + SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("batteryRemainingTimeChanged"), + this, + SLOT(batteryRemainingTimeChanged(qulonglong)))) { + qDebug() << "error connecting to remaining time changes"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement"), + SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("chargeStopThresholdChanged"), + this, + SLOT(chargeStopThresholdChanged(int)))) { + qDebug() << "error connecting to charge stop threshold changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("currentProfileChanged"), + this, + SLOT(updatePowerProfileCurrentProfile(QString)))) { + qDebug() << "error connecting to current profile changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("profileChoicesChanged"), + this, + SLOT(updatePowerProfileChoices(QStringList)))) { + qDebug() << "error connecting to profile choices changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("performanceInhibitedReasonChanged"), + this, + SLOT(updatePowerProfilePerformanceInhibitedReason(QString)))) { + qDebug() << "error connecting to inhibition reason changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("performanceDegradedReasonChanged"), + this, + SLOT(updatePowerProfilePerformanceDegradedReason(QString)))) { + qDebug() << "error connecting to degradation reason changes via dbus"; + } + + if (!QDBusConnection::sessionBus().connect(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("profileHoldsChanged"), + this, + SLOT(updatePowerProfileHolds(QList)))) { + qDebug() << "error connecting to profile hold changes via dbus"; + } + } +} + +QStringList PowermanagementEngine::basicSourceNames() const +{ + QStringList sources; + sources << QStringLiteral("Battery") << QStringLiteral("AC Adapter") << QStringLiteral("Sleep States") << QStringLiteral("PowerDevil") + << QStringLiteral("Inhibitions") << QStringLiteral("Power Profiles"); + return sources; +} + +QStringList PowermanagementEngine::sources() const +{ + return m_sources; +} + +bool PowermanagementEngine::sourceRequestEvent(const QString &name) +{ + if (name == QLatin1String("Battery")) { + const QList listBattery = Solid::Device::listFromType(Solid::DeviceInterface::Battery); + m_batterySources.clear(); + + if (listBattery.isEmpty()) { + setData(QStringLiteral("Battery"), QStringLiteral("Has Battery"), false); + setData(QStringLiteral("Battery"), QStringLiteral("Has Cumulative"), false); + return true; + } + + uint index = 0; + QStringList batterySources; + + for (const Solid::Device &deviceBattery : listBattery) { + const Solid::Battery *battery = deviceBattery.as(); + + const QString source = QStringLiteral("Battery%1").arg(index++); + + batterySources << source; + m_batterySources[deviceBattery.udi()] = source; + + connect(battery, &Solid::Battery::chargeStateChanged, this, &PowermanagementEngine::updateBatteryChargeState); + connect(battery, &Solid::Battery::chargePercentChanged, this, &PowermanagementEngine::updateBatteryChargePercent); + connect(battery, &Solid::Battery::energyChanged, this, &PowermanagementEngine::updateBatteryEnergy); + connect(battery, &Solid::Battery::presentStateChanged, this, &PowermanagementEngine::updateBatteryPresentState); + + // Set initial values + updateBatteryChargeState(battery->chargeState(), deviceBattery.udi()); + updateBatteryChargePercent(battery->chargePercent(), deviceBattery.udi()); + updateBatteryEnergy(battery->energy(), deviceBattery.udi()); + updateBatteryPresentState(battery->isPresent(), deviceBattery.udi()); + updateBatteryPowerSupplyState(battery->isPowerSupply(), deviceBattery.udi()); + + setData(source, QStringLiteral("Vendor"), deviceBattery.vendor()); + setData(source, QStringLiteral("Product"), deviceBattery.product()); + setData(source, QStringLiteral("Capacity"), battery->capacity()); + setData(source, QStringLiteral("Type"), batteryTypeToString(battery)); + } + + updateBatteryNames(); + updateOverallBattery(); + + setData(QStringLiteral("Battery"), QStringLiteral("Sources"), batterySources); + setData(QStringLiteral("Battery"), QStringLiteral("Has Battery"), !batterySources.isEmpty()); + if (!batterySources.isEmpty()) { + QDBusMessage msg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement"), + SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("batteryRemainingTime")); + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + batteryRemainingTimeChanged(reply.value()); + } + watcher->deleteLater(); + }); + } + + QDBusMessage msg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement"), + SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("chargeStopThreshold")); + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + chargeStopThresholdChanged(reply.value()); + } + watcher->deleteLater(); + }); + + m_sources = basicSourceNames() + batterySources; + } else if (name == QLatin1String("AC Adapter")) { + QDBusConnection::sessionBus().connect(QStringLiteral("org.freedesktop.PowerManagement"), + QStringLiteral("/org/freedesktop/PowerManagement"), + QStringLiteral("org.freedesktop.PowerManagement"), + QStringLiteral("PowerSaveStatusChanged"), + this, + SLOT(updateAcPlugState(bool))); + + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PowerManagement"), + QStringLiteral("/org/freedesktop/PowerManagement"), + QStringLiteral("org.freedesktop.PowerManagement"), + QStringLiteral("GetPowerSaveStatus")); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + updateAcPlugState(reply.isValid() ? reply.value() : false); + } else if (name == QLatin1String("Sleep States")) { + setData(QStringLiteral("Sleep States"), QStringLiteral("Standby"), m_session->canSuspend()); + setData(QStringLiteral("Sleep States"), QStringLiteral("Suspend"), m_session->canSuspend()); + setData(QStringLiteral("Sleep States"), QStringLiteral("Hibernate"), m_session->canHibernate()); + setData(QStringLiteral("Sleep States"), QStringLiteral("HybridSuspend"), m_session->canHybridSuspend()); + setData(QStringLiteral("Sleep States"), QStringLiteral("LockScreen"), m_session->canLock()); + setData(QStringLiteral("Sleep States"), QStringLiteral("Logout"), m_session->canLogout()); + } else if (name == QLatin1String("PowerDevil")) { + QDBusMessage screenMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + QStringLiteral("brightness")); + QDBusPendingReply screenReply = QDBusConnection::sessionBus().asyncCall(screenMsg); + QDBusPendingCallWatcher *screenWatcher = new QDBusPendingCallWatcher(screenReply, this); + QObject::connect(screenWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + screenBrightnessChanged(reply.value()); + } + watcher->deleteLater(); + }); + + QDBusMessage maxScreenMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + QStringLiteral("brightnessMax")); + QDBusPendingReply maxScreenReply = QDBusConnection::sessionBus().asyncCall(maxScreenMsg); + QDBusPendingCallWatcher *maxScreenWatcher = new QDBusPendingCallWatcher(maxScreenReply, this); + QObject::connect(maxScreenWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + maximumScreenBrightnessChanged(reply.value()); + } + watcher->deleteLater(); + }); + + QDBusMessage keyboardMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/KeyboardBrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.KeyboardBrightnessControl"), + QStringLiteral("keyboardBrightness")); + QDBusPendingReply keyboardReply = QDBusConnection::sessionBus().asyncCall(keyboardMsg); + QDBusPendingCallWatcher *keyboardWatcher = new QDBusPendingCallWatcher(keyboardReply, this); + QObject::connect(keyboardWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + keyboardBrightnessChanged(reply.value()); + } + watcher->deleteLater(); + }); + + QDBusMessage maxKeyboardMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/KeyboardBrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.KeyboardBrightnessControl"), + QStringLiteral("keyboardBrightnessMax")); + QDBusPendingReply maxKeyboardReply = QDBusConnection::sessionBus().asyncCall(maxKeyboardMsg); + QDBusPendingCallWatcher *maxKeyboardWatcher = new QDBusPendingCallWatcher(maxKeyboardReply, this); + QObject::connect(maxKeyboardWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + maximumKeyboardBrightnessChanged(reply.value()); + } + watcher->deleteLater(); + }); + + QDBusMessage lidIsPresentMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement"), + SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("isLidPresent")); + QDBusPendingReply lidIsPresentReply = QDBusConnection::sessionBus().asyncCall(lidIsPresentMsg); + QDBusPendingCallWatcher *lidIsPresentWatcher = new QDBusPendingCallWatcher(lidIsPresentReply, this); + QObject::connect(lidIsPresentWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + setData(QStringLiteral("PowerDevil"), QStringLiteral("Is Lid Present"), reply.value()); + } + watcher->deleteLater(); + }); + + QDBusMessage triggersLidActionMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/HandleButtonEvents"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.HandleButtonEvents"), + QStringLiteral("triggersLidAction")); + QDBusPendingReply triggersLidActionReply = QDBusConnection::sessionBus().asyncCall(triggersLidActionMsg); + QDBusPendingCallWatcher *triggersLidActionWatcher = new QDBusPendingCallWatcher(triggersLidActionReply, this); + QObject::connect(triggersLidActionWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + triggersLidActionChanged(reply.value()); + } + watcher->deleteLater(); + }); + + } else if (name == QLatin1String("Inhibitions")) { + QDBusMessage inhibitionsMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/PolicyAgent"), + QStringLiteral("org.kde.Solid.PowerManagement.PolicyAgent"), + QStringLiteral("ListInhibitions")); + QDBusPendingReply> inhibitionsReply = QDBusConnection::sessionBus().asyncCall(inhibitionsMsg); + QDBusPendingCallWatcher *inhibitionsWatcher = new QDBusPendingCallWatcher(inhibitionsReply, this); + QObject::connect(inhibitionsWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply> reply = *watcher; + watcher->deleteLater(); + + if (!reply.isError()) { + removeAllData(QStringLiteral("Inhibitions")); + + inhibitionsChanged(reply.value(), QStringList()); + } + }); + + // any info concerning lock screen/screensaver goes here + } else if (name == QLatin1String("UserActivity")) { + setData(QStringLiteral("UserActivity"), QStringLiteral("IdleTime"), KIdleTime::instance()->idleTime()); + } else if (name == QLatin1String("Power Profiles")) { + auto profileMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("currentProfile")); + auto profileWatcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(profileMsg)); + connect(profileWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + } + updatePowerProfileCurrentProfile(reply.value()); + }); + + auto choicesMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("profileChoices")); + auto choicesWatcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(choicesMsg)); + connect(choicesWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + } + updatePowerProfileChoices(reply.value()); + }); + + auto inhibitedMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("performanceInhibitedReason")); + auto inhibitedWatcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(inhibitedMsg)); + connect(inhibitedWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + } + updatePowerProfilePerformanceInhibitedReason(reply.value()); + }); + + auto degradedMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("performanceDegradedReason")); + auto degradedWatcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(degradedMsg)); + connect(degradedWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + }; + updatePowerProfilePerformanceDegradedReason(reply.value()); + }); + + auto holdsMsg = QDBusMessage::createMethodCall(SOLID_POWERMANAGEMENT_SERVICE, + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("profileHolds")); + auto holdsWatcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(holdsMsg)); + connect(holdsWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply> reply = *watcher; + if (reply.isError()) { + return; + }; + updatePowerProfileHolds(reply.value()); + }); + } else { + qDebug() << "Data for '" << name << "' not found"; + return false; + } + return true; +} + +QString PowermanagementEngine::batteryTypeToString(const Solid::Battery *battery) const +{ + switch (battery->type()) { + case Solid::Battery::PrimaryBattery: + return QStringLiteral("Battery"); + case Solid::Battery::UpsBattery: + return QStringLiteral("Ups"); + case Solid::Battery::MonitorBattery: + return QStringLiteral("Monitor"); + case Solid::Battery::MouseBattery: + return QStringLiteral("Mouse"); + case Solid::Battery::KeyboardBattery: + return QStringLiteral("Keyboard"); + case Solid::Battery::PdaBattery: + return QStringLiteral("Pda"); + case Solid::Battery::PhoneBattery: + return QStringLiteral("Phone"); + case Solid::Battery::GamingInputBattery: + return QStringLiteral("GamingInput"); + case Solid::Battery::BluetoothBattery: + return QStringLiteral("Bluetooth"); + default: + return QStringLiteral("Unknown"); + } +} + +bool PowermanagementEngine::updateSourceEvent(const QString &source) +{ + if (source == QLatin1String("UserActivity")) { + setData(QStringLiteral("UserActivity"), QStringLiteral("IdleTime"), KIdleTime::instance()->idleTime()); + return true; + } + return Plasma::DataEngine::updateSourceEvent(source); +} + +Plasma::Service *PowermanagementEngine::serviceForSource(const QString &source) +{ + if (source == QLatin1String("PowerDevil")) { + return new PowerManagementService(this); + } + + return nullptr; +} + +QString PowermanagementEngine::batteryStateToString(int newState) const +{ + QString state(QStringLiteral("Unknown")); + if (newState == Solid::Battery::NoCharge) { + state = QLatin1String("NoCharge"); + } else if (newState == Solid::Battery::Charging) { + state = QLatin1String("Charging"); + } else if (newState == Solid::Battery::Discharging) { + state = QLatin1String("Discharging"); + } else if (newState == Solid::Battery::FullyCharged) { + state = QLatin1String("FullyCharged"); + } + + return state; +} + +void PowermanagementEngine::updateBatteryChargeState(int newState, const QString &udi) +{ + const QString source = m_batterySources[udi]; + setData(source, QStringLiteral("State"), batteryStateToString(newState)); + updateOverallBattery(); +} + +void PowermanagementEngine::updateBatteryPresentState(bool newState, const QString &udi) +{ + const QString source = m_batterySources[udi]; + setData(source, QStringLiteral("Plugged in"), newState); // FIXME This needs to be renamed and Battery Monitor adjusted +} + +void PowermanagementEngine::updateBatteryChargePercent(int newValue, const QString &udi) +{ + const QString source = m_batterySources[udi]; + setData(source, QStringLiteral("Percent"), newValue); + updateOverallBattery(); +} + +void PowermanagementEngine::updateBatteryEnergy(double newValue, const QString &udi) +{ + const QString source = m_batterySources[udi]; + setData(source, QStringLiteral("Energy"), newValue); +} + +void PowermanagementEngine::updateBatteryPowerSupplyState(bool newState, const QString &udi) +{ + const QString source = m_batterySources[udi]; + setData(source, QStringLiteral("Is Power Supply"), newState); +} + +void PowermanagementEngine::updateBatteryNames() +{ + uint unnamedBatteries = 0; + for (const QString &source : std::as_const(m_batterySources)) { + DataContainer *batteryDataContainer = containerForSource(source); + if (batteryDataContainer) { + const QString batteryVendor = batteryDataContainer->data()[QStringLiteral("Vendor")].toString(); + const QString batteryProduct = batteryDataContainer->data()[QStringLiteral("Product")].toString(); + + // Don't show battery name for primary power supply batteries. They usually have cryptic serial number names. + const bool showBatteryName = batteryDataContainer->data()[QStringLiteral("Type")].toString() != QLatin1String("Battery") + || !batteryDataContainer->data()[QStringLiteral("Is Power Supply")].toBool(); + + if (!batteryProduct.isEmpty() && batteryProduct != QLatin1String("Unknown Battery") && showBatteryName) { + if (!batteryVendor.isEmpty()) { + setData(source, QStringLiteral("Pretty Name"), QString(batteryVendor + ' ' + batteryProduct)); + } else { + setData(source, QStringLiteral("Pretty Name"), batteryProduct); + } + } else { + ++unnamedBatteries; + if (unnamedBatteries > 1) { + setData(source, QStringLiteral("Pretty Name"), i18nc("Placeholder is the battery number", "Battery %1", unnamedBatteries)); + } else { + setData(source, QStringLiteral("Pretty Name"), i18n("Battery")); + } + } + } + } +} + +void PowermanagementEngine::updateOverallBattery() +{ + const QList listBattery = Solid::Device::listFromType(Solid::DeviceInterface::Battery); + bool hasCumulative = false; + + double energy = 0; + double totalEnergy = 0; + bool allFullyCharged = true; + bool charging = false; + bool noCharge = false; + double totalPercentage = 0; + int count = 0; + + for (const Solid::Device &deviceBattery : listBattery) { + const Solid::Battery *battery = deviceBattery.as(); + + if (battery && battery->isPowerSupply()) { + hasCumulative = true; + + energy += battery->energy(); + totalEnergy += battery->energyFull(); + totalPercentage += battery->chargePercent(); + allFullyCharged = allFullyCharged && (battery->chargeState() == Solid::Battery::FullyCharged); + charging = charging || (battery->chargeState() == Solid::Battery::Charging); + noCharge = noCharge || (battery->chargeState() == Solid::Battery::NoCharge); + ++count; + } + } + + if (count == 1) { + // Energy is sometimes way off causing us to show rubbish; this is a UPower issue + // but anyway having just one battery and the tooltip showing strange readings + // compared to the popup doesn't look polished. + setData(QStringLiteral("Battery"), QStringLiteral("Percent"), qRound(totalPercentage)); + } else if (totalEnergy > 0) { + setData(QStringLiteral("Battery"), QStringLiteral("Percent"), qRound(energy / totalEnergy * 100)); + } else if (count > 0) { // UPS don't have energy, see Bug 348588 + setData(QStringLiteral("Battery"), QStringLiteral("Percent"), qRound(totalPercentage / static_cast(count))); + } else { + setData(QStringLiteral("Battery"), QStringLiteral("Percent"), int(0)); + } + + if (hasCumulative) { + if (allFullyCharged) { + setData(QStringLiteral("Battery"), QStringLiteral("State"), "FullyCharged"); + } else if (charging) { + setData(QStringLiteral("Battery"), QStringLiteral("State"), "Charging"); + } else if (noCharge) { + setData(QStringLiteral("Battery"), QStringLiteral("State"), "NoCharge"); + } else { + setData(QStringLiteral("Battery"), QStringLiteral("State"), "Discharging"); + } + } else { + setData(QStringLiteral("Battery"), QStringLiteral("State"), "Unknown"); + } + + setData(QStringLiteral("Battery"), QStringLiteral("Has Cumulative"), hasCumulative); +} + +void PowermanagementEngine::updateAcPlugState(bool onBattery) +{ + setData(QStringLiteral("AC Adapter"), QStringLiteral("Plugged in"), !onBattery); +} + +void PowermanagementEngine::updatePowerProfileCurrentProfile(const QString &activeProfile) +{ + setData(QStringLiteral("Power Profiles"), QStringLiteral("Current Profile"), activeProfile); +} + +void PowermanagementEngine::updatePowerProfileChoices(const QStringList &choices) +{ + setData(QStringLiteral("Power Profiles"), QStringLiteral("Profiles"), choices); +} + +void PowermanagementEngine::updatePowerProfilePerformanceInhibitedReason(const QString &reason) +{ + setData(QStringLiteral("Power Profiles"), QStringLiteral("Performance Inhibited Reason"), reason); +} + +void PowermanagementEngine::updatePowerProfilePerformanceDegradedReason(const QString &reason) +{ + setData(QStringLiteral("Power Profiles"), QStringLiteral("Performance Degraded Reason"), reason); +} + +void PowermanagementEngine::updatePowerProfileHolds(const QList &holds) +{ + QList out; + std::transform(holds.cbegin(), holds.cend(), std::back_inserter(out), [this](const QVariantMap &hold) { + QString prettyName; + QString icon; + populateApplicationData(hold[QStringLiteral("ApplicationId")].toString(), &prettyName, &icon); + return QVariantMap{ + {QStringLiteral("Name"), prettyName}, + {QStringLiteral("Icon"), icon}, + {QStringLiteral("Reason"), hold[QStringLiteral("Reason")]}, + {QStringLiteral("Profile"), hold[QStringLiteral("Profile")]}, + }; + }); + setData(QStringLiteral("Power Profiles"), QStringLiteral("Profile Holds"), QVariant::fromValue(out)); +} + +void PowermanagementEngine::deviceRemoved(const QString &udi) +{ + if (m_batterySources.contains(udi)) { + Solid::Device device(udi); + Solid::Battery *battery = device.as(); + if (battery) + battery->disconnect(); + + const QString source = m_batterySources[udi]; + m_batterySources.remove(udi); + removeSource(source); + + QStringList sourceNames(m_batterySources.values()); + sourceNames.removeAll(source); + setData(QStringLiteral("Battery"), QStringLiteral("Sources"), sourceNames); + setData(QStringLiteral("Battery"), QStringLiteral("Has Battery"), !sourceNames.isEmpty()); + + updateOverallBattery(); + } +} + +void PowermanagementEngine::deviceAdded(const QString &udi) +{ + Solid::Device device(udi); + if (device.isValid()) { + const Solid::Battery *battery = device.as(); + + if (battery) { + int index = 0; + QStringList sourceNames(m_batterySources.values()); + while (sourceNames.contains(QStringLiteral("Battery%1").arg(index))) { + index++; + } + + const QString source = QStringLiteral("Battery%1").arg(index); + sourceNames << source; + m_batterySources[device.udi()] = source; + + connect(battery, &Solid::Battery::chargeStateChanged, this, &PowermanagementEngine::updateBatteryChargeState); + connect(battery, &Solid::Battery::chargePercentChanged, this, &PowermanagementEngine::updateBatteryChargePercent); + connect(battery, &Solid::Battery::energyChanged, this, &PowermanagementEngine::updateBatteryEnergy); + connect(battery, &Solid::Battery::presentStateChanged, this, &PowermanagementEngine::updateBatteryPresentState); + connect(battery, &Solid::Battery::powerSupplyStateChanged, this, &PowermanagementEngine::updateBatteryPowerSupplyState); + + // Set initial values + updateBatteryChargeState(battery->chargeState(), device.udi()); + updateBatteryChargePercent(battery->chargePercent(), device.udi()); + updateBatteryEnergy(battery->energy(), device.udi()); + updateBatteryPresentState(battery->isPresent(), device.udi()); + updateBatteryPowerSupplyState(battery->isPowerSupply(), device.udi()); + + setData(source, QStringLiteral("Vendor"), device.vendor()); + setData(source, QStringLiteral("Product"), device.product()); + setData(source, QStringLiteral("Capacity"), battery->capacity()); + setData(source, QStringLiteral("Type"), batteryTypeToString(battery)); + + setData(QStringLiteral("Battery"), QStringLiteral("Sources"), sourceNames); + setData(QStringLiteral("Battery"), QStringLiteral("Has Battery"), !sourceNames.isEmpty()); + + updateBatteryNames(); + updateOverallBattery(); + } + } +} + +void PowermanagementEngine::batteryRemainingTimeChanged(qulonglong time) +{ + // qDebug() << "Remaining time 2:" << time; + setData(QStringLiteral("Battery"), QStringLiteral("Remaining msec"), time); +} + +void PowermanagementEngine::screenBrightnessChanged(int brightness) +{ + setData(QStringLiteral("PowerDevil"), QStringLiteral("Screen Brightness"), brightness); +} + +void PowermanagementEngine::maximumScreenBrightnessChanged(int maximumBrightness) +{ + setData(QStringLiteral("PowerDevil"), QStringLiteral("Maximum Screen Brightness"), maximumBrightness); + setData(QStringLiteral("PowerDevil"), QStringLiteral("Screen Brightness Available"), maximumBrightness > 0); +} + +void PowermanagementEngine::keyboardBrightnessChanged(int brightness) +{ + setData(QStringLiteral("PowerDevil"), QStringLiteral("Keyboard Brightness"), brightness); +} + +void PowermanagementEngine::maximumKeyboardBrightnessChanged(int maximumBrightness) +{ + setData(QStringLiteral("PowerDevil"), QStringLiteral("Maximum Keyboard Brightness"), maximumBrightness); + setData(QStringLiteral("PowerDevil"), QStringLiteral("Keyboard Brightness Available"), maximumBrightness > 0); +} + +void PowermanagementEngine::triggersLidActionChanged(bool triggers) +{ + setData(QStringLiteral("PowerDevil"), QStringLiteral("Triggers Lid Action"), triggers); +} + +void PowermanagementEngine::inhibitionsChanged(const QList &added, const QStringList &removed) +{ + for (auto it = removed.constBegin(); it != removed.constEnd(); ++it) { + removeData(QStringLiteral("Inhibitions"), (*it)); + } + + for (auto it = added.constBegin(); it != added.constEnd(); ++it) { + const QString &name = (*it).first; + QString prettyName; + QString icon; + const QString &reason = (*it).second; + + populateApplicationData(name, &prettyName, &icon); + + setData(QStringLiteral("Inhibitions"), + name, + QVariantMap{{QStringLiteral("Name"), prettyName}, {QStringLiteral("Icon"), icon}, {QStringLiteral("Reason"), reason}}); + } +} + +void PowermanagementEngine::populateApplicationData(const QString &name, QString *prettyName, QString *icon) +{ + if (m_applicationInfo.contains(name)) { + const auto &info = m_applicationInfo.value(name); + *prettyName = info.first; + *icon = info.second; + } else { + KService::Ptr service = KService::serviceByStorageId(name + ".desktop"); + if (service) { + *prettyName = service->property(QStringLiteral("Name"), QVariant::Invalid).toString(); // cannot be null + *icon = service->icon(); + + m_applicationInfo.insert(name, qMakePair(*prettyName, *icon)); + } else { + *prettyName = name; + *icon = name.section(QLatin1Char('/'), -1).toLower(); + } + } +} + +void PowermanagementEngine::chargeStopThresholdChanged(int threshold) +{ + setData(QStringLiteral("Battery"), QStringLiteral("Charge Stop Threshold"), threshold); +} + +K_PLUGIN_CLASS_WITH_JSON(PowermanagementEngine, "plasma-dataengine-powermanagement.json") + +#include "powermanagementengine.moc" diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementengine.h b/plasma/workspace/dataengines/powermanagement/powermanagementengine.h new file mode 100644 index 0000000000..b86b74173b --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementengine.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2007-2008 Sebastian Kuegler + SPDX-FileCopyrightText: 2008 Dario Freddi + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include + +#include +#include +#include + +class SessionManagement; + +using InhibitionInfo = QPair; + +/** + * This class provides runtime information about the battery and AC status + * for use in power management Plasma applets. + */ +class PowermanagementEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + PowermanagementEngine(QObject *parent, const QVariantList &args); + ~PowermanagementEngine() override; + QStringList sources() const override; + Plasma::Service *serviceForSource(const QString &source) override; + +protected: + bool sourceRequestEvent(const QString &name) override; + bool updateSourceEvent(const QString &source) override; + void init(); + +private Q_SLOTS: + void updateBatteryChargeState(int newState, const QString &udi); + void updateBatteryPresentState(bool newState, const QString &udi); + void updateBatteryChargePercent(int newValue, const QString &udi); + void updateBatteryEnergy(double newValue, const QString &udi); + void updateBatteryPowerSupplyState(bool newState, const QString &udi); + void updateAcPlugState(bool onBattery); + void updateBatteryNames(); + void updateOverallBattery(); + + void deviceRemoved(const QString &udi); + void deviceAdded(const QString &udi); + void batteryRemainingTimeChanged(qulonglong time); + void screenBrightnessChanged(int brightness); + void maximumScreenBrightnessChanged(int maximumBrightness); + void keyboardBrightnessChanged(int brightness); + void maximumKeyboardBrightnessChanged(int maximumBrightness); + void triggersLidActionChanged(bool triggers); + void inhibitionsChanged(const QList &added, const QStringList &removed); + void chargeStopThresholdChanged(int threshold); + + void updatePowerProfileCurrentProfile(const QString &profile); + void updatePowerProfileChoices(const QStringList &choices); + void updatePowerProfilePerformanceInhibitedReason(const QString &reason); + void updatePowerProfilePerformanceDegradedReason(const QString &reason); + void updatePowerProfileHolds(const QList &holds); + +private: + void populateApplicationData(const QString &name, QString *prettyName, QString *icon); + QString batteryTypeToString(const Solid::Battery *battery) const; + QStringList basicSourceNames() const; + QString batteryStateToString(int newState) const; + + QStringList m_sources; + + QHash m_batterySources; // + QHash> m_applicationInfo; // > + + SessionManagement *m_session; +}; diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementjob.cpp b/plasma/workspace/dataengines/powermanagement/powermanagementjob.cpp new file mode 100644 index 0000000000..dc7883d108 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementjob.cpp @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include +#include +#include +#include + +#include + +// kde-workspace/libs +#include + +#include + +#include "powermanagementjob.h" + +PowerManagementJob::PowerManagementJob(const QString &operation, QMap ¶meters, QObject *parent) + : ServiceJob(parent->objectName(), operation, parameters, parent) + , m_session(new SessionManagement(this)) +{ +} + +PowerManagementJob::~PowerManagementJob() +{ +} + +static void callWhenFinished(const QDBusPendingCall &pending, std::function func, QObject *parent) +{ + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, parent); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, parent, [func](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + func(!watcher->isError()); + }); +} + +void PowerManagementJob::start() +{ + const QString operation = operationName(); + // qDebug() << "starting operation ... " << operation; + + if (operation == QLatin1String("lockScreen")) { + if (m_session->canLock()) { + m_session->lock(); + setResult(true); + return; + } + qDebug() << "operation denied " << operation; + setResult(false); + return; + } else if (operation == QLatin1String("suspend") || operation == QLatin1String("suspendToRam")) { + if (m_session->canSuspend()) { + m_session->suspend(); + setResult(true); + } else { + setResult(false); + } + return; + } else if (operation == QLatin1String("suspendToDisk")) { + if (m_session->canHibernate()) { + m_session->hibernate(); + setResult(true); + } else { + setResult(false); + } + return; + } else if (operation == QLatin1String("suspendHybrid")) { + if (m_session->canHybridSuspend()) { + m_session->hybridSuspend(); + setResult(true); + } else { + setResult(false); + } + return; + } else if (operation == QLatin1String("requestShutDown")) { + if (m_session->canShutdown()) { + m_session->requestShutdown(); + setResult(true); + } else { + setResult(false); + } + return; + } else if (operation == QLatin1String("switchUser")) { + if (m_session->canSwitchUser()) { + m_session->switchUser(); + setResult(true); + } + setResult(false); + return; + } else if (operation == QLatin1String("beginSuppressingSleep")) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("Inhibit")); + msg << QCoreApplication::applicationName() << parameters().value(QStringLiteral("reason")).toString(); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + setResult(reply.isValid() ? reply.value() : -1); + return; + } else if (operation == QLatin1String("stopSuppressingSleep")) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("/org/freedesktop/PowerManagement/Inhibit"), + QStringLiteral("org.freedesktop.PowerManagement.Inhibit"), + QStringLiteral("UnInhibit")); + msg << parameters().value(QStringLiteral("cookie")).toUInt(); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + setResult(reply.isValid()); + return; + } else if (operation == QLatin1String("beginSuppressingScreenPowerManagement")) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), + QStringLiteral("/ScreenSaver"), + QStringLiteral("org.freedesktop.ScreenSaver"), + QStringLiteral("Inhibit")); + msg << QCoreApplication::applicationName() << parameters().value(QStringLiteral("reason")).toString(); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + setResult(reply.isValid() ? reply.value() : -1); + return; + } else if (operation == QLatin1String("stopSuppressingScreenPowerManagement")) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), + QStringLiteral("/ScreenSaver"), + QStringLiteral("org.freedesktop.ScreenSaver"), + QStringLiteral("UnInhibit")); + msg << parameters().value(QStringLiteral("cookie")).toUInt(); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + setResult(reply.isValid()); + return; + } else if (operation == QLatin1String("setBrightness")) { + auto pending = setScreenBrightness(parameters().value(QStringLiteral("brightness")).toInt(), parameters().value(QStringLiteral("silent")).toBool()); + callWhenFinished( + pending, + [this] (bool success) { + setResult(success); + }, + this); + return; + } else if (operation == QLatin1String("setKeyboardBrightness")) { + auto pending = setKeyboardBrightness(parameters().value(QStringLiteral("brightness")).toInt(), parameters().value(QStringLiteral("silent")).toBool()); + callWhenFinished( + pending, + [this] (bool success) { + setResult(success); + }, + this); + return; + } else if (operation == QLatin1String("setPowerProfile")) { + auto pending = setPowerProfile(parameters().value(QStringLiteral("profile")).toString()); + callWhenFinished( + pending, + [this] (bool success) { + setResult(success); + }, + this); + return; + } + + qDebug() << "don't know what to do with " << operation; + setResult(false); +} + +QDBusPendingCall PowerManagementJob::setScreenBrightness(int value, bool silent) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/BrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.BrightnessControl"), + silent ? "setBrightnessSilent" : "setBrightness"); + msg << value; + return QDBusConnection::sessionBus().asyncCall(msg); +} + +QDBusPendingCall PowerManagementJob::setKeyboardBrightness(int value, bool silent) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/KeyboardBrightnessControl"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.KeyboardBrightnessControl"), + silent ? "setKeyboardBrightnessSilent" : "setKeyboardBrightness"); + msg << value; + return QDBusConnection::sessionBus().asyncCall(msg); +} + +QDBusPendingCall PowerManagementJob::setPowerProfile(const QString &value) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/PowerProfile"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.PowerProfile"), + QStringLiteral("setProfile")); + msg << value; + return QDBusConnection::sessionBus().asyncCall(msg); +} diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementjob.h b/plasma/workspace/dataengines/powermanagement/powermanagementjob.h new file mode 100644 index 0000000000..9fab68c8d0 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementjob.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// plasma +#include + +class SessionManagement; +class QDBusPendingCall; + +class PowerManagementJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + PowerManagementJob(const QString &operation, QMap ¶meters, QObject *parent = nullptr); + ~PowerManagementJob() override; + +protected: + void start() override; + +private: + QDBusPendingCall setScreenBrightness(int value, bool silent); + QDBusPendingCall setKeyboardBrightness(int value, bool silent); + QDBusPendingCall setPowerProfile(const QString &value); + SessionManagement *m_session; +}; diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementservice.cpp b/plasma/workspace/dataengines/powermanagement/powermanagementservice.cpp new file mode 100644 index 0000000000..2c4557059c --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementservice.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "powermanagementservice.h" +#include "powermanagementjob.h" + +PowerManagementService::PowerManagementService(QObject *parent) + : Plasma::Service(parent) +{ + setName(QStringLiteral("powermanagementservice")); +} + +ServiceJob *PowerManagementService::createJob(const QString &operation, QMap ¶meters) +{ + return new PowerManagementJob(operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementservice.h b/plasma/workspace/dataengines/powermanagement/powermanagementservice.h new file mode 100644 index 0000000000..07a0e76d17 --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementservice.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +using namespace Plasma; + +class PowerManagementService : public Plasma::Service +{ + Q_OBJECT + +public: + explicit PowerManagementService(QObject *parent = nullptr); + ServiceJob *createJob(const QString &operation, QMap ¶meters) override; +}; diff --git a/plasma/workspace/dataengines/powermanagement/powermanagementservice.operations b/plasma/workspace/dataengines/powermanagement/powermanagementservice.operations new file mode 100644 index 0000000000..100d6d95fc --- /dev/null +++ b/plasma/workspace/dataengines/powermanagement/powermanagementservice.operations @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/soliddevice/CMakeLists.txt b/plasma/workspace/dataengines/soliddevice/CMakeLists.txt new file mode 100644 index 0000000000..0c874b105f --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/CMakeLists.txt @@ -0,0 +1,24 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_soliddevice\") + +set(soliddevice_engine_SRCS + soliddeviceengine.cpp + devicesignalmapper.cpp + devicesignalmapmanager.cpp + hddtemp.cpp + soliddeviceservice.cpp + soliddevicejob.cpp +) + +kcoreaddons_add_plugin(plasma_engine_soliddevice SOURCES ${soliddevice_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_soliddevice + Qt::Network + KF5::I18n + KF5::KIOCore + KF5::Plasma + KF5::Solid + KF5::CoreAddons + KF5::Notifications +) + +install(FILES soliddevice.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services ) diff --git a/plasma/workspace/dataengines/soliddevice/Messages.sh b/plasma/workspace/dataengines/soliddevice/Messages.sh new file mode 100644 index 0000000000..ea6e6f04bf --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/plasma_engine_soliddevice.pot diff --git a/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.cpp b/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.cpp new file mode 100644 index 0000000000..ab9916b980 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.cpp @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "devicesignalmapmanager.h" + +DeviceSignalMapManager::DeviceSignalMapManager(QObject *parent) + : QObject(parent) +{ + user = parent; +} + +DeviceSignalMapManager::~DeviceSignalMapManager() +{ +} + +void DeviceSignalMapManager::mapDevice(Solid::Battery *battery, const QString &udi) +{ + BatterySignalMapper *map = nullptr; + if (!signalmap.contains(Solid::DeviceInterface::Battery)) { + map = new BatterySignalMapper(this); + signalmap[Solid::DeviceInterface::Battery] = map; + connect(map, SIGNAL(deviceChanged(QString, QString, QVariant)), user, SLOT(deviceChanged(QString, QString, QVariant))); + } else { + map = (BatterySignalMapper *)signalmap[Solid::DeviceInterface::Battery]; + } + + connect(battery, &Solid::Battery::chargePercentChanged, map, &BatterySignalMapper::chargePercentChanged); + connect(battery, &Solid::Battery::chargeStateChanged, map, &BatterySignalMapper::chargeStateChanged); + connect(battery, &Solid::Battery::presentStateChanged, map, &BatterySignalMapper::presentStateChanged); + map->setMapping(battery, udi); +} + +void DeviceSignalMapManager::mapDevice(Solid::StorageAccess *storageaccess, const QString &udi) +{ + StorageAccessSignalMapper *map = nullptr; + if (!signalmap.contains(Solid::DeviceInterface::StorageAccess)) { + map = new StorageAccessSignalMapper(this); + signalmap[Solid::DeviceInterface::StorageAccess] = map; + connect(map, SIGNAL(deviceChanged(QString, QString, QVariant)), user, SLOT(deviceChanged(QString, QString, QVariant))); + } else { + map = (StorageAccessSignalMapper *)signalmap[Solid::DeviceInterface::StorageAccess]; + } + + connect(storageaccess, &Solid::StorageAccess::accessibilityChanged, map, &StorageAccessSignalMapper::accessibilityChanged); + map->setMapping(storageaccess, udi); +} + +void DeviceSignalMapManager::unmapDevice(Solid::Battery *battery) +{ + BatterySignalMapper *map = (BatterySignalMapper *)signalmap.value(Solid::DeviceInterface::Battery); + if (!map) { + return; + } + + disconnect(battery, &Solid::Battery::chargePercentChanged, map, &BatterySignalMapper::chargePercentChanged); + disconnect(battery, &Solid::Battery::chargeStateChanged, map, &BatterySignalMapper::chargeStateChanged); + disconnect(battery, &Solid::Battery::presentStateChanged, map, &BatterySignalMapper::presentStateChanged); +} + +void DeviceSignalMapManager::unmapDevice(Solid::StorageAccess *storageaccess) +{ + StorageAccessSignalMapper *map = (StorageAccessSignalMapper *)signalmap.value(Solid::DeviceInterface::StorageAccess); + if (!map) { + return; + } + + disconnect(storageaccess, &Solid::StorageAccess::accessibilityChanged, map, &StorageAccessSignalMapper::accessibilityChanged); +} diff --git a/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.h b/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.h new file mode 100644 index 0000000000..241ef892b5 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/devicesignalmapmanager.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include "devicesignalmapper.h" + +class DeviceSignalMapManager : public QObject +{ + Q_OBJECT + +public: + explicit DeviceSignalMapManager(QObject *parent = nullptr); + ~DeviceSignalMapManager() override; + + void mapDevice(Solid::Battery *battery, const QString &udi); + void mapDevice(Solid::StorageAccess *storageaccess, const QString &udi); + + void unmapDevice(Solid::Battery *battery); + void unmapDevice(Solid::StorageAccess *storageaccess); + +private: + QMap signalmap; + QObject *user; +}; diff --git a/plasma/workspace/dataengines/soliddevice/devicesignalmapper.cpp b/plasma/workspace/dataengines/soliddevice/devicesignalmapper.cpp new file mode 100644 index 0000000000..4ddcc291cc --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/devicesignalmapper.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "devicesignalmapper.h" + +DeviceSignalMapper::DeviceSignalMapper(QObject *parent) + : QSignalMapper(parent) +{ +} + +DeviceSignalMapper::~DeviceSignalMapper() +{ +} + +void DeviceSignalMapper::setMapping(QObject *device, const QString &udi) +{ + signalmap[device] = udi; +} + +BatterySignalMapper::BatterySignalMapper(QObject *parent) + : DeviceSignalMapper(parent) +{ +} + +BatterySignalMapper::~BatterySignalMapper() +{ +} + +void BatterySignalMapper::chargePercentChanged(int value) +{ + Q_EMIT deviceChanged(signalmap[sender()], QStringLiteral("Charge Percent"), value); +} + +void BatterySignalMapper::chargeStateChanged(int newState) +{ + QStringList chargestate; + chargestate << QStringLiteral("Fully Charged") << QStringLiteral("Charging") << QStringLiteral("Discharging"); + Q_EMIT deviceChanged(signalmap[sender()], QStringLiteral("Charge State"), chargestate.at(newState)); +} + +void BatterySignalMapper::presentStateChanged(bool newState) +{ + Q_EMIT deviceChanged(signalmap[sender()], QStringLiteral("Plugged In"), newState); +} + +StorageAccessSignalMapper::StorageAccessSignalMapper(QObject *parent) + : DeviceSignalMapper(parent) +{ +} + +StorageAccessSignalMapper::~StorageAccessSignalMapper() +{ +} + +void StorageAccessSignalMapper::accessibilityChanged(bool accessible) +{ + Q_EMIT deviceChanged(signalmap[sender()], QStringLiteral("Accessible"), accessible); +} diff --git a/plasma/workspace/dataengines/soliddevice/devicesignalmapper.h b/plasma/workspace/dataengines/soliddevice/devicesignalmapper.h new file mode 100644 index 0000000000..c52b16fb9d --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/devicesignalmapper.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DeviceSignalMapper : public QSignalMapper +{ + Q_OBJECT + +public: + explicit DeviceSignalMapper(QObject *parent = nullptr); + ~DeviceSignalMapper() override; + + void setMapping(QObject *device, const QString &udi); + +Q_SIGNALS: + void deviceChanged(const QString &udi, const QString &property, QVariant value); + +protected: + QMap signalmap; +}; + +class BatterySignalMapper : public DeviceSignalMapper +{ + Q_OBJECT + +public: + explicit BatterySignalMapper(QObject *parent = nullptr); + ~BatterySignalMapper() override; + +public Q_SLOTS: + void chargePercentChanged(int value); + void chargeStateChanged(int newState); + void presentStateChanged(bool newState); +}; + +class StorageAccessSignalMapper : public DeviceSignalMapper +{ + Q_OBJECT + +public: + explicit StorageAccessSignalMapper(QObject *parent = nullptr); + ~StorageAccessSignalMapper() override; + +public Q_SLOTS: + void accessibilityChanged(bool accessible); +}; diff --git a/plasma/workspace/dataengines/soliddevice/hddtemp.cpp b/plasma/workspace/dataengines/soliddevice/hddtemp.cpp new file mode 100644 index 0000000000..6a266b2d20 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/hddtemp.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2007 Petri Damsten + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "hddtemp.h" + +#include + +#include + +#include + +HddTemp::HddTemp(QObject *parent) + : QObject(parent) + , m_failCount(0) + , m_cacheValid(false) +{ + updateData(); +} + +HddTemp::~HddTemp() +{ +} + +QStringList HddTemp::sources() +{ + updateData(); + return m_data.keys(); +} + +void HddTemp::timerEvent(QTimerEvent *event) +{ + killTimer(event->timerId()); + m_cacheValid = false; +} + +bool HddTemp::updateData() +{ + if (m_cacheValid) { + return true; + } + + if (m_failCount > 4) { + return false; + } + + QTcpSocket socket; + QString data; + + socket.connectToHost(QStringLiteral("localhost"), 7634); + if (socket.waitForConnected(500)) { + while (data.length() < 1024) { + if (!socket.waitForReadyRead(500)) { + if (data.length() > 0) { + break; + } else { + // qDebug() << socket.errorString(); + return false; + } + } + data += QString(socket.readAll()); + } + socket.disconnectFromHost(); + // on success retry fail count + m_failCount = 0; + } else { + m_failCount++; + // qDebug() << socket.errorString(); + return false; + } + const QStringList list = data.split('|'); + int i = 1; + m_data.clear(); + while (i + 4 < list.size()) { + m_data[list[i]].append(list[i + 2]); + m_data[list[i]].append(list[i + 3]); + i += 5; + } + m_cacheValid = true; + startTimer(0); + + return true; +} + +QVariant HddTemp::data(const QString source, const DataType type) const +{ + return m_data[source][type]; +} diff --git a/plasma/workspace/dataengines/soliddevice/hddtemp.h b/plasma/workspace/dataengines/soliddevice/hddtemp.h new file mode 100644 index 0000000000..7148c71c8c --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/hddtemp.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2007 Petri Damsten + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class HddTemp : public QObject +{ + Q_OBJECT + +public: + enum DataType { + Temperature = 0, + Unit, + }; + + explicit HddTemp(QObject *parent = nullptr); + ~HddTemp() override; + QStringList sources(); + QVariant data(const QString source, const DataType type) const; + +protected: + void timerEvent(QTimerEvent *event) override; + +private: + int m_failCount; + bool m_cacheValid; + QMap> m_data; + bool updateData(); +}; diff --git a/plasma/workspace/dataengines/soliddevice/plasma-dataengine-soliddevice.json b/plasma/workspace/dataengines/soliddevice/plasma-dataengine-soliddevice.json new file mode 100644 index 0000000000..9e1e96ed3c --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/plasma-dataengine-soliddevice.json @@ -0,0 +1,155 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "wilderkde@gmail.com", + "Name": "The Plasma Team", + "Name[ar]": "فريق بلازما", + "Name[az]": "Plasma komandası", + "Name[ca]": "L'equip del Plasma", + "Name[cs]": "Team Plasma", + "Name[de]": "Das Plasma-Team", + "Name[en_GB]": "The Plasma Team", + "Name[es]": "El equipo de Plasma", + "Name[eu]": "Plasma taldea", + "Name[fi]": "Plasma-työryhmä", + "Name[fr]": "L'équipe de Plasma", + "Name[hu]": "A Plasma fejlesztői", + "Name[ia]": "Le equipa de Plasma", + "Name[it]": "La squadra di Plasma", + "Name[ko]": "Plasma 팀", + "Name[lt]": "Plasma komanda", + "Name[nl]": "Het team van Plasma", + "Name[nn]": "Utviklingslaget for Plasma", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਟੀਮ", + "Name[pl]": "Zespół Plazmy", + "Name[pt_BR]": "Temas do Plasma", + "Name[ro]": "Echipa Plasma", + "Name[ru]": "Команда разработчиков Plasma", + "Name[sk]": "Plasma Tím", + "Name[sl]": "Ekipa Plasme", + "Name[sv]": "Plasma-gruppen", + "Name[ta]": "பிளாஸ்மா குழு", + "Name[tr]": "Plazma Takımı", + "Name[uk]": "Команда розробників Плазми", + "Name[vi]": "Đội Plasma", + "Name[x-test]": "xxThe Plasma Teamxx", + "Name[zh_CN]": "Plasma 开发团队" + } + ], + "Category": "", + "Description": "Device data via Solid", + "Description[ar]": "بيانات الأجهزة عبر Solid", + "Description[az]": "Solid üzərindən cihaz məlumatları", + "Description[ca]": "Dades dels dispositius via Solid", + "Description[cs]": "Data zařízení pomocí Solid", + "Description[de]": "Gerätedaten mittels Solid", + "Description[en_GB]": "Device data via Solid", + "Description[es]": "Datos de dispositivo vía Solid", + "Description[eu]": "Gailuen datuak Solid bidez", + "Description[fi]": "Laitetietoa Solid-rajapinnalta", + "Description[fr]": "Données de périphériques utilisant « Solid »", + "Description[hu]": "Eszközjellemzők a Solid alrendszerből", + "Description[ia]": "Datos de dispositivos via Solid", + "Description[it]": "Dati sul dispositivo con Solid", + "Description[ko]": "Solid를 통한 장치 데이터", + "Description[lt]": "Įrenginių duomenys per Solid", + "Description[nl]": "Apparaatgegevens via Solid", + "Description[nn]": "SolidDevice-data", + "Description[pa]": "ਡਿਵਾਈਸ ਡਾਟਾ ਸਾਲਡ ਵਜੋਂ", + "Description[pl]": "Dane urządzenia przez Solid", + "Description[pt_BR]": "Dados do dispositivo através do Solid", + "Description[ro]": "Date despre dispozitive via Solid", + "Description[ru]": "Сведения об устройствах от Solid", + "Description[sk]": "Dáta zariadenia pomocou Solid", + "Description[sl]": "Podatki o napravah s pomočjo programa Solid", + "Description[sv]": "Enhetsdata via Solid", + "Description[ta]": "Solid மூலம் சாதன விவரங்கள்", + "Description[tr]": "Solid üzerinden aygıt verisi", + "Description[uk]": "Дані щодо пристроїв з Solid", + "Description[vi]": "Dữ liệu về thiết bị thông qua Solid", + "Description[x-test]": "xxDevice data via Solidxx", + "Description[zh_CN]": "通过 Solid 获得设备数据", + "Icon": "drive-harddisk", + "Id": "soliddevice", + "Name": "Device Information", + "Name[ar]": "معلومات الأجهزة", + "Name[az]": "Cihaz haqqında", + "Name[be@latin]": "Źviestki z pryłady", + "Name[bg]": "Данни за устройства", + "Name[bn]": "ডিভাইস তথ্য", + "Name[bn_IN]": "ডিভাইস সংক্রান্ত তথ্য", + "Name[bs]": "Podaci o uređajima", + "Name[ca@valencia]": "Informació dels dispositius", + "Name[ca]": "Informació dels dispositius", + "Name[cs]": "Informace o zařízení", + "Name[csb]": "Wëdowiédzô ò ùrządzeniach", + "Name[da]": "Enhedsinformation", + "Name[de]": "Geräteinformationen", + "Name[el]": "Πληροφορίες συσκευής", + "Name[en_GB]": "Device Information", + "Name[eo]": "Aparataj Informoj", + "Name[es]": "Información del dispositivo", + "Name[et]": "Seadmete teave", + "Name[eu]": "Gailuari buruzko informazioa", + "Name[fi]": "Laitetiedot", + "Name[fr]": "Informations sur les périphériques", + "Name[fy]": "Apparaatynformaasje", + "Name[ga]": "Eolas faoi Ghléas", + "Name[gl]": "Información do dispositivo", + "Name[gu]": "ઉપકરણ માહિતી", + "Name[he]": "מידע על התקנים", + "Name[hi]": "उपकरण जानकारी", + "Name[hne]": "उपकरन जानकारी", + "Name[hr]": "Podaci o uređaju", + "Name[hsb]": "Informacija wo graće", + "Name[hu]": "Eszközjellemzők", + "Name[ia]": "Information de dispositivo", + "Name[id]": "Informasi Perangkat", + "Name[is]": "Upplýsingar tækis", + "Name[it]": "Informazioni sul dispositivo", + "Name[ja]": "デバイス情報", + "Name[kk]": "Құрылғы мәліметі", + "Name[km]": "ព័ត៌មាន​ឧបករណ៍", + "Name[kn]": "ಸಾಧನದ ಮಾಹಿತಿ", + "Name[ko]": "장치 정보", + "Name[ku]": "Agahiya Cîhazê", + "Name[lt]": "Informacija apie įrenginius", + "Name[lv]": "Ierīču informācija", + "Name[mai]": "डिवायस सूचना", + "Name[mk]": "Информации за уреди", + "Name[ml]": "ഉപകരണ വിവരം", + "Name[mr]": "साधन माहिती", + "Name[nb]": "Enhetsinformasjon", + "Name[nds]": "Reedschap-Informatschonen", + "Name[nl]": "Apparaatinformatie", + "Name[nn]": "Einingsinformasjon", + "Name[or]": "ଯନ୍ତ୍ର ସୂଚନା", + "Name[pa]": "ਜੰਤਰ ਜਾਣਕਾਰੀ", + "Name[pl]": "Informacje o urządzeniach", + "Name[pt]": "Informação dos Dispositivos", + "Name[pt_BR]": "Informações do dispositivo", + "Name[ro]": "Informații dispozitiv", + "Name[ru]": "Сведения об устройствах", + "Name[si]": "මෙවලම් තොරතුරු", + "Name[sk]": "Informácie o zariadení", + "Name[sl]": "Podatki o napravah", + "Name[sr@ijekavian]": "подаци о уређајима", + "Name[sr@ijekavianlatin]": "podaci o uređajima", + "Name[sr@latin]": "podaci o uređajima", + "Name[sr]": "подаци о уређајима", + "Name[sv]": "Enhetsinformation", + "Name[ta]": "சாதன விவரங்கள்", + "Name[tg]": "Иттилооти дастгоҳ", + "Name[th]": "รายละเอียดอุปกรณ์", + "Name[tr]": "Aygıt Bilgileri", + "Name[ug]": "ئۈسكۈنە ئۇچۇرى", + "Name[uk]": "Інформація про пристрої", + "Name[vi]": "Thông tin thiết bị", + "Name[wa]": "Informåcions so l' éndjin", + "Name[x-test]": "xxDevice Informationxx", + "Name[zh_CN]": "设备信息", + "Name[zh_TW]": "裝置資訊", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/soliddevice/soliddevice.operations b/plasma/workspace/dataengines/soliddevice/soliddevice.operations new file mode 100644 index 0000000000..49d8c4715f --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddevice.operations @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/soliddevice/soliddeviceengine.cpp b/plasma/workspace/dataengines/soliddevice/soliddeviceengine.cpp new file mode 100644 index 0000000000..43a654e382 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddeviceengine.cpp @@ -0,0 +1,678 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "soliddeviceengine.h" +#include "soliddeviceservice.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +// TODO: implement in libsolid2 +namespace +{ +template +DevIface *getAncestorAs(const Solid::Device &device) +{ + for (Solid::Device parent = device.parent(); parent.isValid(); parent = parent.parent()) { + if (parent.is()) { + return parent.as(); + } + } + return nullptr; +} +} + +SolidDeviceEngine::SolidDeviceEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_temperature(nullptr) + , m_notifier(nullptr) +{ + Q_UNUSED(args) + m_signalmanager = new DeviceSignalMapManager(this); + + listenForNewDevices(); + setMinimumPollingInterval(1000); + connect(this, &Plasma::DataEngine::sourceRemoved, this, &SolidDeviceEngine::sourceWasRemoved); +} + +SolidDeviceEngine::~SolidDeviceEngine() +{ +} + +Plasma::Service *SolidDeviceEngine::serviceForSource(const QString &source) +{ + return new SolidDeviceService(this, source); +} + +void SolidDeviceEngine::listenForNewDevices() +{ + if (m_notifier) { + return; + } + + // detect when new devices are added + m_notifier = Solid::DeviceNotifier::instance(); + connect(m_notifier, &Solid::DeviceNotifier::deviceAdded, this, &SolidDeviceEngine::deviceAdded); + connect(m_notifier, &Solid::DeviceNotifier::deviceRemoved, this, &SolidDeviceEngine::deviceRemoved); +} + +bool SolidDeviceEngine::sourceRequestEvent(const QString &name) +{ + if (name.startsWith('/')) { + Solid::Device device = Solid::Device(name); + if (device.isValid()) { + if (m_devicemap.contains(name)) { + return true; + } else { + m_devicemap[name] = device; + return populateDeviceData(name); + } + } + } else { + Solid::Predicate predicate = Solid::Predicate::fromString(name); + if (predicate.isValid() && !m_predicatemap.contains(name)) { + foreach (const Solid::Device &device, Solid::Device::listFromQuery(predicate)) { + m_predicatemap[name] << device.udi(); + } + + setData(name, m_predicatemap[name]); + return true; + } + } + + qDebug() << "Source is not a predicate or a device."; + return false; +} + +void SolidDeviceEngine::sourceWasRemoved(const QString &source) +{ + m_devicemap.remove(source); + m_predicatemap.remove(source); +} + +bool SolidDeviceEngine::populateDeviceData(const QString &name) +{ + Solid::Device device = m_devicemap.value(name); + if (!device.isValid()) { + return false; + } + + QStringList devicetypes; + setData(name, I18N_NOOP("Parent UDI"), device.parentUdi()); + setData(name, I18N_NOOP("Vendor"), device.vendor()); + setData(name, I18N_NOOP("Product"), device.product()); + setData(name, I18N_NOOP("Description"), device.description()); + setData(name, I18N_NOOP("Icon"), device.icon()); + setData(name, I18N_NOOP("Emblems"), device.emblems()); + setData(name, I18N_NOOP("State"), Idle); + setData(name, I18N_NOOP("Operation result"), Working); + setData(name, I18N_NOOP("Timestamp"), QDateTime::currentDateTimeUtc()); + + if (device.is()) { + Solid::Processor *processor = device.as(); + if (!processor) { + return false; + } + + devicetypes << I18N_NOOP("Processor"); + setData(name, I18N_NOOP("Number"), processor->number()); + setData(name, I18N_NOOP("Max Speed"), processor->maxSpeed()); + setData(name, I18N_NOOP("Can Change Frequency"), processor->canChangeFrequency()); + } + if (device.is()) { + Solid::Block *block = device.as(); + if (!block) { + return false; + } + + devicetypes << I18N_NOOP("Block"); + setData(name, I18N_NOOP("Major"), block->deviceMajor()); + setData(name, I18N_NOOP("Minor"), block->deviceMinor()); + setData(name, I18N_NOOP("Device"), block->device()); + } + if (device.is()) { + Solid::StorageAccess *storageaccess = device.as(); + if (!storageaccess) { + return false; + } + + devicetypes << I18N_NOOP("Storage Access"); + setData(name, I18N_NOOP("Accessible"), storageaccess->isAccessible()); + setData(name, I18N_NOOP("File Path"), storageaccess->filePath()); + + if (storageaccess->isAccessible()) { + updateStorageSpace(name); + } + + m_signalmanager->mapDevice(storageaccess, device.udi()); + } + + if (device.is()) { + Solid::StorageDrive *storagedrive = device.as(); + if (!storagedrive) { + return false; + } + + devicetypes << I18N_NOOP("Storage Drive"); + + QStringList bus; + bus << I18N_NOOP("Ide") << I18N_NOOP("Usb") << I18N_NOOP("Ieee1394") << I18N_NOOP("Scsi") << I18N_NOOP("Sata") << I18N_NOOP("Platform"); + QStringList drivetype; + drivetype << I18N_NOOP("Hard Disk") << I18N_NOOP("Cdrom Drive") << I18N_NOOP("Floppy") << I18N_NOOP("Tape") << I18N_NOOP("Compact Flash") + << I18N_NOOP("Memory Stick") << I18N_NOOP("Smart Media") << I18N_NOOP("SdMmc") << I18N_NOOP("Xd"); + + setData(name, I18N_NOOP("Bus"), bus.at((int)storagedrive->bus())); + setData(name, I18N_NOOP("Drive Type"), drivetype.at((int)storagedrive->driveType())); + setData(name, I18N_NOOP("Removable"), storagedrive->isRemovable()); + setData(name, I18N_NOOP("Hotpluggable"), storagedrive->isHotpluggable()); + + updateHardDiskTemperature(name); + } else { + bool isRemovable = false; + bool isHotpluggable = false; + Solid::StorageDrive *drive = getAncestorAs(device); + if (drive) { + // remove check for isHotpluggable() when plasmoids are changed to check for both properties + isRemovable = (drive->isRemovable() || drive->isHotpluggable()); + isHotpluggable = drive->isHotpluggable(); + } + setData(name, I18N_NOOP("Removable"), isRemovable); + setData(name, I18N_NOOP("Hotpluggable"), isHotpluggable); + } + + if (device.is()) { + Solid::OpticalDrive *opticaldrive = device.as(); + if (!opticaldrive) { + return false; + } + + devicetypes << I18N_NOOP("Optical Drive"); + + QStringList supportedtypes; + Solid::OpticalDrive::MediumTypes mediatypes = opticaldrive->supportedMedia(); + if (mediatypes & Solid::OpticalDrive::Cdr) { + supportedtypes << I18N_NOOP("CD-R"); + } + if (mediatypes & Solid::OpticalDrive::Cdrw) { + supportedtypes << I18N_NOOP("CD-RW"); + } + if (mediatypes & Solid::OpticalDrive::Dvd) { + supportedtypes << I18N_NOOP("DVD"); + } + if (mediatypes & Solid::OpticalDrive::Dvdr) { + supportedtypes << I18N_NOOP("DVD-R"); + } + if (mediatypes & Solid::OpticalDrive::Dvdrw) { + supportedtypes << I18N_NOOP("DVD-RW"); + } + if (mediatypes & Solid::OpticalDrive::Dvdram) { + supportedtypes << I18N_NOOP("DVD-RAM"); + } + if (mediatypes & Solid::OpticalDrive::Dvdplusr) { + supportedtypes << I18N_NOOP("DVD+R"); + } + if (mediatypes & Solid::OpticalDrive::Dvdplusrw) { + supportedtypes << I18N_NOOP("DVD+RW"); + } + if (mediatypes & Solid::OpticalDrive::Dvdplusdl) { + supportedtypes << I18N_NOOP("DVD+DL"); + } + if (mediatypes & Solid::OpticalDrive::Dvdplusdlrw) { + supportedtypes << I18N_NOOP("DVD+DLRW"); + } + if (mediatypes & Solid::OpticalDrive::Bd) { + supportedtypes << I18N_NOOP("BD"); + } + if (mediatypes & Solid::OpticalDrive::Bdr) { + supportedtypes << I18N_NOOP("BD-R"); + } + if (mediatypes & Solid::OpticalDrive::Bdre) { + supportedtypes << I18N_NOOP("BD-RE"); + } + if (mediatypes & Solid::OpticalDrive::HdDvd) { + supportedtypes << I18N_NOOP("HDDVD"); + } + if (mediatypes & Solid::OpticalDrive::HdDvdr) { + supportedtypes << I18N_NOOP("HDDVD-R"); + } + if (mediatypes & Solid::OpticalDrive::HdDvdrw) { + supportedtypes << I18N_NOOP("HDDVD-RW"); + } + setData(name, I18N_NOOP("Supported Media"), supportedtypes); + + setData(name, I18N_NOOP("Read Speed"), opticaldrive->readSpeed()); + setData(name, I18N_NOOP("Write Speed"), opticaldrive->writeSpeed()); + + // the following method return QList so we need to convert it to QList + const QList writespeeds = opticaldrive->writeSpeeds(); + QList variantlist; + foreach (int num, writespeeds) { + variantlist << num; + } + setData(name, I18N_NOOP("Write Speeds"), variantlist); + } + if (device.is()) { + Solid::StorageVolume *storagevolume = device.as(); + if (!storagevolume) { + return false; + } + + devicetypes << I18N_NOOP("Storage Volume"); + + QStringList usagetypes; + usagetypes << i18n("Other") << i18n("Unused") << i18n("File System") << i18n("Partition Table") << i18n("Raid") << i18n("Encrypted"); + + if (usagetypes.count() > storagevolume->usage()) { + setData(name, I18N_NOOP("Usage"), usagetypes.at((int)storagevolume->usage())); + } else { + setData(name, I18N_NOOP("Usage"), i18n("Unknown")); + } + + setData(name, I18N_NOOP("Ignored"), storagevolume->isIgnored()); + setData(name, I18N_NOOP("File System Type"), storagevolume->fsType()); + setData(name, I18N_NOOP("Label"), storagevolume->label()); + setData(name, I18N_NOOP("UUID"), storagevolume->uuid()); + updateInUse(name); + + // Check if the volume is part of an encrypted container + // This needs to trigger an update for the encrypted container volume since + // libsolid cannot notify us when the accessibility of the container changes + Solid::Device encryptedContainer = storagevolume->encryptedContainer(); + if (encryptedContainer.isValid()) { + const QString containerUdi = encryptedContainer.udi(); + setData(name, I18N_NOOP("Encrypted Container"), containerUdi); + m_encryptedContainerMap[name] = containerUdi; + // TODO: compress the calls? + forceUpdateAccessibility(containerUdi); + } + } + if (device.is()) { + Solid::OpticalDisc *opticaldisc = device.as(); + if (!opticaldisc) { + return false; + } + + devicetypes << I18N_NOOP("OpticalDisc"); + + // get the content types + QStringList contenttypelist; + const Solid::OpticalDisc::ContentTypes contenttypes = opticaldisc->availableContent(); + if (contenttypes.testFlag(Solid::OpticalDisc::Audio)) { + contenttypelist << I18N_NOOP("Audio"); + } + if (contenttypes.testFlag(Solid::OpticalDisc::Data)) { + contenttypelist << I18N_NOOP("Data"); + } + if (contenttypes.testFlag(Solid::OpticalDisc::VideoCd)) { + contenttypelist << I18N_NOOP("Video CD"); + } + if (contenttypes.testFlag(Solid::OpticalDisc::SuperVideoCd)) { + contenttypelist << I18N_NOOP("Super Video CD"); + } + if (contenttypes.testFlag(Solid::OpticalDisc::VideoDvd)) { + contenttypelist << I18N_NOOP("Video DVD"); + } + if (contenttypes.testFlag(Solid::OpticalDisc::VideoBluRay)) { + contenttypelist << I18N_NOOP("Video Blu Ray"); + } + setData(name, I18N_NOOP("Available Content"), contenttypelist); + + QStringList disctypes; + disctypes << I18N_NOOP("Unknown Disc Type") << I18N_NOOP("CD Rom") << I18N_NOOP("CD Recordable") << I18N_NOOP("CD Rewritable") << I18N_NOOP("DVD Rom") + << I18N_NOOP("DVD Ram") << I18N_NOOP("DVD Recordable") << I18N_NOOP("DVD Rewritable") << I18N_NOOP("DVD Plus Recordable") + << I18N_NOOP("DVD Plus Rewritable") << I18N_NOOP("DVD Plus Recordable Duallayer") << I18N_NOOP("DVD Plus Rewritable Duallayer") + << I18N_NOOP("Blu Ray Rom") << I18N_NOOP("Blu Ray Recordable") << I18N_NOOP("Blu Ray Rewritable") << I18N_NOOP("HD DVD Rom") + << I18N_NOOP("HD DVD Recordable") << I18N_NOOP("HD DVD Rewritable"); + //+1 because the enum starts at -1 + setData(name, I18N_NOOP("Disc Type"), disctypes.at((int)opticaldisc->discType() + 1)); + setData(name, I18N_NOOP("Appendable"), opticaldisc->isAppendable()); + setData(name, I18N_NOOP("Blank"), opticaldisc->isBlank()); + setData(name, I18N_NOOP("Rewritable"), opticaldisc->isRewritable()); + setData(name, I18N_NOOP("Capacity"), opticaldisc->capacity()); + } + if (device.is()) { + Solid::Camera *camera = device.as(); + if (!camera) { + return false; + } + + devicetypes << I18N_NOOP("Camera"); + + setData(name, I18N_NOOP("Supported Protocols"), camera->supportedProtocols()); + setData(name, I18N_NOOP("Supported Drivers"), camera->supportedDrivers()); + // Cameras are necessarily Removable and Hotpluggable + setData(name, I18N_NOOP("Removable"), true); + setData(name, I18N_NOOP("Hotpluggable"), true); + } + if (device.is()) { + Solid::PortableMediaPlayer *mediaplayer = device.as(); + if (!mediaplayer) { + return false; + } + + devicetypes << I18N_NOOP("Portable Media Player"); + + setData(name, I18N_NOOP("Supported Protocols"), mediaplayer->supportedProtocols()); + setData(name, I18N_NOOP("Supported Drivers"), mediaplayer->supportedDrivers()); + // Portable Media Players are necessarily Removable and Hotpluggable + setData(name, I18N_NOOP("Removable"), true); + setData(name, I18N_NOOP("Hotpluggable"), true); + } + if (device.is()) { + Solid::Battery *battery = device.as(); + if (!battery) { + return false; + } + + devicetypes << I18N_NOOP("Battery"); + + QStringList batterytype; + batterytype << I18N_NOOP("Unknown Battery") << I18N_NOOP("PDA Battery") << I18N_NOOP("UPS Battery") << I18N_NOOP("Primary Battery") + << I18N_NOOP("Mouse Battery") << I18N_NOOP("Keyboard Battery") << I18N_NOOP("Keyboard Mouse Battery") << I18N_NOOP("Camera Battery") + << I18N_NOOP("Phone Battery") << I18N_NOOP("Monitor Battery") << I18N_NOOP("Gaming Input Battery") << I18N_NOOP("Bluetooth Battery"); + + QStringList chargestate; + chargestate << I18N_NOOP("Not Charging") << I18N_NOOP("Charging") << I18N_NOOP("Discharging") << I18N_NOOP("Fully Charged"); + + setData(name, I18N_NOOP("Plugged In"), battery->isPresent()); // FIXME Rename when interested parties are adjusted + setData(name, I18N_NOOP("Type"), batterytype.value((int)battery->type())); + setData(name, I18N_NOOP("Charge Percent"), battery->chargePercent()); + setData(name, I18N_NOOP("Rechargeable"), battery->isRechargeable()); + setData(name, I18N_NOOP("Charge State"), chargestate.at((int)battery->chargeState())); + + m_signalmanager->mapDevice(battery, device.udi()); + } + + using namespace Solid; + // we cannot just iterate the enum in reverse order since Battery comes second to last + // and then our phone which also has a battery gets treated as battery :( + static const Solid::DeviceInterface::Type typeOrder[] = { + Solid::DeviceInterface::PortableMediaPlayer, + Solid::DeviceInterface::Camera, + Solid::DeviceInterface::OpticalDisc, + Solid::DeviceInterface::StorageVolume, + Solid::DeviceInterface::OpticalDrive, + Solid::DeviceInterface::StorageDrive, + Solid::DeviceInterface::NetworkShare, + Solid::DeviceInterface::StorageAccess, + Solid::DeviceInterface::Block, + Solid::DeviceInterface::Battery, + Solid::DeviceInterface::Processor, + }; + + for (int i = 0; i < 11; ++i) { + const Solid::DeviceInterface *interface = device.asDeviceInterface(typeOrder[i]); + if (interface) { + setData(name, I18N_NOOP("Type Description"), Solid::DeviceInterface::typeDescription(typeOrder[i])); + break; + } + } + + setData(name, I18N_NOOP("Device Types"), devicetypes); + return true; +} + +void SolidDeviceEngine::deviceAdded(const QString &udi) +{ + Solid::Device device(udi); + + foreach (const QString &query, m_predicatemap.keys()) { + Solid::Predicate predicate = Solid::Predicate::fromString(query); + if (predicate.matches(device)) { + m_predicatemap[query] << udi; + setData(query, m_predicatemap[query]); + } + } + + if (device.is()) { + Solid::OpticalDrive *drive = getAncestorAs(device); + if (drive) { + connect(drive, &Solid::OpticalDrive::ejectRequested, this, &SolidDeviceEngine::setUnmountingState); + connect(drive, &Solid::OpticalDrive::ejectDone, this, &SolidDeviceEngine::setIdleState); + } + } else if (device.is()) { + // update the volume in case of 2-stage devices + if (m_devicemap.contains(udi) && containerForSource(udi)->data().value(I18N_NOOP("Size")).toULongLong() == 0) { + Solid::GenericInterface *iface = device.as(); + if (iface) { + iface->setProperty("udi", udi); + connect(iface, SIGNAL(propertyChanged(QMap)), this, SLOT(deviceChanged(QMap))); + } + } + + Solid::StorageAccess *access = device.as(); + if (access) { + connect(access, &Solid::StorageAccess::setupRequested, this, &SolidDeviceEngine::setMountingState); + connect(access, &Solid::StorageAccess::setupDone, this, &SolidDeviceEngine::setIdleState); + connect(access, &Solid::StorageAccess::teardownRequested, this, &SolidDeviceEngine::setUnmountingState); + connect(access, &Solid::StorageAccess::teardownDone, this, &SolidDeviceEngine::setIdleState); + } + } +} + +void SolidDeviceEngine::setMountingState(const QString &udi) +{ + setData(udi, I18N_NOOP("State"), Mounting); + setData(udi, I18N_NOOP("Operation result"), Working); +} + +void SolidDeviceEngine::setUnmountingState(const QString &udi) +{ + setData(udi, I18N_NOOP("State"), Unmounting); + setData(udi, I18N_NOOP("Operation result"), Working); +} + +void SolidDeviceEngine::setIdleState(Solid::ErrorType error, QVariant errorData, const QString &udi) +{ + Q_UNUSED(errorData) + + if (error == Solid::NoError) { + setData(udi, I18N_NOOP("Operation result"), Successful); + } else { + setData(udi, I18N_NOOP("Operation result"), Unsuccessful); + } + setData(udi, I18N_NOOP("State"), Idle); + + Solid::Device device = m_devicemap.value(udi); + if (!device.isValid()) { + return; + } + + Solid::StorageAccess *storageaccess = device.as(); + if (!storageaccess) { + return; + } + + setData(udi, I18N_NOOP("Accessible"), storageaccess->isAccessible()); + setData(udi, I18N_NOOP("File Path"), storageaccess->filePath()); +} + +void SolidDeviceEngine::deviceChanged(const QMap &props) +{ + Solid::GenericInterface *iface = qobject_cast(sender()); + if (iface && iface->isValid() && props.contains(QLatin1String("Size")) && iface->property(QStringLiteral("Size")).toInt() > 0) { + const QString udi = qobject_cast(iface)->property("udi").toString(); + if (populateDeviceData(udi)) + forceImmediateUpdateOfAllVisualizations(); + } +} + +bool SolidDeviceEngine::updateStorageSpace(const QString &udi) +{ + Solid::Device device = m_devicemap.value(udi); + + Solid::StorageAccess *storageaccess = device.as(); + if (!storageaccess || !storageaccess->isAccessible()) { + return false; + } + + QString path = storageaccess->filePath(); + if (!m_paths.contains(path)) { + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); + connect(timer, &QTimer::timeout, [path]() { + KNotification::event(KNotification::Error, i18n("Filesystem is not responding"), i18n("Filesystem mounted at '%1' is not responding", path)); + }); + + m_paths.insert(path); + + // create job + KIO::FileSystemFreeSpaceJob *job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(path)); + + // delete later timer + connect(job, &KIO::FileSystemFreeSpaceJob::result, timer, &QTimer::deleteLater); + + // collect and process info + connect(job, &KIO::FileSystemFreeSpaceJob::result, this, [this, timer, path, udi](KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { + timer->stop(); + + if (!job->error()) { + setData(udi, I18N_NOOP("Free Space"), QVariant(available).toDouble()); + setData(udi, I18N_NOOP("Free Space Text"), KFormat().formatByteSize(available)); + setData(udi, I18N_NOOP("Size"), QVariant(size).toDouble()); + setData(udi, I18N_NOOP("Size Text"), KFormat().formatByteSize(size)); + } + + m_paths.remove(path); + }); + + // start timer: after 15 seconds we will get an error + timer->start(15000); + } + + return false; +} + +bool SolidDeviceEngine::updateHardDiskTemperature(const QString &udi) +{ + Solid::Device device = m_devicemap.value(udi); + Solid::Block *block = device.as(); + if (!block) { + return false; + } + + if (!m_temperature) { + m_temperature = new HddTemp(this); + } + + if (m_temperature->sources().contains(block->device())) { + setData(udi, I18N_NOOP("Temperature"), m_temperature->data(block->device(), HddTemp::Temperature)); + setData(udi, I18N_NOOP("Temperature Unit"), m_temperature->data(block->device(), HddTemp::Unit)); + return true; + } + + return false; +} + +bool SolidDeviceEngine::updateEmblems(const QString &udi) +{ + Solid::Device device = m_devicemap.value(udi); + + setData(udi, I18N_NOOP("Emblems"), device.emblems()); + return true; +} + +bool SolidDeviceEngine::forceUpdateAccessibility(const QString &udi) +{ + Solid::Device device = m_devicemap.value(udi); + if (!device.isValid()) { + return false; + } + + updateEmblems(udi); + Solid::StorageAccess *storageaccess = device.as(); + if (storageaccess) { + setData(udi, I18N_NOOP("Accessible"), storageaccess->isAccessible()); + } + + return true; +} + +bool SolidDeviceEngine::updateInUse(const QString &udi) +{ + Solid::Device device = m_devicemap.value(udi); + if (!device.isValid()) { + return false; + } + + Solid::StorageAccess *storageaccess = device.as(); + if (!storageaccess) { + return false; + } + + if (storageaccess->isAccessible()) { + setData(udi, I18N_NOOP("In Use"), true); + } else { + Solid::StorageDrive *drive = getAncestorAs(Solid::Device(udi)); + if (drive) { + setData(udi, I18N_NOOP("In Use"), drive->isInUse()); + } + } + + return true; +} + +bool SolidDeviceEngine::updateSourceEvent(const QString &source) +{ + bool update1 = updateStorageSpace(source); + bool update2 = updateHardDiskTemperature(source); + bool update3 = updateEmblems(source); + bool update4 = updateInUse(source); + + return (update1 || update2 || update3 || update4); +} + +void SolidDeviceEngine::deviceRemoved(const QString &udi) +{ + // libsolid cannot notify us when an encrypted container is closed, + // hence we trigger an update when a device contained in an encrypted container device dies + const QString containerUdi = m_encryptedContainerMap.value(udi, QString()); + + if (!containerUdi.isEmpty()) { + forceUpdateAccessibility(containerUdi); + m_encryptedContainerMap.remove(udi); + } + + foreach (const QString &query, m_predicatemap.keys()) { + m_predicatemap[query].removeAll(udi); + setData(query, m_predicatemap[query]); + } + + Solid::Device device(udi); + if (device.is()) { + Solid::StorageAccess *access = device.as(); + if (access) { + disconnect(access, nullptr, this, nullptr); + } + } else if (device.is()) { + Solid::OpticalDrive *drive = getAncestorAs(device); + if (drive) { + disconnect(drive, nullptr, this, nullptr); + } + } + + m_devicemap.remove(udi); + removeSource(udi); +} + +void SolidDeviceEngine::deviceChanged(const QString &udi, const QString &property, const QVariant &value) +{ + setData(udi, property, value); + updateSourceEvent(udi); +} + +K_PLUGIN_CLASS_WITH_JSON(SolidDeviceEngine, "plasma-dataengine-soliddevice.json") + +#include "soliddeviceengine.moc" diff --git a/plasma/workspace/dataengines/soliddevice/soliddeviceengine.h b/plasma/workspace/dataengines/soliddevice/soliddeviceengine.h new file mode 100644 index 0000000000..ac2002ee2f --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddeviceengine.h @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2007 Christopher Blauvelt + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "devicesignalmapmanager.h" +#include "devicesignalmapper.h" +#include "hddtemp.h" +#include +#include +#include + +enum State { + Idle = 0, + Mounting = 1, + Unmounting = 2, +}; + +enum OperationResult { + Working = 0, + Successful = 1, + Unsuccessful = 2, +}; + +/** + * This class evaluates the basic expressions given in the interface. + */ +class SolidDeviceEngine : public Plasma::DataEngine +{ + Q_OBJECT + friend class SolidDeviceService; + +public: + SolidDeviceEngine(QObject *parent, const QVariantList &args); + ~SolidDeviceEngine() override; + Plasma::Service *serviceForSource(const QString &source) override; + +protected: + bool sourceRequestEvent(const QString &name) override; + bool updateSourceEvent(const QString &source) override; + +private: + bool populateDeviceData(const QString &name); + bool updateStorageSpace(const QString &udi); + bool updateHardDiskTemperature(const QString &udi); + bool updateEmblems(const QString &udi); + bool updateInUse(const QString &udi); + bool forceUpdateAccessibility(const QString &udi); + void listenForNewDevices(); + + // predicate in string form, list of devices by udi + QMap m_predicatemap; + // udi, corresponding device + QMap m_devicemap; + // udi, corresponding encrypted container udi; + QMap m_encryptedContainerMap; + // path, for pending file system free space jobs + QSet m_paths; + DeviceSignalMapManager *m_signalmanager; + + HddTemp *m_temperature; + Solid::DeviceNotifier *m_notifier; + +private Q_SLOTS: + void deviceAdded(const QString &udi); + void deviceRemoved(const QString &udi); + void deviceChanged(const QString &udi, const QString &property, const QVariant &value); + void sourceWasRemoved(const QString &source); + void setMountingState(const QString &udi); + void setUnmountingState(const QString &udi); + void setIdleState(Solid::ErrorType error, QVariant errorData, const QString &udi); + void deviceChanged(const QMap &props); +}; diff --git a/plasma/workspace/dataengines/soliddevice/soliddevicejob.cpp b/plasma/workspace/dataengines/soliddevice/soliddevicejob.cpp new file mode 100644 index 0000000000..3a82766040 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddevicejob.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "soliddevicejob.h" + +#include +#include +#include +#include + +#include + +void SolidDeviceJob::start() +{ + Solid::Device device(m_dest); + QString operation = operationName(); + + if (operation == QLatin1String("mount")) { + if (device.is()) { + Solid::StorageAccess *access = device.as(); + if (access && !access->isAccessible()) { + access->setup(); + } + } + } else if (operation == QLatin1String("unmount")) { + if (device.is()) { + Solid::OpticalDrive *drive = device.as(); + if (!drive) { + drive = device.parent().as(); + } + if (drive) { + drive->eject(); + } + } else if (device.is()) { + Solid::StorageAccess *access = device.as(); + if (access && access->isAccessible()) { + access->teardown(); + } + } + } + + emitResult(); +} diff --git a/plasma/workspace/dataengines/soliddevice/soliddevicejob.h b/plasma/workspace/dataengines/soliddevice/soliddevicejob.h new file mode 100644 index 0000000000..df869a494f --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddevicejob.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "soliddeviceengine.h" + +#include + +class SolidDeviceJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + SolidDeviceJob(SolidDeviceEngine *engine, + const QString &destination, + const QString &operation, + QMap ¶meters, + QObject *parent = nullptr) + : ServiceJob(destination, operation, parameters, parent) + , m_engine(engine) + , m_dest(destination) + { + } + + void start() override; + +private: + SolidDeviceEngine *m_engine; + QString m_dest; +}; diff --git a/plasma/workspace/dataengines/soliddevice/soliddeviceservice.cpp b/plasma/workspace/dataengines/soliddevice/soliddeviceservice.cpp new file mode 100644 index 0000000000..2f937f66a4 --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddeviceservice.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "soliddeviceservice.h" +#include "soliddeviceengine.h" +#include "soliddevicejob.h" + +SolidDeviceService::SolidDeviceService(SolidDeviceEngine *parent, const QString &source) + : Plasma::Service(parent) + , m_engine(parent) +{ + setName(QStringLiteral("soliddevice")); + setDestination(source); +} + +Plasma::ServiceJob *SolidDeviceService::createJob(const QString &operation, QMap ¶meters) +{ + if (operation == QLatin1String("updateFreespace")) { + m_engine->updateStorageSpace(destination()); + return nullptr; + } + + return new SolidDeviceJob(m_engine, destination(), operation, parameters); +} diff --git a/plasma/workspace/dataengines/soliddevice/soliddeviceservice.h b/plasma/workspace/dataengines/soliddevice/soliddeviceservice.h new file mode 100644 index 0000000000..e73e65806d --- /dev/null +++ b/plasma/workspace/dataengines/soliddevice/soliddeviceservice.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2011 Viranch Mehta + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +class SolidDeviceEngine; + +class SolidDeviceService : public Plasma::Service +{ + Q_OBJECT + +public: + SolidDeviceService(SolidDeviceEngine *parent, const QString &source); + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + SolidDeviceEngine *m_engine; + QString m_dest; +}; diff --git a/plasma/workspace/dataengines/statusnotifieritem/CMakeLists.txt b/plasma/workspace/dataengines/statusnotifieritem/CMakeLists.txt new file mode 100644 index 0000000000..05becfbad9 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/CMakeLists.txt @@ -0,0 +1,41 @@ +include_directories(${plasma-workspace_SOURCE_DIR}/statusnotifierwatcher) + +# We add our source code here +set(statusnotifieritem_engine_SRCS + statusnotifieritem_engine.cpp + statusnotifieritemsource.cpp + statusnotifieritemservice.cpp + statusnotifieritemjob.cpp + systemtraytypes.cpp +) + +set(statusnotifierwatcher_xml ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierWatcher.xml) +qt_add_dbus_interface(statusnotifieritem_engine_SRCS ${statusnotifierwatcher_xml} statusnotifierwatcher_interface) +qt_add_dbus_interface(statusnotifieritem_engine_SRCS ../mpris2/org.freedesktop.DBus.Properties.xml dbusproperties) + +set(statusnotifieritem_xml ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierItem.xml) + +set_source_files_properties(${statusnotifieritem_xml} PROPERTIES + NO_NAMESPACE false + INCLUDE "systemtraytypes.h" + CLASSNAME OrgKdeStatusNotifierItem +) +qt_add_dbus_interface(statusnotifieritem_engine_SRCS ${statusnotifieritem_xml} statusnotifieritem_interface) + +ecm_qt_declare_logging_category(statusnotifieritem_engine_SRCS HEADER debug.h + IDENTIFIER DATAENGINE_SNI + CATEGORY_NAME kde.dataengine.sni + DEFAULT_SEVERITY Info) + +kcoreaddons_add_plugin(plasma_engine_statusnotifieritem SOURCES ${statusnotifieritem_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_link_libraries(plasma_engine_statusnotifieritem + Qt::DBus + KF5::Service + KF5::Plasma + KF5::IconThemes + KF5::WindowSystem + dbusmenuqt +) + +install(FILES statusnotifieritem.operations DESTINATION ${PLASMA_DATA_INSTALL_DIR}/services) + diff --git a/plasma/workspace/dataengines/statusnotifieritem/plasma-dataengine-statusnotifieritem.json b/plasma/workspace/dataengines/statusnotifieritem/plasma-dataengine-statusnotifieritem.json new file mode 100644 index 0000000000..1e8ffca202 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/plasma-dataengine-statusnotifieritem.json @@ -0,0 +1,138 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "matthieu_gallien@yahoo.fr", + "Name": "Matthieu Gallien", + "Name[ar]": "Matthieu Gallien", + "Name[az]": "Matthieu Gallien", + "Name[ca]": "Matthieu Gallien", + "Name[cs]": "Matthieu Gallien", + "Name[de]": "Matthieu Gallien", + "Name[en_GB]": "Matthieu Gallien", + "Name[es]": "Matthieu Gallien", + "Name[eu]": "Matthieu Gallien", + "Name[fi]": "Matthieu Gallien", + "Name[fr]": "Matthieu Gallien", + "Name[hu]": "Matthieu Gallien", + "Name[ia]": "Matthieu Gallien", + "Name[it]": "Matthieu Gallien", + "Name[ko]": "Matthieu Gallien", + "Name[lt]": "Matthieu Gallien", + "Name[nl]": "Matthieu Gallien", + "Name[nn]": "Matthieu Gallien", + "Name[pl]": "Matthieu Gallien", + "Name[pt_BR]": "Matthieu Gallien", + "Name[ro]": "Matthieu Gallien", + "Name[ru]": "Matthieu Gallien", + "Name[sk]": "Matthieu Gallien", + "Name[sl]": "Matthieu Gallien", + "Name[sv]": "Matthieu Gallien", + "Name[tr]": "Matthieu Gallien", + "Name[uk]": "Matthieu Gallien", + "Name[vi]": "Matthieu Gallien", + "Name[x-test]": "xxMatthieu Gallienxx", + "Name[zh_CN]": "Matthieu Gallien" + } + ], + "Description": "Engine for applications' status information, based on the Status Notifier protocol.", + "Description[ar]": "محرّك لمعلومات حالة التطبيقات، بناءً على ميفاق مُخطِر الحالة.", + "Description[az]": "Tətbiqlərin vəziyyəti haqqında, vəziyyət Bildirişi protokoluna əsaslanan məlumatlar mənbəyi.", + "Description[ca]": "Motor d'informació d'estat de les aplicacions, basat en el protocol notificador d'estat.", + "Description[cs]": "Stroj pro informace o stavu aplikací, založen na protokolu Status Notifier", + "Description[de]": "Treiber für Status-Informationen von Anwendungen, basierend auf dem Status-Benachrichtigung-Protokoll.", + "Description[en_GB]": "Engine for applications' status information, based on the Status Notifier protocol.", + "Description[es]": "Motor para informar del estado de aplicaciones, basado en el protocolo Status Notifier.", + "Description[eu]": "Aplikazioen egoerari buruzko informazioa emateko motorra, egoera-jakinarazlearen protokoloan oinarritua.", + "Description[fi]": "Tilailmoitinyhteyskäytäntöön perustuva sovellusten tilatietomoottori.", + "Description[fr]": "Moteur d'informations sur l'état des applications utilisant le protocole de notification d'état.", + "Description[hu]": "Az állaporértesítés protokollon alapuló modul az alkalmazások állapotértesítési információihoz.", + "Description[ia]": "Motor pro information de stato de applicationes basate sur le protocollo de Notificator de Stato", + "Description[it]": "Motore per le informazioni di stato delle applicazioni, basato sul protocollo del notificatore di stato.", + "Description[ko]": "상태 알림 프로토콜을 사용하는 프로그램 상태 정보 엔진입니다.", + "Description[lt]": "Informacijos apie programų būseną variklis, pagrįstas būsenos pranešėjo protokolu.", + "Description[nl]": "Engine voor statusinformatie van toepassingen gebaseerde op het statusnotificatieprotocol.", + "Description[nn]": "Motor for programstatusinformasjon basert på Status Notifier-protokollen.", + "Description[pa]": "ਹਾਲਤ ਨੋਟੀਫਾਇਰ ਪਰੋਟੋਕਾਲ ਉੱਤੇ ਅਧਾਰਿਤ ਐਪਲੀਕੇਸ਼ਨ ਹਾਲਤ ਜਾਣਕਾਰੀ ਲਈ ਇੰਜਣ।", + "Description[pl]": "Silnik do przekazywania informacji o stanie programów, oparty o protokół powiadomień o stanie.", + "Description[pt_BR]": "Mecanismo para informação de status de aplicativos baseado no protocolo do Notificador de Status.", + "Description[ro]": "Motor pentru informațiile de stare ale aplicațiilor, bazat pe protocolul Notificator de Stare.", + "Description[ru]": "Источник данных о состоянии программ, основанный на протоколе уведомлений о состоянии.", + "Description[sk]": "Nástroj pre informácie o stave aplikácie, založený na protokole o oznámení stavu.", + "Description[sl]": "Pogon za podatke o stanju programov, ki temelji na protokolu obvestilnika o stanju.", + "Description[sv]": "Gränssnitt för statusinformation om program baserat på protokollet för statusunderrättelser.", + "Description[ta]": "செயலிகளின் நிலை குறித்த விவரங்களை Status Notifier (SNI) நெறிமுறை மூலம் வழங்கும்.", + "Description[tr]": "Uygulamaların durum bilgisi için motor, Durum Bildiricisi protokolü temelli.", + "Description[uk]": "Рушій даних щодо стану програм, заснований на протоколі сповіщення про стан.", + "Description[vi]": "Dụng cụ cấp thông tin về trạng thái ứng dụng, dựa trên giao thức \"Trình thông báo trạng thái\".", + "Description[x-test]": "xxEngine for applications' status information, based on the Status Notifier protocol.xx", + "Description[zh_CN]": "用于提供应用程序状态信息的引擎,基于状态通知协议。", + "EnabledByDefault": true, + "Icon": "preferences-desktop-notification", + "Id": "statusnotifieritem", + "License": "GPL", + "Name": "Status Notifier Information", + "Name[ar]": "معلومات مُخطِر الحالة", + "Name[ast]": "Información del avisador d'estaos", + "Name[az]": "Vəziyyət haqqında bildiriş məlumatları", + "Name[bs]": "Podaci izveštavača o stanju", + "Name[ca@valencia]": "Informació del notificador d'estats", + "Name[ca]": "Informació del notificador d'estats", + "Name[cs]": "Informace o upozornění stavu", + "Name[da]": "Information om statusbekendtgørelse", + "Name[de]": "Status-Informationen", + "Name[el]": "Πληροφορίες για τις ειδοποιήσεις κατάστασης", + "Name[en_GB]": "Status Notifier Information", + "Name[es]": "Información de notificación de estado", + "Name[et]": "Oleku märguandja teave", + "Name[eu]": "Egoera-jakinarazleari buruzko informazioa", + "Name[fi]": "Järjestelmäilmoittimen tiedot", + "Name[fr]": "Informations sur le notificateur d'état", + "Name[gl]": "Información do notificador do estado", + "Name[he]": "מידע מ־Status Notifier", + "Name[hi]": "तंत्र सूचक जानकारी", + "Name[hr]": "Informacije glasnika stanja", + "Name[hu]": "Állaportértesítési információk", + "Name[ia]": "Informationes de notificator de stato", + "Name[id]": "Informasi Penotifikasi Status", + "Name[is]": "Stöðutilkynningaþjónn", + "Name[it]": "Informazioni sul notificatore di stato", + "Name[ja]": "ステータス通知情報", + "Name[kk]": "Күй-жайы туралы құлақтандыру мәліметі", + "Name[km]": "ព័ត៌មាន​កម្មវិធី​ជំនួន​ដំណឹង​ស្ថានភាព", + "Name[kn]": "ಸ್ಥಿತಿ ಸೂಚನಾ ಮಾಹಿತಿ", + "Name[ko]": "장치 알림이 정보", + "Name[lt]": "Būsenos pranešėjo informacija", + "Name[lv]": "Statusa ziņotāja informācija", + "Name[ml]": "സ്ഥിതിവിവര അറിയിപ്പിന്റെ വിവരങ്ങള്‍", + "Name[mr]": "स्थिती निदर्शक माहिती", + "Name[nb]": "StatusNotifier-opplysninger", + "Name[nds]": "Statusbescheed-Informatschonen", + "Name[nl]": "Melding van statusinformatie", + "Name[nn]": "Status Notifier-informasjon", + "Name[pa]": "ਹਾਲਤ ਨੋਟੀਫਾਇਰ ਜਾਣਕਾਰੀ", + "Name[pl]": "Informacja o powiadomieniach stanu", + "Name[pt]": "Informações do Item de Notificação do Estado", + "Name[pt_BR]": "Informações do Notificador de Status", + "Name[ro]": "Informații notificator de stare", + "Name[ru]": "Сведения уведомлений о состоянии", + "Name[si]": "තත්ව දැනුම්දෙන්නාගේ තොරතුරු", + "Name[sk]": "Informácie o oznámení stavu", + "Name[sl]": "Informacije obvestilnika o stanju", + "Name[sr@ijekavian]": "Подаци извјештавача о стању", + "Name[sr@ijekavianlatin]": "Podaci izvještavača o stanju", + "Name[sr@latin]": "Podaci izveštavača o stanju", + "Name[sr]": "Подаци извештавача о стању", + "Name[sv]": "Information från statusunderrättelser", + "Name[ta]": "நிலை அறிவிப்பான்", + "Name[th]": "ข้อมูลการแจ้งสถานะ", + "Name[tr]": "Durum Bildirici Bilgileri", + "Name[ug]": "ھالەت بىلدۈرگۈ ئۇچۇرى", + "Name[uk]": "Відомості сповіщення про стан", + "Name[vi]": "Thông tin Trình thông báo trạng thái", + "Name[wa]": "Informåcion do notifieu d' sitatut", + "Name[x-test]": "xxStatus Notifier Informationxx", + "Name[zh_CN]": "状态通知信息", + "Name[zh_TW]": "狀態通知器資訊" + } +} diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem.operations b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem.operations new file mode 100644 index 0000000000..4515850c40 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem.operations @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.cpp b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.cpp new file mode 100644 index 0000000000..55b480abfd --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.cpp @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusnotifieritem_engine.h" +#include "statusnotifieritemsource.h" +#include + +#include "dbusproperties.h" + +#include "debug.h" +#include + +static const QString s_watcherServiceName(QStringLiteral("org.kde.StatusNotifierWatcher")); + +StatusNotifierItemEngine::StatusNotifierItemEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , m_statusNotifierWatcher(nullptr) +{ + Q_UNUSED(args); + init(); +} + +StatusNotifierItemEngine::~StatusNotifierItemEngine() +{ + QDBusConnection::sessionBus().unregisterService(m_serviceName); +} + +Plasma::Service *StatusNotifierItemEngine::serviceForSource(const QString &name) +{ + StatusNotifierItemSource *source = dynamic_cast(containerForSource(name)); + // if source does not exist, return null service + if (!source) { + return Plasma::DataEngine::serviceForSource(name); + } + + Plasma::Service *service = source->createService(); + service->setParent(this); + return service; +} + +void StatusNotifierItemEngine::init() +{ + if (QDBusConnection::sessionBus().isConnected()) { + m_serviceName = "org.kde.StatusNotifierHost-" + QString::number(QCoreApplication::applicationPid()); + QDBusConnection::sessionBus().registerService(m_serviceName); + + QDBusServiceWatcher *watcher = + new QDBusServiceWatcher(s_watcherServiceName, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &StatusNotifierItemEngine::serviceChange); + + registerWatcher(s_watcherServiceName); + } +} + +void StatusNotifierItemEngine::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + qCDebug(DATAENGINE_SNI) << "Service" << name << "status change, old owner:" << oldOwner << "new:" << newOwner; + + if (newOwner.isEmpty()) { + // unregistered + unregisterWatcher(name); + } else if (oldOwner.isEmpty()) { + // registered + registerWatcher(name); + } +} + +void StatusNotifierItemEngine::registerWatcher(const QString &service) +{ + // qCDebug(DATAENGINE_SNI)<<"service appeared"<isValid()) { + m_statusNotifierWatcher->call(QDBus::NoBlock, QStringLiteral("RegisterStatusNotifierHost"), m_serviceName); + + OrgFreedesktopDBusPropertiesInterface propetriesIface(m_statusNotifierWatcher->service(), + m_statusNotifierWatcher->path(), + m_statusNotifierWatcher->connection()); + + QDBusPendingReply pendingItems = propetriesIface.Get(m_statusNotifierWatcher->interface(), "RegisteredStatusNotifierItems"); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingItems, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() { + watcher->deleteLater(); + QDBusReply reply = *watcher; + QStringList registeredItems = reply.value().variant().toStringList(); + foreach (const QString &service, registeredItems) { + newItem(service); + } + }); + + connect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemRegistered, + this, + &StatusNotifierItemEngine::serviceRegistered); + connect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemUnregistered, + this, + &StatusNotifierItemEngine::serviceUnregistered); + + } else { + delete m_statusNotifierWatcher; + m_statusNotifierWatcher = nullptr; + qCDebug(DATAENGINE_SNI) << "System tray daemon not reachable"; + } + } +} + +void StatusNotifierItemEngine::unregisterWatcher(const QString &service) +{ + if (service == s_watcherServiceName) { + qCDebug(DATAENGINE_SNI) << s_watcherServiceName << "disappeared"; + + disconnect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemRegistered, + this, + &StatusNotifierItemEngine::serviceRegistered); + disconnect(m_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierItemUnregistered, + this, + &StatusNotifierItemEngine::serviceUnregistered); + + removeAllSources(); + + delete m_statusNotifierWatcher; + m_statusNotifierWatcher = nullptr; + } +} + +void StatusNotifierItemEngine::serviceRegistered(const QString &service) +{ + qCDebug(DATAENGINE_SNI) << "Registering" << service; + newItem(service); +} + +void StatusNotifierItemEngine::serviceUnregistered(const QString &service) +{ + removeSource(service); +} + +void StatusNotifierItemEngine::newItem(const QString &service) +{ + StatusNotifierItemSource *itemSource = new StatusNotifierItemSource(service, this); + addSource(itemSource); +} + +K_PLUGIN_CLASS_WITH_JSON(StatusNotifierItemEngine, "plasma-dataengine-statusnotifieritem.json") + +#include "statusnotifieritem_engine.moc" diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.h b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.h new file mode 100644 index 0000000000..4576b75ea1 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritem_engine.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "statusnotifierwatcher_interface.h" +#include +#include +#include + +// Define our plasma Runner +class StatusNotifierItemEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + // Basic Create/Destroy + StatusNotifierItemEngine(QObject *parent, const QVariantList &args); + ~StatusNotifierItemEngine() override; + Plasma::Service *serviceForSource(const QString &name) override; + +protected: + virtual void init(); + void newItem(const QString &service); + +protected Q_SLOTS: + void serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner); + void registerWatcher(const QString &service); + void unregisterWatcher(const QString &service); + void serviceRegistered(const QString &service); + void serviceUnregistered(const QString &service); + +private: + org::kde::StatusNotifierWatcher *m_statusNotifierWatcher; + QString m_serviceName; + static const int s_protocolVersion = 0; +}; diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.cpp b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.cpp new file mode 100644 index 0000000000..01b74d7c39 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.cpp @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "statusnotifieritemjob.h" +#include + +StatusNotifierItemJob::StatusNotifierItemJob(StatusNotifierItemSource *source, const QString &operation, QMap ¶meters, QObject *parent) + : ServiceJob(source->objectName(), operation, parameters, parent) + , m_source(source) +{ + // Queue connection, so that all 'deleteLater' are performed before we use updated menu. + connect(source, SIGNAL(contextMenuReady(QMenu *)), this, SLOT(contextMenuReady(QMenu *)), Qt::QueuedConnection); + connect(source, &StatusNotifierItemSource::activateResult, this, &StatusNotifierItemJob::activateCallback); +} + +StatusNotifierItemJob::~StatusNotifierItemJob() +{ +} + +void StatusNotifierItemJob::start() +{ + if (operationName() == QLatin1String("Scroll")) { + performJob(); + return; + } + + QWindow *window = nullptr; + const quint32 launchedSerial = KWindowSystem::lastInputSerial(window); + connect(KWindowSystem::self(), &KWindowSystem::xdgActivationTokenArrived, this, [this, launchedSerial](quint32 serial, const QString &token) { + if (serial == launchedSerial) { + m_source->provideXdgActivationToken(token); + performJob(); + } + }); + KWindowSystem::requestXdgActivationToken(window, launchedSerial, {}); +} + +void StatusNotifierItemJob::performJob() +{ + if (operationName() == QString::fromLatin1("Activate")) { + m_source->activate(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + } else if (operationName() == QString::fromLatin1("SecondaryActivate")) { + m_source->secondaryActivate(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + setResult(0); + } else if (operationName() == QString::fromLatin1("ContextMenu")) { + m_source->contextMenu(parameters()[QStringLiteral("x")].toInt(), parameters()[QStringLiteral("y")].toInt()); + } else if (operationName() == QString::fromLatin1("Scroll")) { + m_source->scroll(parameters()[QStringLiteral("delta")].toInt(), parameters()[QStringLiteral("direction")].toString()); + setResult(0); + } +} + +void StatusNotifierItemJob::activateCallback(bool success) +{ + if (operationName() == QString::fromLatin1("Activate")) { + setResult(QVariant(success)); + } +} + +void StatusNotifierItemJob::contextMenuReady(QMenu *menu) +{ + if (operationName() == QString::fromLatin1("ContextMenu")) { + setResult(QVariant::fromValue((QObject *)menu)); + } +} diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.h b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.h new file mode 100644 index 0000000000..142d881e7a --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemjob.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// Qt +#include + +// own +#include "statusnotifieritemsource.h" + +// plasma +#include + +/** + * Task Job + */ +class StatusNotifierItemJob : public Plasma::ServiceJob +{ + Q_OBJECT + +public: + StatusNotifierItemJob(StatusNotifierItemSource *source, const QString &operation, QMap ¶meters, QObject *parent = nullptr); + ~StatusNotifierItemJob() override; + +protected: + void start() override; + +private Q_SLOTS: + void activateCallback(bool success); + void contextMenuReady(QMenu *menu); + +private: + void performJob(); + StatusNotifierItemSource *m_source; +}; diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.cpp b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.cpp new file mode 100644 index 0000000000..0665c05b9a --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "statusnotifieritemservice.h" + +// own +#include "statusnotifieritemjob.h" + +StatusNotifierItemService::StatusNotifierItemService(StatusNotifierItemSource *source) + : Plasma::Service(source) + , m_source(source) +{ + setName(QStringLiteral("statusnotifieritem")); +} + +StatusNotifierItemService::~StatusNotifierItemService() +{ +} + +Plasma::ServiceJob *StatusNotifierItemService::createJob(const QString &operation, QMap ¶meters) +{ + return new StatusNotifierItemJob(m_source, operation, parameters, this); +} diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.h b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.h new file mode 100644 index 0000000000..59bcf24088 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemservice.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2008 Alain Boyer + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +// own +#include "statusnotifieritemsource.h" + +// plasma +#include +#include + +/** + * StatusNotifierItem Service + */ +class StatusNotifierItemService : public Plasma::Service +{ + Q_OBJECT + +public: + explicit StatusNotifierItemService(StatusNotifierItemSource *source); + ~StatusNotifierItemService() override; + +protected: + Plasma::ServiceJob *createJob(const QString &operation, QMap ¶meters) override; + +private: + StatusNotifierItemSource *m_source; +}; diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.cpp b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.cpp new file mode 100644 index 0000000000..ab1fdc51be --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.cpp @@ -0,0 +1,536 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusnotifieritemsource.h" +#include "statusnotifieritem_interface.h" +#include "statusnotifieritemservice.h" +#include "systemtraytypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +class PlasmaDBusMenuImporter : public DBusMenuImporter +{ +public: + PlasmaDBusMenuImporter(const QString &service, const QString &path, KIconLoader *iconLoader, QObject *parent) + : DBusMenuImporter(service, path, parent) + , m_iconLoader(iconLoader) + { + } + +protected: + QIcon iconForName(const QString &name) override + { + return QIcon(new KIconEngine(name, m_iconLoader)); + } + +private: + KIconLoader *m_iconLoader; +}; + +StatusNotifierItemSource::StatusNotifierItemSource(const QString ¬ifierItemId, QObject *parent) + : Plasma::DataContainer(parent) + , m_customIconLoader(nullptr) + , m_menuImporter(nullptr) + , m_refreshing(false) + , m_needsReRefreshing(false) + , m_titleUpdate(true) + , m_iconUpdate(true) + , m_tooltipUpdate(true) + , m_statusUpdate(true) +{ + setObjectName(notifierItemId); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + m_typeId = notifierItemId; + m_name = notifierItemId; + + // set the initial values for all the things + // this is important as Plasma::DataModel has an unsolvable bug + // when it gets data with a new key it tries to update the QAIM roleNames + // from QML this achieves absolutely nothing as there is no signal to tell QQmlDelegateModel to reload the roleNames in QQmlAdapatorModel + // no matter if the row changes or the model refreshes + // this means it does not re-evaluate what bindings exist (watchedRoleIds) - and we get properties that don't bind and thus system tray icons + + // by setting everything up-front so that we have all role names when we call the first checkForUpdate() + setData(QStringLiteral("AttentionIcon"), QIcon()); + setData(QStringLiteral("AttentionIconName"), QString()); + setData(QStringLiteral("AttentionMovieName"), QString()); + setData(QStringLiteral("Category"), QString()); + setData(QStringLiteral("Icon"), QIcon()); + setData(QStringLiteral("IconName"), QString()); + setData(QStringLiteral("IconsChanged"), false); + setData(QStringLiteral("IconThemePath"), QString()); + setData(QStringLiteral("Id"), QString()); + setData(QStringLiteral("ItemIsMenu"), false); + setData(QStringLiteral("OverlayIconName"), QString()); + setData(QStringLiteral("StatusChanged"), false); + setData(QStringLiteral("Status"), QString()); + setData(QStringLiteral("TitleChanged"), false); + setData(QStringLiteral("Title"), QString()); + setData(QStringLiteral("ToolTipChanged"), false); + setData(QStringLiteral("ToolTipIcon"), QString()); + setData(QStringLiteral("ToolTipSubTitle"), QString()); + setData(QStringLiteral("ToolTipTitle"), QString()); + setData(QStringLiteral("WindowId"), QVariant()); + + int slash = notifierItemId.indexOf('/'); + if (slash == -1) { + qWarning() << "Invalid notifierItemId:" << notifierItemId; + m_valid = false; + m_statusNotifierItemInterface = nullptr; + return; + } + QString service = notifierItemId.left(slash); + QString path = notifierItemId.mid(slash); + + m_statusNotifierItemInterface = new org::kde::StatusNotifierItem(service, path, QDBusConnection::sessionBus(), this); + + m_refreshTimer.setSingleShot(true); + m_refreshTimer.setInterval(10); + connect(&m_refreshTimer, &QTimer::timeout, this, &StatusNotifierItemSource::performRefresh); + + m_valid = !service.isEmpty() && m_statusNotifierItemInterface->isValid(); + if (m_valid) { + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewTitle, this, &StatusNotifierItemSource::refreshTitle); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewAttentionIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewOverlayIcon, this, &StatusNotifierItemSource::refreshIcons); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewToolTip, this, &StatusNotifierItemSource::refreshToolTip); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewStatus, this, &StatusNotifierItemSource::syncStatus); + connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewMenu, this, &StatusNotifierItemSource::refreshMenu); + refresh(); + } +} + +StatusNotifierItemSource::~StatusNotifierItemSource() +{ + delete m_statusNotifierItemInterface; +} + +KIconLoader *StatusNotifierItemSource::iconLoader() const +{ + return m_customIconLoader ? m_customIconLoader : KIconLoader::global(); +} + +Plasma::Service *StatusNotifierItemSource::createService() +{ + return new StatusNotifierItemService(this); +} + +void StatusNotifierItemSource::syncStatus(QString status) +{ + setData(QStringLiteral("TitleChanged"), false); + setData(QStringLiteral("IconsChanged"), false); + setData(QStringLiteral("TooltipChanged"), false); + setData(QStringLiteral("StatusChanged"), true); + setData(QStringLiteral("Status"), status); + checkForUpdate(); +} + +void StatusNotifierItemSource::refreshTitle() +{ + m_titleUpdate = true; + refresh(); +} + +void StatusNotifierItemSource::refreshIcons() +{ + m_iconUpdate = true; + refresh(); +} + +void StatusNotifierItemSource::refreshToolTip() +{ + m_tooltipUpdate = true; + refresh(); +} + +void StatusNotifierItemSource::refreshMenu() +{ + if (m_menuImporter) { + delete m_menuImporter; + m_menuImporter = nullptr; + } + refresh(); +} + +void StatusNotifierItemSource::refresh() +{ + if (!m_refreshTimer.isActive()) { + m_refreshTimer.start(); + } +} + +void StatusNotifierItemSource::performRefresh() +{ + if (m_refreshing) { + m_needsReRefreshing = true; + return; + } + + m_refreshing = true; + QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(), + m_statusNotifierItemInterface->path(), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("GetAll")); + + message << m_statusNotifierItemInterface->interface(); + QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::refreshCallback); +} + +/** + \todo add a smart pointer to guard call and to automatically delete it at the end of the function + */ +void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call) +{ + m_refreshing = false; + if (m_needsReRefreshing) { + m_needsReRefreshing = false; + performRefresh(); + call->deleteLater(); + return; + } + + QDBusPendingReply reply = *call; + if (reply.isError()) { + m_valid = false; + } else { + // record what has changed + setData(QStringLiteral("TitleChanged"), m_titleUpdate); + m_titleUpdate = false; + setData(QStringLiteral("IconsChanged"), m_iconUpdate); + m_iconUpdate = false; + setData(QStringLiteral("ToolTipChanged"), m_tooltipUpdate); + m_tooltipUpdate = false; + setData(QStringLiteral("StatusChanged"), m_statusUpdate); + m_statusUpdate = false; + + // IconThemePath (handle this one first, because it has an impact on + // others) + QVariantMap properties = reply.argumentAt<0>(); + QString path = properties[QStringLiteral("IconThemePath")].toString(); + + if (!path.isEmpty() && path != data()[QStringLiteral("IconThemePath")].toString()) { + if (!m_customIconLoader) { + m_customIconLoader = new KIconLoader(QString(), QStringList(), this); + } + // FIXME: If last part of path is not "icons", this won't work! + QString appName; + auto tokens = path.splitRef('/', Qt::SkipEmptyParts); + if (tokens.length() >= 3 && tokens.takeLast() == QLatin1String("icons")) + appName = tokens.takeLast().toString(); + + // icons may be either in the root directory of the passed path or in a appdir format + // i.e hicolor/32x32/iconname.png + + m_customIconLoader->reconfigure(appName, QStringList(path)); + + // add app dir requires an app name, though this is completely unused in this context + m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path); + + connect(m_customIconLoader, &KIconLoader::iconChanged, this, [=] { + m_customIconLoader->reconfigure(appName, QStringList(path)); + m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path); + }); + } + setData(QStringLiteral("IconThemePath"), path); + + setData(QStringLiteral("Category"), properties[QStringLiteral("Category")]); + setData(QStringLiteral("Status"), properties[QStringLiteral("Status")]); + setData(QStringLiteral("Title"), properties[QStringLiteral("Title")]); + setData(QStringLiteral("Id"), properties[QStringLiteral("Id")]); + setData(QStringLiteral("WindowId"), properties[QStringLiteral("WindowId")]); + setData(QStringLiteral("ItemIsMenu"), properties[QStringLiteral("ItemIsMenu")]); + + // Attention Movie + setData(QStringLiteral("AttentionMovieName"), properties[QStringLiteral("AttentionMovieName")]); + + QIcon overlay; + QStringList overlayNames; + + // Icon + { + KDbusImageVector image; + QIcon icon; + QString iconName; + + properties[QStringLiteral("OverlayIconPixmap")].value() >> image; + if (image.isEmpty()) { + QString iconName = properties[QStringLiteral("OverlayIconName")].toString(); + setData(QStringLiteral("OverlayIconName"), iconName); + if (!iconName.isEmpty()) { + overlayNames << iconName; + overlay = QIcon(new KIconEngine(iconName, iconLoader())); + } + } else { + overlay = imageVectorToPixmap(image); + } + + properties[QStringLiteral("IconPixmap")].value() >> image; + if (image.isEmpty()) { + iconName = properties[QStringLiteral("IconName")].toString(); + if (!iconName.isEmpty()) { + icon = QIcon(new KIconEngine(iconName, iconLoader(), overlayNames)); + + if (overlayNames.isEmpty() && !overlay.isNull()) { + overlayIcon(&icon, &overlay); + } + } + } else { + icon = imageVectorToPixmap(image); + if (!icon.isNull() && !overlay.isNull()) { + overlayIcon(&icon, &overlay); + } + } + setData(QStringLiteral("Icon"), icon); + setData(QStringLiteral("IconName"), iconName); + } + + // Attention icon + { + KDbusImageVector image; + QIcon attentionIcon; + + properties[QStringLiteral("AttentionIconPixmap")].value() >> image; + if (image.isEmpty()) { + QString iconName = properties[QStringLiteral("AttentionIconName")].toString(); + setData(QStringLiteral("AttentionIconName"), iconName); + if (!iconName.isEmpty()) { + attentionIcon = QIcon(new KIconEngine(iconName, iconLoader(), overlayNames)); + + if (overlayNames.isEmpty() && !overlay.isNull()) { + overlayIcon(&attentionIcon, &overlay); + } + } + } else { + attentionIcon = imageVectorToPixmap(image); + if (!attentionIcon.isNull() && !overlay.isNull()) { + overlayIcon(&attentionIcon, &overlay); + } + } + setData(QStringLiteral("AttentionIcon"), attentionIcon); + } + + // ToolTip + { + KDbusToolTipStruct toolTip; + properties[QStringLiteral("ToolTip")].value() >> toolTip; + if (toolTip.title.isEmpty()) { + setData(QStringLiteral("ToolTipTitle"), QString()); + setData(QStringLiteral("ToolTipSubTitle"), QString()); + setData(QStringLiteral("ToolTipIcon"), QString()); + } else { + QIcon toolTipIcon; + if (toolTip.image.size() == 0) { + toolTipIcon = QIcon(new KIconEngine(toolTip.icon, iconLoader())); + } else { + toolTipIcon = imageVectorToPixmap(toolTip.image); + } + setData(QStringLiteral("ToolTipTitle"), toolTip.title); + setData(QStringLiteral("ToolTipSubTitle"), toolTip.subTitle); + if (toolTipIcon.isNull() || toolTipIcon.availableSizes().isEmpty()) { + setData(QStringLiteral("ToolTipIcon"), QString()); + } else { + setData(QStringLiteral("ToolTipIcon"), toolTipIcon); + } + } + } + + // Menu + if (!m_menuImporter) { + QString menuObjectPath = properties[QStringLiteral("Menu")].value().path(); + if (!menuObjectPath.isEmpty()) { + if (menuObjectPath == QLatin1String("/NO_DBUSMENU")) { + // This is a hack to make it possible to disable DBusMenu in an + // application. The string "/NO_DBUSMENU" must be the same as in + // KStatusNotifierItem::setContextMenu(). + qWarning() << "DBusMenu disabled for this application"; + } else { + m_menuImporter = new PlasmaDBusMenuImporter(m_statusNotifierItemInterface->service(), menuObjectPath, iconLoader(), this); + connect(m_menuImporter, &PlasmaDBusMenuImporter::menuUpdated, this, [this](QMenu *menu) { + if (menu == m_menuImporter->menu()) { + contextMenuReady(); + } + }); + } + } + } + } + + checkForUpdate(); + call->deleteLater(); +} + +void StatusNotifierItemSource::contextMenuReady() +{ + Q_EMIT contextMenuReady(m_menuImporter->menu()); +} + +QPixmap StatusNotifierItemSource::KDbusImageStructToPixmap(const KDbusImageStruct &image) const +{ + // swap from network byte order if we are little endian + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { + uint *uintBuf = (uint *)image.data.data(); + for (uint i = 0; i < image.data.size() / sizeof(uint); ++i) { + *uintBuf = ntohl(*uintBuf); + ++uintBuf; + } + } + if (image.width == 0 || image.height == 0) { + return QPixmap(); + } + + // avoid a deep copy of the image data + // we need to keep a reference to the image.data alive for the lifespan of the image, even if the image is copied + // we create a new QByteArray with a shallow copy of the original data on the heap, then delete this in the QImage cleanup + auto dataRef = new QByteArray(image.data); + + QImage iconImage( + reinterpret_cast(dataRef->data()), + image.width, + image.height, + QImage::Format_ARGB32, + [](void *ptr) { + delete static_cast(ptr); + }, + dataRef); + return QPixmap::fromImage(iconImage); +} + +QIcon StatusNotifierItemSource::imageVectorToPixmap(const KDbusImageVector &vector) const +{ + QIcon icon; + + for (int i = 0; i < vector.size(); ++i) { + icon.addPixmap(KDbusImageStructToPixmap(vector[i])); + } + + return icon; +} + +void StatusNotifierItemSource::overlayIcon(QIcon *icon, QIcon *overlay) +{ + QIcon tmp; + QPixmap m_iconPixmap = icon->pixmap(KIconLoader::SizeSmall, KIconLoader::SizeSmall); + + QPainter p(&m_iconPixmap); + + const int size = KIconLoader::SizeSmall / 2; + p.drawPixmap(QRect(size, size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + + // if an m_icon exactly that size wasn't found don't add it to the vector + m_iconPixmap = icon->pixmap(KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium); + if (m_iconPixmap.width() == KIconLoader::SizeSmallMedium) { + const int size = KIconLoader::SizeSmall / 2; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + m_iconPixmap = icon->pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium); + if (m_iconPixmap.width() == KIconLoader::SizeMedium) { + const int size = KIconLoader::SizeSmall / 2; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + m_iconPixmap = icon->pixmap(KIconLoader::SizeLarge, KIconLoader::SizeLarge); + if (m_iconPixmap.width() == KIconLoader::SizeLarge) { + const int size = KIconLoader::SizeSmall; + QPainter p(&m_iconPixmap); + p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size)); + p.end(); + tmp.addPixmap(m_iconPixmap); + } + + // We can't do 'm_icon->addPixmap()' because if 'm_icon' uses KIconEngine, + // it will ignore the added pixmaps. This is not a bug in KIconEngine, + // QIcon::addPixmap() doc says: "Custom m_icon engines are free to ignore + // additionally added pixmaps". + *icon = tmp; + // hopefully huge and enormous not necessary right now, since it's quite costly +} + +void StatusNotifierItemSource::activate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(), + m_statusNotifierItemInterface->path(), + m_statusNotifierItemInterface->interface(), + QStringLiteral("Activate")); + + message << x << y; + QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::activateCallback); + } +} + +void StatusNotifierItemSource::activateCallback(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + Q_EMIT activateResult(!reply.isError()); + call->deleteLater(); +} + +void StatusNotifierItemSource::secondaryActivate(int x, int y) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("SecondaryActivate"), x, y); + } +} + +void StatusNotifierItemSource::scroll(int delta, const QString &direction) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("Scroll"), delta, direction); + } +} + +void StatusNotifierItemSource::contextMenu(int x, int y) +{ + if (m_menuImporter) { + m_menuImporter->updateMenu(); + } else { + qWarning() << "Could not find DBusMenu interface, falling back to calling ContextMenu()"; + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("ContextMenu"), x, y); + } + } +} + +void StatusNotifierItemSource::provideXdgActivationToken(const QString &token) +{ + if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) { + m_statusNotifierItemInterface->ProvideXdgActivationToken(token); + } +} diff --git a/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.h b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.h new file mode 100644 index 0000000000..2aea1f1556 --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/statusnotifieritemsource.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + SPDX-FileCopyrightText: 2009 Matthieu Gallien + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include "statusnotifieritem_interface.h" + +class KIconLoader; + +class DBusMenuImporter; + +class StatusNotifierItemSource : public Plasma::DataContainer +{ + Q_OBJECT + +public: + StatusNotifierItemSource(const QString &service, QObject *parent); + ~StatusNotifierItemSource() override; + Plasma::Service *createService(); + + void activate(int x, int y); + void secondaryActivate(int x, int y); + void scroll(int delta, const QString &direction); + void contextMenu(int x, int y); + void provideXdgActivationToken(const QString &token); + +Q_SIGNALS: + void contextMenuReady(QMenu *menu); + void activateResult(bool success); + +private Q_SLOTS: + void contextMenuReady(); + void refreshTitle(); + void refreshIcons(); + void refreshToolTip(); + void refreshMenu(); + void refresh(); + void performRefresh(); + void syncStatus(QString); + void refreshCallback(QDBusPendingCallWatcher *); + void activateCallback(QDBusPendingCallWatcher *); + +private: + QPixmap KDbusImageStructToPixmap(const KDbusImageStruct &image) const; + QIcon imageVectorToPixmap(const KDbusImageVector &vector) const; + void overlayIcon(QIcon *icon, QIcon *overlay); + KIconLoader *iconLoader() const; + + bool m_valid; + QString m_typeId; + QString m_name; + QTimer m_refreshTimer; + KIconLoader *m_customIconLoader; + DBusMenuImporter *m_menuImporter; + org::kde::StatusNotifierItem *m_statusNotifierItemInterface; + bool m_refreshing : 1; + bool m_needsReRefreshing : 1; + bool m_titleUpdate : 1; + bool m_iconUpdate : 1; + bool m_tooltipUpdate : 1; + bool m_statusUpdate : 1; +}; diff --git a/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.cpp b/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.cpp new file mode 100644 index 0000000000..2cc1d8304e --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systemtraytypes.h" + +// Marshall the ImageStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.data; + argument.endStructure(); + return argument; +} +#include + +// Retrieve the ImageStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon) +{ + qint32 width = 0; + qint32 height = 0; + QByteArray data; + + if (argument.currentType() == QDBusArgument::StructureType) { + argument.beginStructure(); + // qCDebug(DATAENGINE_SNI)() << "begun structure"; + argument >> width; + // qCDebug(DATAENGINE_SNI)() << width; + argument >> height; + // qCDebug(DATAENGINE_SNI)() << height; + argument >> data; + // qCDebug(DATAENGINE_SNI)() << data.size(); + argument.endStructure(); + } + + icon.width = width; + icon.height = height; + icon.data = data; + + return argument; +} + +// Marshall the ImageVector data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector) +{ + argument.beginArray(qMetaTypeId()); + for (int i = 0; i < iconVector.size(); ++i) { + argument << iconVector[i]; + } + argument.endArray(); + return argument; +} + +// Retrieve the ImageVector data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector) +{ + iconVector.clear(); + + if (argument.currentType() == QDBusArgument::ArrayType) { + argument.beginArray(); + + while (!argument.atEnd()) { + KDbusImageStruct element; + argument >> element; + iconVector.append(element); + } + + argument.endArray(); + } + + return argument; +} + +// Marshall the ToolTipStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip) +{ + argument.beginStructure(); + argument << toolTip.icon; + argument << toolTip.image; + argument << toolTip.title; + argument << toolTip.subTitle; + argument.endStructure(); + + return argument; +} + +// Retrieve the ToolTipStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip) +{ + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; + + if (argument.currentType() == QDBusArgument::StructureType) { + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + } + + toolTip.icon = icon; + toolTip.image = image; + toolTip.title = title; + toolTip.subTitle = subTitle; + + return argument; +} diff --git a/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.h b/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.h new file mode 100644 index 0000000000..aec1f7974c --- /dev/null +++ b/plasma/workspace/dataengines/statusnotifieritem/systemtraytypes.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "systemtraytypedefs.h" + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip); diff --git a/plasma/workspace/dataengines/systemmonitor/CMakeLists.txt b/plasma/workspace/dataengines/systemmonitor/CMakeLists.txt new file mode 100644 index 0000000000..769ae8705c --- /dev/null +++ b/plasma/workspace/dataengines/systemmonitor/CMakeLists.txt @@ -0,0 +1,14 @@ + +set(systemmonitor_engine_SRCS + systemmonitor.cpp +) + +kcoreaddons_add_plugin(plasma_engine_systemmonitor SOURCES ${systemmonitor_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_systemmonitor + Qt::Network + KF5::I18n + KF5::Plasma + KF5::Service + KSysGuard::SysGuard +) diff --git a/plasma/workspace/dataengines/systemmonitor/Messages.sh b/plasma/workspace/dataengines/systemmonitor/Messages.sh new file mode 100644 index 0000000000..73d19d81db --- /dev/null +++ b/plasma/workspace/dataengines/systemmonitor/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_systemmonitor.pot diff --git a/plasma/workspace/dataengines/systemmonitor/plasma-dataengine-systemmonitor.json b/plasma/workspace/dataengines/systemmonitor/plasma-dataengine-systemmonitor.json new file mode 100644 index 0000000000..545fabc0e3 --- /dev/null +++ b/plasma/workspace/dataengines/systemmonitor/plasma-dataengine-systemmonitor.json @@ -0,0 +1,131 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "" + } + ], + "Category": "", + "Description": "System status information", + "Description[ar]": "معلومات حالة النظام", + "Description[az]": "Sistemin vəziyyəti haqqında", + "Description[ca]": "Informació de l'estat del sistema", + "Description[cs]": "Informace o stavu systému", + "Description[de]": "Systemstatus-Informationen", + "Description[en_GB]": "System status information", + "Description[es]": "Información de estado del sistema", + "Description[eu]": "Sistema-egoeren gaineko informazioa", + "Description[fi]": "Järjestelmän tilatiedot", + "Description[fr]": "Informations sur l'état du système", + "Description[hu]": "Rendszerjellemzők", + "Description[ia]": "Information de stato de systema", + "Description[it]": "Informazioni sullo stato del sistema", + "Description[ko]": "시스템 상태 정보", + "Description[lt]": "Sistemos būsenos informacija", + "Description[nl]": "Informatie over systeemstatus", + "Description[nn]": "Informasjon om systemstatus", + "Description[pa]": "ਸਿਸਟਮ ਹਾਲਤ ਜਾਣਕਾਰੀ", + "Description[pl]": "Informacje o stanie systemu", + "Description[pt_BR]": "Informação do status do sistema", + "Description[ro]": "Informații despre starea sistemului", + "Description[ru]": "Сведения о системе", + "Description[sk]": "Informácie o stave systému", + "Description[sl]": "Informacije o stanju sistema", + "Description[sv]": "Information om systemstatus", + "Description[ta]": "கணினியின் நிலையைப் பற்றிய விவரங்கள்", + "Description[tr]": "Sistem durum bilgileri", + "Description[uk]": "Інформація про стан системи", + "Description[vi]": "Thông tin về trạng thái hệ thống", + "Description[x-test]": "xxSystem status informationxx", + "Description[zh_CN]": "系统状态信息", + "Icon": "utilities-system-monitor", + "Id": "systemmonitor", + "Name": "System Monitor", + "Name[af]": "Stelsel Monitor", + "Name[ar]": "مرقاب النظام", + "Name[ast]": "Supervisor del sistema", + "Name[az]": "Sistem İzləyici", + "Name[be@latin]": "Systemny nazirańnik", + "Name[be]": "Сістэмны назіральнік", + "Name[bg]": "Наблюдение на системата", + "Name[bn]": "সিস্টেম মনিটর", + "Name[bn_IN]": "সিস্টেম নিরীক্ষণ ব্যবস্থা", + "Name[bs]": "Monitor sistema", + "Name[ca@valencia]": "Monitor del sistema", + "Name[ca]": "Monitor del sistema", + "Name[cs]": "Monitor systému", + "Name[csb]": "Mònitór systemë", + "Name[da]": "Systemovervågning", + "Name[de]": "Systemmonitor", + "Name[el]": "Επόπτης συστήματος", + "Name[en_GB]": "System Monitor", + "Name[eo]": "Sistemstato-programo", + "Name[es]": "Monitor del sistema", + "Name[et]": "Süsteemi jälgija", + "Name[eu]": "Sistema-monitorea", + "Name[fa]": "نمایشگر سیستم", + "Name[fi]": "Järjestelmän valvonta", + "Name[fr]": "Surveillance du système", + "Name[fy]": "Systeemmonitor", + "Name[ga]": "Monatóir an Chórais", + "Name[gl]": "Vixilante do sistema", + "Name[gu]": "સિસ્ટમ દેખરેખ", + "Name[he]": "מוניטור המערכת", + "Name[hi]": "तंत्र मॉनीटर", + "Name[hne]": "तंत्र मानीटर", + "Name[hr]": "Nadzor sustava", + "Name[hsb]": "Systemowy monitor", + "Name[hu]": "Rendszermonitor", + "Name[ia]": "Monitor de systema", + "Name[id]": "Pemantau Sistem", + "Name[is]": "Kerfiseftirlit", + "Name[it]": "Monitor di sistema", + "Name[ja]": "システムモニタ", + "Name[kk]": "Жүйе мониторы", + "Name[km]": "កម្មវិធី​ត្រួត​​ពិនិត្យ​ប្រព័ន្ធ", + "Name[kn]": "ವ್ಯವಸ್ಥೆಯ ಪ್ರದರ್ಶಕ", + "Name[ko]": "시스템 모니터", + "Name[ku]": "Temaşekerê Pergalê", + "Name[lt]": "Sistemos prižiūryklė", + "Name[lv]": "Sistēmas monitors", + "Name[mai]": "सिस्टम मानीटर", + "Name[mk]": "Системски монитор", + "Name[ml]": "സിസ്റ്റം നിരീക്ഷകന്‍", + "Name[mr]": "प्रणाली मॉनिटर", + "Name[nb]": "Systemovervåker", + "Name[nds]": "Systeemkieker", + "Name[ne]": "प्रणाली मनिटर", + "Name[nl]": "Systeemmonitor", + "Name[nn]": "Systemovervaking", + "Name[oc]": "Monitor sistèma", + "Name[pa]": "ਸਿਸਟਮ ਮਾਨੀਟਰ", + "Name[pl]": "Monitor systemowy", + "Name[pt]": "Monitor do Sistema", + "Name[pt_BR]": "Monitor do sistema", + "Name[ro]": "Monitor de sistem", + "Name[ru]": "Системный монитор", + "Name[se]": "Vuogádatgoziheaddji", + "Name[si]": "පද්ධති නිරීක්‍ෂකය", + "Name[sk]": "Monitor systému", + "Name[sl]": "Sistemski nadzornik", + "Name[sr@ijekavian]": "надзорник система", + "Name[sr@ijekavianlatin]": "nadzornik sistema", + "Name[sr@latin]": "nadzornik sistema", + "Name[sr]": "надзорник система", + "Name[sv]": "Systemövervakare", + "Name[ta]": "கணினி நோட்டம்", + "Name[te]": "సిస్టమ్ మానిటర్", + "Name[tg]": "Назорати низом", + "Name[th]": "ติดตามการทำงานของระบบ", + "Name[tr]": "Sistem İzleyici", + "Name[ug]": "سىستېما كۆزەتكۈچ", + "Name[uk]": "Монітор системи", + "Name[vi]": "Trình giám sát hệ thống", + "Name[wa]": "Corwaitoe do sistinme", + "Name[x-test]": "xxSystem Monitorxx", + "Name[zh_CN]": "系统监视器", + "Name[zh_TW]": "系統監視器", + "Website": "https://kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/systemmonitor/systemmonitor.cpp b/plasma/workspace/dataengines/systemmonitor/systemmonitor.cpp new file mode 100644 index 0000000000..ecfbd629ae --- /dev/null +++ b/plasma/workspace/dataengines/systemmonitor/systemmonitor.cpp @@ -0,0 +1,179 @@ +/* + SPDX-FileCopyrightText: 2007 John Tapsell + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "systemmonitor.h" + +#include +#include + +#include +#include + +#include + +#include + +SystemMonitorEngine::SystemMonitorEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + KSGRD::SensorMgr = new KSGRD::SensorManager(this); + KSGRD::SensorMgr->engage(QStringLiteral("localhost"), QLatin1String(""), QStringLiteral("ksysguardd")); + + m_waitingFor = 0; + connect(KSGRD::SensorMgr, &KSGRD::SensorManager::update, this, &SystemMonitorEngine::updateMonitorsList); + updateMonitorsList(); +} + +SystemMonitorEngine::~SystemMonitorEngine() +{ +} + +void SystemMonitorEngine::updateMonitorsList() +{ + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("monitors"), (KSGRD::SensorClient *)this, -1); +} + +QStringList SystemMonitorEngine::sources() const +{ + return m_sensors.toList(); +} + +bool SystemMonitorEngine::sourceRequestEvent(const QString &name) +{ + setData(name, DataEngine::Data()); + return true; +} + +bool SystemMonitorEngine::updateSourceEvent(const QString &sensorName) +{ + const int index = m_sensors.indexOf(sensorName); + + if (index != -1) { + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), sensorName, (KSGRD::SensorClient *)this, index); + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("%1?").arg(sensorName), (KSGRD::SensorClient *)this, -(index + 2)); + } + + return false; +} + +void SystemMonitorEngine::updateSensors() +{ + DataEngine::SourceDict sources = containerDict(); + DataEngine::SourceDict::iterator it = sources.begin(); + + m_waitingFor = 0; + + while (it != sources.end()) { + m_waitingFor++; + QString sensorName = it.key(); + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), sensorName, (KSGRD::SensorClient *)this, -1); + ++it; + } +} + +void SystemMonitorEngine::answerReceived(int id, const QList &answer) +{ + if (id < -1) { + if (answer.isEmpty() || m_sensors.count() <= (-id - 2)) { + qDebug() << "sensor info answer was empty, (" << answer.isEmpty() << ") or sensors does not exist to us (" << (m_sensors.count() < (-id - 2)) + << ") for index" << (-id - 2); + return; + } + + DataEngine::SourceDict sources = containerDict(); + DataEngine::SourceDict::const_iterator it = sources.constFind(m_sensors.value(-id - 2)); + + const QStringList newSensorInfo = QString::fromUtf8(answer[0]).split('\t'); + + if (newSensorInfo.count() < 4) { + qDebug() << "bad sensor info, only" << newSensorInfo.count() << "entries, and we were expecting 4. Answer was " << answer; + if (it != sources.constEnd()) + qDebug() << "value =" << it.value()->data()[QStringLiteral("value")] << "type=" << it.value()->data()[QStringLiteral("type")]; + return; + } + + const QString &sensorName = newSensorInfo[0]; + const QString &min = newSensorInfo[1]; + const QString &max = newSensorInfo[2]; + const QString &unit = newSensorInfo[3]; + + if (it != sources.constEnd()) { + it.value()->setData(QStringLiteral("name"), sensorName); + it.value()->setData(QStringLiteral("min"), min); + it.value()->setData(QStringLiteral("max"), max); + it.value()->setData(QStringLiteral("units"), unit); + } + + return; + } + + if (id == -1) { + QSet sensors; + m_sensors.clear(); + int count = 0; + + foreach (const QByteArray &sens, answer) { + const QString sensStr{QString::fromUtf8(sens)}; + const QVector newSensorInfo = sensStr.splitRef('\t'); + if (newSensorInfo.count() < 2) { + continue; + } + if (newSensorInfo.at(1) == QLatin1String("logfile")) + continue; // logfile data type not currently supported + + const QString newSensor = newSensorInfo[0].toString(); + sensors.insert(newSensor); + m_sensors.append(newSensor); + { + // HACK: for backwards compatibility + // in case this source was created in sourceRequestEvent, stop it being + // automagically removed when disconnected from + Plasma::DataContainer *s = containerForSource(newSensor); + if (s) { + disconnect(s, &Plasma::DataContainer::becameUnused, this, &SystemMonitorEngine::removeSource); + } + } + DataEngine::Data d; + d.insert(QStringLiteral("value"), QVariant()); + d.insert(QStringLiteral("type"), newSensorInfo[1].toString()); + setData(newSensor, d); + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("%1?").arg(newSensor), (KSGRD::SensorClient *)this, -(count + 2)); + ++count; + } + + QHash sourceDict = containerDict(); + QHashIterator it(sourceDict); + while (it.hasNext()) { + it.next(); + if (!sensors.contains(it.key())) { + removeSource(it.key()); + } + } + + return; + } + + m_waitingFor--; + QString reply; + if (!answer.isEmpty()) { + reply = QString::fromUtf8(answer[0]); + } + + DataEngine::SourceDict sources = containerDict(); + DataEngine::SourceDict::const_iterator it = sources.constFind(m_sensors.value(id)); + if (it != sources.constEnd()) { + it.value()->setData(QStringLiteral("value"), reply); + } +} + +void SystemMonitorEngine::sensorLost(int) +{ + m_waitingFor--; +} + +K_PLUGIN_CLASS_WITH_JSON(SystemMonitorEngine, "plasma-dataengine-systemmonitor.json") + +#include "systemmonitor.moc" diff --git a/plasma/workspace/dataengines/systemmonitor/systemmonitor.h b/plasma/workspace/dataengines/systemmonitor/systemmonitor.h new file mode 100644 index 0000000000..66e4ff70a1 --- /dev/null +++ b/plasma/workspace/dataengines/systemmonitor/systemmonitor.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2007 John Tapsell + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include + +#include +#include + +class QTimer; + +/** + * This class evaluates the basic expressions given in the interface. + */ +class SystemMonitorEngine : public Plasma::DataEngine, public KSGRD::SensorClient +{ + Q_OBJECT + +public: + /** Inherited from Plasma::DataEngine. Returns a list of all the sensors that ksysguardd knows about. */ + QStringList sources() const override; + SystemMonitorEngine(QObject *parent, const QVariantList &args); + ~SystemMonitorEngine() override; + +protected: + bool sourceRequestEvent(const QString &name) override; + /** inherited from SensorClient */ + void answerReceived(int id, const QList &answer) override; + void sensorLost(int) override; + bool updateSourceEvent(const QString &sensorName) override; + +protected Q_SLOTS: + void updateSensors(); + void updateMonitorsList(); + +private: + QVector m_sensors; + QTimer *m_timer; + int m_waitingFor; +}; diff --git a/plasma/workspace/dataengines/time/CMakeLists.txt b/plasma/workspace/dataengines/time/CMakeLists.txt new file mode 100644 index 0000000000..d1a1341d83 --- /dev/null +++ b/plasma/workspace/dataengines/time/CMakeLists.txt @@ -0,0 +1,22 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_time\") + +set(time_engine_SRCS + timeengine.cpp + timesource.cpp + solarsystem.cpp +) + +ecm_qt_declare_logging_category(time_engine_SRCS HEADER debug.h + IDENTIFIER DATAENGINE_TIME + CATEGORY_NAME kde.dataengine.time + DEFAULT_SEVERITY Info) + +kcoreaddons_add_plugin(plasma_engine_time SOURCES ${time_engine_SRCS} INSTALL_NAMESPACE plasma/dataengine) + +target_link_libraries(plasma_engine_time + Qt::DBus + KF5::Solid + KF5::Plasma + KF5::I18n + KF5::Service +) diff --git a/plasma/workspace/dataengines/time/Messages.sh b/plasma/workspace/dataengines/time/Messages.sh new file mode 100644 index 0000000000..5ece181fc7 --- /dev/null +++ b/plasma/workspace/dataengines/time/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_engine_time.pot diff --git a/plasma/workspace/dataengines/time/plasma-dataengine-time.json b/plasma/workspace/dataengines/time/plasma-dataengine-time.json new file mode 100644 index 0000000000..542a4cc775 --- /dev/null +++ b/plasma/workspace/dataengines/time/plasma-dataengine-time.json @@ -0,0 +1,158 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "aseigo@kde.org", + "Name": "Aaron Seigo", + "Name[ar]": "Aaron Seigo", + "Name[az]": "Aaron Seigo", + "Name[ca]": "Aaron Seigo", + "Name[cs]": "Aaron Seigo", + "Name[de]": "Aaron Seigo", + "Name[en_GB]": "Aaron Seigo", + "Name[es]": "Aaron Seigo", + "Name[eu]": "Aaron Seigo", + "Name[fi]": "Aaron Seigo", + "Name[fr]": "Aaron Seigo", + "Name[hu]": "Aaron Seigo", + "Name[ia]": "Aaron Seigo", + "Name[it]": "Aaron Seigo", + "Name[ko]": "Aaron Seigo", + "Name[lt]": "Aaron Seigo", + "Name[nl]": "Aaron Seigo", + "Name[nn]": "Aaron Seigo", + "Name[pl]": "Aaron Seigo", + "Name[pt_BR]": "Aaron Seigo", + "Name[ro]": "Aaron Seigo", + "Name[ru]": "Aaron Seigo", + "Name[sk]": "Aaron Seigo", + "Name[sl]": "Aaron Seigo", + "Name[sv]": "Aaron Seigo", + "Name[tr]": "Aaron Seigo", + "Name[uk]": "Aaron Seigo", + "Name[vi]": "Aaron Seigo", + "Name[x-test]": "xxAaron Seigoxx", + "Name[zh_CN]": "Aaron Seigo" + } + ], + "Category": "Date and Time", + "Description": "Date and time by timezone", + "Description[ar]": "التاريخ والوقت بالمنطقة الزمنية", + "Description[az]": "Saat qurşağna uyğun tarix və vaxt", + "Description[ca]": "Data i hora per zona horària", + "Description[cs]": "Datum a čas podle časového pásma", + "Description[de]": "Datum und Zeit nach Zeitzone", + "Description[en_GB]": "Date and time by timezone", + "Description[es]": "Fecha y hora por zona horaria", + "Description[eu]": "Data eta ordua ordu-eremuaren arabera", + "Description[fi]": "Aika ja päiväys aikavyöhykkeittäin", + "Description[fr]": "Date et heure par fuseau horaire", + "Description[hu]": "Dátum és idő (időzónánként)", + "Description[ia]": "Data e tempore per fuso horari", + "Description[it]": "Data e ora per fuso orario", + "Description[ko]": "시간대에 따른 날짜와 시간", + "Description[lt]": "Data ir laikas pagal laiko juostą", + "Description[nl]": "Datum en tijd per tijdzone", + "Description[nn]": "Dato og klokkeslett i ulike tidssoner", + "Description[pa]": "ਤਾਰੀਖ ਅਤੇ ਟਾਈਮ ਸਮਾਂ-ਖੇਤਰ ਰਾਹੀਂ", + "Description[pl]": "Ustawienia daty i czasu na podstawie strefy czasowej", + "Description[pt_BR]": "Data e hora por fuso horário", + "Description[ro]": "Data și ora după fusul orar", + "Description[ru]": "Дата и время в различных часовых поясах", + "Description[sk]": "Dátum a čas podľa časového pásma", + "Description[sl]": "Datum in čas po časovnih pasovih", + "Description[sv]": "Datum och tid enligt tidszon", + "Description[ta]": "கால மண்டலத்தை பொறுத்த தேதி மற்றும் நேரம்", + "Description[tr]": "Zaman dilimine göre tarih ve saat", + "Description[uk]": "Дата і час за часовими поясами", + "Description[vi]": "Ngày giờ theo múi giờ", + "Description[x-test]": "xxDate and time by timezonexx", + "Description[zh_CN]": "根据时区提供日期和时间", + "EnabledByDefault": true, + "Icon": "preferences-system-time", + "Id": "time", + "License": "LGPL", + "Name": "Date and Time", + "Name[ar]": "التاريخ والوقت", + "Name[as]": "তাৰিখ আৰু সময়", + "Name[ast]": "Data y hora", + "Name[az]": "Tarix və Vaxt", + "Name[be@latin]": "Data j čas", + "Name[bg]": "Дата и час", + "Name[bn]": "তারিখ এবং সময়", + "Name[bn_IN]": "তারিখ ও সময়", + "Name[bs]": "Datum i vrijeme", + "Name[ca@valencia]": "Data i hora", + "Name[ca]": "Data i hora", + "Name[cs]": "Datum a čas", + "Name[csb]": "Datum ë czas", + "Name[da]": "Dato og klokkeslæt", + "Name[de]": "Datum und Zeit", + "Name[el]": "Ημερομηνία και ώρα", + "Name[en_GB]": "Date and Time", + "Name[eo]": "Dato kaj Tempo", + "Name[es]": "Fecha y hora", + "Name[et]": "Kuupäev ja kellaaeg", + "Name[eu]": "Data eta ordua", + "Name[fa]": "تاریخ و زمان", + "Name[fi]": "Aika ja päiväys", + "Name[fr]": "Date et heure", + "Name[fy]": "Datum en tiid", + "Name[ga]": "Dáta agus Am", + "Name[gl]": "Data e hora", + "Name[gu]": "તારીખ અને સમય", + "Name[he]": "תאריך ושעה", + "Name[hi]": "तारीख़ और समय", + "Name[hne]": "तारीक अउ समय", + "Name[hr]": "Datum i vrijeme", + "Name[hsb]": "Datum a čas", + "Name[hu]": "Dátum és idő", + "Name[ia]": "Data e Tempore", + "Name[id]": "Tanggal dan Waktu", + "Name[is]": "Dagur og tími", + "Name[it]": "Data e ora", + "Name[ja]": "日付と時刻", + "Name[kk]": "Күні мен уақыты", + "Name[km]": "កាល​បរិច្ឆេទ និង​ពេលវេលា", + "Name[kn]": "ದಿನಾಂಕ ಮತ್ತು ಸಮಯ", + "Name[ko]": "날짜와 시간", + "Name[ku]": "Dîrok û Dem", + "Name[lt]": "Data ir laikas", + "Name[lv]": "Datums un laiks", + "Name[mai]": "दिनाँक आ समय", + "Name[mk]": "Датум и време", + "Name[ml]": "തീയതിയും സമയവും", + "Name[mr]": "दिनांक व वेळ", + "Name[nb]": "Dato og klokkeslett", + "Name[nds]": "Datum un Tiet", + "Name[nl]": "Datum en tijd", + "Name[nn]": "Dato og klokkeslett", + "Name[or]": "ତାରିଖ ଏବଂ ସମୟ", + "Name[pa]": "ਮਿਤੀ ਅਤੇ ਟਾਈਮ", + "Name[pl]": "Data i czas", + "Name[pt]": "Data e Hora", + "Name[pt_BR]": "Data e hora", + "Name[ro]": "Data și ora", + "Name[ru]": "Дата и время", + "Name[si]": "දිනය සහ වේලාව", + "Name[sk]": "Dátum a čas", + "Name[sl]": "Datum in čas", + "Name[sr@ijekavian]": "датум и време", + "Name[sr@ijekavianlatin]": "datum i vreme", + "Name[sr@latin]": "datum i vreme", + "Name[sr]": "датум и време", + "Name[sv]": "Datum och tid", + "Name[ta]": "தேதி மற்றும் நேரம்", + "Name[tg]": "Сана ва вақт", + "Name[th]": "วันและเวลา", + "Name[tr]": "Tarih ve Saat", + "Name[ug]": "چېسلا ۋە ۋاقىت", + "Name[uk]": "Дата і час", + "Name[vi]": "Ngày giờ", + "Name[wa]": "Date eyet eure", + "Name[x-test]": "xxDate and Timexx", + "Name[zh_CN]": "日期和时间", + "Name[zh_TW]": "日期與時間", + "Website": "https://www.kde.org/plasma-desktop" + } +} diff --git a/plasma/workspace/dataengines/time/solarsystem.cpp b/plasma/workspace/dataengines/time/solarsystem.cpp new file mode 100644 index 0000000000..79c290aee1 --- /dev/null +++ b/plasma/workspace/dataengines/time/solarsystem.cpp @@ -0,0 +1,316 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damsten + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "solarsystem.h" +#include +#include + +/* + * Mathematics, ideas, public domain code used for these classes from: + * https://www.stjarnhimlen.se/comp/tutorial.html + * https://www.stjarnhimlen.se/comp/riset.html + * https://www.esrl.noaa.gov/gmd/grad/solcalc/azel.html + * https://www.esrl.noaa.gov/gmd/grad/solcalc/sunrise.html + * http://web.archive.org/web/20080309162302/http://bodmas.org/astronomy/riset.html + * moontool.c by John Walker + * Wikipedia + */ + +Sun::Sun() + : SolarSystemObject() +{ +} + +void Sun::calcForDateTime(const QDateTime &local, int offset) +{ + SolarSystemObject::calcForDateTime(local, offset); + + N = 0.0; + i = 0.0; + w = rev(282.9404 + 4.70935E-5 * m_day); + a = 1.0; + e = rev(0.016709 - 1.151E-9 * m_day); + M = rev(356.0470 + 0.9856002585 * m_day); + + calc(); +} + +void Sun::rotate(double *y, double *z) +{ + *y *= cosd(m_obliquity); + *z *= sind(m_obliquity); +} + +Moon::Moon(Sun *sun) + : m_sun(sun) +{ +} + +void Moon::calcForDateTime(const QDateTime &local, int offset) +{ + if (m_sun->dateTime() != local) { + m_sun->calcForDateTime(local, offset); + } + + SolarSystemObject::calcForDateTime(local, offset); + + N = rev(125.1228 - 0.0529538083 * m_day); + i = 5.1454; + w = rev(318.0634 + 0.1643573223 * m_day); + a = 60.2666; + e = 0.054900; + M = rev(115.3654 + 13.0649929509 * m_day); + + calc(); +} + +bool Moon::calcPerturbations(double *lo, double *la, double *r) +{ + double Ms = m_sun->meanAnomaly(); + double D = L - m_sun->meanLongitude(); + double F = L - N; + // clang-format off + *lo += -1.274 * sind(M - 2 * D) + +0.658 * sind(2 * D) + -0.186 * sind(Ms) + -0.059 * sind(2 * M - 2 * D) + -0.057 * sind(M - 2 * D + Ms) + +0.053 * sind(M + 2 * D) + +0.046 * sind(2 * D - Ms) + +0.041 * sind(M - Ms) + -0.035 * sind(D) + -0.031 * sind(M + Ms) + -0.015 * sind(2 * F - 2 * D) + +0.011 * sind(M - 4 * D); + *la += -0.173 * sind(F - 2 * D) + -0.055 * sind(M - F - 2 * D) + -0.046 * sind(M + F - 2 * D) + +0.033 * sind(F + 2 * D) + +0.017 * sind(2 * M + F); + *r += -0.58 * cosd(M - 2 * D) + -0.46 * cosd(2 * D); + // clang-format on + return true; +} + +void Moon::topocentricCorrection(double *RA, double *dec) +{ + double HA = rev(siderealTime() - *RA); + double gclat = m_latitude - 0.1924 * sind(2 * m_latitude); + double rho = 0.99833 + 0.00167 * cosd(2 * m_latitude); + double mpar = asind(1 / rad); + double g = atand(tand(gclat) / cosd(HA)); + + *RA -= mpar * rho * cosd(gclat) * sind(HA) / cosd(*dec); + *dec -= mpar * rho * sind(gclat) * sind(g - *dec) / sind(g); +} + +double Moon::phase() +{ + return rev(m_eclipticLongitude - m_sun->lambda()); +} + +void Moon::rotate(double *y, double *z) +{ + double t = *y; + *y = t * cosd(m_obliquity) - *z * sind(m_obliquity); + *z = t * sind(m_obliquity) + *z * cosd(m_obliquity); +} + +void SolarSystemObject::calc() +{ + double x, y, z; + double la, r; + + L = rev(N + w + M); + double E0 = 720.0; + double E = M + (180.0 / M_PI) * e * sind(M) * (1.0 + e * cosd(M)); + for (int j = 0; fabs(E0 - E) > 0.005 && j < 10; ++j) { + E0 = E; + E = E0 - (E0 - (180.0 / M_PI) * e * sind(E0) - M) / (1 - e * cosd(E0)); + } + x = a * (cosd(E) - e); + y = a * sind(E) * sqrt(1.0 - e * e); + r = sqrt(x * x + y * y); + double v = rev(atan2d(y, x)); + m_lambda = rev(v + w); + x = r * (cosd(N) * cosd(m_lambda) - sind(N) * sind(m_lambda) * cosd(i)); + y = r * (sind(N) * cosd(m_lambda) + cosd(N) * sind(m_lambda) * cosd(i)); + z = r * sind(m_lambda); + if (!qFuzzyCompare(i, 0.0)) { + z *= sind(i); + } + toSpherical(x, y, z, &m_eclipticLongitude, &la, &r); + if (calcPerturbations(&m_eclipticLongitude, &la, &r)) { + toRectangular(m_eclipticLongitude, la, r, &x, &y, &z); + } + rotate(&y, &z); + toSpherical(x, y, z, &RA, &dec, &rad); + topocentricCorrection(&RA, &dec); + + HA = rev(siderealTime() - RA); + x = cosd(HA) * cosd(dec) * sind(m_latitude) - sind(dec) * cosd(m_latitude); + y = sind(HA) * cosd(dec); + z = cosd(HA) * cosd(dec) * cosd(m_latitude) + sind(dec) * sind(m_latitude); + m_azimuth = atan2d(y, x) + 180.0; + m_altitude = asind(z); +} + +double SolarSystemObject::siderealTime() +{ + double UT = m_utc.time().hour() + m_utc.time().minute() / 60.0 + m_utc.time().second() / 3600.0; + double GMST0 = rev(282.9404 + 4.70935E-5 * m_day + 356.0470 + 0.9856002585 * m_day + 180.0); + return GMST0 + UT * 15.0 + m_longitude; +} + +void SolarSystemObject::calcForDateTime(const QDateTime &local, int offset) +{ + m_local = local; + m_utc = local.addSecs(-offset); + m_day = 367 * m_utc.date().year() - (7 * (m_utc.date().year() + ((m_utc.date().month() + 9) / 12))) / 4 + (275 * m_utc.date().month()) / 9 + + m_utc.date().day() - 730530; + m_day += m_utc.time().hour() / 24.0 + m_utc.time().minute() / (24.0 * 60.0) + m_utc.time().second() / (24.0 * 60.0 * 60.0); + m_obliquity = 23.4393 - 3.563E-7 * m_day; +} + +SolarSystemObject::SolarSystemObject() + : m_latitude(0.0) + , m_longitude(0.0) +{ +} + +SolarSystemObject::~SolarSystemObject() +{ +} + +void SolarSystemObject::setPosition(double latitude, double longitude) +{ + m_latitude = latitude; + m_longitude = longitude; +} + +double SolarSystemObject::rev(double x) +{ + return x - floor(x / 360.0) * 360.0; +} + +double SolarSystemObject::asind(double x) +{ + return asin(x) * 180.0 / M_PI; +} + +double SolarSystemObject::sind(double x) +{ + return sin(x * M_PI / 180.0); +} + +double SolarSystemObject::cosd(double x) +{ + return cos(x * M_PI / 180.0); +} + +double SolarSystemObject::tand(double x) +{ + return tan(x * M_PI / 180.0); +} + +double SolarSystemObject::atan2d(double y, double x) +{ + return atan2(y, x) * 180.0 / M_PI; +} + +double SolarSystemObject::atand(double x) +{ + return atan(x) * 180.0 / M_PI; +} + +void SolarSystemObject::toRectangular(double lo, double la, double r, double *x, double *y, double *z) +{ + *x = r * cosd(lo) * cosd(la); + *y = r * sind(lo) * cosd(la); + *z = r * sind(la); +} + +void SolarSystemObject::toSpherical(double x, double y, double z, double *lo, double *la, double *r) +{ + *r = sqrt(x * x + y * y + z * z); + *la = asind(z / *r); + *lo = rev(atan2d(y, x)); +} + +QPair SolarSystemObject::zeroPoints(QPointF p1, QPointF p2, QPointF p3) +{ + double a = ((p2.y() - p1.y()) * (p1.x() - p3.x()) + (p3.y() - p1.y()) * (p2.x() - p1.x())) + / ((p1.x() - p3.x()) * (p2.x() * p2.x() - p1.x() * p1.x()) + (p2.x() - p1.x()) * (p3.x() * p3.x() - p1.x() * p1.x())); + double b = ((p2.y() - p1.y()) - a * (p2.x() * p2.x() - p1.x() * p1.x())) / (p2.x() - p1.x()); + double c = p1.y() - a * p1.x() * p1.x() - b * p1.x(); + double discriminant = b * b - 4.0 * a * c; + double z1 = -1.0, z2 = -1.0; + + if (discriminant >= 0.0) { + z1 = (-b + sqrt(discriminant)) / (2 * a); + z2 = (-b - sqrt(discriminant)) / (2 * a); + } + return QPair(z1, z2); +} + +QList> SolarSystemObject::timesForAngles(const QList &angles, const QDateTime &dt, int offset) +{ + QList altitudes; + QDate d = dt.date(); + QDateTime local(d, QTime(0, 0)); + for (int j = 0; j <= 25; ++j) { + calcForDateTime(local, offset); + altitudes.append(altitude()); + local = local.addSecs(60 * 60); + } + QList> result; + QTime rise, set; + foreach (double angle, angles) { + for (int j = 3; j <= 25; j += 2) { + QPointF p1((j - 2) * 60 * 60, altitudes[j - 2] - angle); + QPointF p2((j - 1) * 60 * 60, altitudes[j - 1] - angle); + QPointF p3(j * 60 * 60, altitudes[j] - angle); + QPair z = zeroPoints(p1, p2, p3); + if (z.first > p1.x() && z.first < p3.x()) { + if (p1.y() < 0.0) { + rise = QTime(0, 0).addSecs(z.first); + } else { + set = QTime(0, 0).addSecs(z.first); + } + } + if (z.second > p1.x() && z.second < p3.x()) { + if (p3.y() < 0.0) { + set = QTime(0, 0).addSecs(z.second); + } else { + rise = QTime(0, 0).addSecs(z.second); + } + } + } + result.append(QPair(QDateTime(d, rise), QDateTime(d, set))); + } + return result; +} + +double SolarSystemObject::calcElevation() +{ + double refractionCorrection; + + if (m_altitude > 85.0) { + refractionCorrection = 0.0; + } else { + double te = tand(m_altitude); + if (m_altitude > 5.0) { + refractionCorrection = 58.1 / te - 0.07 / (te * te * te) + 0.000086 / (te * te * te * te * te); + } else if (m_altitude > -0.575) { + refractionCorrection = 1735.0 + m_altitude * (-518.2 + m_altitude * (103.4 + m_altitude * (-12.79 + m_altitude * 0.711))); + } else { + refractionCorrection = -20.774 / te; + } + refractionCorrection = refractionCorrection / 3600.0; + } + return m_altitude + refractionCorrection; +} diff --git a/plasma/workspace/dataengines/time/solarsystem.h b/plasma/workspace/dataengines/time/solarsystem.h new file mode 100644 index 0000000000..96c60e86ef --- /dev/null +++ b/plasma/workspace/dataengines/time/solarsystem.h @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2009 Petri Damsten + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +/* + * Mathematics, ideas, public domain code used for these classes from: + * https://www.stjarnhimlen.se/comp/tutorial.html + * https://www.stjarnhimlen.se/comp/riset.html + * https://www.esrl.noaa.gov/gmd/grad/solcalc/azel.html + * https://www.esrl.noaa.gov/gmd/grad/solcalc/sunrise.html + * http://web.archive.org/web/20080309162302/http://bodmas.org/astronomy/riset.html + * moontool.c by John Walker + * Wikipedia + */ + +class SolarSystemObject +{ +public: + SolarSystemObject(); + virtual ~SolarSystemObject(); + + double meanLongitude() const + { + return L; + }; + double meanAnomaly() const + { + return M; + }; + double siderealTime(); + double altitude() const + { + return m_altitude; + }; + double azimuth() const + { + return m_azimuth; + }; + double calcElevation(); + QDateTime dateTime() const + { + return m_local; + }; + double lambda() const + { + return m_lambda; + }; + double eclipticLongitude() const + { + return m_eclipticLongitude; + }; + void setPosition(double latitude, double longitude); + + virtual void calcForDateTime(const QDateTime &local, int offset); + QList> timesForAngles(const QList &angles, const QDateTime &dt, int offset); + +protected: + void calc(); + virtual bool calcPerturbations(double *, double *, double *) + { + return false; + }; + virtual void rotate(double *, double *){}; + virtual void topocentricCorrection(double *, double *){}; + + inline double rev(double x); + inline double asind(double x); + inline double sind(double x); + inline double cosd(double x); + inline double atand(double x); + inline double tand(double x); + inline double atan2d(double y, double x); + void toRectangular(double lo, double la, double r, double *x, double *y, double *z); + void toSpherical(double x, double y, double z, double *lo, double *la, double *r); + QPair zeroPoints(QPointF p1, QPointF p2, QPointF p3); + + double N; + double i; + double w; + double a; + double e; + double M; + double m_obliquity; + + QDateTime m_utc; + QDateTime m_local; + double m_day; + double m_latitude; + double m_longitude; + + double L; + double rad; + double RA; + double dec; + double HA; + double m_altitude; + double m_azimuth; + double m_eclipticLongitude; + double m_lambda; +}; + +class Sun : public SolarSystemObject +{ +public: + Sun(); + void calcForDateTime(const QDateTime &local, int offset) override; + +protected: + void rotate(double *, double *) override; +}; + +class Moon : public SolarSystemObject +{ +public: + explicit Moon(Sun *sun); + ~Moon() override{}; // to not delete the Sun + + void calcForDateTime(const QDateTime &local, int offset) override; + double phase(); + +protected: + bool calcPerturbations(double *RA, double *dec, double *r) override; + void rotate(double *, double *) override; + void topocentricCorrection(double *, double *) override; + +private: + Sun *m_sun; +}; diff --git a/plasma/workspace/dataengines/time/timeengine.cpp b/plasma/workspace/dataengines/time/timeengine.cpp new file mode 100644 index 0000000000..f10bda7a05 --- /dev/null +++ b/plasma/workspace/dataengines/time/timeengine.cpp @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2008 Alex Merry + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "timeengine.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_LINUX +#include +#include +#include +#endif + +#include "debug.h" +#include "timesource.h" + +// timezone is defined in msvc +#ifdef timezone +#undef timezone +#endif + +TimeEngine::TimeEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + Q_UNUSED(args) + setMinimumPollingInterval(333); + + // To have translated timezone names + // (effectively a noop if the catalog is already present). + ////KF5 port: remove this line and define TRANSLATION_DOMAIN in CMakeLists.txt instead + // KLocale::global()->insertCatalog("timezones4"); + QTimer::singleShot(0, this, &TimeEngine::init); +} + +TimeEngine::~TimeEngine() +{ +} + +void TimeEngine::init() +{ + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.connect(QString(), QString(), QStringLiteral("org.kde.KTimeZoned"), QStringLiteral("timeZoneChanged"), this, SLOT(tzConfigChanged())); + +#ifdef Q_OS_LINUX + // monitor for the system clock being changed + auto timeChangedFd = timerfd_create(CLOCK_REALTIME, O_CLOEXEC | O_NONBLOCK); + itimerspec timespec; + memset(×pec, 0, sizeof(timespec)); // set all timers to 0 seconds, which creates a timer that won't do anything + + int err = timerfd_settime(timeChangedFd, 3, ×pec, nullptr); // monitor for the time changing + //(flags == TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET). However these are not exposed in glibc so value is hardcoded + if (err) { + qCWarning(DATAENGINE_TIME) << "Could not create timer with TFD_TIMER_CANCEL_ON_SET. Clock skews will not be detected. Error:" + << qPrintable(strerror(err)); + } + + connect(this, &QObject::destroyed, [timeChangedFd]() { + close(timeChangedFd); + }); + + auto notifier = new QSocketNotifier(timeChangedFd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, [this](int fd) { + uint64_t c; + read(fd, &c, 8); + clockSkewed(); + }); +#else + dbus.connect(QString(), "/org/kde/kcmshell_clock", "org.kde.kcmshell_clock", "clockUpdated", this, SLOT(clockSkewed())); + dbus.connect(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/SuspendSession"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.SuspendSession"), + QStringLiteral("resumingFromSuspend"), + this, + SLOT(clockSkewed())); +#endif +} + +void TimeEngine::clockSkewed() +{ + qCDebug(DATAENGINE_TIME) << "Time engine Clock skew signaled"; + updateAllSources(); + forceImmediateUpdateOfAllVisualizations(); +} + +void TimeEngine::tzConfigChanged() +{ + qCDebug(DATAENGINE_TIME) << "Local timezone changed signaled"; + TimeSource *s = qobject_cast(containerForSource(QStringLiteral("Local"))); + + if (s) { + s->setTimeZone(QStringLiteral("Local")); + } + + updateAllSources(); + forceImmediateUpdateOfAllVisualizations(); +} + +QStringList TimeEngine::sources() const +{ + QStringList sources; + Q_FOREACH (const QByteArray &tz, QTimeZone::availableTimeZoneIds()) { + sources << QString(tz.constData()); + } + sources << QStringLiteral("Local"); + return sources; +} + +bool TimeEngine::sourceRequestEvent(const QString &name) +{ + addSource(new TimeSource(name, this)); + return true; +} + +bool TimeEngine::updateSourceEvent(const QString &tz) +{ + TimeSource *s = qobject_cast(containerForSource(tz)); + + if (s) { + s->updateTime(); + return true; + } + + return false; +} + +K_PLUGIN_CLASS_WITH_JSON(TimeEngine, "plasma-dataengine-time.json") + +#include "timeengine.moc" diff --git a/plasma/workspace/dataengines/time/timeengine.h b/plasma/workspace/dataengines/time/timeengine.h new file mode 100644 index 0000000000..6ba4bbc79a --- /dev/null +++ b/plasma/workspace/dataengines/time/timeengine.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +/** + * This engine provides the current date and time for a given + * timezone. Optionally it can also provide solar position info. + * + * "Local" is a special source that is an alias for the current + * timezone. + */ +class TimeEngine : public Plasma::DataEngine +{ + Q_OBJECT + +public: + explicit TimeEngine(const KPluginInfo &plugin, QObject *parent = nullptr); + TimeEngine(QObject *parent, const QVariantList &args); + ~TimeEngine() override; + + QStringList sources() const override; + +protected: + bool sourceRequestEvent(const QString &name) override; + bool updateSourceEvent(const QString &source) override; + +protected Q_SLOTS: + void clockSkewed(); // call when system time changed and all clocks should be updated + void tzConfigChanged(); + +private Q_SLOTS: + void init(); +}; diff --git a/plasma/workspace/dataengines/time/timesource.cpp b/plasma/workspace/dataengines/time/timesource.cpp new file mode 100644 index 0000000000..00e22e6d8f --- /dev/null +++ b/plasma/workspace/dataengines/time/timesource.cpp @@ -0,0 +1,245 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + Moon Phase: + SPDX-FileCopyrightText: 1998, 2000 Stephan Kulow + SPDX-FileCopyrightText: 2009 Davide Bettio + + Solar position: + SPDX-FileCopyrightText: 2009 Petri Damsten + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "timesource.h" + +#include + +#include + +#include "solarsystem.h" + +// timezone is defined in msvc +#ifdef timezone +#undef timezone +#endif + +TimeSource::TimeSource(const QString &name, QObject *parent) + : Plasma::DataContainer(parent) + , m_offset(0) + , m_latitude(0) + , m_longitude(0) + , m_sun(nullptr) + , m_moon(nullptr) + , m_moonPosition(false) + , m_solarPosition(false) + , m_local(false) +{ + setObjectName(name); + setTimeZone(parseName(name)); +} + +void TimeSource::setTimeZone(const QString &tz) +{ + m_tzName = tz; + m_local = m_tzName == I18N_NOOP("Local"); + if (m_local) { + m_tzName = QString::fromUtf8(QTimeZone::systemTimeZoneId()); + } + + if (m_local) { + m_tz = QTimeZone(QTimeZone::systemTimeZoneId()); + } else { + m_tz = QTimeZone(m_tzName.toUtf8()); + if (!m_tz.isValid()) { + m_tz = QTimeZone(QTimeZone::systemTimeZoneId()); + } + } + + const QString trTimezone = i18n(m_tzName.toUtf8()); + setData(I18N_NOOP("Timezone"), trTimezone); + + const QStringList tzParts = trTimezone.split('/', Qt::SkipEmptyParts); + if (tzParts.count() == 1) { + // no '/' so just set it as the city + setData(I18N_NOOP("Timezone City"), trTimezone); + } else if (tzParts.count() == 2) { + setData(I18N_NOOP("Timezone Continent"), tzParts.value(0)); + setData(I18N_NOOP("Timezone City"), tzParts.value(1)); + } else { // for zones like America/Argentina/Buenos_Aires + setData(I18N_NOOP("Timezone Continent"), tzParts.value(0)); + setData(I18N_NOOP("Timezone Country"), tzParts.value(1)); + setData(I18N_NOOP("Timezone City"), tzParts.value(2)); + } + + updateTime(); +} + +TimeSource::~TimeSource() +{ + // First delete the moon, that does not delete the Sun, and then the Sun + // If the Sun is deleted before the moon, the moon has a invalid pointer + // to where the Sun was pointing. + delete m_moon; + delete m_sun; +} + +void TimeSource::updateTime() +{ + QDateTime timeZoneDateTime = QDateTime::currentDateTime().toTimeZone(m_tz); + + int offset = m_tz.offsetFromUtc(timeZoneDateTime); + if (m_offset != offset) { + m_offset = offset; + } + + setData(I18N_NOOP("Offset"), m_offset); + + QString abbreviation = m_tz.abbreviation(timeZoneDateTime); + setData(I18N_NOOP("Timezone Abbreviation"), abbreviation); + + QDateTime dt; + if (m_userDateTime) { + dt = data()[QStringLiteral("DateTime")].toDateTime(); + } else { + dt = timeZoneDateTime; + } + + if (m_solarPosition || m_moonPosition) { + const QDate prev = data()[QStringLiteral("DateTime")].toDate(); + const bool updateDailies = prev != dt.date(); + + if (m_solarPosition) { + if (updateDailies) { + addDailySolarPositionData(dt); + } + + addSolarPositionData(dt); + } + + if (m_moonPosition) { + if (updateDailies) { + addDailyMoonPositionData(dt); + } + + addMoonPositionData(dt); + } + } + + if (!m_userDateTime) { + setData(I18N_NOOP("DateTime"), dt); + + forceImmediateUpdate(); + } +} + +QString TimeSource::parseName(const QString &name) +{ + m_userDateTime = false; + if (!name.contains('|')) { + // the simple case where it's just a timezone request + return name; + } + + // the various keys we recognize + static const QString latitude = I18N_NOOP("Latitude"); + static const QString longitude = I18N_NOOP("Longitude"); + static const QString solar = I18N_NOOP("Solar"); + static const QString moon = I18N_NOOP("Moon"); + static const QString datetime = I18N_NOOP("DateTime"); + + // now parse out what we got handed in + const QStringList list = name.split('|', Qt::SkipEmptyParts); + + const int listSize = list.size(); + for (int i = 1; i < listSize; ++i) { + const QString arg = list[i]; + const int n = arg.indexOf('='); + + if (n != -1) { + const QString key = arg.mid(0, n); + const QString value = arg.mid(n + 1); + + if (key == latitude) { + m_latitude = value.toDouble(); + } else if (key == longitude) { + m_longitude = value.toDouble(); + } else if (key == datetime) { + QDateTime dt = QDateTime::fromString(value, Qt::ISODate); + if (dt.isValid()) { + setData(I18N_NOOP("DateTime"), dt); + m_userDateTime = true; + } + } + } else if (arg == solar) { + m_solarPosition = true; + } else if (arg == moon) { + m_moonPosition = true; + } + } + + // timezone is first item ... + return list.at(0); +} + +Sun *TimeSource::sun() +{ + if (!m_sun) { + m_sun = new Sun(); + } + m_sun->setPosition(m_latitude, m_longitude); + return m_sun; +} + +Moon *TimeSource::moon() +{ + if (!m_moon) { + m_moon = new Moon(sun()); + } + m_moon->setPosition(m_latitude, m_longitude); + return m_moon; +} + +void TimeSource::addMoonPositionData(const QDateTime &dt) +{ + Moon *m = moon(); + m->calcForDateTime(dt, m_offset); + setData(QStringLiteral("Moon Azimuth"), m->azimuth()); + setData(QStringLiteral("Moon Zenith"), 90 - m->altitude()); + setData(QStringLiteral("Moon Corrected Elevation"), m->calcElevation()); + setData(QStringLiteral("MoonPhaseAngle"), m->phase()); +} + +void TimeSource::addDailyMoonPositionData(const QDateTime &dt) +{ + Moon *m = moon(); + QList> times = m->timesForAngles(QList() << -0.833, dt, m_offset); + setData(QStringLiteral("Moonrise"), times[0].first); + setData(QStringLiteral("Moonset"), times[0].second); + m->calcForDateTime(QDateTime(dt.date(), QTime(12, 0)), m_offset); + setData(QStringLiteral("MoonPhase"), int(m->phase() / 360.0 * 29.0)); +} + +void TimeSource::addSolarPositionData(const QDateTime &dt) +{ + Sun *s = sun(); + s->calcForDateTime(dt, m_offset); + setData(QStringLiteral("Azimuth"), s->azimuth()); + setData(QStringLiteral("Zenith"), 90.0 - s->altitude()); + setData(QStringLiteral("Corrected Elevation"), s->calcElevation()); +} + +void TimeSource::addDailySolarPositionData(const QDateTime &dt) +{ + Sun *s = sun(); + QList> times = s->timesForAngles(QList() << -0.833 << -6.0 << -12.0 << -18.0, dt, m_offset); + + setData(QStringLiteral("Sunrise"), times[0].first); + setData(QStringLiteral("Sunset"), times[0].second); + setData(QStringLiteral("Civil Dawn"), times[1].first); + setData(QStringLiteral("Civil Dusk"), times[1].second); + setData(QStringLiteral("Nautical Dawn"), times[2].first); + setData(QStringLiteral("Nautical Dusk"), times[2].second); + setData(QStringLiteral("Astronomical Dawn"), times[3].first); + setData(QStringLiteral("Astronomical Dusk"), times[3].second); +} diff --git a/plasma/workspace/dataengines/time/timesource.h b/plasma/workspace/dataengines/time/timesource.h new file mode 100644 index 0000000000..e2c71f2c12 --- /dev/null +++ b/plasma/workspace/dataengines/time/timesource.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class Sun; +class Moon; + +class TimeSource : public Plasma::DataContainer +{ + Q_OBJECT + +public: + explicit TimeSource(const QString &name, QObject *parent = nullptr); + ~TimeSource() override; + void setTimeZone(const QString &name); + void updateTime(); + +private: + QString parseName(const QString &name); + void addMoonPositionData(const QDateTime &dt); + void addDailyMoonPositionData(const QDateTime &dt); + void addSolarPositionData(const QDateTime &dt); + void addDailySolarPositionData(const QDateTime &dt); + Sun *sun(); + Moon *moon(); + + QString m_tzName; + int m_offset; + double m_latitude; + double m_longitude; + Sun *m_sun; + Moon *m_moon; + bool m_moonPosition : 1; + bool m_solarPosition : 1; + bool m_userDateTime : 1; + bool m_local : 1; + QTimeZone m_tz; +}; diff --git a/plasma/workspace/dataengines/weather/CMakeLists.txt b/plasma/workspace/dataengines/weather/CMakeLists.txt new file mode 100644 index 0000000000..95dd586fc2 --- /dev/null +++ b/plasma/workspace/dataengines/weather/CMakeLists.txt @@ -0,0 +1,31 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_engine_weather\") +remove_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) # needed for QNetworkConfigurationManager + + +add_definitions( + -DQT_USE_QSTRINGBUILDER + -DQT_NO_CAST_TO_ASCII + -DQT_NO_CAST_FROM_ASCII + -DQT_NO_CAST_FROM_BYTEARRAY +) + +add_subdirectory(ions) + +set(weather_SRCS weatherengine.cpp) +ecm_qt_declare_logging_category(weather_SRCS + HEADER weatherenginedebug.h + IDENTIFIER WEATHER + CATEGORY_NAME kde.dataengine.weather + DEFAULT_SEVERITY Info +) + +kcoreaddons_add_plugin(plasma_engine_weather SOURCES ${weather_SRCS} INSTALL_NAMESPACE plasma/dataengine) +target_compile_definitions(plasma_engine_weather PRIVATE -DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) # needed for QNetworkConfigurationManager + +target_link_libraries (plasma_engine_weather + KF5::KIOCore + KF5::Solid + KF5::Plasma + Qt::Network + weather_ion) diff --git a/plasma/workspace/dataengines/weather/Messages.sh b/plasma/workspace/dataengines/weather/Messages.sh new file mode 100644 index 0000000000..a0b026d99f --- /dev/null +++ b/plasma/workspace/dataengines/weather/Messages.sh @@ -0,0 +1,10 @@ +#! /usr/bin/env bash +for file in ions/data/*.dat +do + awk -F'|' '$0 ~ /\|/ { + print "// i18n: file: '`basename $file`':"NR; + printf("i18nc(\"%s\", \"%s\");\n", $1, $2) + }' $file >> rc.cpp +done + +$XGETTEXT `find . -name \*.cpp` -o $podir/plasma_engine_weather.pot diff --git a/plasma/workspace/dataengines/weather/ions/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/CMakeLists.txt new file mode 100644 index 0000000000..ecd9281c82 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/CMakeLists.txt @@ -0,0 +1,33 @@ +# the Ion shared library +set (ionlib_SRCS ion.cpp) +ecm_qt_declare_logging_category(ionlib_SRCS + HEADER iondebug.h + IDENTIFIER IONENGINE + CATEGORY_NAME kde.dataengine.ion + DEFAULT_SEVERITY Info +) + +add_library (weather_ion SHARED ${ionlib_SRCS}) +generate_export_header(weather_ion BASE_NAME ion) +target_link_libraries (weather_ion PRIVATE KF5::I18n PUBLIC Qt::Core KF5::Plasma) + +set_target_properties(weather_ion PROPERTIES + VERSION 7.0.0 + SOVERSION 7 +) + +install (TARGETS weather_ion EXPORT kdeworkspaceLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +install (FILES ion.h + ${CMAKE_CURRENT_BINARY_DIR}/ion_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/plasma/weather COMPONENT Devel) + +# install (FILES includes/Ion +# DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KDE/Plasma/Weather COMPONENT Devel) + +# the individual ion plugins +add_subdirectory(bbcukmet) +add_subdirectory(envcan) +add_subdirectory(noaa) +add_subdirectory(wetter.com) +add_subdirectory(dwd) diff --git a/plasma/workspace/dataengines/weather/ions/bbcukmet/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/bbcukmet/CMakeLists.txt new file mode 100644 index 0000000000..c3cb72e7be --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/bbcukmet/CMakeLists.txt @@ -0,0 +1,17 @@ +set (ion_bbcukmet_SRCS ion_bbcukmet.cpp) +ecm_qt_declare_logging_category(ion_bbcukmet_SRCS + HEADER ion_bbcukmetdebug.h + IDENTIFIER IONENGINE_BBCUKMET + CATEGORY_NAME kde.dataengine.ion.bbcukmet + DEFAULT_SEVERITY Info +) +add_library(ion_bbcukmet MODULE ${ion_bbcukmet_SRCS}) +target_link_libraries (ion_bbcukmet + weather_ion + KF5::KIOCore + KF5::UnitConversion + KF5::I18n +) + +install (TARGETS ion_bbcukmet DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + diff --git a/plasma/workspace/dataengines/weather/ions/bbcukmet/ion-bbcukmet.json b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion-bbcukmet.json new file mode 100644 index 0000000000..f83cd3d113 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion-bbcukmet.json @@ -0,0 +1,79 @@ +{ + "KPlugin": { + "Description": "XML Data from the British Broadcasting Corporation", + "Description[ar]": "بيانات XML من هيئة الإذاعة البريطانية ", + "Description[az]": "Britaniya Yayım Korporasiyasının XML formatında məlumatları", + "Description[ca]": "Dades XML de la British Broadcasting Corporation", + "Description[de]": "XML-Daten von der BBC", + "Description[en_GB]": "XML Data from the British Broadcasting Corporation", + "Description[es]": "Datos XML de la British Broadcasting Corporation", + "Description[eu]": "«British Broadcasting Corporation»eko XML datuak", + "Description[fi]": "XML-data BBC:ltä", + "Description[fr]": "Données « XML » de la British Broadcasting Corporation", + "Description[hu]": "XML-adatok a British Broadcasting Corporationtől", + "Description[ia]": "Datos XML ex le British Broadcasting Corporation", + "Description[it]": "Dati XML dalla British Broadcasting Corporation", + "Description[ko]": "BBC의 XML 데이터", + "Description[lt]": "XML duomenys iš BBC (British Broadcasting Corporation)", + "Description[nl]": "XML-gegevens van de British Broadcasting Corporation", + "Description[nn]": "XML-data frå British Broadcasting Corporation", + "Description[pl]": "Dane XML z British Broadcasting Corporation", + "Description[pt_BR]": "Dados em XML da BBC (British Broadcasting Corporation)", + "Description[ro]": "Date XML de la BBC", + "Description[ru]": "Данные в формате XML от Британской вещательной корпорации", + "Description[sk]": "XML údaje od British Broadcasting Corporation", + "Description[sl]": "XML podatki od British Broadcasting Corporation", + "Description[sv]": "XML-data från British Broadcasting Corporation", + "Description[tr]": "British Broadcasting Corporation'dan XML verileri", + "Description[uk]": "Дані XML від BBC", + "Description[vi]": "Dữ liệu XML từ Hiệp hội Phát thanh truyền hình Anh quốc (BBC)", + "Description[x-test]": "xxXML Data from the British Broadcasting Corporationxx", + "Description[zh_CN]": "BBC 提供的 XML 数据", + "Icon": "noneyet", + "Id": "bbcukmet", + "Name": "BBC Weather", + "Name[ar]": "الطقس بي بي سي", + "Name[az]": "BBC Hava Məlumatı", + "Name[ca@valencia]": "BBC Weather", + "Name[ca]": "BBC Weather", + "Name[cs]": "BBC Weather", + "Name[da]": "BBC Weather", + "Name[de]": "BBC-Wetterdienst", + "Name[en_GB]": "BBC Weather", + "Name[es]": "BBC Weather", + "Name[et]": "BBC ilmateade", + "Name[eu]": "BBC Weather", + "Name[fi]": "BBC Weather", + "Name[fr]": "Météo de la BBC", + "Name[gl]": "BBC Weather", + "Name[hi]": "बीबीसी मौसम", + "Name[hu]": "BBC Weather", + "Name[ia]": "BBC Weather", + "Name[id]": "Cuaca BBC", + "Name[it]": "BBC Weather", + "Name[ko]": "BBC 날씨", + "Name[lt]": "BBC orai", + "Name[lv]": "BBC laikapstākļi", + "Name[ml]": "ബിബിസി കാലാവസ്ഥ", + "Name[nl]": "BBC Weather", + "Name[nn]": "BBC Weather", + "Name[pa]": "BBC ਮੌਸਮ", + "Name[pl]": "Pogoda BBC", + "Name[pt]": "Meteorologia da BBC", + "Name[pt_BR]": "BBC Weather", + "Name[ro]": "Vremea BBC", + "Name[ru]": "Погода от Би-би-си", + "Name[sk]": "BBC Weather", + "Name[sl]": "BBC Vreme", + "Name[sv]": "BBC Weather", + "Name[ta]": "BBC வானிலை", + "Name[tg]": "Обу ҳавои BBC", + "Name[tr]": "BBC Hava Durumu", + "Name[uk]": "Погода BBC", + "Name[vi]": "Ban Thời tiết BBC", + "Name[x-test]": "xxBBC Weatherxx", + "Name[zh_CN]": "BBC 天气", + "Name[zh_TW]": "BBC 天氣" + }, + "X-KDE-ParentApp": "weatherengine" +} diff --git a/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.cpp b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.cpp new file mode 100644 index 0000000000..a5399b8ea0 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.cpp @@ -0,0 +1,1053 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for BBC's Weather from the UK Met Office */ + +#include "ion_bbcukmet.h" + +#include "ion_bbcukmetdebug.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +WeatherData::WeatherData() + : stationLatitude(qQNaN()) + , stationLongitude(qQNaN()) + , condition() + , temperature_C(qQNaN()) + , windSpeed_miles(qQNaN()) + , humidity(qQNaN()) + , pressure(qQNaN()) +{ +} + +WeatherData::ForecastInfo::ForecastInfo() + : tempHigh(qQNaN()) + , tempLow(qQNaN()) + , windSpeed(qQNaN()) +{ +} + +// ctor, dtor +UKMETIon::UKMETIon(QObject *parent, const QVariantList &args) + : IonInterface(parent, args) + +{ + setInitialized(true); +} + +UKMETIon::~UKMETIon() +{ + deleteForecasts(); +} + +void UKMETIon::reset() +{ + deleteForecasts(); + m_sourcesToReset = sources(); + updateAllSources(); +} + +void UKMETIon::deleteForecasts() +{ + // Destroy each forecast stored in a QVector + QHash::iterator it = m_weatherData.begin(), end = m_weatherData.end(); + for (; it != end; ++it) { + qDeleteAll(it.value().forecasts); + it.value().forecasts.clear(); + } +} + +QMap UKMETIon::setupDayIconMappings() const +{ + // ClearDay, FewCloudsDay, PartlyCloudyDay, Overcast, + // Showers, ScatteredShowers, Thunderstorm, Snow, + // FewCloudsNight, PartlyCloudyNight, ClearNight, + // Mist, NotAvailable + + return QMap{ + {QStringLiteral("sunny"), ClearDay}, + // { QStringLiteral("sunny night"), ClearNight }, + {QStringLiteral("clear"), ClearDay}, + {QStringLiteral("clear sky"), ClearDay}, + {QStringLiteral("sunny intervals"), PartlyCloudyDay}, + // { QStringLiteral("sunny intervals night"), ClearNight }, + {QStringLiteral("light cloud"), PartlyCloudyDay}, + {QStringLiteral("partly cloudy"), PartlyCloudyDay}, + {QStringLiteral("cloudy"), PartlyCloudyDay}, + {QStringLiteral("white cloud"), PartlyCloudyDay}, + {QStringLiteral("grey cloud"), Overcast}, + {QStringLiteral("thick cloud"), Overcast}, + // { QStringLiteral("low level cloud"), NotAvailable }, + // { QStringLiteral("medium level cloud"), NotAvailable }, + // { QStringLiteral("sandstorm"), NotAvailable }, + {QStringLiteral("drizzle"), LightRain}, + {QStringLiteral("misty"), Mist}, + {QStringLiteral("mist"), Mist}, + {QStringLiteral("fog"), Mist}, + {QStringLiteral("foggy"), Mist}, + {QStringLiteral("tropical storm"), Thunderstorm}, + {QStringLiteral("hazy"), NotAvailable}, + {QStringLiteral("light shower"), Showers}, + {QStringLiteral("light rain shower"), Showers}, + {QStringLiteral("light rain showers"), Showers}, + {QStringLiteral("light showers"), Showers}, + {QStringLiteral("light rain"), Showers}, + {QStringLiteral("heavy rain"), Rain}, + {QStringLiteral("heavy showers"), Rain}, + {QStringLiteral("heavy shower"), Rain}, + {QStringLiteral("heavy rain shower"), Rain}, + {QStringLiteral("heavy rain showers"), Rain}, + {QStringLiteral("thundery shower"), Thunderstorm}, + {QStringLiteral("thundery showers"), Thunderstorm}, + {QStringLiteral("thunderstorm"), Thunderstorm}, + {QStringLiteral("cloudy with sleet"), RainSnow}, + {QStringLiteral("sleet shower"), RainSnow}, + {QStringLiteral("sleet showers"), RainSnow}, + {QStringLiteral("sleet"), RainSnow}, + {QStringLiteral("cloudy with hail"), Hail}, + {QStringLiteral("hail shower"), Hail}, + {QStringLiteral("hail showers"), Hail}, + {QStringLiteral("hail"), Hail}, + {QStringLiteral("light snow"), LightSnow}, + {QStringLiteral("light snow shower"), Flurries}, + {QStringLiteral("light snow showers"), Flurries}, + {QStringLiteral("cloudy with light snow"), LightSnow}, + {QStringLiteral("heavy snow"), Snow}, + {QStringLiteral("heavy snow shower"), Snow}, + {QStringLiteral("heavy snow showers"), Snow}, + {QStringLiteral("cloudy with heavy snow"), Snow}, + {QStringLiteral("na"), NotAvailable}, + }; +} + +QMap UKMETIon::setupNightIconMappings() const +{ + return QMap{ + {QStringLiteral("clear"), ClearNight}, + {QStringLiteral("clear sky"), ClearNight}, + {QStringLiteral("clear intervals"), PartlyCloudyNight}, + {QStringLiteral("sunny intervals"), PartlyCloudyDay}, // it's not really sunny + {QStringLiteral("sunny"), ClearDay}, + {QStringLiteral("light cloud"), PartlyCloudyNight}, + {QStringLiteral("partly cloudy"), PartlyCloudyNight}, + {QStringLiteral("cloudy"), PartlyCloudyNight}, + {QStringLiteral("white cloud"), PartlyCloudyNight}, + {QStringLiteral("grey cloud"), Overcast}, + {QStringLiteral("thick cloud"), Overcast}, + {QStringLiteral("drizzle"), LightRain}, + {QStringLiteral("misty"), Mist}, + {QStringLiteral("mist"), Mist}, + {QStringLiteral("fog"), Mist}, + {QStringLiteral("foggy"), Mist}, + {QStringLiteral("tropical storm"), Thunderstorm}, + {QStringLiteral("hazy"), NotAvailable}, + {QStringLiteral("light shower"), Showers}, + {QStringLiteral("light rain shower"), Showers}, + {QStringLiteral("light rain showers"), Showers}, + {QStringLiteral("light showers"), Showers}, + {QStringLiteral("light rain"), Showers}, + {QStringLiteral("heavy rain"), Rain}, + {QStringLiteral("heavy showers"), Rain}, + {QStringLiteral("heavy shower"), Rain}, + {QStringLiteral("heavy rain shower"), Rain}, + {QStringLiteral("heavy rain showers"), Rain}, + {QStringLiteral("thundery shower"), Thunderstorm}, + {QStringLiteral("thundery showers"), Thunderstorm}, + {QStringLiteral("thunderstorm"), Thunderstorm}, + {QStringLiteral("cloudy with sleet"), RainSnow}, + {QStringLiteral("sleet shower"), RainSnow}, + {QStringLiteral("sleet showers"), RainSnow}, + {QStringLiteral("sleet"), RainSnow}, + {QStringLiteral("cloudy with hail"), Hail}, + {QStringLiteral("hail shower"), Hail}, + {QStringLiteral("hail showers"), Hail}, + {QStringLiteral("hail"), Hail}, + {QStringLiteral("light snow"), LightSnow}, + {QStringLiteral("light snow shower"), Flurries}, + {QStringLiteral("light snow showers"), Flurries}, + {QStringLiteral("cloudy with light snow"), LightSnow}, + {QStringLiteral("heavy snow"), Snow}, + {QStringLiteral("heavy snow shower"), Snow}, + {QStringLiteral("heavy snow showers"), Snow}, + {QStringLiteral("cloudy with heavy snow"), Snow}, + {QStringLiteral("na"), NotAvailable}, + }; +} + +QMap UKMETIon::setupWindIconMappings() const +{ + return QMap{ + {QStringLiteral("northerly"), N}, + {QStringLiteral("north north easterly"), NNE}, + {QStringLiteral("north easterly"), NE}, + {QStringLiteral("east north easterly"), ENE}, + {QStringLiteral("easterly"), E}, + {QStringLiteral("east south easterly"), ESE}, + {QStringLiteral("south easterly"), SE}, + {QStringLiteral("south south easterly"), SSE}, + {QStringLiteral("southerly"), S}, + {QStringLiteral("south south westerly"), SSW}, + {QStringLiteral("south westerly"), SW}, + {QStringLiteral("west south westerly"), WSW}, + {QStringLiteral("westerly"), W}, + {QStringLiteral("west north westerly"), WNW}, + {QStringLiteral("north westerly"), NW}, + {QStringLiteral("north north westerly"), NNW}, + {QStringLiteral("calm"), VR}, + }; +} + +QMap const &UKMETIon::dayIcons() const +{ + static QMap const dval = setupDayIconMappings(); + return dval; +} + +QMap const &UKMETIon::nightIcons() const +{ + static QMap const nval = setupNightIconMappings(); + return nval; +} + +QMap const &UKMETIon::windIcons() const +{ + static QMap const wval = setupWindIconMappings(); + return wval; +} + +// Get a specific Ion's data +bool UKMETIon::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 + + const QStringList sourceAction = source.split(QLatin1Char('|')); + + // Guard: if the size of array is not 3 then we have bad data, return an error + if (sourceAction.size() < 3) { + setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed")); + return true; + } + + if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) { + // Look for places to match + findPlace(sourceAction[2], source); + return true; + } + + if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) { + if (sourceAction.count() >= 3) { + if (sourceAction[2].isEmpty()) { + setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed")); + return true; + } + + XMLMapInfo &place = m_place[QLatin1String("bbcukmet|") + sourceAction[2]]; + + // backward compatibility after rss feed url change in 2018/03 + place.sourceExtraArg = sourceAction[3]; + if (place.sourceExtraArg.startsWith(QLatin1String("http://open.live.bbc.co.uk/"))) { + // Old data source id stored the full (now outdated) observation feed url + // http://open.live.bbc.co.uk/weather/feeds/en/STATIOID/observations.rss + // as extra argument, so extract the id from that + place.stationId = place.sourceExtraArg.section(QLatin1Char('/'), -2, -2); + } else { + place.stationId = place.sourceExtraArg; + } + getXMLData(sourceAction[0] + QLatin1Char('|') + sourceAction[2]); + return true; + } + return false; + } + + setData(source, QStringLiteral("validate"), QStringLiteral("bbcukmet|malformed")); + return true; +} + +// Gets specific city XML data +void UKMETIon::getXMLData(const QString &source) +{ + for (const QString &fetching : qAsConst(m_obsJobList)) { + if (fetching == source) { + // already getting this source and awaiting the data + return; + } + } + + const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/observation/rss/") + m_place[source].stationId); + + KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies + m_obsJobXml.insert(getJob, new QXmlStreamReader); + m_obsJobList.insert(getJob, source); + + connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::observation_slotDataArrived); + connect(getJob, &KJob::result, this, &UKMETIon::observation_slotJobFinished); +} + +// Parses city list and gets the correct city based on ID number +void UKMETIon::findPlace(const QString &place, const QString &source) +{ + // the API needs auto=true for partial-text searching + // but unlike the normal query, using auto=true doesn't show locations which match the text but with different unicode + // for example "hyderabad" with no auto matches "Hyderabad" and "Hyderābād" + // but with auto matches only "Hyderabad" + // so we merge the two results + const QUrl url(QLatin1String("https://open.live.bbc.co.uk/locator/locations?s=") + place + QLatin1String("&format=json")); + const QUrl autoUrl(QLatin1String("https://open.live.bbc.co.uk/locator/locations?s=") + place + QLatin1String("&format=json&auto=true")); + + m_normalSearchArrived = false; + m_autoSearchArrived = false; + + KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies + m_jobHtml.insert(getJob, new QByteArray()); + m_jobList.insert(getJob, source); + + connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::setup_slotDataArrived); + + KIO::TransferJob *autoGetJob = KIO::get(autoUrl, KIO::Reload, KIO::HideProgressInfo); + autoGetJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies + m_jobHtml.insert(autoGetJob, new QByteArray()); + m_jobList.insert(autoGetJob, source); + + connect(autoGetJob, &KIO::TransferJob::data, this, &UKMETIon::setup_slotDataArrived); + + connect(getJob, &KJob::result, this, [&](KJob *job) { + setup_slotJobFinished(job, QStringLiteral("normal")); + }); + connect(autoGetJob, &KJob::result, this, [&](KJob *job) { + setup_slotJobFinished(job, QStringLiteral("auto")); + }); +} + +void UKMETIon::getFiveDayForecast(const QString &source) +{ + XMLMapInfo &place = m_place[source]; + + const QUrl url(QStringLiteral("https://weather-broker-cdn.api.bbci.co.uk/en/forecast/rss/3day/") + place.stationId); + + KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies + m_forecastJobXml.insert(getJob, new QXmlStreamReader); + m_forecastJobList.insert(getJob, source); + + connect(getJob, &KIO::TransferJob::data, this, &UKMETIon::forecast_slotDataArrived); + connect(getJob, &KJob::result, this, &UKMETIon::forecast_slotJobFinished); +} + +void UKMETIon::readSearchHTMLData(const QString &source, const QList htmls) +{ + int counter = 2; + + for (const QByteArray *html : htmls) { + if (!html) { + continue; + } + + QJsonObject jsonDocumentObject = QJsonDocument::fromJson(*html).object().value(QStringLiteral("response")).toObject(); + + if (!jsonDocumentObject.isEmpty()) { + QJsonValue resultsVariant = jsonDocumentObject.value(QStringLiteral("locations")); + + if (resultsVariant.isUndefined()) { + // this is a response from an auto=true query + resultsVariant = jsonDocumentObject.value(QStringLiteral("results")).toObject().value(QStringLiteral("results")); + } + + const QJsonArray results = resultsVariant.toArray(); + + for (const QJsonValue &resultValue : results) { + QJsonObject result = resultValue.toObject(); + const QString id = result.value(QStringLiteral("id")).toString(); + const QString name = result.value(QStringLiteral("name")).toString(); + const QString area = result.value(QStringLiteral("container")).toString(); + const QString country = result.value(QStringLiteral("country")).toString(); + + if (!id.isEmpty() && !name.isEmpty() && !area.isEmpty() && !country.isEmpty()) { + const QString fullName = name + QLatin1String(", ") + area + QLatin1String(", ") + country; + QString tmp = QLatin1String("bbcukmet|") + fullName; + + // Duplicate places can exist, show them too + // but not if they have the exact same id, which can happen since we're merging two results + if (m_locations.contains(tmp) && m_place[tmp].stationId != id) { + tmp += QLatin1String(" (#") + QString::number(counter) + QLatin1Char(')'); + counter++; + } + XMLMapInfo &place = m_place[tmp]; + place.stationId = id; + place.place = fullName; + m_locations.append(tmp); + } + } + } + } + + validate(source); +} + +// handle when no XML tag is found +void UKMETIon::parseUnknownElement(QXmlStreamReader &xml) const +{ + while (!xml.atEnd()) { + xml.readNext(); + + if (xml.isEndElement()) { + break; + } + + if (xml.isStartElement()) { + parseUnknownElement(xml); + } + } +} + +void UKMETIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + if (data.isEmpty() || !m_jobHtml.contains(job)) { + return; + } + + m_jobHtml[job]->append(data); +} + +void UKMETIon::setup_slotJobFinished(KJob *job, const QString &type) +{ + if (job->error() == KIO::ERR_SERVER_TIMEOUT) { + setData(m_jobList[job], QStringLiteral("validate"), QStringLiteral("bbcukmet|timeout")); + disconnectSource(m_jobList[job], this); + m_jobList.remove(job); + delete m_jobHtml[job]; + m_jobHtml.remove(job); + return; + } + + if (type == QStringLiteral("normal")) { + m_normalSearchArrived = true; + } + if (type == QStringLiteral("auto")) { + m_autoSearchArrived = true; + } + if (!(m_normalSearchArrived && m_autoSearchArrived)) { + return; + } + + // If Redirected, don't go to this routine + if (!m_locations.contains(QLatin1String("bbcukmet|") + m_jobList[job])) { + readSearchHTMLData(m_jobList[job] /* source is same for both */, m_jobHtml.values()); + } + + m_jobList.clear(); + for (auto html : m_jobHtml.values()) { + delete html; + } + m_jobHtml.clear(); +} + +void UKMETIon::observation_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + if (data.isEmpty() || !m_obsJobXml.contains(job)) { + return; + } + + // Send to xml. + m_obsJobXml[job]->addData(local); +} + +void UKMETIon::observation_slotJobFinished(KJob *job) +{ + const QString source = m_obsJobList.value(job); + setData(source, Data()); + + QXmlStreamReader *reader = m_obsJobXml.value(job); + if (reader) { + readObservationXMLData(m_obsJobList[job], *reader); + } + + m_obsJobList.remove(job); + delete m_obsJobXml[job]; + m_obsJobXml.remove(job); + + if (m_sourcesToReset.contains(source)) { + m_sourcesToReset.removeAll(source); + Q_EMIT forceUpdate(this, source); + } +} + +void UKMETIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + if (data.isEmpty() || !m_forecastJobXml.contains(job)) { + return; + } + + // Send to xml. + m_forecastJobXml[job]->addData(local); +} + +void UKMETIon::forecast_slotJobFinished(KJob *job) +{ + setData(m_forecastJobList[job], Data()); + QXmlStreamReader *reader = m_forecastJobXml.value(job); + if (reader) { + readFiveDayForecastXMLData(m_forecastJobList[job], *reader); + } + + m_forecastJobList.remove(job); + delete m_forecastJobXml[job]; + m_forecastJobXml.remove(job); +} + +void UKMETIon::parsePlaceObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss")); + + while (!xml.atEnd()) { + xml.readNext(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement() && elementName == QLatin1String("rss")) { + break; + } + + if (xml.isStartElement() && elementName == QLatin1String("channel")) { + parseWeatherChannel(source, data, xml); + } + } +} + +void UKMETIon::parsePlaceForecast(const QString &source, QXmlStreamReader &xml) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("rss")); + + while (!xml.atEnd()) { + xml.readNext(); + + if (xml.isStartElement() && xml.name() == QLatin1String("channel")) { + parseWeatherForecast(source, xml); + } + } +} + +void UKMETIon::parseWeatherChannel(const QString &source, WeatherData &data, QXmlStreamReader &xml) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel")); + + while (!xml.atEnd()) { + xml.readNext(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement() && elementName == QLatin1String("channel")) { + break; + } + + if (xml.isStartElement()) { + if (elementName == QLatin1String("title")) { + data.stationName = xml.readElementText().section(QStringLiteral("Observations for"), 1, 1).trimmed(); + data.stationName.replace(QStringLiteral("United Kingdom"), i18n("UK")); + data.stationName.replace(QStringLiteral("United States of America"), i18n("USA")); + + } else if (elementName == QLatin1String("item")) { + parseWeatherObservation(source, data, xml); + } else { + parseUnknownElement(xml); + } + } + } +} + +void UKMETIon::parseWeatherForecast(const QString &source, QXmlStreamReader &xml) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("channel")); + + while (!xml.atEnd()) { + xml.readNext(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement() && elementName == QLatin1String("channel")) { + break; + } + + if (xml.isStartElement()) { + if (elementName == QLatin1String("item")) { + parseFiveDayForecast(source, xml); + } else if (elementName == QLatin1String("link") && xml.namespaceUri().isEmpty()) { + m_place[source].forecastHTMLUrl = xml.readElementText(); + } else { + parseUnknownElement(xml); + } + } + } +} + +void UKMETIon::parseWeatherObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml) +{ + Q_UNUSED(source); + + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item")); + + while (!xml.atEnd()) { + xml.readNext(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement() && elementName == QLatin1String("item")) { + break; + } + + if (xml.isStartElement()) { + if (elementName == QLatin1String("title")) { + QString conditionString = xml.readElementText(); + + // Get the observation time and condition + int splitIndex = conditionString.lastIndexOf(QLatin1Char(':')); + if (splitIndex >= 0) { + QString conditionData = conditionString.mid(splitIndex + 1); // Skip ':' + data.obsTime = conditionString.left(splitIndex); + + if (data.obsTime.contains(QLatin1Char('-'))) { + // Saturday - 13:00 CET + // Saturday - 12:00 GMT + // timezone parsing is not yet supported by QDateTime, also is there just a dayname + // so try manually + // guess date from day + const QString dayString = data.obsTime.section(QLatin1Char('-'), 0, 0).trimmed(); + QDate date = QDate::currentDate(); + const QString dayFormat = QStringLiteral("dddd"); + const int testDayJumps[4] = { + -1, // first to weekday yesterday + 2, // then to weekday tomorrow + -3, // then to weekday before yesterday, not sure if such day offset can happen? + 4, // then to weekday after tomorrow, not sure if such day offset can happen? + }; + const int dayJumps = sizeof(testDayJumps) / sizeof(testDayJumps[0]); + QLocale cLocale = QLocale::c(); + int dayJump = 0; + while (true) { + if (cLocale.toString(date, dayFormat) == dayString) { + break; + } + + if (dayJump >= dayJumps) { + // no weekday found near-by, set date invalid + date = QDate(); + break; + } + date = date.addDays(testDayJumps[dayJump]); + ++dayJump; + } + + if (date.isValid()) { + const QString timeString = data.obsTime.section(QLatin1Char('-'), 1, 1).trimmed(); + const QTime time = QTime::fromString(timeString.section(QLatin1Char(' '), 0, 0), QStringLiteral("hh:mm")); + const QTimeZone timeZone = QTimeZone(timeString.section(QLatin1Char(' '), 1, 1).toUtf8()); + // TODO: if non-IANA timezone id is not known, try to guess timezone from other data + + if (time.isValid() && timeZone.isValid()) { + data.observationDateTime = QDateTime(date, time, timeZone); + } + } + } + + if (conditionData.contains(QLatin1Char(','))) { + data.condition = conditionData.section(QLatin1Char(','), 0, 0).trimmed(); + + if (data.condition == QLatin1String("null") || data.condition == QLatin1String("Not Available")) { + data.condition.clear(); + } + } + } + + } else if (elementName == QLatin1String("description")) { + QString observeString = xml.readElementText(); + const QStringList observeData = observeString.split(QLatin1Char(':')); + + // FIXME: We should make this use a QRegExp but I need some help here :) -spstarr + + QString temperature_C = observeData[1].section(QChar(176), 0, 0).trimmed(); + parseFloat(data.temperature_C, temperature_C); + + data.windDirection = observeData[2].section(QLatin1Char(','), 0, 0).trimmed(); + if (data.windDirection.contains(QLatin1String("null"))) { + data.windDirection.clear(); + } + + QString windSpeed_miles = observeData[3].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1).remove(QStringLiteral("mph")); + parseFloat(data.windSpeed_miles, windSpeed_miles); + + QString humidity = observeData[4].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1); + if (humidity.endsWith(QLatin1Char('%'))) { + humidity.chop(1); + } + parseFloat(data.humidity, humidity); + + QString pressure = observeData[5].section(QLatin1Char(','), 0, 0).section(QLatin1Char(' '), 1, 1).section(QStringLiteral("mb"), 0, 0); + parseFloat(data.pressure, pressure); + + data.pressureTendency = observeData[5].section(QLatin1Char(','), 1, 1).toLower().trimmed(); + if (data.pressureTendency == QLatin1String("no change")) { + data.pressureTendency = QStringLiteral("steady"); + } + + data.visibilityStr = observeData[6].trimmed(); + if (data.visibilityStr == QLatin1String("--")) { + data.visibilityStr.clear(); + } + + } else if (elementName == QLatin1String("lat")) { + const QString ordinate = xml.readElementText(); + data.stationLatitude = ordinate.toDouble(); + } else if (elementName == QLatin1String("long")) { + const QString ordinate = xml.readElementText(); + data.stationLongitude = ordinate.toDouble(); + } else if (elementName == QLatin1String("point") && xml.namespaceUri() == QLatin1String("http://www.georss.org/georss")) { + const QStringList ordinates = xml.readElementText().split(QLatin1Char(' ')); + data.stationLatitude = ordinates[0].toDouble(); + data.stationLongitude = ordinates[1].toDouble(); + } else { + parseUnknownElement(xml); + } + } + } +} + +bool UKMETIon::readObservationXMLData(const QString &source, QXmlStreamReader &xml) +{ + WeatherData data; + data.isForecastsDataPending = true; + bool haveObservation = false; + while (!xml.atEnd()) { + xml.readNext(); + + if (xml.isEndElement()) { + break; + } + + if (xml.isStartElement()) { + if (xml.name() == QLatin1String("rss")) { + parsePlaceObservation(source, data, xml); + haveObservation = true; + } else { + parseUnknownElement(xml); + } + } + } + + if (!haveObservation) { + return false; + } + + 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); + } + + // Get the 5 day forecast info next. + getFiveDayForecast(source); + + return !xml.error(); +} + +bool UKMETIon::readFiveDayForecastXMLData(const QString &source, QXmlStreamReader &xml) +{ + bool haveFiveDay = false; + while (!xml.atEnd()) { + xml.readNext(); + + if (xml.isEndElement()) { + break; + } + + if (xml.isStartElement()) { + if (xml.name() == QLatin1String("rss")) { + parsePlaceForecast(source, xml); + haveFiveDay = true; + } else { + parseUnknownElement(xml); + } + } + } + if (!haveFiveDay) + return false; + updateWeather(source); + return !xml.error(); +} + +void UKMETIon::parseFiveDayForecast(const QString &source, QXmlStreamReader &xml) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("item")); + + WeatherData &weatherData = m_weatherData[source]; + QVector &forecasts = weatherData.forecasts; + + // Flush out the old forecasts when updating. + forecasts.clear(); + + WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo; + QString line; + QString period; + QString summary; + const QRegularExpression high(QStringLiteral("Maximum Temperature: (-?\\d+).C"), QRegularExpression::CaseInsensitiveOption); + const QRegularExpression low(QStringLiteral("Minimum Temperature: (-?\\d+).C"), QRegularExpression::CaseInsensitiveOption); + while (!xml.atEnd()) { + xml.readNext(); + if (xml.name() == QLatin1String("title")) { + line = xml.readElementText().trimmed(); + + // FIXME: We should make this all use QRegExps in UKMETIon::parseFiveDayForecast() for forecast -spstarr + + const QString p = line.section(QLatin1Char(','), 0, 0); + period = p.section(QLatin1Char(':'), 0, 0); + summary = p.section(QLatin1Char(':'), 1, 1).trimmed(); + + const QString temps = line.section(QLatin1Char(','), 1, 1); + // Sometimes only one of min or max are reported + QRegularExpressionMatch rmatch; + if (temps.contains(high, &rmatch)) { + parseFloat(forecast->tempHigh, rmatch.captured(1)); + } + if (temps.contains(low, &rmatch)) { + parseFloat(forecast->tempLow, rmatch.captured(1)); + } + + const QString summaryLC = summary.toLower(); + forecast->period = period; + if (forecast->period == QLatin1String("Tonight")) { + forecast->iconName = getWeatherIcon(nightIcons(), summaryLC); + } else { + forecast->iconName = getWeatherIcon(dayIcons(), summaryLC); + } + // db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation + const QString summaryTranslated = i18nc("weather forecast", summaryLC.toUtf8().data()); + forecast->summary = (summaryTranslated != summaryLC) ? summaryTranslated : summary; + qCDebug(IONENGINE_BBCUKMET) << "i18n summary string: " << forecast->summary; + forecasts.append(forecast); + // prepare next + forecast = new WeatherData::ForecastInfo; + } + } + + weatherData.isForecastsDataPending = false; + + // remove unused + delete forecast; +} + +void UKMETIon::parseFloat(float &value, const QString &string) +{ + bool ok = false; + const float result = string.toFloat(&ok); + if (ok) { + value = result; + } +} + +void UKMETIon::validate(const QString &source) +{ + if (m_locations.isEmpty()) { + const QString invalidPlace = source.section(QLatin1Char('|'), 2, 2); + if (m_place[QStringLiteral("bbcukmet|") + invalidPlace].place.isEmpty()) { + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|invalid|multiple|") + invalidPlace)); + } + return; + } + + QString placeList; + for (const QString &place : qAsConst(m_locations)) { + const QString p = place.section(QLatin1Char('|'), 1, 1); + placeList.append(QStringLiteral("|place|") + p + QStringLiteral("|extra|") + m_place[place].stationId); + } + if (m_locations.count() > 1) { + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|valid|multiple") + placeList)); + } else { + placeList[7] = placeList[7].toUpper(); + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("bbcukmet|valid|single") + placeList)); + } + m_locations.clear(); +} + +void UKMETIon::updateWeather(const QString &source) +{ + const WeatherData &weatherData = m_weatherData[source]; + + if (weatherData.isForecastsDataPending || weatherData.isSolarDataPending) { + return; + } + + const XMLMapInfo &place = m_place[source]; + + QString weatherSource = source; + // TODO: why the replacement here instead of just a new string? + weatherSource.replace(QStringLiteral("bbcukmet|"), QStringLiteral("bbcukmet|weather|")); + weatherSource.append(QLatin1Char('|') + place.sourceExtraArg); + + Plasma::DataEngine::Data data; + + // work-around for buggy observation RSS feed missing the station name + QString stationName = weatherData.stationName; + if (stationName.isEmpty() || stationName == QLatin1Char(',')) { + stationName = source.section(QLatin1Char('|'), 1, 1); + } + + data.insert(QStringLiteral("Place"), stationName); + data.insert(QStringLiteral("Station"), stationName); + if (weatherData.observationDateTime.isValid()) { + data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); + } + if (!weatherData.obsTime.isEmpty()) { + data.insert(QStringLiteral("Observation Period"), weatherData.obsTime); + } + if (!weatherData.condition.isEmpty()) { + // db uses original strings normalized to lowercase, but we prefer the unnormalized if without translation + const QString conditionLC = weatherData.condition.toLower(); + const QString conditionTranslated = i18nc("weather condition", conditionLC.toUtf8().data()); + data.insert(QStringLiteral("Current Conditions"), (conditionTranslated != conditionLC) ? conditionTranslated : weatherData.condition); + } + // qCDebug(IONENGINE_BBCUKMET) << "i18n condition string: " << i18nc("weather condition", weatherData.condition.toUtf8().data()); + + const bool stationCoordsValid = (!qIsNaN(weatherData.stationLatitude) && !qIsNaN(weatherData.stationLongitude)); + + if (stationCoordsValid) { + data.insert(QStringLiteral("Latitude"), weatherData.stationLatitude); + data.insert(QStringLiteral("Longitude"), weatherData.stationLongitude); + } + + data.insert(QStringLiteral("Condition Icon"), getWeatherIcon(weatherData.isNight ? nightIcons() : dayIcons(), weatherData.condition)); + + if (!qIsNaN(weatherData.humidity)) { + data.insert(QStringLiteral("Humidity"), weatherData.humidity); + data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); + } + + if (!weatherData.visibilityStr.isEmpty()) { + data.insert(QStringLiteral("Visibility"), i18nc("visibility", weatherData.visibilityStr.toUtf8().data())); + data.insert(QStringLiteral("Visibility Unit"), KUnitConversion::NoUnit); + } + + if (!qIsNaN(weatherData.temperature_C)) { + data.insert(QStringLiteral("Temperature"), weatherData.temperature_C); + } + + // Used for all temperatures + data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius); + + if (!qIsNaN(weatherData.pressure)) { + data.insert(QStringLiteral("Pressure"), weatherData.pressure); + data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Millibar); + if (!weatherData.pressureTendency.isEmpty()) { + data.insert(QStringLiteral("Pressure Tendency"), weatherData.pressureTendency); + } + } + + if (!qIsNaN(weatherData.windSpeed_miles)) { + data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed_miles); + data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::MilePerHour); + if (!weatherData.windDirection.isEmpty()) { + data.insert(QStringLiteral("Wind Direction"), getWindDirectionIcon(windIcons(), weatherData.windDirection.toLower())); + } + } + + // 5 Day forecast info + 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 period = forecastInfo->period; + // same day + period.replace(QStringLiteral("Today"), i18nc("Short for Today", "Today")); + period.replace(QStringLiteral("Tonight"), i18nc("Short for Tonight", "Tonight")); + // upcoming days + period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat")); + period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun")); + period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon")); + period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue")); + period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed")); + period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu")); + period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri")); + + const QString tempHigh = qIsNaN(forecastInfo->tempHigh) ? QString() : QString::number(forecastInfo->tempHigh); + const QString tempLow = qIsNaN(forecastInfo->tempLow) ? QString() : QString::number(forecastInfo->tempLow); + + data.insert(QStringLiteral("Short Forecast Day %1").arg(i), + QStringLiteral("%1|%2|%3|%4|%5|%6").arg(period, forecastInfo->iconName, forecastInfo->summary, tempHigh, tempLow, QString())); + //.arg(forecastInfo->windSpeed) + // arg(forecastInfo->windDirection)); + + ++i; + } + + data.insert(QStringLiteral("Credit"), i18nc("credit line, keep string short", "Data from BBC\302\240Weather")); + data.insert(QStringLiteral("Credit Url"), place.forecastHTMLUrl); + + setData(weatherSource, data); +} + +void UKMETIon::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_PLUGIN_CLASS_WITH_JSON(UKMETIon, "ion-bbcukmet.json") + +#include "ion_bbcukmet.moc" diff --git a/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.h b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.h new file mode 100644 index 0000000000..2d467570b6 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/bbcukmet/ion_bbcukmet.h @@ -0,0 +1,170 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for BBC Weather from UKMET Office */ + +#pragma once + +#include "../ion.h" + +#include + +#include +#include + +class KJob; +namespace KIO +{ +class Job; +class TransferJob; +} +class QXmlStreamReader; + +class WeatherData +{ +public: + WeatherData(); + + QString place; + QString stationName; + double stationLatitude; + double stationLongitude; + + // Current observation information. + QString obsTime; + QDateTime observationDateTime; + + QString condition; + QString conditionIcon; + float temperature_C; + QString windDirection; + float windSpeed_miles; + float humidity; + float pressure; + QString pressureTendency; + QString visibilityStr; + + QString solarDataTimeEngineSourceName; + bool isNight = false; + bool isSolarDataPending = false; + + // Five day forecast + struct ForecastInfo { + ForecastInfo(); + QString period; + QString iconName; + QString summary; + float tempHigh; + float tempLow; + float windSpeed; + QString windDirection; + }; + + // 5 day Forecast + QVector forecasts; + + bool isForecastsDataPending = false; +}; + +Q_DECLARE_TYPEINFO(WeatherData::ForecastInfo, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData, Q_MOVABLE_TYPE); + +class Q_DECL_EXPORT UKMETIon : public IonInterface, public Plasma::DataEngineConsumer +{ + Q_OBJECT + +public: + UKMETIon(QObject *parent, const QVariantList &args); + ~UKMETIon() override; + +public: // IonInterface API + bool updateIonSource(const QString &source) override; + +public Q_SLOTS: + // for solar data pushes from the time engine + void dataUpdated(const QString &sourceName, const Plasma::DataEngine::Data &data); + +protected: // IonInterface API + void reset() override; + +private Q_SLOTS: + void setup_slotDataArrived(KIO::Job *, const QByteArray &); + void setup_slotJobFinished(KJob *, const QString &); + // void setup_slotRedirected(KIO::Job *, const KUrl &url); + + void observation_slotDataArrived(KIO::Job *, const QByteArray &); + void observation_slotJobFinished(KJob *); + + void forecast_slotDataArrived(KIO::Job *, const QByteArray &); + void forecast_slotJobFinished(KJob *); + +private: + void updateWeather(const QString &source); + + // bool night(const QString& source) const; + + /* UKMET Methods - Internal for Ion */ + QMap setupDayIconMappings() const; + QMap setupNightIconMappings() const; + QMap setupWindIconMappings() const; + + QMap const &nightIcons() const; + QMap const &dayIcons() const; + QMap const &windIcons() const; + + // Load and Parse the place search XML listings + void findPlace(const QString &place, const QString &source); + void validate(const QString &source); // Sync data source with Applet + void getFiveDayForecast(const QString &source); + void getXMLData(const QString &source); + void readSearchHTMLData(const QString &source, const QList htmls); + bool readFiveDayForecastXMLData(const QString &source, QXmlStreamReader &xml); + void parseSearchLocations(const QString &source, QXmlStreamReader &xml); + + // Observation parsing methods + bool readObservationXMLData(const QString &source, QXmlStreamReader &xml); + void parsePlaceObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml); + void parseWeatherChannel(const QString &source, WeatherData &data, QXmlStreamReader &xml); + void parseWeatherObservation(const QString &source, WeatherData &data, QXmlStreamReader &xml); + void parseFiveDayForecast(const QString &source, QXmlStreamReader &xml); + void parsePlaceForecast(const QString &source, QXmlStreamReader &xml); + void parseWeatherForecast(const QString &source, QXmlStreamReader &xml); + void parseUnknownElement(QXmlStreamReader &xml) const; + + void parseFloat(float &value, const QString &string); + + void deleteForecasts(); + +private: + struct XMLMapInfo { + QString stationId; + QString place; + QString forecastHTMLUrl; + QString sourceExtraArg; + }; + + // Key dicts + QHash m_place; + QVector m_locations; + + // Weather information + QHash m_weatherData; + + // Store KIO jobs - Search list + QHash m_jobHtml; + QHash m_jobList; + + bool m_normalSearchArrived = false; + bool m_autoSearchArrived = false; + + QHash m_obsJobXml; + QHash m_obsJobList; + + QHash m_forecastJobXml; + QHash m_forecastJobList; + + QStringList m_sourcesToReset; +}; diff --git a/plasma/workspace/dataengines/weather/ions/data/bbcukmet_i18n.dat b/plasma/workspace/dataengines/weather/ions/data/bbcukmet_i18n.dat new file mode 100644 index 0000000000..8f6c09d9ee --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/data/bbcukmet_i18n.dat @@ -0,0 +1,105 @@ +weather condition|clear +weather condition|clear intervals +weather condition|clear sky +weather condition|cloudy +weather condition|cloudy with hail +weather condition|cloudy with heavy snow +weather condition|cloudy with light snow +weather condition|cloudy with sleet +weather condition|drizzle +weather condition|fog +weather condition|foggy +weather condition|grey cloud +weather condition|hail +weather condition|hail shower +weather condition|hail showers +weather condition|hazy +weather condition|heavy rain +weather condition|heavy rain shower +weather condition|heavy rain showers +weather condition|heavy shower +weather condition|heavy showers +weather condition|heavy snow +weather condition|heavy snow shower +weather condition|heavy snow showers +weather condition|light cloud +weather condition|light rain +weather condition|light rain shower +weather condition|light rain showers +weather condition|light shower +weather condition|light showers +weather condition|light snow +weather condition|light snow shower +weather condition|light snow showers +weather condition|mist +weather condition|misty +weather condition|N/A +weather condition|na +weather condition|partly cloudy +weather condition|sandstorm +weather condition|sleet +weather condition|sleet shower +weather condition|sleet showers +weather condition|sunny +weather condition|sunny intervals +weather condition|thick cloud +weather condition|thunderstorm +weather condition|thundery shower +weather condition|thundery showers +weather condition|tropical storm +weather condition|white cloud +weather forecast|clear +weather forecast|clear intervals +weather forecast|clear sky +weather forecast|cloudy +weather forecast|cloudy with hail +weather forecast|cloudy with heavy snow +weather forecast|cloudy with light snow +weather forecast|cloudy with sleet +weather forecast|drizzle +weather forecast|fog +weather forecast|foggy +weather forecast|grey cloud +weather forecast|hail +weather forecast|hail shower +weather forecast|hail showers +weather forecast|hazy +weather forecast|heavy rain +weather forecast|heavy rain shower +weather forecast|heavy rain showers +weather forecast|heavy shower +weather forecast|heavy showers +weather forecast|heavy snow +weather forecast|heavy snow shower +weather forecast|heavy snow showers +weather forecast|light cloud +weather forecast|light rain +weather forecast|light rain shower +weather forecast|light rain showers +weather forecast|light shower +weather forecast|light showers +weather forecast|light snow +weather forecast|light snow shower +weather forecast|light snow showers +weather forecast|mist +weather forecast|misty +weather forecast|na +weather forecast|partly cloudy +weather forecast|sandstorm +weather forecast|sleet +weather forecast|sleet shower +weather forecast|sleet showers +weather forecast|sunny +weather forecast|sunny intervals +weather forecast|thick cloud +weather forecast|thunderstorm +weather forecast|thundery shower +weather forecast|thundery showers +weather forecast|tropical storm +weather forecast|white cloud +visibility|Excellent +visibility|Very Good +visibility|Good +visibility|Moderate +visibility|Poor +visibility|Very Poor diff --git a/plasma/workspace/dataengines/weather/ions/data/envcan_i18n.dat b/plasma/workspace/dataengines/weather/ions/data/envcan_i18n.dat new file mode 100644 index 0000000000..31eb647001 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/data/envcan_i18n.dat @@ -0,0 +1,342 @@ +weather condition|N/A +weather condition|Blowing Snow +weather condition|Clear +weather condition|Cloudy +weather condition|Decreasing Cloud +weather condition|Distant Precipitation +weather condition|Drifting Snow +weather condition|Drizzle +weather condition|Dust +weather condition|Dust Devils +weather condition|Fog +weather condition|Fog Bank Near Station +weather condition|Fog Depositing Ice +weather condition|Fog Patches +weather condition|Freezing drizzle +weather condition|Freezing rain +weather condition|Funnel Cloud +weather condition|Hail +weather condition|Haze +weather condition|Heavy Blowing Snow +weather condition|Heavy Drifting Snow +weather condition|Heavy Drizzle +weather condition|Heavy Hail +weather condition|Heavy Mixed Rain and Drizzle +weather condition|Heavy Mixed Rain and Snow Shower +weather condition|Heavy Rain +weather condition|Heavy Rain and Snow +weather condition|Heavy Rainshower +weather condition|Heavy Snow +weather condition|Heavy Snow Pellets +weather condition|Heavy Snowshower +weather condition|Thunderstorm +weather condition|Heavy Thunderstorm with Hail +weather condition|Heavy Thunderstorm with Rain +weather condition|Ice Crystals +weather condition|Ice Pellets +weather condition|Increasing Cloud +weather condition|Light Drizzle +weather condition|Light Freezing Drizzle +weather condition|Light Freezing Rain +weather condition|Light Rain +weather condition|Light Rainshower +weather condition|Light Snow +weather condition|Light Snow Pellets +weather condition|Light Snowshower +weather condition|Lightning Visible +weather condition|Mainly Clear +weather condition|Mainly Sunny +weather condition|Mist +weather condition|Mixed Rain and Drizzle +weather condition|Mixed Rain and Snow Shower +weather condition|Mostly Cloudy +weather condition|Not Reported +weather condition|Partly Cloudy +weather condition|Rain +weather condition|Rain and Snow +weather condition|Rainshower +weather condition|Recent Drizzle +weather condition|Recent Dust or Sand Storm +weather condition|Recent Fog +weather condition|Recent Freezing Precipitation +weather condition|Recent Hail +weather condition|Recent Rain +weather condition|Recent Rain and Snow +weather condition|Recent Rainshower +weather condition|Recent Snow +weather condition|Recent Snowshower +weather condition|Recent Thunderstorm +weather condition|Recent Thunderstorm with Hail +weather condition|Recent Thunderstorm with Heavy Hail +weather condition|Recent Thunderstorm with Heavy Rain +weather condition|Recent Thunderstorm with Rain +weather condition|Sand or Dust Storm +weather condition|Severe Sand or Dust Storm +weather condition|Shallow Fog +weather condition|Smoke +weather condition|Snow +weather condition|Snow Crystals +weather condition|Snow Grains +weather condition|Squalls +weather condition|Sunny +weather condition|Thunderstorm with Hail +weather condition|Thunderstorm with Rain +weather condition|Thunderstorm with light rainshowers +weather condition|Thunderstorm with heavy rainshowers +weather condition|Thunderstorm with Sand or Dust Storm +weather condition|Thunderstorm without Precipitation +weather condition|Tornado +weather forecast|A few clouds +weather forecast|A few flurries +weather forecast|A few flurries mixed with ice pellets +weather forecast|A few flurries or rain showers +weather forecast|A few flurries or thundershowers +weather forecast|A few rain showers or flurries +weather forecast|A few rain showers or wet flurries +weather forecast|A few showers +weather forecast|A few showers or drizzle +weather forecast|A few showers or thundershowers +weather forecast|A few showers or thunderstorms +weather forecast|A few thundershowers +weather forecast|A few thunderstorms +weather forecast|A few wet flurries +weather forecast|A few wet flurries or rain showers +weather forecast|A mix of sun and cloud +weather forecast|Blizzard +weather forecast|Chance of drizzle +weather forecast|Chance of drizzle mixed with freezing drizzle +weather forecast|Chance of drizzle mixed with rain +weather forecast|Chance of drizzle or rain +weather forecast|Chance of flurries +weather forecast|Chance of flurries at times heavy +weather forecast|Chance of flurries mixed with ice pellets +weather forecast|Chance of flurries or ice pellets +weather forecast|Chance of flurries or rain showers +weather forecast|Chance of flurries or thundershowers +weather forecast|Chance of freezing drizzle +weather forecast|Chance of freezing rain +weather forecast|Chance of freezing rain mixed with snow +weather forecast|Chance of freezing rain or rain +weather forecast|Chance of freezing rain or snow +weather forecast|Chance of light snow +weather forecast|Chance of light snow and blowing snow +weather forecast|Chance of light snow mixed with freezing drizzle +weather forecast|Chance of light snow mixed with ice pellets +weather forecast|Chance of light snow mixed with rain +weather forecast|Chance of light snow or freezing rain +weather forecast|Chance of light snow or ice pellets +weather forecast|Chance of light snow or rain +weather forecast|Chance of light wet snow +weather forecast|Chance of rain +weather forecast|Chance of rain at times heavy +weather forecast|Chance of rain mixed with snow +weather forecast|Chance of rain or drizzle +weather forecast|Chance of rain or freezing rain +weather forecast|Chance of rain or snow +weather forecast|Chance of rain showers or flurries +weather forecast|Chance of rain showers or wet flurries +weather forecast|Chance of severe thunderstorms +weather forecast|Chance of showers +weather forecast|Chance of showers at times heavy +weather forecast|Chance of showers at times heavy or thundershowers +weather forecast|Chance of showers at times heavy or thunderstorms +weather forecast|Chance of showers or drizzle +weather forecast|Chance of showers or thundershowers +weather forecast|Chance of showers or thunderstorms +weather forecast|Chance of snow +weather forecast|Chance of snow and blizzard +weather forecast|Chance of snow mixed with freezing drizzle +weather forecast|Chance of snow mixed with freezing rain +weather forecast|Chance of snow mixed with rain +weather forecast|Chance of snow or rain +weather forecast|Chance of snow squalls +weather forecast|Chance of thundershowers +weather forecast|Chance of thunderstorms +weather forecast|Chance of thunderstorms and possible hail +weather forecast|Chance of wet flurries +weather forecast|Chance of wet flurries at times heavy +weather forecast|Chance of wet flurries or rain showers +weather forecast|Chance of wet snow +weather forecast|Chance of wet snow mixed with rain +weather forecast|Chance of wet snow or rain +weather forecast|Clear +weather forecast|Clearing +weather forecast|Cloudy +weather forecast|Partly cloudy +weather forecast|Mainly cloudy +weather forecast|Cloudy periods +weather forecast|Cloudy with sunny periods +weather forecast|Drizzle +weather forecast|Drizzle mixed with freezing drizzle +weather forecast|Drizzle mixed with rain +weather forecast|Drizzle or freezing drizzle +weather forecast|Drizzle or rain +weather forecast|Flurries +weather forecast|Flurries at times heavy +weather forecast|Flurries at times heavy or rain showers +weather forecast|Flurries mixed with ice pellets +weather forecast|Flurries or ice pellets +weather forecast|Flurries or rain showers +weather forecast|Flurries or thundershowers +weather forecast|Fog +weather forecast|Fog developing +weather forecast|Fog dissipating +weather forecast|Fog patches +weather forecast|Freezing drizzle +weather forecast|Freezing rain +weather forecast|Freezing rain mixed with ice pellets +weather forecast|Freezing rain mixed with rain +weather forecast|Freezing rain mixed with snow +weather forecast|Freezing rain or ice pellets +weather forecast|Freezing rain or rain +weather forecast|Freezing rain or snow +weather forecast|Ice fog +weather forecast|Ice fog developing +weather forecast|Ice fog dissipating +weather forecast|Ice pellets +weather forecast|Ice pellets mixed with freezing rain +weather forecast|Ice pellets mixed with snow +weather forecast|Ice pellets or freezing rain +weather forecast|Ice pellets or snow +weather forecast|Increasing cloudiness +weather forecast|Increasing clouds +weather forecast|Light snow +weather forecast|Light snow and blizzard +weather forecast|Light snow and blizzard and blowing snow +weather forecast|Light snow and blowing snow +weather forecast|Light snow mixed with freezing drizzle +weather forecast|Light snow mixed with freezing rain +weather forecast|Light snow mixed with ice pellets +weather forecast|Light snow mixed with rain +weather forecast|Light snow or freezing drizzle +weather forecast|Light snow or freezing rain +weather forecast|Light snow or ice pellets +weather forecast|Light snow or rain +weather forecast|Light wet snow +weather forecast|Light wet snow or rain +weather forecast|Local snow squalls +weather forecast|Near blizzard +weather forecast|Overcast +weather forecast|Periods of drizzle +weather forecast|Periods of drizzle mixed with freezing drizzle +weather forecast|Periods of drizzle mixed with rain +weather forecast|Periods of drizzle or freezing drizzle +weather forecast|Periods of drizzle or rain +weather forecast|Periods of freezing drizzle +weather forecast|Periods of freezing drizzle or drizzle +weather forecast|Periods of freezing drizzle or rain +weather forecast|Periods of freezing rain +weather forecast|Periods of freezing rain mixed with ice pellets +weather forecast|Periods of freezing rain mixed with rain +weather forecast|Periods of freezing rain mixed with snow +weather forecast|Periods of freezing rain or ice pellets +weather forecast|Periods of freezing rain or rain +weather forecast|Periods of freezing rain or snow +weather forecast|Periods of ice pellets +weather forecast|Periods of ice pellets mixed with freezing rain +weather forecast|Periods of ice pellets mixed with snow +weather forecast|Periods of ice pellets or freezing rain +weather forecast|Periods of ice pellets or snow +weather forecast|Periods of light snow +weather forecast|Periods of light snow and blizzard +weather forecast|Periods of light snow and blizzard and blowing snow +weather forecast|Periods of light snow and blowing snow +weather forecast|Periods of light snow mixed with freezing drizzle +weather forecast|Periods of light snow mixed with freezing rain +weather forecast|Periods of light snow mixed with ice pellets +weather forecast|Periods of light snow mixed with rain +weather forecast|Periods of light snow or freezing drizzle +weather forecast|Periods of light snow or freezing rain +weather forecast|Periods of light snow or ice pellets +weather forecast|Periods of light snow or rain +weather forecast|Periods of light wet snow +weather forecast|Periods of light wet snow mixed with rain +weather forecast|Periods of light wet snow or rain +weather forecast|Periods of rain +weather forecast|Periods of rain mixed with freezing rain +weather forecast|Periods of rain mixed with snow +weather forecast|Periods of rain or drizzle +weather forecast|Periods of rain or freezing rain +weather forecast|Periods of rain or snow +weather forecast|Periods of rain or thundershowers +weather forecast|Periods of rain or thunderstorms +weather forecast|Periods of snow +weather forecast|Periods of snow and blizzard +weather forecast|Periods of snow and blizzard and blowing snow +weather forecast|Periods of snow and blowing snow +weather forecast|Periods of snow mixed with freezing drizzle +weather forecast|Periods of snow mixed with freezing rain +weather forecast|Periods of snow mixed with ice pellets +weather forecast|Periods of snow mixed with rain +weather forecast|Periods of snow or freezing drizzle +weather forecast|Periods of snow or freezing rain +weather forecast|Periods of snow or ice pellets +weather forecast|Periods of snow or rain +weather forecast|Periods of wet snow +weather forecast|Periods of wet snow mixed with rain +weather forecast|Periods of wet snow or rain +weather forecast|Rain +weather forecast|Rain at times heavy +weather forecast|Rain at times heavy mixed with freezing rain +weather forecast|Rain at times heavy mixed with snow +weather forecast|Rain at times heavy or drizzle +weather forecast|Rain at times heavy or freezing rain +weather forecast|Rain at times heavy or snow +weather forecast|Rain at times heavy or thundershowers +weather forecast|Rain at times heavy or thunderstorms +weather forecast|Rain mixed with freezing rain +weather forecast|Rain mixed with snow +weather forecast|Rain or drizzle +weather forecast|Rain or freezing rain +weather forecast|Rain or snow +weather forecast|Rain or thundershowers +weather forecast|Rain or thunderstorms +weather forecast|Rain showers or flurries +weather forecast|Rain showers or wet flurries +weather forecast|Showers +weather forecast|Showers at times heavy +weather forecast|Showers at times heavy or thundershowers +weather forecast|Showers at times heavy or thunderstorms +weather forecast|Showers or drizzle +weather forecast|Showers or thundershowers +weather forecast|Showers or thunderstorms +weather forecast|Smoke +weather forecast|Snow +weather forecast|Snow and blizzard +weather forecast|Snow and blizzard and blowing snow +weather forecast|Snow and blowing snow +weather forecast|Snow at times heavy +weather forecast|Snow at times heavy and blizzard +weather forecast|Snow at times heavy and blowing snow +weather forecast|Snow at times heavy mixed with freezing drizzle +weather forecast|Snow at times heavy mixed with freezing rain +weather forecast|Snow at times heavy mixed with ice pellets +weather forecast|Snow at times heavy mixed with rain +weather forecast|Snow at times heavy or freezing rain +weather forecast|Snow at times heavy or ice pellets +weather forecast|Snow at times heavy or rain +weather forecast|Snow mixed with freezing drizzle +weather forecast|Snow mixed with freezing rain +weather forecast|Snow mixed with ice pellets +weather forecast|Snow mixed with rain +weather forecast|Snow or freezing drizzle +weather forecast|Snow or freezing rain +weather forecast|Snow or ice pellets +weather forecast|Snow or rain +weather forecast|Snow squalls +weather forecast|Sunny +weather forecast|Mainly sunny +weather forecast|Sunny with cloudy periods +weather forecast|Thunderstorms +weather forecast|Thunderstorms and possible hail +weather forecast|Wet flurries +weather forecast|Wet flurries at times heavy +weather forecast|Wet flurries at times heavy or rain showers +weather forecast|Wet flurries or rain showers +weather forecast|Wet snow +weather forecast|Wet snow at times heavy +weather forecast|Wet snow at times heavy mixed with rain +weather forecast|Wet snow mixed with rain +weather forecast|Wet snow or rain +weather forecast|Windy +Trace diff --git a/plasma/workspace/dataengines/weather/ions/data/noaa_i18n.dat b/plasma/workspace/dataengines/weather/ions/data/noaa_i18n.dat new file mode 100644 index 0000000000..2a6d182414 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/data/noaa_i18n.dat @@ -0,0 +1,356 @@ +weather condition|A Few Clouds +weather condition|A Few Clouds and Breezy +weather condition|A Few Clouds and Windy +weather condition|A Few Clouds with Haze +weather condition|Blowing Dust +weather condition|Blowing Sand +weather condition|Blowing Snow +weather condition|Blowing Snow in Vicinity +weather condition|Breezy +weather condition|Clear +weather condition|Clear and Breezy +weather condition|Clear with Haze +weather condition|Drizzle +weather condition|Drizzle Fog +weather condition|Drizzle Fog/Mist +weather condition|Drizzle Ice Pellets +weather condition|Drizzle Snow +weather condition|Dust +weather condition|Dust/Sand Whirls +weather condition|Dust/Sand Whirls in Vicinity +weather condition|Dust Storm +weather condition|Dust Storm in Vicinity +weather condition|Fair +weather condition|Fair and Breezy +weather condition|Fair and Windy +weather condition|Fair with Haze +weather condition|Fog +weather condition|Fog in Vicinity +weather condition|Fog/Mist +weather condition|Freezing Drizzle +weather condition|Freezing Drizzle in Vicinity +weather condition|Freezing Drizzle Rain +weather condition|Freezing Drizzle Snow +weather condition|Freezing Fog +weather condition|Freezing Fog in Vicinity +weather condition|Freezing Rain +weather condition|Freezing Rain in Vicinity +weather condition|Freezing Rain Rain +weather condition|Freezing Rain Snow +weather condition|Funnel Cloud +weather condition|Funnel Cloud in Vicinity +weather condition|Hail +weather condition|Hail Showers +weather condition|Haze +weather condition|Heavy Blowing Snow +weather condition|Heavy Drizzle +weather condition|Heavy Drizzle Fog +weather condition|Heavy Drizzle Fog/Mist +weather condition|Heavy Drizzle Ice Pellets +weather condition|Heavy Drizzle Snow +weather condition|Heavy Dust Storm +weather condition|Heavy Freezing Drizzle +weather condition|Heavy Freezing Drizzle Rain +weather condition|Heavy Freezing Drizzle Snow +weather condition|Heavy Freezing Fog +weather condition|Heavy Freezing Rain +weather condition|Heavy Freezing Rain Rain +weather condition|Heavy Freezing Rain Snow +weather condition|Heavy Ice Pellets +weather condition|Heavy Ice Pellets Drizzle +weather condition|Heavy Ice Pellets Rain +weather condition|Heavy Rain +weather condition|Heavy Rain Fog +weather condition|Heavy Rain Fog/Mist +weather condition|Heavy Rain Freezing Drizzle +weather condition|Heavy Rain Freezing Rain +weather condition|Heavy Rain Ice Pellets +weather condition|Heavy Rain Showers +weather condition|Heavy Rain Showers Fog/Mist +weather condition|Heavy Rain Snow +weather condition|Heavy Sand Storm +weather condition|Heavy Showers Rain +weather condition|Heavy Showers Rain Fog/Mist +weather condition|Heavy Showers Snow +weather condition|Heavy Showers Snow Fog +weather condition|Heavy Showers Snow Fog/Mist +weather condition|Heavy small Hail/Snow Pellets +weather condition|Heavy Snow +weather condition|Heavy Snow Blowing Snow +weather condition|Heavy Snow Fog +weather condition|Heavy Snow Fog/Mist +weather condition|Heavy Snow Freezing Drizzle +weather condition|Heavy Snow Freezing Rain +weather condition|Heavy Snow Grains +weather condition|Heavy Snow Low Drifting Snow +weather condition|Heavy Snow Rain +weather condition|Heavy Snow Showers +weather condition|Heavy Snow Showers Fog +weather condition|Heavy Snow Showers Fog/Mist +weather condition|Heavy Thunderstorm Rain +weather condition|Heavy Thunderstorm Rain Fog +weather condition|Heavy Thunderstorm Rain Fog and Windy +weather condition|Heavy Thunderstorm Rain Fog/Mist +weather condition|Heavy Thunderstorm Rain Hail +weather condition|Heavy Thunderstorm Rain Hail Fog +weather condition|Heavy Thunderstorm Rain Hail Fog/Hail +weather condition|Heavy Thunderstorm Rain Hail Haze +weather condition|Heavy Thunderstorm Rain Haze +weather condition|Heavy Thunderstorm Rain Small Hail/Snow Pellets +weather condition|Heavy Thunderstorm Snow +weather condition|Ice Crystals +weather condition|Ice Pellets +weather condition|Ice Pellets Drizzle +weather condition|Ice Pellets in Vicinity +weather condition|Ice Pellets Rain +weather condition|Light Drizzle +weather condition|Light Drizzle Fog +weather condition|Light Drizzle Fog/Mist +weather condition|Light Drizzle Ice Pellets +weather condition|Light Drizzle Snow +weather condition|Light Freezing Drizzle +weather condition|Light Freezing Drizzle Rain +weather condition|Light Freezing Drizzle Snow +weather condition|Light Freezing Fog +weather condition|Light Freezing Rain +weather condition|Light Freezing Rain Rain +weather condition|Light Freezing Rain Snow +weather condition|Light Ice Pellets +weather condition|Light Ice Pellets Drizzle +weather condition|Light Ice Pellets Rain +weather condition|Light Rain +weather condition|Light Rain and Breezy +weather condition|Light Rain Fog +weather condition|Light Rain Fog/Mist +weather condition|Light Rain Freezing Drizzle +weather condition|Light Rain Freezing Rain +weather condition|Light Rain Ice Pellets +weather condition|Light Rain Showers +weather condition|Light Rain Showers Fog/Mist +weather condition|Light Rain Snow +weather condition|Light Showers Rain +weather condition|Light Showers Rain Fog/Mist +weather condition|Light Showers Snow +weather condition|Light Showers Snow Fog +weather condition|Light Showers Snow Fog/Mist +weather condition|Light Small Hail/Snow Pellets +weather condition|Light Snow +weather condition|Light Snow Blowing Snow +weather condition|Light Snow Blowing Snow Fog/Mist +weather condition|Light Snow Drizzle +weather condition|Light Snow Fog +weather condition|Light Snow Fog/Mist +weather condition|Light Snow Freezing Drizzle +weather condition|Light Snow Freezing Rain +weather condition|Light Snow Grains +weather condition|Light Snow Low Drifting Snow +weather condition|Light Snow Rain +weather condition|Light Snow Showers +weather condition|Light Snow Showers Fog +weather condition|Light Snow Showers Fog/Mist +weather condition|Light Thunderstorm Rain +weather condition|Light Thunderstorm Rain Fog +weather condition|Light Thunderstorm Rain Fog/Mist +weather condition|Light Thunderstorm Rain Hail +weather condition|Light Thunderstorm Rain Hail Fog +weather condition|Light Thunderstorm Rain Hail Fog/Mist +weather condition|Light Thunderstorm Rain Hail Haze +weather condition|Light Thunderstorm Rain Haze +weather condition|Light Thunderstorm Rain Small Hail/Snow Pellets +weather condition|Light Thunderstorm Snow +weather condition|Low Drifting Dust +weather condition|Low Drifting Sand +weather condition|Low Drifting Snow +weather condition|Mostly Cloudy +weather condition|Mostly Cloudy and Breezy +weather condition|Mostly Cloudy and Windy +weather condition|Mostly Cloudy with Haze +weather condition|Overcast +weather condition|Overcast and Breezy +weather condition|Overcast and Windy +weather condition|Overcast with Haze +weather condition|Partial Fog +weather condition|Partial Fog in Vicinity +weather condition|Partly Cloudy +weather condition|Partly Cloudy and Breezy +weather condition|Partly Cloudy and Windy +weather condition|Partly Cloudy with Haze +weather condition|Patchy Freezing Fog +weather condition|Patches of Fog +weather condition|Patches of Fog in Vicinity +weather condition|Rain Fog +weather condition|Rain Fog/Mist +weather condition|Rain Freezing Drizzle +weather condition|Rain Freezing Rain +weather condition|Rain Ice Pellets +weather condition|Rain Showers +weather condition|Rain Showers Fog/Mist +weather condition|Rain Showers in Vicinity +weather condition|Rain Showers in Vicinity Fog/Mist +weather condition|Rain Snow +weather condition|Sand +weather condition|Sand Storm +weather condition|Sand Storm in Vicinity +weather condition|Shallow Fog +weather condition|Shallow Fog in Vicinity +weather condition|Showers Hail +weather condition|Showers Ice Pellets +weather condition|Showers in Vicinity Fog +weather condition|Showers in Vicinity Snow +weather condition|Showers Rain +weather condition|Showers Rain Fog/Mist +weather condition|Showers Rain in Vicinity +weather condition|Showers Rain in Vicinity Fog/Mist +weather condition|Showers Snow +weather condition|Showers Snow Fog +weather condition|Showers Snow Fog/Mist +weather condition|Small Hail/Snow Pellets +weather condition|Smoke +weather condition|Snow +weather condition|Snow Blowing Snow +weather condition|Snow Drizzle +weather condition|Snow Fog +weather condition|Snow Fog/Mist +weather condition|Snow Freezing Drizzle +weather condition|Snow Freezing Rain +weather condition|Snow Grains +weather condition|Snow Low Drifting Snow +weather condition|Snow Rain +weather condition|Snow Showers +weather condition|Snow Showers Fog +weather condition|Snow Showers Fog/Mist +weather condition|Snow Showers in Vicinity +weather condition|Snow Showers in Vicinity Fog +weather condition|Snow Showers in Vicinity Fog/Mist +weather condition|Thunderstorm +weather condition|Thunderstorm Fog +weather condition|Thunderstorm Hail +weather condition|Thunderstorm Hail Fog +weather condition|Thunderstorm Haze in Vicinity +weather condition|Thunderstorm Haze in Vicinity Hail +weather condition|Thunderstorm Heavy Rain +weather condition|Thunderstorm Heavy Rain Fog +weather condition|Thunderstorm Heavy Rain Fog/Mist +weather condition|Thunderstorm Heavy Rain Hail +weather condition|Thunderstorm Heavy Rain Hail Fog +weather condition|Thunderstorm Heavy Rain Hail Fog/Mist +weather condition|Thunderstorm Heavy Rain Hail Haze +weather condition|Thunderstorm Heavy Rain Haze +weather condition|Thunderstorm Ice Pellets +weather condition|Thunderstorm in Vicinity +weather condition|Thunderstorm in Vicinity Fog +weather condition|Thunderstorm in Vicinity Fog/Mist +weather condition|Thunderstorm in Vicinity Hail +weather condition|Thunderstorm in Vicinity Hail Haze +weather condition|Thunderstorm in Vicinity Haze +weather condition|Thunderstorm Light Rain +weather condition|Thunderstorm Light Rain Fog +weather condition|Thunderstorm Light Rain Fog/Mist +weather condition|Thunderstorm Light Rain Hail +weather condition|Thunderstorm Light Rain Hail Fog +weather condition|Thunderstorm Light Rain Hail Fog/Mist +weather condition|Thunderstorm Light Rain Hail Haze +weather condition|Thunderstorm Light Rain Haze +weather condition|Thunderstorm Rain +weather condition|Thunderstorm Rain Fog/Mist +weather condition|Thunderstorm Rain Hail Fog/Mist +weather condition|Thunderstorm Rain Small Hail/Snow Pellets +weather condition|Thunderstorm Showers in Vicinity +weather condition|Thunderstorm Showers in Vicinity Hail +weather condition|Thunderstorm Small Hail/Snow Pellets +weather condition|Thunderstorm Snow +weather condition|Tornado/Water Spout +weather condition|Windy +weather condition|N/A +weather forecast|Ice Crystals +weather forecast|Volcanic Ash +weather forecast|Water Spout +weather forecast|Freezing Spray +weather forecast|Frost +weather forecast|Slight Chance Thunderstorms +weather forecast|Chance Thunderstorms +weather forecast|Thunderstorms Likely +weather forecast|Thunderstorms +weather forecast|Severe Tstms +weather forecast|Slight Chance Snow/Sleet +weather forecast|Chance Snow/Sleet +weather forecast|Snow/Sleet Likely +weather forecast|Snow/Sleet +weather forecast|Slight Chance Rain/Sleet +weather forecast|Chance Rain/Sleet +weather forecast|Rain/Sleet Likely +weather forecast|Rain/Sleet +weather forecast|Slight Chance Rain/Freezing Rain +weather forecast|Chance Rain/Freezing Rain +weather forecast|Rain/Freezing Rain Likely +weather forecast|Rain/Freezing Rain +weather forecast|Wintry Mix Likely +weather forecast|Wintry Mix +weather forecast|Slight Chance Freezing Drizzle +weather forecast|Chance Freezing Drizzle +weather forecast|Freezing Drizzle Likely +weather forecast|Freezing Drizzle +weather forecast|Slight Chance Freezing Rain +weather forecast|Chance Freezing Rain +weather forecast|Freezing Rain Likely +weather forecast|Freezing Rain +weather forecast|Slight Chance Rain/Snow +weather forecast|Chance Rain/Snow +weather forecast|Rain/Snow Likely +weather forecast|Rain/Snow +weather forecast|Slight Chance Snow +weather forecast|Chance Snow +weather forecast|Snow Likely +weather forecast|Snow +weather forecast|Heavy Snow +weather forecast|Slight Chance Flurries +weather forecast|Chance Flurries +weather forecast|Flurries Likely +weather forecast|Flurries +weather forecast|Slight Chance Snow Showers +weather forecast|Chance Snow Showers +weather forecast|Snow Showers Likely +weather forecast|Snow Showers +weather forecast|Slight Chance Drizzle +weather forecast|Chance Drizzle +weather forecast|Drizzle Likely +weather forecast|Drizzle +weather forecast|Slight Chance Rain +weather forecast|Chance Rain +weather forecast|Rain Likely +weather forecast|Rain +weather forecast|Heavy Rain +weather forecast|Slight Chance Rain Showers +weather forecast|Chance Rain Showers +weather forecast|Rain Showers Likely +weather forecast|Rain Showers +weather forecast|Sleet +weather forecast|Smoke +weather forecast|Freezing Fog +weather forecast|Ice Fog +weather forecast|Haze +weather forecast|Blowing Sand +weather forecast|Blowing Dust +weather forecast|Blowing Snow +weather forecast|Dense Fog +weather forecast|Fog +weather forecast|Windy +weather forecast|Blustery +weather forecast|Breezy +weather forecast|Cold +weather forecast|Hot +weather forecast|Cloudy +weather forecast|Mostly Cloudy +weather forecast|Partly Cloudy +weather forecast|Mostly Sunny +weather forecast|Partly Sunny +weather forecast|Sunny +weather forecast|Increasing Clouds +weather forecast|Becoming Cloudy +weather forecast|Clearing +weather forecast|Gradual Clearing +weather forecast|Clearing Late +weather forecast|Decreasing Clouds +weather forecast|Becoming Sunny +weather forecast|Clear +weather forecast|Mostly Clear diff --git a/plasma/workspace/dataengines/weather/ions/dwd/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/dwd/CMakeLists.txt new file mode 100644 index 0000000000..4284499fc8 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/dwd/CMakeLists.txt @@ -0,0 +1,17 @@ +set(ion_dwd_SRCS ion_dwd.cpp) +ecm_qt_declare_logging_category(ion_dwd_SRCS + HEADER ion_dwddebug.h + IDENTIFIER IONENGINE_dwd + CATEGORY_NAME kde.dataengine.ion.dwd + DEFAULT_SEVERITY Info +) +add_library(ion_dwd MODULE ${ion_dwd_SRCS}) +target_link_libraries(ion_dwd + weather_ion + KF5::KIOCore + KF5::UnitConversion + KF5::I18n +) + +install(TARGETS ion_dwd DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + diff --git a/plasma/workspace/dataengines/weather/ions/dwd/ion-dwd.json b/plasma/workspace/dataengines/weather/ions/dwd/ion-dwd.json new file mode 100644 index 0000000000..e8ab842a41 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/dwd/ion-dwd.json @@ -0,0 +1,70 @@ +{ + "KPlugin": { + "Description": "Weather forecast by German Weather Service", + "Description[ar]": "نشرة الطقس الجوية من الأرصاد الجوية الألمانية", + "Description[az]": "Almaniya Hava Xidməti tərəfindən hava məlumatı", + "Description[ca]": "Previsió meteorològica del Servei meteorològic alemany", + "Description[cs]": "Předpověď počasí od Německé informační služby o počasí", + "Description[de]": "Wettervorhersage vom Deutschen Wetterdienst", + "Description[en_GB]": "Weather forecast by German Weather Service", + "Description[es]": "Previsión meteorológica del servicio meteorológico alemán", + "Description[eu]": "Eguraldi zerbitzu alemaniarraren eguraldi-iragarpena", + "Description[fi]": "Saksan sääpalvelun sääennuste", + "Description[fr]": "Prévisions météorologiques du service de météorologie d'Allemagne", + "Description[hu]": "Időjárás-előrejelzés a Német Meteorológiai Szolgálattól", + "Description[ia]": "Prevision Meteorologic per Servicio Meteorologic de Germania", + "Description[it]": "Previsioni meteo del servizio meteorologico tedesco", + "Description[ko]": "독일 기상 서비스의 일기 예보", + "Description[lt]": "Orų prognozės iš Vokietijos orų tarnybos", + "Description[nl]": "Weersvoorspelling door Duitse weerdienst", + "Description[nn]": "Vêrmelding frå German Weather Service", + "Description[pa]": "ਜਰਮਨ ਮੌਸਮ ਸੇਵਾ ਵਲੋਂ ਮੌਸਮ ਭਵਿੱਖਬਾਣੀ", + "Description[pl]": "Prognoza pogody wg niemieckiej usługi pogodowej", + "Description[pt_BR]": "Previsão do tempo pelo Serviço Meteorológico Alemão", + "Description[ro]": "Prognoza vremii de la Serviciul Meteorologic German", + "Description[ru]": "Прогноз погоды от немецкой службы погоды", + "Description[sk]": "Predpoveď počasia z German Weather Service", + "Description[sl]": "Vremenska napoved Nemške vremenske službe", + "Description[sv]": "Väderprognos av Tyska vädertjänsten", + "Description[tr]": "Alman Hava Durumu Hizmeti'nden hava durumu tahmini", + "Description[uk]": "Прогноз погоди від Німецької служби погоди", + "Description[vi]": "Dự báo thời tiết của Dịch vụ Thời tiết Đức", + "Description[x-test]": "xxWeather forecast by German Weather Servicexx", + "Description[zh_CN]": "德国天气服务提供的天气预报", + "Icon": "noneyet", + "Id": "dwd", + "Name": "German Weather Service", + "Name[ar]": "خدمة الأرصاد الجوية الألمانية", + "Name[az]": "Almaniya Hava Xidməti", + "Name[ca]": "Servei meteorològic alemany", + "Name[cs]": "Německá informační služba o počasí", + "Name[de]": "Deutscher Wetterdienst", + "Name[en_GB]": "German Weather Service", + "Name[es]": "Servicio meteorológico alemán", + "Name[eu]": "Alemaniako eguraldi-zerbitzua", + "Name[fi]": "Saksalainen sääpalvelu", + "Name[fr]": "service de météorologie d'Allemagne", + "Name[hu]": "Német Meteorológiai Szolgálat", + "Name[ia]": "Servicio Meteorologic de Germania", + "Name[it]": "Servizio meteorologico tedesco", + "Name[ko]": "독일 기상 서비스", + "Name[lt]": "Debian orų tarnyba", + "Name[nl]": "Duitse weerdienst", + "Name[nn]": "Tysk vêrteneste", + "Name[pa]": "ਜਰਮਨ ਮੌਸਮ ਸਰਵਿਸ", + "Name[pl]": "Niemiecka usługa pogodowa", + "Name[pt_BR]": "Serviço Meteorológico Alemão", + "Name[ro]": "Serviciul Meteorologic German", + "Name[ru]": "Немецкая служба погоды", + "Name[sk]": "German Weather Service", + "Name[sl]": "Nemška vremenska služba", + "Name[sv]": "Tyska vädertjänsten", + "Name[ta]": "ஜெர்மன் வானிலை சேவை", + "Name[tr]": "Alman Hava Durumu Hizmeti", + "Name[uk]": "Німецька служба погоди", + "Name[vi]": "Dịch vụ Thời tiết Đức", + "Name[x-test]": "xxGerman Weather Servicexx", + "Name[zh_CN]": "德国天气服务" + }, + "X-KDE-ParentApp": "weatherengine" +} diff --git a/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.cpp b/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.cpp new file mode 100644 index 0000000000..e3da901801 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.cpp @@ -0,0 +1,733 @@ +/* + SPDX-FileCopyrightText: 2021 Emily Ehlert + + Based upon BBC Weather Ion and ENV Canada Ion by Shawn Starr + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + also + + the wetter.com Ion by Thilo-Alexander Ginkel + SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for weather data from Deutscher Wetterdienst (DWD) / German Weather Service */ + +#include "ion_dwd.h" + +#include "ion_dwddebug.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* + * Initialization + */ + +WeatherData::WeatherData() + : temperature(qQNaN()) + , humidity(qQNaN()) + , pressure(qQNaN()) + , windSpeed(qQNaN()) + , gustSpeed(qQNaN()) + , dewpoint(qQNaN()) + , windSpeedAlt(qQNaN()) + , gustSpeedAlt(qQNaN()) +{ +} + +WeatherData::ForecastInfo::ForecastInfo() + : tempHigh(qQNaN()) + , tempLow(qQNaN()) + , windSpeed(qQNaN()) +{ +} + +DWDIon::DWDIon(QObject *parent, const QVariantList &args) + : IonInterface(parent, args) + +{ + setInitialized(true); +} + +DWDIon::~DWDIon() +{ + deleteForecasts(); +} + +void DWDIon::reset() +{ + deleteForecasts(); + m_sourcesToReset = sources(); + updateAllSources(); +} + +void DWDIon::deleteForecasts() +{ + // Destroy each forecast stored in a QVector + for (auto it = m_weatherData.begin(), end = m_weatherData.end(); it != end; ++it) { + qDeleteAll(it.value().forecasts); + it.value().forecasts.clear(); + } +} + +QMap DWDIon::setupDayIconMappings() const +{ + // DWD supplies it's own icon number which we can use to determine a condition + + return QMap{{QStringLiteral("1"), ClearDay}, + {QStringLiteral("2"), PartlyCloudyDay}, + {QStringLiteral("3"), PartlyCloudyDay}, + {QStringLiteral("4"), Overcast}, + {QStringLiteral("5"), Mist}, + {QStringLiteral("6"), Mist}, + {QStringLiteral("7"), LightRain}, + {QStringLiteral("8"), Rain}, + {QStringLiteral("9"), Rain}, + {QStringLiteral("10"), LightRain}, + {QStringLiteral("11"), Rain}, + {QStringLiteral("12"), Flurries}, + {QStringLiteral("13"), RainSnow}, + {QStringLiteral("14"), LightSnow}, + {QStringLiteral("15"), Snow}, + {QStringLiteral("16"), Snow}, + {QStringLiteral("17"), Hail}, + {QStringLiteral("18"), LightRain}, + {QStringLiteral("19"), Rain}, + {QStringLiteral("20"), Flurries}, + {QStringLiteral("21"), RainSnow}, + {QStringLiteral("22"), LightSnow}, + {QStringLiteral("23"), Snow}, + {QStringLiteral("24"), Hail}, + {QStringLiteral("25"), Hail}, + {QStringLiteral("26"), Thunderstorm}, + {QStringLiteral("27"), Thunderstorm}, + {QStringLiteral("28"), Thunderstorm}, + {QStringLiteral("29"), Thunderstorm}, + {QStringLiteral("30"), Thunderstorm}, + {QStringLiteral("31"), ClearWindyDay}}; +} + +QMap DWDIon::setupWindIconMappings() const +{ + return QMap{ + {QStringLiteral("0"), N}, {QStringLiteral("10"), N}, {QStringLiteral("20"), NNE}, {QStringLiteral("30"), NNE}, {QStringLiteral("40"), NE}, + {QStringLiteral("50"), NE}, {QStringLiteral("60"), ENE}, {QStringLiteral("70"), ENE}, {QStringLiteral("80"), E}, {QStringLiteral("90"), E}, + {QStringLiteral("100"), E}, {QStringLiteral("120"), ESE}, {QStringLiteral("130"), ESE}, {QStringLiteral("140"), SE}, {QStringLiteral("150"), SE}, + {QStringLiteral("160"), SSE}, {QStringLiteral("170"), SSE}, {QStringLiteral("180"), S}, {QStringLiteral("190"), S}, {QStringLiteral("200"), SSW}, + {QStringLiteral("210"), SSW}, {QStringLiteral("220"), SW}, {QStringLiteral("230"), SW}, {QStringLiteral("240"), WSW}, {QStringLiteral("250"), WSW}, + {QStringLiteral("260"), W}, {QStringLiteral("270"), W}, {QStringLiteral("280"), W}, {QStringLiteral("290"), WNW}, {QStringLiteral("300"), WNW}, + {QStringLiteral("310"), NW}, {QStringLiteral("320"), NW}, {QStringLiteral("330"), NNW}, {QStringLiteral("340"), NNW}, {QStringLiteral("350"), N}, + {QStringLiteral("360"), N}, + }; +} + +QMap const &DWDIon::dayIcons() const +{ + static QMap const dval = setupDayIconMappings(); + return dval; +} + +QMap const &DWDIon::windIcons() const +{ + static QMap const wval = setupWindIconMappings(); + return wval; +} + +bool DWDIon::updateIonSource(const QString &source) +{ + // We expect the applet to send the source in the following tokenization: + // ionname|validate|place_name|extra - Triggers validation (search) of place + // ionname|weather|place_name|extra - Triggers receiving weather of place + const QStringList sourceAction = source.split(QLatin1Char('|')); + + if (sourceAction.size() < 3) { + setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); + return true; + } + + if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) { + // Look for places to match + findPlace(sourceAction[2]); + return true; + } + if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) { + if (sourceAction.count() >= 4) { + if (sourceAction[2].isEmpty()) { + setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); + return true; + } + + // Extra data: station_id + m_place[sourceAction[2]] = sourceAction[3]; + + qCDebug(IONENGINE_dwd) << "About to retrieve forecast for source: " << sourceAction[2]; + + fetchWeather(sourceAction[2], m_place[sourceAction[2]]); + + return true; + } + + return false; + } + + setData(source, QStringLiteral("validate"), QStringLiteral("dwd|malformed")); + return true; +} + +void DWDIon::findPlace(const QString &searchText) +{ + // Checks if the stations have already been loaded, always contains the currently active one + if (m_place.size() > 1) { + setData(QStringLiteral("dwd|validate|") + searchText, Data()); + searchInStationList(searchText); + } else { + const QUrl forecastURL(QStringLiteral(CATALOGUE_URL)); + KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); + + m_searchJobList.insert(getJob, searchText); + m_searchJobData.insert(getJob, QByteArray("")); + + connect(getJob, &KIO::TransferJob::data, this, &DWDIon::setup_slotDataArrived); + connect(getJob, &KJob::result, this, &DWDIon::setup_slotJobFinished); + } +} + +void DWDIon::fetchWeather(QString placeName, QString placeID) +{ + for (const QString &fetching : qAsConst(m_forecastJobList)) { + if (fetching == placeName) { + // already fetching! + return; + } + } + + // Fetch forecast data + + const QUrl forecastURL(QStringLiteral(FORECAST_URL).arg(placeID)); + KIO::TransferJob *getJob = KIO::get(forecastURL, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); + + m_forecastJobList.insert(getJob, placeName); + m_forecastJobJSON.insert(getJob, QByteArray("")); + + qCDebug(IONENGINE_dwd) << "Requesting URL: " << forecastURL; + + connect(getJob, &KIO::TransferJob::data, this, &DWDIon::forecast_slotDataArrived); + connect(getJob, &KJob::result, this, &DWDIon::forecast_slotJobFinished); + m_weatherData[placeName].isForecastsDataPending = true; + + // Fetch current measurements (different url AND different API, AMAZING) + + const QUrl measureURL(QStringLiteral(MEASURE_URL).arg(placeID)); + KIO::TransferJob *getMeasureJob = KIO::get(measureURL, KIO::Reload, KIO::HideProgressInfo); + getMeasureJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); + + m_measureJobList.insert(getMeasureJob, placeName); + m_measureJobJSON.insert(getMeasureJob, QByteArray("")); + + qCDebug(IONENGINE_dwd) << "Requesting URL: " << measureURL; + + connect(getMeasureJob, &KIO::TransferJob::data, this, &DWDIon::measure_slotDataArrived); + connect(getMeasureJob, &KJob::result, this, &DWDIon::measure_slotJobFinished); + m_weatherData[placeName].isMeasureDataPending = true; +} + +void DWDIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + + if (data.isEmpty() || !m_searchJobData.contains(job)) { + return; + } + + m_searchJobData[job].append(local); +} + +void DWDIon::measure_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + + if (data.isEmpty() || !m_measureJobJSON.contains(job)) { + return; + } + + m_measureJobJSON[job].append(local); +} + +void DWDIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + + if (data.isEmpty() || !m_forecastJobJSON.contains(job)) { + return; + } + + m_forecastJobJSON[job].append(local); +} + +void DWDIon::setup_slotJobFinished(KJob *job) +{ + const QString searchText(m_searchJobList.value(job)); + setData(QStringLiteral("dwd|validate|") + searchText, Data()); + + QByteArray catalogueData = m_searchJobData[job]; + if (!catalogueData.isNull()) { + parseStationData(catalogueData); + searchInStationList(searchText); + } + + m_searchJobList.remove(job); + m_searchJobData.remove(job); +} + +void DWDIon::measure_slotJobFinished(KJob *job) +{ + const QString source(m_measureJobList.value(job)); + setData(source, Data()); + + QJsonDocument doc = QJsonDocument::fromJson(m_measureJobJSON.value(job)); + + // Not all stations have current measurements + if (!doc.isNull()) { + parseMeasureData(source, doc); + } else { + m_weatherData[source].isMeasureDataPending = false; + updateWeather(source); + } + + m_measureJobList.remove(job); + m_measureJobJSON.remove(job); +} + +void DWDIon::forecast_slotJobFinished(KJob *job) +{ + const QString source(m_forecastJobList.value(job)); + setData(source, Data()); + + QJsonDocument doc = QJsonDocument::fromJson(m_forecastJobJSON.value(job)); + + if (!doc.isNull()) { + parseForecastData(source, doc); + } + + m_forecastJobList.remove(job); + m_forecastJobJSON.remove(job); + + if (m_sourcesToReset.contains(source)) { + m_sourcesToReset.removeAll(source); + const QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, m_place[source]); + + // so the weather engine updates it's data + forceImmediateUpdateOfAllVisualizations(); + + // update the clients of our engine + Q_EMIT forceUpdate(this, weatherSource); + } +} + +void DWDIon::calculatePositions(QStringList lines, QVector &namePositionalInfo, QVector &stationIdPositionalInfo) +{ + QStringList stringLengths = lines[3].split(QChar::Space); + QVector lengths; + for (const QString &length : qAsConst(stringLengths)) { + lengths.append(length.count()); + } + + int curpos = 0; + + for (int labelLength : lengths) { + QString label = lines[2].mid(curpos, labelLength); + + if (label.contains(QStringLiteral("name"))) { + namePositionalInfo[0] = curpos; + namePositionalInfo[1] = labelLength; + } else if (label.contains(QStringLiteral("id"))) { + stationIdPositionalInfo[0] = curpos; + stationIdPositionalInfo[1] = labelLength; + } + + curpos += labelLength + 1; + } +} + +void DWDIon::parseStationData(QByteArray data) +{ + QString stringData = QString::fromLatin1(data); + QStringList lines = stringData.split(QChar::LineFeed); + + QVector namePositionalInfo(2); + QVector stationIdPositionalInfo(2); + calculatePositions(lines, namePositionalInfo, stationIdPositionalInfo); + + // This loop parses the station file (https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg) + // clu CofX id ICAO name nb. el. elev Hmod-H type + // ===== ----- ===== ---- -------------------- ------ ------- ----- ------ ---- + // 99801 504 07335 LFBI POITIERS 46.35 0.18 120 -10 LAND + // 99802 504 07354 LFLX CHATEAUROUX 46.52 1.43 155 12 LAND + // 99803 470 07379 LFLN SAINT-YAN. 46.25 4.01 242 9 LAND + bool start = true; + for (const QString &line : qAsConst(lines)) { + if (!start && line.isEmpty()) { + break; + } + start = false; + + QString name = line.mid(namePositionalInfo[0], namePositionalInfo[1]).trimmed(); + QString id = line.mid(stationIdPositionalInfo[0], stationIdPositionalInfo[1]).trimmed(); + + // This checks if this station is a station we know is working + // With this we remove all non working but also a lot of working ones. + if (id.startsWith(QLatin1Char('0')) || id.startsWith(QLatin1Char('1'))) { + m_place.insert(camelCaseString(name), id); + } + } + qCDebug(IONENGINE_dwd) << "Number of parsed stations: " << m_place.size(); +} + +void DWDIon::searchInStationList(const QString searchText) +{ + qCDebug(IONENGINE_dwd) << searchText; + + QMap::const_iterator it = m_place.constBegin(); + auto end = m_place.constEnd(); + + while (it != end) { + QString name = it.key(); + if (name.contains(searchText, Qt::CaseInsensitive)) { + m_locations.append(it.key()); + } + ++it; + } + + validate(searchText); +} + +void DWDIon::parseForecastData(const QString source, QJsonDocument doc) +{ + QVariantMap weatherMap = doc.object().toVariantMap().first().toMap(); + if (!weatherMap.isEmpty()) { + // Forecast data + QVariantList daysList = weatherMap[QStringLiteral("days")].toList(); + + WeatherData &weatherData = m_weatherData[source]; + QVector &forecasts = weatherData.forecasts; + + // Flush out the old forecasts when updating. + forecasts.clear(); + + WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo; + + int dayNumber = 0; + + for (const QVariant &day : daysList) { + QMap dayMap = day.toMap(); + QString period = dayMap[QStringLiteral("dayDate")].toString(); + QString cond = dayMap[QStringLiteral("icon")].toString(); + + forecast->period = QDateTime::fromString(period, QStringLiteral("yyyy-MM-dd")); + forecast->tempHigh = parseNumber(dayMap[QStringLiteral("temperatureMax")].toInt()); + forecast->tempLow = parseNumber(dayMap[QStringLiteral("temperatureMin")].toInt()); + forecast->iconName = getWeatherIcon(dayIcons(), cond); + ; + + if (dayNumber == 0) { + // These alternative measurements are used, when the stations doesn't have it's own measurements, uses forecast data from the current day + weatherData.windSpeedAlt = parseNumber(dayMap[QStringLiteral("windSpeed")].toInt()); + weatherData.gustSpeedAlt = parseNumber(dayMap[QStringLiteral("windGust")].toInt()); + QString windDirection = roundWindDirections(dayMap[QStringLiteral("windDirection")].toInt()); + weatherData.windDirectionAlt = getWindDirectionIcon(windIcons(), windDirection); + } + + forecasts.append(forecast); + forecast = new WeatherData::ForecastInfo; + + dayNumber++; + // Only get the next 7 days (including today) + if (dayNumber == 7) + break; + } + + delete forecast; + + // Warnings data + QVariantList warningData = weatherMap[QStringLiteral("warnings")].toList(); + + QVector &warningList = weatherData.warnings; + + // Flush out the old forecasts when updating. + warningList.clear(); + + WeatherData::WarningInfo *warning = new WeatherData::WarningInfo; + + for (const QVariant &warningElement : warningData) { + QMap warningMap = warningElement.toMap(); + + QString warningDesc = warningMap[QStringLiteral("description")].toString(); + + // This loop adds line breaks because the weather widget doesn't seem to do that which completly ruins the layout + // Should be removed if the weather widget is ever fixed + for (int i = 1; i <= (warningDesc.size() / 50); i++) { + warningDesc.insert(i * 50, QChar::LineFeed); + } + + warning->description = warningDesc; + warning->priority = warningMap[QStringLiteral("level")].toInt(); + warning->type = warningMap[QStringLiteral("event")].toString(); + warning->timestamp = QDateTime::fromMSecsSinceEpoch(warningMap[QStringLiteral("start")].toLongLong()); + + warningList.append(warning); + warning = new WeatherData::WarningInfo; + } + + delete warning; + + weatherData.isForecastsDataPending = false; + + updateWeather(source); + } +} + +void DWDIon::parseMeasureData(const QString source, QJsonDocument doc) +{ + WeatherData &weatherData = m_weatherData[source]; + QVariantMap weatherMap = doc.object().toVariantMap(); + + if (!weatherMap.isEmpty()) { + bool windIconValid = false; + bool tempValid = false; + bool humidityValid = false; + bool pressureValid = false; + bool windSpeedValid = false; + bool gustSpeedValid = false; + bool dewpointValid = false; + + QDateTime time = QDateTime::fromMSecsSinceEpoch(weatherMap[QStringLiteral("time")].toLongLong()); + QString condIconNumber = weatherMap[QStringLiteral("icon")].toString(); + int windDirection = weatherMap[QStringLiteral("winddirection")].toInt(&windIconValid); + float temp = parseNumber(weatherMap[QStringLiteral("temperature")].toInt(&tempValid)); + float humidity = parseNumber(weatherMap[QStringLiteral("humidity")].toInt(&humidityValid)); + float pressure = parseNumber(weatherMap[QStringLiteral("pressure")].toInt(&pressureValid)); + float windSpeed = parseNumber(weatherMap[QStringLiteral("meanwind")].toInt(&windSpeedValid)); + float gustSpeed = parseNumber(weatherMap[QStringLiteral("maxwind")].toInt(&gustSpeedValid)); + float dewpoint = parseNumber(weatherMap[QStringLiteral("dewpoint")].toInt(&dewpointValid)); + + if (condIconNumber != QLatin1String("")) + weatherData.conditionIcon = getWeatherIcon(dayIcons(), condIconNumber); + if (windIconValid) + weatherData.windDirection = getWindDirectionIcon(windIcons(), roundWindDirections(windDirection)); + if (tempValid) + weatherData.temperature = temp; + if (humidityValid) + weatherData.humidity = humidity; + if (pressureValid) + weatherData.pressure = pressure; + if (windSpeedValid) + weatherData.windSpeed = windSpeed; + if (gustSpeedValid) + weatherData.gustSpeed = gustSpeed; + if (dewpointValid) + weatherData.dewpoint = dewpoint; + weatherData.observationDateTime = time; + } + + weatherData.isMeasureDataPending = false; + + updateWeather(source); +} + +void DWDIon::validate(const QString &searchText) +{ + const QString source(QStringLiteral("dwd|validate|") + searchText); + + if (m_locations.isEmpty()) { + const QString invalidPlace = searchText; + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|invalid|multiple|") + invalidPlace)); + return; + } + + QString placeList; + for (const QString &place : qAsConst(m_locations)) { + placeList.append(QStringLiteral("|place|") + place + QStringLiteral("|extra|") + m_place[place]); + } + if (m_locations.count() > 1) { + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|multiple") + placeList)); + } else { + placeList[7] = placeList[7].toUpper(); + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("dwd|valid|single") + placeList)); + } + m_locations.clear(); +} + +void DWDIon::updateWeather(const QString &source) +{ + const WeatherData &weatherData = m_weatherData[source]; + + if (weatherData.isForecastsDataPending || weatherData.isMeasureDataPending) { + return; + } + + QString placeCode = m_place[source]; + QString weatherSource = QStringLiteral("dwd|weather|%1|%2").arg(source, placeCode); + + Plasma::DataEngine::Data data; + + data.insert(QStringLiteral("Place"), source); + data.insert(QStringLiteral("Station"), source); + + data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius); + data.insert(QStringLiteral("Wind Speed Unit"), KUnitConversion::KilometerPerHour); + data.insert(QStringLiteral("Humidity Unit"), KUnitConversion::Percent); + data.insert(QStringLiteral("Pressure Unit"), KUnitConversion::Hectopascal); + + if (!weatherData.observationDateTime.isNull()) + data.insert(QStringLiteral("Observation Timestamp"), weatherData.observationDateTime); + else + data.insert(QStringLiteral("Observation Timestamp"), QDateTime::currentDateTime()); + + if (!weatherData.conditionIcon.isEmpty()) + data.insert(QStringLiteral("Condition Icon"), weatherData.conditionIcon); + + if (!qIsNaN(weatherData.temperature)) + data.insert(QStringLiteral("Temperature"), weatherData.temperature); + + if (!qIsNaN(weatherData.humidity)) + data.insert(QStringLiteral("Humidity"), weatherData.humidity); + + if (!qIsNaN(weatherData.pressure)) + data.insert(QStringLiteral("Pressure"), weatherData.pressure); + + if (!qIsNaN(weatherData.dewpoint)) + data.insert(QStringLiteral("Dewpoint"), weatherData.dewpoint); + + if (!qIsNaN(weatherData.windSpeed)) + data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeed); + else + data.insert(QStringLiteral("Wind Speed"), weatherData.windSpeedAlt); + + if (!qIsNaN(weatherData.gustSpeed)) + data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeed); + else + data.insert(QStringLiteral("Wind Gust Speed"), weatherData.gustSpeedAlt); + + if (!weatherData.windDirection.isEmpty()) { + data.insert(QStringLiteral("Wind Direction"), weatherData.windDirection); + } else { + data.insert(QStringLiteral("Wind Direction"), weatherData.windDirectionAlt); + } + + int dayNumber = 0; + for (const WeatherData::ForecastInfo *dayForecast : weatherData.forecasts) { + if (dayNumber > 0) { + QString period = dayForecast->period.toString(QStringLiteral("dddd")); + + period.replace(QStringLiteral("Saturday"), i18nc("Short for Saturday", "Sat")); + period.replace(QStringLiteral("Sunday"), i18nc("Short for Sunday", "Sun")); + period.replace(QStringLiteral("Monday"), i18nc("Short for Monday", "Mon")); + period.replace(QStringLiteral("Tuesday"), i18nc("Short for Tuesday", "Tue")); + period.replace(QStringLiteral("Wednesday"), i18nc("Short for Wednesday", "Wed")); + period.replace(QStringLiteral("Thursday"), i18nc("Short for Thursday", "Thu")); + period.replace(QStringLiteral("Friday"), i18nc("Short for Friday", "Fri")); + + data.insert(QStringLiteral("Short Forecast Day %1").arg(dayNumber), + QStringLiteral("%1|%2|%3|%4|%5|%6") + .arg(period, dayForecast->iconName, QLatin1String("")) + .arg(dayForecast->tempHigh) + .arg(dayForecast->tempLow) + .arg(QLatin1String(""))); + dayNumber++; + } else { + data.insert(QStringLiteral("Short Forecast Day %1").arg(dayNumber), + QStringLiteral("%1|%2|%3|%4|%5|%6") + .arg(i18nc("Short for Today", "Today"), dayForecast->iconName, QLatin1String("")) + .arg(dayForecast->tempHigh) + .arg(dayForecast->tempLow) + .arg(QLatin1String(""))); + dayNumber++; + } + } + + int k = 0; + + for (const WeatherData::WarningInfo *warning : weatherData.warnings) { + 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 Timestamp ") + number, warning->timestamp.toString(QStringLiteral("dd.MM.yyyy"))); + + ++k; + } + + data.insert(QStringLiteral("Total Weather Days"), weatherData.forecasts.size()); + data.insert(QStringLiteral("Total Warnings Issued"), weatherData.warnings.size()); + data.insert(QStringLiteral("Credit"), i18nc("credit line, don't change name!", "Source: Deutscher Wetterdienst")); + data.insert(QStringLiteral("Credit Url"), QStringLiteral("https://www.dwd.de/")); + + setData(weatherSource, data); +} + +/* + * Helper methods + */ + +// e.g. DWD API int 17 equals 1.7 +float DWDIon::parseNumber(int number) +{ + return ((float)number) / 10; +} + +QString DWDIon::roundWindDirections(int windDirection) +{ + QString roundedWindDirection = QString::number(qRound(((float)windDirection) / 100) * 10); + return roundedWindDirection; +} + +QString DWDIon::extractString(QByteArray array, int start, int length) +{ + QString string; + + for (int i = start; i < start + length; i++) { + string.append(QLatin1Char(array[i])); + } + + return string; +} + +QString DWDIon::camelCaseString(const QString text) +{ + QString result; + bool nextBig = true; + + for (QChar c : text) { + if (c.isLetter()) { + if (nextBig) { + result.append(c.toUpper()); + nextBig = false; + } else { + result.append(c.toLower()); + } + } else { + if (c == QChar::Space || c == QLatin1Char('-')) { + nextBig = true; + } + result.append(c); + } + } + + return result; +} + +K_PLUGIN_CLASS_WITH_JSON(DWDIon, "ion-dwd.json") + +#include "ion_dwd.moc" diff --git a/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.h b/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.h new file mode 100644 index 0000000000..ae7628fd79 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/dwd/ion_dwd.h @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2021 Emily Ehlert + + Based upon BBC Weather Ion and ENV Canada Ion by Shawn Starr + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + also + + the wetter.com Ion by Thilo-Alexander Ginkel + SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for weather data from Deutscher Wetterdienst (DWD) / German Weather Service */ + +#pragma once + +#include "../ion.h" + +#include + +#define CATALOGUE_URL "https://www.dwd.de/DE/leistungen/met_verfahren_mosmix/mosmix_stationskatalog.cfg?view=nasPublication&nn=16102" +#define FORECAST_URL "https://app-prod-ws.warnwetter.de/v30/stationOverviewExtended?stationIds=%1" +#define MEASURE_URL "https://s3.eu-central-1.amazonaws.com/app-prod-static.warnwetter.de/v16/current_measurement_%1.json" + +class KJob; +namespace KIO +{ +class Job; +class TransferJob; +} + +class WeatherData +{ +public: + WeatherData(); + + QString place; + + // Current observation information. + QDateTime observationDateTime; + + QString conditionIcon; + QString windDirection; + float temperature; + float humidity; + float pressure; + float windSpeed; + float gustSpeed; + float dewpoint; + + // If current observations not available, use forecast data for current day + QString windDirectionAlt; + float windSpeedAlt; + float gustSpeedAlt; + + // 7 forecast + struct ForecastInfo { + ForecastInfo(); + QDateTime period; + QString iconName; + QString summary; + float tempHigh; + float tempLow; + float windSpeed; + QString windDirection; + }; + + // 7 day forecast + QVector forecasts; + + struct WarningInfo { + QString type; + int priority; + QString description; + QDateTime timestamp; + }; + + QVector warnings; + + bool isForecastsDataPending = false; + bool isMeasureDataPending = false; +}; + +class Q_DECL_EXPORT DWDIon : public IonInterface +{ + Q_OBJECT + +public: + DWDIon(QObject *parent, const QVariantList &args); + ~DWDIon() override; + +public: // IonInterface API + bool updateIonSource(const QString &source) override; + +protected: // IonInterface API + void reset() override; + +private Q_SLOTS: + void setup_slotDataArrived(KIO::Job *, const QByteArray &); + void setup_slotJobFinished(KJob *); + + void measure_slotDataArrived(KIO::Job *, const QByteArray &); + void measure_slotJobFinished(KJob *); + + void forecast_slotDataArrived(KIO::Job *, const QByteArray &); + void forecast_slotJobFinished(KJob *); + +private: + QMap setupDayIconMappings() const; + QMap setupWindIconMappings() const; + + QMap const &dayIcons() const; + QMap const &windIcons() const; + + void findPlace(const QString &searchText); + void parseStationData(QByteArray data); + void searchInStationList(const QString place); + + void validate(const QString &source); + + void fetchWeather(QString placeName, QString placeID); + void parseForecastData(const QString source, QJsonDocument doc); + void parseMeasureData(const QString source, QJsonDocument doc); + void updateWeather(const QString &source); + + void deleteForecasts(); + + // Helper methods + void calculatePositions(QStringList lines, QVector &namePositionalInfo, QVector &stationIdPositionalInfo); + QString camelCaseString(const QString text); + QString extractString(QByteArray array, int start, int length); + QString roundWindDirections(int windDirection); + float parseNumber(int number); + +private: + // Key dicts + QMap m_place; + QList m_locations; + + // Weather information + QHash m_weatherData; + + QHash m_searchJobData; + QHash m_searchJobList; + + QHash m_forecastJobJSON; + QHash m_forecastJobList; + + QHash m_measureJobJSON; + QHash m_measureJobList; + + QStringList m_sourcesToReset; +}; +// kate: indent-mode cstyle; space-indent on; indent-width 4; diff --git a/plasma/workspace/dataengines/weather/ions/envcan/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/envcan/CMakeLists.txt new file mode 100644 index 0000000000..b30af6f0af --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/envcan/CMakeLists.txt @@ -0,0 +1,17 @@ +set (ion_envcan_SRCS ion_envcan.cpp) +ecm_qt_declare_logging_category(ion_envcan_SRCS + HEADER ion_envcandebug.h + IDENTIFIER IONENGINE_ENVCAN + CATEGORY_NAME kde.dataengine.ion.envcan + DEFAULT_SEVERITY Info +) +add_library(ion_envcan MODULE ${ion_envcan_SRCS}) +target_link_libraries (ion_envcan + weather_ion + KF5::KIOCore + KF5::UnitConversion + KF5::I18n +) + +install (TARGETS ion_envcan DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + diff --git a/plasma/workspace/dataengines/weather/ions/envcan/ion-envcan.json b/plasma/workspace/dataengines/weather/ions/envcan/ion-envcan.json new file mode 100644 index 0000000000..757e44dfd5 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/envcan/ion-envcan.json @@ -0,0 +1,110 @@ +{ + "KPlugin": { + "Description": "XML Data from Environment Canada", + "Description[ar]": "بيانات XML من وزارة البيئة الكندية", + "Description[az]": "Environment Canada-dan XML formatında məlumatlar", + "Description[ca]": "Dades XML des de Medi ambient del Canadà", + "Description[cs]": "XML data z meteorologického úřadu Kanady (EC)", + "Description[de]": "XML-Daten von Environment Canada", + "Description[en_GB]": "XML Data from Environment Canada", + "Description[es]": "Datos XML de medio ambiente de Canada", + "Description[eu]": "«Environment Canada»ko XML datuak", + "Description[fi]": "XML-tietoa Environment Canadalta", + "Description[fr]": "Données au format « XML » du service de météorologie du Canada", + "Description[hu]": "XML-adatok az Environment Canada szervezettől", + "Description[ia]": "Datos XML ex Environment Canada", + "Description[it]": "Dati XML da Environment Canada", + "Description[ko]": "캐나다 환경부의 XML 데이터", + "Description[lt]": "XML duomenys iš Environment Canada", + "Description[nl]": "XML-gegevens van Environment Canada", + "Description[nn]": "XML-data frå Environment Canada", + "Description[pa]": "ਇੰਨਵਾਇਰਨਮੈਂਟ ਕੇਨੈਡਾ ਤੋਂ XML ਡਾਟਾ", + "Description[pl]": "Dane XML z Environment Canada", + "Description[pt_BR]": "Dados em XML do Environment Canada", + "Description[ro]": "Date XML de la Environment Canada", + "Description[ru]": "Данные в формате XML от Environment Canada", + "Description[sk]": "XML dáta z Environment Canada", + "Description[sl]": "Podatki XML od Environment Canada", + "Description[sv]": "XML-data från Environment Canada", + "Description[tr]": "Environment Canada'dan XML Verisi", + "Description[uk]": "Дані XML з метеорологічного відділу Канади", + "Description[vi]": "Dữ liệu XML từ Bộ Môi trường Canada", + "Description[x-test]": "xxXML Data from Environment Canadaxx", + "Description[zh_CN]": "加拿大环境部提供的 XML 数据", + "Icon": "noneyet", + "Id": "envcan", + "Name": "Environment Canada", + "Name[ar]": "بيئة كندا", + "Name[az]": "Environment Canada", + "Name[be@latin]": "Environment Canada", + "Name[bg]": "Environment Canada", + "Name[bs]": "Prirodna sredina Kanada", + "Name[ca@valencia]": "Medi ambient de Canadà", + "Name[ca]": "Medi ambient del Canadà", + "Name[cs]": "Meteorologický úřad Kanady (EC)", + "Name[da]": "Environment Canada", + "Name[de]": "Environment Canada", + "Name[el]": "Περιβάλλον Καναδά", + "Name[en_GB]": "Environment Canada", + "Name[eo]": "Environment Canada", + "Name[es]": "Medio ambiente de Canada", + "Name[et]": "Environment Canada", + "Name[eu]": "Kanadako Ingurumen Ministerioa", + "Name[fi]": "Environment Canada -palvelu", + "Name[fr]": "Météorologie du Canada", + "Name[fy]": "Omjouwing Kanada", + "Name[ga]": "Environment Canada", + "Name[gl]": "Environment Canada", + "Name[gu]": "એન્વાર્યમેન્ટ કેનેડા", + "Name[he]": "Environment Canada", + "Name[hi]": "एनवायरनमेंट कनाडा", + "Name[hne]": "कनाडा मौसम", + "Name[hr]": "Okoliš Kanade", + "Name[hu]": "Environment Canada", + "Name[ia]": "Environment Canada", + "Name[id]": "Environment Canada", + "Name[is]": "Environment Canada", + "Name[it]": "Environment Canada", + "Name[ja]": "Environment Canada", + "Name[kk]": "Environment Canada", + "Name[km]": "បរិស្ថាន​ប្រទេស​កាណាដា", + "Name[kn]": "ಎನ್ವಯರನ್ಮೆಂಟ್ ಕೆನಡಾ", + "Name[ko]": "캐나다 환경부", + "Name[lt]": "Environment Canada", + "Name[lv]": "Environment Canada", + "Name[mk]": "Environment Canada", + "Name[ml]": "കാനഡയുടെ പരിസ്ഥിതി", + "Name[mr]": "हवामान कॅनडा", + "Name[nb]": "Environment Canada", + "Name[nds]": "Ümwelt Kanada", + "Name[ne]": "वातावरण क्यानाडा", + "Name[nl]": "Environment Canada", + "Name[nn]": "Environment Canada", + "Name[pa]": "ਇੰਨਵਾਇਰਮੈਂਟ ਕੇਨੈਡਾ", + "Name[pl]": "Environment Canada", + "Name[pt]": "Environment Canada", + "Name[pt_BR]": "Environment Canada", + "Name[ro]": "Environment Canada", + "Name[ru]": "Environment Canada", + "Name[se]": "Environment Canada", + "Name[si]": "කැනඩා පාරිසරිකය", + "Name[sk]": "Počasie - Environment Canada", + "Name[sl]": "Environment Canada", + "Name[sr@ijekavian]": "Природна средина Канада", + "Name[sr@ijekavianlatin]": "Prirodna sredina Kanada", + "Name[sr@latin]": "Prirodna sredina Kanada", + "Name[sr]": "Природна средина Канада", + "Name[sv]": "Environment Canada", + "Name[te]": "కెనడా వాతావరణం", + "Name[th]": "Environment Canada", + "Name[tr]": "Environment Canada", + "Name[ug]": "كانادا مۇھىتى", + "Name[uk]": "Погода в Канаді", + "Name[vi]": "Bộ Môi trường Canada", + "Name[wa]": "Evironmint Canada", + "Name[x-test]": "xxEnvironment Canadaxx", + "Name[zh_CN]": "加拿大环境部", + "Name[zh_TW]": "Environment Canada" + }, + "X-KDE-ParentApp": "weatherengine" +} diff --git a/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.cpp b/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.cpp new file mode 100644 index 0000000000..6cbe5218b6 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.cpp @@ -0,0 +1,1627 @@ +/* + SPDX-FileCopyrightText: 2007-2011, 2019 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* 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('|')) + : 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.weather.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.weather.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 + Q_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 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)); + ++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 + : 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_PLUGIN_CLASS_WITH_JSON(EnvCanadaIon, "ion-envcan.json") + +#include "ion_envcan.moc" diff --git a/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.h b/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.h new file mode 100644 index 0000000000..e014bcc569 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/envcan/ion_envcan.h @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2007-2009, 2019 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for Environment Canada XML data */ + +#pragma once + +#include "../ion.h" + +#include + +#include +#include + +class KJob; +namespace KIO +{ +class Job; +} // namespace KIO + +class WeatherData +{ +public: + WeatherData(); + + // WeatherEvent can have more than one, especially in Canada, eh? :) + struct WeatherEvent { + QString url; + QString type; + QString priority; + QString description; + QString timestamp; + }; + + // Five day forecast + struct ForecastInfo { + ForecastInfo(); + + QString forecastPeriod; + QString forecastSummary; + QString iconName; + QString shortForecast; + + float tempHigh; + float tempLow; + float popPrecent; + QString windForecast; + + QString precipForecast; + QString precipType; + QString precipTotalExpected; + int forecastHumidity; + }; + + QString creditUrl; + QString countryName; + QString longTerritoryName; + QString shortTerritoryName; + QString cityName; + QString regionName; + QString stationID; + double stationLatitude; + double stationLongitude; + + // Current observation information. + QString obsTimestamp; + QDateTime observationDateTime; + + QString condition; + float temperature; + float dewpoint; + + // In winter windchill, in summer, humidex + QString humidex; + float windchill; + + float pressure; + QString pressureTendency; + + float visibility; + float humidity; + + float windSpeed; + float windGust; + QString windDirection; + QString windDegrees; + + QVector watches; + QVector warnings; + + float normalHigh; + float normalLow; + + QString forecastTimestamp; + + QString UVIndex; + QString UVRating; + + // 5 day Forecast + QVector forecasts; + + // Historical data from previous day. + float prevHigh; + float prevLow; + QString prevPrecipType; + QString prevPrecipTotal; + + // Almanac info + QString sunriseTimestamp; + QString sunsetTimestamp; + QString moonriseTimestamp; + QString moonsetTimestamp; + + // Historical Records + float recordHigh; + float recordLow; + float recordRain; + float recordSnow; + + QString solarDataTimeEngineSourceName; + bool isNight = false; +}; + +Q_DECLARE_TYPEINFO(WeatherData::WeatherEvent, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData::ForecastInfo, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData, Q_MOVABLE_TYPE); + +/** + * https://weather.gc.ca/mainmenu/disclaimer_e.html + */ +class Q_DECL_EXPORT EnvCanadaIon : public IonInterface, public Plasma::DataEngineConsumer +{ + Q_OBJECT + +public: + EnvCanadaIon(QObject *parent, const QVariantList &args); + ~EnvCanadaIon() override; + +public: // IonInterface API + bool updateIonSource(const QString &source) override; + +public Q_SLOTS: + // for solar data pushes from the time engine + void dataUpdated(const QString &sourceName, const Plasma::DataEngine::Data &data); + +protected: // IonInterface API + void reset() override; + +private Q_SLOTS: + void setup_slotDataArrived(KIO::Job *, const QByteArray &); + void setup_slotJobFinished(KJob *); + + void slotDataArrived(KIO::Job *, const QByteArray &); + void slotJobFinished(KJob *); + +private: + void updateWeather(const QString &source); + + /* Environment Canada Methods - Internal for Ion */ + void deleteForecasts(); + + QMap setupConditionIconMappings() const; + QMap setupForecastIconMappings() const; + + QMap const &conditionIcons() const; + QMap const &forecastIcons() const; + + // Load and Parse the place XML listing + void getXMLSetup(); + bool readXMLSetup(); + + // Load and parse the specific place(s) + void getXMLData(const QString &source); + bool readXMLData(const QString &source, QXmlStreamReader &xml); + + // Check if place specified is valid or not + QStringList validate(const QString &source) const; + + // Catchall for unknown XML tags + void parseUnknownElement(QXmlStreamReader &xml) const; + + // Parse weather XML data + void parseWeatherSite(WeatherData &data, QXmlStreamReader &xml); + void parseDateTime(WeatherData &data, QXmlStreamReader &xml, WeatherData::WeatherEvent *event = nullptr); + void parseLocations(WeatherData &data, QXmlStreamReader &xml); + void parseConditions(WeatherData &data, QXmlStreamReader &xml); + void parseWarnings(WeatherData &data, QXmlStreamReader &xml); + void parseWindInfo(WeatherData &data, QXmlStreamReader &xml); + void parseWeatherForecast(WeatherData &data, QXmlStreamReader &xml); + void parseRegionalNormals(WeatherData &data, QXmlStreamReader &xml); + void parseForecast(WeatherData &data, QXmlStreamReader &xml, WeatherData::ForecastInfo *forecast); + void parseShortForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader &xml); + void parseForecastTemperatures(WeatherData::ForecastInfo *forecast, QXmlStreamReader &xml); + void parseWindForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader &xml); + void parsePrecipitationForecast(WeatherData::ForecastInfo *forecast, QXmlStreamReader &xml); + void parsePrecipTotals(WeatherData::ForecastInfo *forecast, QXmlStreamReader &xml); + void parseUVIndex(WeatherData &data, QXmlStreamReader &xml); + void parseYesterdayWeather(WeatherData &data, QXmlStreamReader &xml); + void parseAstronomicals(WeatherData &data, QXmlStreamReader &xml); + void parseWeatherRecords(WeatherData &data, QXmlStreamReader &xml); + + void parseFloat(float &value, QXmlStreamReader &xml); + +private: + struct XMLMapInfo { + QString cityName; + QString territoryName; + QString cityCode; + }; + + // Key dicts + QHash m_places; + + // Weather information + QHash m_weatherData; + + // Store KIO jobs + QHash m_jobXml; + QHash m_jobList; + QStringList m_sourcesToReset; + QXmlStreamReader m_xmlSetup; + + bool emitWhenSetup; +}; diff --git a/plasma/workspace/dataengines/weather/ions/includes/Ion b/plasma/workspace/dataengines/weather/ions/includes/Ion new file mode 100644 index 0000000000..4ebf1cf797 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/includes/Ion @@ -0,0 +1 @@ +#include "../../../plasma/weather/ion.h" diff --git a/plasma/workspace/dataengines/weather/ions/ion.cpp b/plasma/workspace/dataengines/weather/ions/ion.cpp new file mode 100644 index 0000000000..c208d19655 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/ion.cpp @@ -0,0 +1,214 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "ion.h" + +#include "iondebug.h" + +#include + +class Q_DECL_HIDDEN IonInterface::Private +{ +public: + Private(IonInterface *i) + : ion(i) + , initialized(false) + { + } + + IonInterface *ion; + bool initialized; +}; + +IonInterface::IonInterface(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) + , d(new Private(this)) +{ +} + +IonInterface::~IonInterface() +{ + delete d; +} + +/** + * If the ion is not initialized just set the initial data source up even if it's empty, we'll retry once the initialization is done + */ +bool IonInterface::sourceRequestEvent(const QString &source) +{ + qCDebug(IONENGINE) << "sourceRequested(): " << source; + + // init anyway the data as it's going to be used + // sooner or later (doesn't depend upon initialization + // this will avoid problems if updateIonSource() fails for any reason + // but later it's able to retrieve the data + setData(source, Plasma::DataEngine::Data()); + + // if initialized, then we can try to grab the data + if (d->initialized) { + return updateIonSource(source); + } + + return true; +} + +/** + * Update the ion's datasource. Triggered when a Plasma::DataEngine::connectSource() timeout occurs. + */ +bool IonInterface::updateSourceEvent(const QString &source) +{ + qCDebug(IONENGINE) << "updateSource(" << source << ")"; + if (d->initialized) { + qCDebug(IONENGINE) << "Calling updateIonSource(" << source << ")"; + return updateIonSource(source); + } + + return false; +} + +/** + * Set the ion to make sure it is ready to get real data. + */ +void IonInterface::setInitialized(bool initialized) +{ + d->initialized = initialized; + + if (d->initialized) { + updateAllSources(); + } +} + +/** + * Return wind direction svg element to display in applet when given a wind direction. + */ +QString IonInterface::getWindDirectionIcon(const QMap &windDirList, const QString &windDirection) const +{ + switch (windDirList[windDirection.toLower()]) { + case N: + return QStringLiteral("N"); + case NNE: + return QStringLiteral("NNE"); + case NE: + return QStringLiteral("NE"); + case ENE: + return QStringLiteral("ENE"); + case E: + return QStringLiteral("E"); + case SSE: + return QStringLiteral("SSE"); + case SE: + return QStringLiteral("SE"); + case ESE: + return QStringLiteral("ESE"); + case S: + return QStringLiteral("S"); + case NNW: + return QStringLiteral("NNW"); + case NW: + return QStringLiteral("NW"); + case WNW: + return QStringLiteral("WNW"); + case W: + return QStringLiteral("W"); + case SSW: + return QStringLiteral("SSW"); + case SW: + return QStringLiteral("SW"); + case WSW: + return QStringLiteral("WSW"); + case VR: + return QStringLiteral("VR"); // For now, we'll make a variable wind icon later on + } + + // No icon available, use 'X' + return QString(); +} + +/** + * Return weather icon to display in an applet when given a condition. + */ +QString IonInterface::getWeatherIcon(ConditionIcons condition) const +{ + switch (condition) { + case ClearDay: + return QStringLiteral("weather-clear"); + case ClearWindyDay: + return QStringLiteral("weather-clear-wind"); + case FewCloudsDay: + return QStringLiteral("weather-few-clouds"); + case FewCloudsWindyDay: + return QStringLiteral("weather-few-clouds-wind"); + case PartlyCloudyDay: + return QStringLiteral("weather-clouds"); + case PartlyCloudyWindyDay: + return QStringLiteral("weather-clouds-wind"); + case Overcast: + return QStringLiteral("weather-overcast"); + case OvercastWindy: + return QStringLiteral("weather-overcast-wind"); + case Rain: + return QStringLiteral("weather-showers"); + case LightRain: + return QStringLiteral("weather-showers-scattered"); + case Showers: + return QStringLiteral("weather-showers-scattered"); + case ChanceShowersDay: + return QStringLiteral("weather-showers-scattered-day"); + case ChanceShowersNight: + return QStringLiteral("weather-showers-scattered-night"); + case ChanceSnowDay: + return QStringLiteral("weather-snow-scattered-day"); + case ChanceSnowNight: + return QStringLiteral("weather-snow-scattered-night"); + case Thunderstorm: + return QStringLiteral("weather-storm"); + case Hail: + return QStringLiteral("weather-hail"); + case Snow: + return QStringLiteral("weather-snow"); + case LightSnow: + return QStringLiteral("weather-snow-scattered"); + case Flurries: + return QStringLiteral("weather-snow-scattered"); + case RainSnow: + return QStringLiteral("weather-snow-rain"); + case FewCloudsNight: + return QStringLiteral("weather-few-clouds-night"); + case FewCloudsWindyNight: + return QStringLiteral("weather-few-clouds-wind-night"); + case PartlyCloudyNight: + return QStringLiteral("weather-clouds-night"); + case PartlyCloudyWindyNight: + return QStringLiteral("weather-clouds-wind-night"); + case ClearNight: + return QStringLiteral("weather-clear-night"); + case ClearWindyNight: + return QStringLiteral("weather-clear-wind-night"); + case Mist: + return QStringLiteral("weather-fog"); + case Haze: + return QStringLiteral("weather-fog"); + case FreezingRain: + return QStringLiteral("weather-freezing-rain"); + case FreezingDrizzle: + return QStringLiteral("weather-freezing-rain"); + case ChanceThunderstormDay: + return QStringLiteral("weather-storm-day"); + case ChanceThunderstormNight: + return QStringLiteral("weather-storm-night"); + case NotAvailable: + return QStringLiteral("weather-none-available"); + } + return QStringLiteral("weather-none-available"); +} + +/** + * Return weather icon to display in an applet when given a condition. + */ +QString IonInterface::getWeatherIcon(const QMap &conditionList, const QString &condition) const +{ + return getWeatherIcon(conditionList[condition.toLower()]); +} diff --git a/plasma/workspace/dataengines/weather/ions/ion.h b/plasma/workspace/dataengines/weather/ions/ion.h new file mode 100644 index 0000000000..5667636ce4 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/ion.h @@ -0,0 +1,241 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ion_export.h" + +/** + * @author Shawn Starr + * This is the base class to be used to implement new ions for the WeatherEngine. + * The idea is that you can have multiple ions which provide weather information from different services to the engine from which an applet will request the + * data from. + * + * Basically an ion is a Plasma::DataEngine, which is queried by the WeatherEngine instead of some applet. + * + * + * Find following the currently used entries of the data for a "SOMEION|weather|SOMEPLACE" source. + * Any free text strings should be translated by the dataengine if possible, + * as any dataengine user has less knowledge about the meaning of the strings. + * + * Data about the weather station: + * "Station": string, id of the location of the weather station with the service, required, TODO: ensure it's id + * "Place": string, display name of the location of the weather station, required, TODO: what details here, country? + * "Country": string, display name of country of the weather station, optional + * "Latitude": float, latitude of the weather station in decimal degrees, optional + * "Longitude": float, longitude of the weather station in decimal degrees, optional + * + * Data about last observation: + * "Observation Period": string, free text string for time of observation, optional + * "Observation Timestamp": datetime (with timezone), time of observation, optional + * "Current Conditions": string, free text string for current weather observation, optional + * "Condition Icon": string, xdg icon name for current weather observation, optional + * "Temperature": float, using general temperature unit, optional + * "Windchill": float, felt temperature due to wind, using general temperature unit, optional + * "Heat Index": float, using general temperature unit, optional + * "Humidex": int, humidity index (not to be mixed up with heat index), optional + * "Wind Speed": float, average wind speed, optional TODO: fix "Calm" strings injected on dataengine side, it's a display thing? + * "Wind Speed Unit": int, kunitconversion enum number for the unit with all wind speed values, required if wind speeds are given + * "Wind Gust": float, max wind gust speed, optional + * "Wind Direction": string, wind direction in cardinal directions (up to secondary-intercardinal + VR), optional + * "Visibility": float, visibility in distance, optional + * "Visibility Unit": int, kunitconversion enum number for the unit with all visibility values, required if visibilities are given + * "Pressure": float, air pressure, optional + * "Pressure Tendency": string, "rising", "falling", "steady", optional TODO: turn into enum/string id set, currently passed as string + * "Pressure Unit": int, kunitconversion enum number for the unit with all pressure values, required if pressures are given + * "UV Index": int, value in UV index UN standard, optional + * "UV Rating": string, grouping in which UV index is: "Low"0 &conditionList, const QString &condition) const; + + /** + * Returns wind icon element to display in applet. + * @param windDirList a QList map pair of wind directions mapped to a enumeration of directions. + * @param windDirection the current wind direction. + * @return svg element for wind direction + */ + QString getWindDirectionIcon(const QMap &windDirList, const QString &windDirection) const; + +public Q_SLOTS: + + /** + * Reimplemented from Plasma::DataEngine + * @param source the name of the datasource to be updated + */ + bool updateSourceEvent(const QString &source) override; + + /** + * Reimplement for ion to reload data if network status comes back up + */ + virtual void reset() = 0; + +Q_SIGNALS: + void forceUpdate(IonInterface *ion, const QString &source); + +protected: + /** + * Call this method to flush waiting source requests that may be pending + * initialization + * + * @arg initialized whether or not the ion is currently ready to fetch data + */ + void setInitialized(bool initialized); + + /** + * Reimplemented from Plasma::DataEngine + * @param source The datasource being requested + */ + bool sourceRequestEvent(const QString &source) override; + + /** + * Reimplement to fetch the data from the ion. + * @arg source the name of the datasource. + * @return true if update was successful, false if failed + */ + virtual bool updateIonSource(const QString &source) = 0; + + friend class WeatherEngine; + +private: + class Private; + Private *const d; +}; diff --git a/plasma/workspace/dataengines/weather/ions/noaa/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/noaa/CMakeLists.txt new file mode 100644 index 0000000000..4a805c95e3 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/noaa/CMakeLists.txt @@ -0,0 +1,17 @@ +set (ion_noaa_SRCS ion_noaa.cpp) +ecm_qt_declare_logging_category(ion_noaa_SRCS + HEADER ion_noaadebug.h + IDENTIFIER IONENGINE_NOAA + CATEGORY_NAME kde.dataengine.ion.noaa + DEFAULT_SEVERITY Info +) +add_library(ion_noaa MODULE ${ion_noaa_SRCS}) +target_link_libraries (ion_noaa + weather_ion + KF5::KIOCore + KF5::UnitConversion + KF5::I18n +) + +install (TARGETS ion_noaa DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + diff --git a/plasma/workspace/dataengines/weather/ions/noaa/ion-noaa.json b/plasma/workspace/dataengines/weather/ions/noaa/ion-noaa.json new file mode 100644 index 0000000000..fb1b343248 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/noaa/ion-noaa.json @@ -0,0 +1,109 @@ +{ + "KPlugin": { + "Description": "XML Data from NOAA's National Weather Service", + "Description[ar]": "بيانات XML من خدمة الطقس الوطنية التابعة لـ NOAA", + "Description[az]": "ABŞ Milli Hava Xidmətindən (NOAA) XML formatında məlumatlar", + "Description[ca]": "Dades XML del servei meteorològic nacional de NOAA", + "Description[cs]": "XML data služby o počasí agentury NOAA", + "Description[de]": "XML-Daten von NOAA's nationalem Wetterdienst", + "Description[en_GB]": "XML Data from NOAA's National Weather Service", + "Description[es]": "Datos XML del servicio nacional meteorológico norteamericano NOAA", + "Description[eu]": "NOAA-ren Eguraldi Zerbitzu Nazionaleko XML datuak", + "Description[fi]": "XML-tietoa NOAA:n kansalliselta sääpalvelulta", + "Description[fr]": "Données « XML » du service météorologique national du NOAA", + "Description[hu]": "XML adatok a NOAA időjárásjelző szolgálattól", + "Description[ia]": "Datos XML ex Servicio National Meteorologic NOAA", + "Description[it]": "Dati XML dal servizio meteorologico nazionale del NOAA", + "Description[ko]": "NOAA 미국 기상 서비스의 XML 데이터", + "Description[lt]": "XML duomenys iš NOAA nacionalinės orų tarnybos", + "Description[nl]": "XML-gegevens van NOAA's nationale weerdienst", + "Description[nn]": "XML-data frå NOAAs amerikanske vêrteneste", + "Description[pl]": "Dane XML z Narodowej Usługi Pogodowej NOAA", + "Description[pt_BR]": "Dados em XML do Serviço Meteorológico Nacional da NOAA", + "Description[ro]": "Date XML de la „NOAA's National Weather Service”", + "Description[ru]": "Данные в формате XML от NOAA", + "Description[sk]": "XML dáta z NOAA National Weather Service", + "Description[sl]": "Podatki XML od NOAA's National Weather Service", + "Description[sv]": "XML-data från NOAA:s nationella vädertjänst", + "Description[tr]": "NOAA Ulusal Hava Durumu Servisi'nden XML Verisi", + "Description[uk]": "Дані XML з національної служби погоди NOAA", + "Description[vi]": "Dữ liệu XML từ Dịch vụ Thời tiết Quốc gia của NOAA", + "Description[x-test]": "xxXML Data from NOAA's National Weather Servicexx", + "Description[zh_CN]": "NOAA 国家天气服务提供的 XML 数据", + "Icon": "noneyet", + "Id": "noaa", + "Name": "NOAA's National Weather Service", + "Name[ar]": "خدمة الطقس الوطنية NOAA ", + "Name[az]": "ABŞ Milli Hava Xidməti, NOAA", + "Name[be@latin]": "Nacyjanalnaja słužba nadvorja „NOAA”", + "Name[bg]": "Метеорологична станция NOAA", + "Name[bs]": "NOAA‑ova nacionalna meteorološka služba", + "Name[ca@valencia]": "Servei meteorològic nacional de NOAA", + "Name[ca]": "Servei meteorològic nacional de NOAA", + "Name[cs]": "Informační služba o počasí agentury NOAA", + "Name[da]": "NOAAs nationale vejrtjeneste", + "Name[de]": "NOAA's nationaler Wetterdienst", + "Name[el]": "Υπηρεσία καιρού NOAA", + "Name[en_GB]": "NOAA's National Weather Service", + "Name[eo]": "Nacia veterservo de NOAA", + "Name[es]": "Servicio nacional norteamericano NOAA", + "Name[et]": "NOAA riiklik ilmateenistus", + "Name[eu]": "NOAA Eguraldi Zerbitzu Nazionala", + "Name[fi]": "NOAA:n kansallinen sääpalvelu", + "Name[fr]": "Service météo national du NOAA", + "Name[fy]": "NOAA's nasjonale waar tsjinst", + "Name[ga]": "Seirbhís Náisiúnta na hAimsire de chuid NOAA", + "Name[gl]": "Servizo meteorolóxico nacional da NOAA", + "Name[gu]": "NOAA ની નેશનલ વેધર સર્વિસ", + "Name[he]": "NOAA's National Weather Service", + "Name[hi]": "एनओएए के राष्ट्रीय मौसम सेवा", + "Name[hne]": "एनओएए के रास्ट्रीय मौसम सेवा", + "Name[hr]": "NOAA-ova usluga nacionalne vremenske proznoze", + "Name[hu]": "NOAA időjárásjelző szolgálat", + "Name[ia]": "Servicio Meteorologic National NOAA", + "Name[id]": "Layanan Cuaca NOAA's", + "Name[is]": "NOAA veðurþjónustan", + "Name[it]": "Servizio meteorologico nazionale del NOAA", + "Name[ja]": "NOAA (アメリカ海洋大気圏局) National Weather Service", + "Name[kk]": "NOAA Ұлттық ауа райы қызметі", + "Name[km]": "សេវា​អាកាសធាតុ​ជាតិ​របស់ NOAA", + "Name[kn]": "NOAA ದ ರಾಷ್ಟ್ರೀಯ ಹವಾಮಾನ ಸೇವೆ", + "Name[ko]": "NOAA 미국 기상 서비스", + "Name[lt]": "NOAA nacionalinė orų tarnyba", + "Name[lv]": "NOAA's Nacionālais Laikapstākļu Serviss", + "Name[mk]": "Национален сервис за време на NOAA", + "Name[ml]": "നോആ-യുടെ ദേശീയ കാലവസ്ഥാ സേവനം", + "Name[mr]": "NOAA ची राष्ट्रीय हवामान सेवा", + "Name[nb]": "NOAAs nasjonale værtjeneste", + "Name[nds]": "Wederdeenst vun't US-Ozeaankunn- un Wederamt", + "Name[ne]": "NOAA को राष्ट्रिय मौसम सेवा", + "Name[nl]": "NOAA's nationale weerdienst", + "Name[nn]": "NOAAs amerikanske vêrteneste", + "Name[pa]": "NOAA ਦੀ ਕੌਮੀ ਮੌਸਮ ਸਰਵਿਸ", + "Name[pl]": "Narodowa Usługa Pogodowa NOAA", + "Name[pt]": "Serviço Meteorológico Nacional da NOAA", + "Name[pt_BR]": "Serviço Meteorológico Nacional da NOAA", + "Name[ro]": "NOAA's National Weather Service", + "Name[ru]": "Национальная служба погоды США", + "Name[se]": "NOAA amerihkálaš dálkebálvalus", + "Name[si]": "NOAA's ජාතික කාලගුණ සේවාව", + "Name[sk]": "Počasie - NOAA National Weather Service", + "Name[sl]": "NOAA's National Weather Service", + "Name[sr@ijekavian]": "НОАА‑ова национална метеоролошка служба", + "Name[sr@ijekavianlatin]": "NOAA‑ova nacionalna meteorološka služba", + "Name[sr@latin]": "NOAA‑ova nacionalna meteorološka služba", + "Name[sr]": "НОАА‑ова национална метеоролошка служба", + "Name[sv]": "NOAA:s nationella vädertjänst", + "Name[te]": "NOAA యొక్క జాతీయ వాతావరణ సేవ", + "Name[th]": "บริการพยากรณ์อากาศสากล NOAA", + "Name[tr]": "NOAA Ulusal Hava Durumu Servisi", + "Name[ug]": "NOAA تەمىنلىگەن دۆلەت ھاۋارايى مۇلازىمىتى", + "Name[uk]": "Національна служба погоди NOAA", + "Name[vi]": "Dịch vụ Thời tiết Quốc gia của NOAA", + "Name[wa]": "NOAA's National Weather Service", + "Name[x-test]": "xxNOAA's National Weather Servicexx", + "Name[zh_CN]": "NOAA 美国天气服务", + "Name[zh_TW]": "NOAA 的國家天氣服務" + }, + "X-KDE-ParentApp": "weatherengine" +} diff --git a/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.cpp b/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.cpp new file mode 100644 index 0000000000..27dabb745e --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.cpp @@ -0,0 +1,909 @@ +/* + SPDX-FileCopyrightText: 2007-2009, 2019 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* 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 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 + Q_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_PLUGIN_CLASS_WITH_JSON(NOAAIon, "ion-noaa.json") + +#include "ion_noaa.moc" diff --git a/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.h b/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.h new file mode 100644 index 0000000000..5416efd8c5 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/noaa/ion_noaa.h @@ -0,0 +1,163 @@ +/* + SPDX-FileCopyrightText: 2007-2009, 2019 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for NOAA's National Weather Service XML data */ + +#pragma once + +#include "../ion.h" + +#include + +#include +#include + +class KJob; +namespace KIO +{ +class Job; +} // namespace KIO + +class WeatherData +{ +public: + WeatherData(); + + QString locationName; + QString stationID; + double stationLatitude; + double stationLongitude; + QString stateName; + + // Current observation information. + QString observationTime; + QDateTime observationDateTime; + QString weather; + + float temperature_F; + float temperature_C; + float humidity; + QString windString; + QString windDirection; + float windSpeed; + float windGust; + float pressure; + float dewpoint_F; + float dewpoint_C; + float heatindex_F; + float heatindex_C; + float windchill_F; + float windchill_C; + float visibility; + + struct Forecast { + QString day; + QString summary; + QString low; + QString high; + }; + QVector forecasts; + + bool isForecastsDataPending = false; + + QString solarDataTimeEngineSourceName; + bool isNight = false; + bool isSolarDataPending = false; +}; + +Q_DECLARE_TYPEINFO(WeatherData::Forecast, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData, Q_MOVABLE_TYPE); + +class Q_DECL_EXPORT NOAAIon : public IonInterface, public Plasma::DataEngineConsumer +{ + Q_OBJECT + +public: + NOAAIon(QObject *parent, const QVariantList &args); + ~NOAAIon() override; + +public: // IonInterface API + bool updateIonSource(const QString &source) override; + +public Q_SLOTS: + // for solar data pushes from the time engine + void dataUpdated(const QString &sourceName, const Plasma::DataEngine::Data &data); + +protected: // IonInterface API + void reset() override; + +private Q_SLOTS: + void setup_slotDataArrived(KIO::Job *, const QByteArray &); + void setup_slotJobFinished(KJob *); + + void slotDataArrived(KIO::Job *, const QByteArray &); + void slotJobFinished(KJob *); + + void forecast_slotDataArrived(KIO::Job *, const QByteArray &); + void forecast_slotJobFinished(KJob *); + +private: + void updateWeather(const QString &source); + + /* NOAA Methods - Internal for Ion */ + QMap setupConditionIconMappings() const; + QMap const &conditionIcons() const; + QMap setupWindIconMappings() const; + QMap const &windIcons() const; + + // Current Conditions Weather info + // bool night(const QString& source); + IonInterface::ConditionIcons getConditionIcon(const QString &weather, bool isDayTime) const; + + // Load and Parse the place XML listing + void getXMLSetup() const; + bool readXMLSetup(); + + // Load and parse the specific place(s) + void getXMLData(const QString &source); + bool readXMLData(const QString &source, QXmlStreamReader &xml); + + // Load and parse upcoming forecast for the next N days + void getForecast(const QString &source); + void readForecast(const QString &source, QXmlStreamReader &xml); + + // Check if place specified is valid or not + QStringList validate(const QString &source) const; + + // Catchall for unknown XML tags + void parseUnknownElement(QXmlStreamReader &xml) const; + + // Parse weather XML data + void parseWeatherSite(WeatherData &data, QXmlStreamReader &xml); + void parseStationID(); + void parseStationList(); + + void parseFloat(float &value, const QString &string); + void parseFloat(float &value, QXmlStreamReader &xml); + void parseDouble(double &value, QXmlStreamReader &xml); + +private: + struct XMLMapInfo { + QString stateName; + QString stationName; + QString stationID; + QString XMLurl; + }; + + // Key dicts + QHash m_places; + + // Weather information + QHash m_weatherData; + + // Store KIO jobs + QHash m_jobXml; + QHash m_jobList; + QXmlStreamReader m_xmlSetup; + + // bool emitWhenSetup; + QStringList m_sourcesToReset; +}; diff --git a/plasma/workspace/dataengines/weather/ions/wetter.com/CMakeLists.txt b/plasma/workspace/dataengines/weather/ions/wetter.com/CMakeLists.txt new file mode 100644 index 0000000000..452a66e63b --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/wetter.com/CMakeLists.txt @@ -0,0 +1,17 @@ +set(ion_wettercom_SRCS ion_wettercom.cpp) +ecm_qt_declare_logging_category(ion_wettercom_SRCS + HEADER ion_wettercomdebug.h + IDENTIFIER IONENGINE_WETTERCOM + CATEGORY_NAME kde.dataengine.ion.wettercom + DEFAULT_SEVERITY Info +) +add_library(ion_wettercom MODULE ${ion_wettercom_SRCS}) +target_link_libraries(ion_wettercom + weather_ion + KF5::KIOCore + KF5::UnitConversion + KF5::I18n +) + +install(TARGETS ion_wettercom DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/dataengine) + diff --git a/plasma/workspace/dataengines/weather/ions/wetter.com/ion-wettercom.json b/plasma/workspace/dataengines/weather/ions/wetter.com/ion-wettercom.json new file mode 100644 index 0000000000..d1bca9341d --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/wetter.com/ion-wettercom.json @@ -0,0 +1,109 @@ +{ + "KPlugin": { + "Description": "Weather forecast by wetter.com", + "Description[ar]": "نشرة الطقس الجوية من wetter.com", + "Description[az]": "Wetter.com tərəfindən hava məlumatı", + "Description[ca]": "Previsió meteorològica per wetter.com", + "Description[cs]": "Předpověď počasí od wetter.com", + "Description[de]": "Wettervorhersage von wetter.com", + "Description[en_GB]": "Weather forecast by wetter.com", + "Description[es]": "Previsión meteorológica de wetter.com", + "Description[eu]": "wetter.com-eren eguraldi-iragarpena", + "Description[fi]": "wetter.comin sääennuste", + "Description[fr]": "Prévisions météorologiques par le site « wetter.com »", + "Description[hu]": "Időjárás-előrejelzés a wetter.com-ról", + "Description[ia]": "Prevision Meteorologic per wetter.com", + "Description[it]": "Previsioni del tempo di wetter.com", + "Description[ko]": "wetter.com 일기예보", + "Description[lt]": "Orų prognozės iš wetter.com", + "Description[nl]": "Weersvoorspelling door wetter.com", + "Description[nn]": "Vêrmelding av wetter.com", + "Description[pa]": "wetter.com ਤੋਂ ਮੌਸਮ ਭਵਿੱਖਬਾਣੀ", + "Description[pl]": "Prognoza pogody z wetter.com", + "Description[pt_BR]": "Previsão do tempo por wetter.com", + "Description[ro]": "Prognoza vremii de la wetter.com", + "Description[ru]": "Прогноз погоды с wetter.com", + "Description[sk]": "Predpoveď počasia z wetter.com", + "Description[sl]": "Vremenska napoved iz wetter.com", + "Description[sv]": "Väderprognos av wetter.com", + "Description[tr]": "wetter.com'dan hava tahmini", + "Description[uk]": "Прогноз погоди з wetter.com", + "Description[vi]": "Dự báo thời tiết của wetter.com", + "Description[x-test]": "xxWeather forecast by wetter.comxx", + "Description[zh_CN]": "wetter.com 网站提供的天气预报", + "Icon": "noneyet", + "Id": "wettercom", + "Name": "wetter.com", + "Name[ar]": "wetter.com", + "Name[ast]": "wetter.com", + "Name[az]": "wetter.com", + "Name[bg]": "wetter.com", + "Name[bn]": "wetter.com", + "Name[bs]": "wetter.com", + "Name[ca@valencia]": "wetter.com", + "Name[ca]": "wetter.com", + "Name[cs]": "wetter.com", + "Name[da]": "wetter.com", + "Name[de]": "wetter.com", + "Name[el]": "wetter.com", + "Name[en_GB]": "wetter.com", + "Name[eo]": "wetter.com", + "Name[es]": "wetter.com", + "Name[et]": "wetter.com", + "Name[eu]": "wetter.com", + "Name[fi]": "wetter.com", + "Name[fr]": "wetter.com", + "Name[fy]": "wetter.com", + "Name[ga]": "wetter.com", + "Name[gl]": "wetter.com", + "Name[gu]": "wetter.com", + "Name[he]": "wetter.com", + "Name[hi]": "wetter.com", + "Name[hr]": "wetter.com", + "Name[hu]": "wetter.com", + "Name[ia]": "wetter.com", + "Name[id]": "wetter.com", + "Name[is]": "wetter.com", + "Name[it]": "wetter.com", + "Name[ja]": "wetter.com", + "Name[ka]": "wetter.com", + "Name[kk]": "wetter.com", + "Name[km]": "wetter.com", + "Name[kn]": "wetter.com", + "Name[ko]": "wetter.com", + "Name[lt]": "wetter.com", + "Name[lv]": "wetter.com", + "Name[mk]": "wetter.com", + "Name[ml]": "വെറ്റര്‍.കൊം", + "Name[mr]": "wetter.com", + "Name[nb]": "wetter.com", + "Name[nds]": "wetter.com", + "Name[nl]": "wetter.com", + "Name[nn]": "wetter.com", + "Name[pa]": "wetter.com", + "Name[pl]": "wetter.com", + "Name[pt]": "wetter.com", + "Name[pt_BR]": "wetter.com", + "Name[ro]": "wetter.com", + "Name[ru]": "wetter.com", + "Name[si]": "wetter.com", + "Name[sk]": "wetter.com", + "Name[sl]": "wetter.com", + "Name[sr@ijekavian]": "wetter.com", + "Name[sr@ijekavianlatin]": "wetter.com", + "Name[sr@latin]": "wetter.com", + "Name[sr]": "wetter.com", + "Name[sv]": "wetter.com", + "Name[tg]": "wetter.com", + "Name[th]": "wetter.com", + "Name[tr]": "wetter.com", + "Name[ug]": "wetter.com", + "Name[uk]": "wetter.com", + "Name[vi]": "wetter.com", + "Name[wa]": "wetter.com", + "Name[x-test]": "xxwetter.comxx", + "Name[zh_CN]": "wetter.com", + "Name[zh_TW]": "wetter.com" + }, + "X-KDE-ParentApp": "weatherengine" +} diff --git a/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.cpp b/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.cpp new file mode 100644 index 0000000000..e8a40f4c86 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.cpp @@ -0,0 +1,782 @@ +/* + SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel + + Based upon BBC Weather Ion by Shawn Starr + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for weather data from wetter.com */ + +// Sample URLs: +// https://api.wetter.com/location/index/search/Heidelberg/project/weatherion/cs/9090dec6e783b96bd6a6ca9d451f3fee +// https://api.wetter.com/forecast/weather/city/DE0004329/project/weatherion/cs/89f1264869cce5c6fd5a2db80051f3d8 + +#include "ion_wettercom.h" + +#include "ion_wettercomdebug.h" + +#include +#include +#include + +#include +#include +#include + +/* + * Initialization + */ + +WetterComIon::WetterComIon(QObject *parent, const QVariantList &args) + : IonInterface(parent, args) + +{ +#if defined(MIN_POLL_INTERVAL) + setMinimumPollingInterval(MIN_POLL_INTERVAL); +#endif + setInitialized(true); +} + +WetterComIon::~WetterComIon() +{ + cleanup(); +} + +void WetterComIon::cleanup() +{ + // Clean up dynamically allocated forecasts + QMutableHashIterator it(m_weatherData); + while (it.hasNext()) { + it.next(); + WeatherData &item = it.value(); + qDeleteAll(item.forecasts); + item.forecasts.clear(); + } +} + +void WetterComIon::reset() +{ + cleanup(); + m_sourcesToReset = sources(); + updateAllSources(); +} + +QMap WetterComIon::setupCommonIconMappings() const +{ + return QMap{ + {QStringLiteral("3"), Overcast}, + {QStringLiteral("30"), Overcast}, + {QStringLiteral("4"), Haze}, + {QStringLiteral("40"), Haze}, + {QStringLiteral("45"), Haze}, + {QStringLiteral("48"), Haze}, + {QStringLiteral("49"), Haze}, + {QStringLiteral("5"), Mist}, + {QStringLiteral("50"), Mist}, + {QStringLiteral("51"), Mist}, + {QStringLiteral("53"), Mist}, + {QStringLiteral("55"), Mist}, + {QStringLiteral("56"), FreezingDrizzle}, + {QStringLiteral("57"), FreezingDrizzle}, + {QStringLiteral("6"), Rain}, + {QStringLiteral("60"), LightRain}, + {QStringLiteral("61"), LightRain}, + {QStringLiteral("63"), Rain}, + {QStringLiteral("65"), Rain}, + {QStringLiteral("66"), FreezingRain}, + {QStringLiteral("67"), FreezingRain}, + {QStringLiteral("68"), RainSnow}, + {QStringLiteral("69"), RainSnow}, + {QStringLiteral("7"), Snow}, + {QStringLiteral("70"), LightSnow}, + {QStringLiteral("71"), LightSnow}, + {QStringLiteral("73"), Snow}, + {QStringLiteral("75"), Flurries}, + {QStringLiteral("8"), Showers}, + {QStringLiteral("81"), Showers}, + {QStringLiteral("82"), Showers}, + {QStringLiteral("83"), RainSnow}, + {QStringLiteral("84"), RainSnow}, + {QStringLiteral("85"), Snow}, + {QStringLiteral("86"), Snow}, + {QStringLiteral("9"), Thunderstorm}, + {QStringLiteral("90"), Thunderstorm}, + {QStringLiteral("96"), Thunderstorm}, + {QStringLiteral("999"), NotAvailable}, + }; +} + +QMap WetterComIon::setupDayIconMappings() const +{ + QMap conditionList = setupCommonIconMappings(); + + conditionList.insert(QStringLiteral("0"), ClearDay); + conditionList.insert(QStringLiteral("1"), FewCloudsDay); + conditionList.insert(QStringLiteral("10"), FewCloudsDay); + conditionList.insert(QStringLiteral("2"), PartlyCloudyDay); + conditionList.insert(QStringLiteral("20"), PartlyCloudyDay); + conditionList.insert(QStringLiteral("80"), ChanceShowersDay); + conditionList.insert(QStringLiteral("95"), ChanceThunderstormDay); + + return conditionList; +} + +QMap const &WetterComIon::dayIcons() const +{ + static QMap const val = setupDayIconMappings(); + return val; +} + +QMap WetterComIon::setupNightIconMappings() const +{ + QMap conditionList = setupCommonIconMappings(); + + conditionList.insert(QStringLiteral("0"), ClearNight); + conditionList.insert(QStringLiteral("1"), FewCloudsNight); + conditionList.insert(QStringLiteral("10"), FewCloudsNight); + conditionList.insert(QStringLiteral("2"), PartlyCloudyNight); + conditionList.insert(QStringLiteral("20"), PartlyCloudyNight); + conditionList.insert(QStringLiteral("80"), ChanceShowersNight); + conditionList.insert(QStringLiteral("95"), ChanceThunderstormNight); + + return conditionList; +} + +QMap const &WetterComIon::nightIcons() const +{ + static QMap const val = setupNightIconMappings(); + return val; +} + +QHash WetterComIon::setupCommonConditionMappings() const +{ + return QHash{ + {QStringLiteral("1"), i18nc("weather condition", "few clouds")}, + {QStringLiteral("10"), i18nc("weather condition", "few clouds")}, + {QStringLiteral("2"), i18nc("weather condition", "cloudy")}, + {QStringLiteral("20"), i18nc("weather condition", "cloudy")}, + {QStringLiteral("3"), i18nc("weather condition", "overcast")}, + {QStringLiteral("30"), i18nc("weather condition", "overcast")}, + {QStringLiteral("4"), i18nc("weather condition", "haze")}, + {QStringLiteral("40"), i18nc("weather condition", "haze")}, + {QStringLiteral("45"), i18nc("weather condition", "haze")}, + {QStringLiteral("48"), i18nc("weather condition", "fog with icing")}, + {QStringLiteral("49"), i18nc("weather condition", "fog with icing")}, + {QStringLiteral("5"), i18nc("weather condition", "drizzle")}, + {QStringLiteral("50"), i18nc("weather condition", "drizzle")}, + {QStringLiteral("51"), i18nc("weather condition", "light drizzle")}, + {QStringLiteral("53"), i18nc("weather condition", "drizzle")}, + {QStringLiteral("55"), i18nc("weather condition", "heavy drizzle")}, + {QStringLiteral("56"), i18nc("weather condition", "freezing drizzle")}, + {QStringLiteral("57"), i18nc("weather condition", "heavy freezing drizzle")}, + {QStringLiteral("6"), i18nc("weather condition", "rain")}, + {QStringLiteral("60"), i18nc("weather condition", "light rain")}, + {QStringLiteral("61"), i18nc("weather condition", "light rain")}, + {QStringLiteral("63"), i18nc("weather condition", "moderate rain")}, + {QStringLiteral("65"), i18nc("weather condition", "heavy rain")}, + {QStringLiteral("66"), i18nc("weather condition", "light freezing rain")}, + {QStringLiteral("67"), i18nc("weather condition", "freezing rain")}, + {QStringLiteral("68"), i18nc("weather condition", "light rain snow")}, + {QStringLiteral("69"), i18nc("weather condition", "heavy rain snow")}, + {QStringLiteral("7"), i18nc("weather condition", "snow")}, + {QStringLiteral("70"), i18nc("weather condition", "light snow")}, + {QStringLiteral("71"), i18nc("weather condition", "light snow")}, + {QStringLiteral("73"), i18nc("weather condition", "moderate snow")}, + {QStringLiteral("75"), i18nc("weather condition", "heavy snow")}, + {QStringLiteral("8"), i18nc("weather condition", "showers")}, + {QStringLiteral("80"), i18nc("weather condition", "light showers")}, + {QStringLiteral("81"), i18nc("weather condition", "showers")}, + {QStringLiteral("82"), i18nc("weather condition", "heavy showers")}, + {QStringLiteral("83"), i18nc("weather condition", "light snow rain showers")}, + {QStringLiteral("84"), i18nc("weather condition", "heavy snow rain showers")}, + {QStringLiteral("85"), i18nc("weather condition", "light snow showers")}, + {QStringLiteral("86"), i18nc("weather condition", "snow showers")}, + {QStringLiteral("9"), i18nc("weather condition", "thunderstorm")}, + {QStringLiteral("90"), i18nc("weather condition", "thunderstorm")}, + {QStringLiteral("95"), i18nc("weather condition", "light thunderstorm")}, + {QStringLiteral("96"), i18nc("weather condition", "heavy thunderstorm")}, + {QStringLiteral("999"), i18nc("weather condition", "n/a")}, + }; +} + +QHash WetterComIon::setupDayConditionMappings() const +{ + QHash conditionList = setupCommonConditionMappings(); + conditionList.insert(QStringLiteral("0"), i18nc("weather condition", "sunny")); + return conditionList; +} + +QHash const &WetterComIon::dayConditions() const +{ + static QHash const val = setupDayConditionMappings(); + return val; +} + +QHash WetterComIon::setupNightConditionMappings() const +{ + QHash conditionList = setupCommonConditionMappings(); + conditionList.insert(QStringLiteral("0"), i18nc("weather condition", "clear sky")); + return conditionList; +} + +QHash const &WetterComIon::nightConditions() const +{ + static QHash const val = setupNightConditionMappings(); + return val; +} + +QString WetterComIon::getWeatherCondition(const QHash &conditionList, const QString &condition) const +{ + return conditionList[condition]; +} + +bool WetterComIon::updateIonSource(const QString &source) +{ + // We expect the applet to send the source in the following tokenization: + // ionname|validate|place_name|extra - Triggers validation of place + // ionname|weather|place_name|extra - Triggers receiving weather of place + + const QStringList sourceAction = source.split(QLatin1Char('|')); + + if (sourceAction.size() < 3) { + setData(source, QStringLiteral("validate"), QStringLiteral("wettercom|malformed")); + return true; + } + + if (sourceAction[1] == QLatin1String("validate") && sourceAction.size() >= 3) { + // Look for places to match + findPlace(sourceAction[2], source); + return true; + } + + if (sourceAction[1] == QLatin1String("weather") && sourceAction.size() >= 3) { + if (sourceAction.count() >= 4) { + if (sourceAction[2].isEmpty()) { + setData(source, QStringLiteral("validate"), QStringLiteral("wettercom|malformed")); + return true; + } + + // Extra data format: placeCode;displayName + const QStringList extraData = sourceAction[3].split(QLatin1Char(';')); + + if (extraData.count() != 2) { + setData(source, QStringLiteral("validate"), QStringLiteral("wettercom|malformed")); + return true; + } + + m_place[sourceAction[2]].placeCode = extraData[0]; + + m_place[sourceAction[2]].displayName = extraData[1]; + + qCDebug(IONENGINE_WETTERCOM) << "About to retrieve forecast for source: " << sourceAction[2]; + + fetchForecast(sourceAction[2]); + + return true; + } + + return false; + } + + setData(source, QStringLiteral("validate"), QStringLiteral("wettercom|malformed")); + return true; +} + +/* + * Handling of place searches + */ + +void WetterComIon::findPlace(const QString &place, const QString &source) +{ + QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(QByteArray(PROJECTNAME)); + md5.addData(QByteArray(APIKEY)); + md5.addData(place.toUtf8()); + const QString encodedKey = QString::fromLatin1(md5.result().toHex()); + + const QUrl url(QStringLiteral(SEARCH_URL).arg(place, encodedKey)); + + KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); // Disable displaying cookies + m_searchJobXml.insert(getJob, new QXmlStreamReader); + m_searchJobList.insert(getJob, source); + + connect(getJob, &KIO::TransferJob::data, this, &WetterComIon::setup_slotDataArrived); + connect(getJob, &KJob::result, this, &WetterComIon::setup_slotJobFinished); +} + +void WetterComIon::setup_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + + if (data.isEmpty() || !m_searchJobXml.contains(job)) { + return; + } + + m_searchJobXml[job]->addData(local); +} + +void WetterComIon::setup_slotJobFinished(KJob *job) +{ + if (job->error() == KIO::ERR_SERVER_TIMEOUT) { + setData(m_searchJobList[job], QStringLiteral("validate"), QStringLiteral("wettercom|timeout")); + disconnectSource(m_searchJobList[job], this); + m_searchJobList.remove(job); + delete m_searchJobXml[job]; + m_searchJobXml.remove(job); + return; + } + + QXmlStreamReader *reader = m_searchJobXml.value(job); + + if (reader) { + parseSearchResults(m_searchJobList[job], *reader); + } + + m_searchJobList.remove(job); + + delete m_searchJobXml[job]; + m_searchJobXml.remove(job); +} + +void WetterComIon::parseSearchResults(const QString &source, QXmlStreamReader &xml) +{ + QString name, code, quarter, state, country; + + while (!xml.atEnd()) { + xml.readNext(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement()) { + if (elementName == QLatin1String("search")) { + break; + } else if (elementName == QLatin1String("item")) { + // we parsed a place from the search result + QString placeName; + + if (quarter.isEmpty()) { + placeName = i18nc("Geographical location: city, state, ISO-country-code", "%1, %2, %3", name, state, country); + } else { + placeName = i18nc("Geographical location: quarter (city), state, ISO-country-code", "%1 (%2), %3, %4", quarter, name, state, country); + } + + qCDebug(IONENGINE_WETTERCOM) << "Storing place data for place:" << placeName; + + PlaceInfo &place = m_place[placeName]; + place.name = placeName; + place.displayName = name; + place.placeCode = code; + m_locations.append(placeName); + + name.clear(); + code.clear(); + quarter.clear(); + country.clear(); + state.clear(); + } + } + + if (xml.isStartElement()) { + if (elementName == QLatin1String("name")) { + name = xml.readElementText(); + } else if (elementName == QLatin1String("city_code")) { + code = xml.readElementText(); + } else if (elementName == QLatin1String("quarter")) { + quarter = xml.readElementText(); + } else if (elementName == QLatin1String("adm_1_code")) { + country = xml.readElementText(); + } else if (elementName == QLatin1String("adm_2_name")) { + state = xml.readElementText(); + } + } + } + + validate(source, xml.error() != QXmlStreamReader::NoError); +} + +void WetterComIon::validate(const QString &source, bool parseError) +{ + if (!m_locations.count() || parseError) { + const QString invalidPlace = source.section(QLatin1Char('|'), 2, 2); + + if (m_place[invalidPlace].name.isEmpty()) { + setData(source, QStringLiteral("validate"), QVariant(QLatin1String("wettercom|invalid|multiple|") + invalidPlace)); + } + + m_locations.clear(); + + return; + } + + QString placeList; + for (const QString &place : qAsConst(m_locations)) { + // Extra data format: placeCode;displayName + placeList.append(QLatin1String("|place|") + place + QLatin1String("|extra|") + m_place[place].placeCode + QLatin1Char(';') + + m_place[place].displayName); + } + + qCDebug(IONENGINE_WETTERCOM) << "Returning place list:" << placeList; + + if (m_locations.count() > 1) { + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("wettercom|valid|multiple") + placeList)); + } else { + placeList[7] = placeList[7].toUpper(); + setData(source, QStringLiteral("validate"), QVariant(QStringLiteral("wettercom|valid|single") + placeList)); + } + + m_locations.clear(); +} + +/* + * Handling of forecasts + */ + +void WetterComIon::fetchForecast(const QString &source) +{ + for (const QString &fetching : qAsConst(m_forecastJobList)) { + if (fetching == source) { + // already fetching! + return; + } + } + + QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(QByteArray(PROJECTNAME)); + md5.addData(QByteArray(APIKEY)); + md5.addData(m_place[source].placeCode.toUtf8()); + const QString encodedKey = QString::fromLatin1(md5.result().toHex()); + + const QUrl url(QStringLiteral(FORECAST_URL).arg(m_place[source].placeCode, encodedKey)); + + KIO::TransferJob *getJob = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + getJob->addMetaData(QStringLiteral("cookies"), QStringLiteral("none")); + m_forecastJobXml.insert(getJob, new QXmlStreamReader); + m_forecastJobList.insert(getJob, source); + + connect(getJob, &KIO::TransferJob::data, this, &WetterComIon::forecast_slotDataArrived); + connect(getJob, &KJob::result, this, &WetterComIon::forecast_slotJobFinished); +} + +void WetterComIon::forecast_slotDataArrived(KIO::Job *job, const QByteArray &data) +{ + QByteArray local = data; + + if (data.isEmpty() || !m_forecastJobXml.contains(job)) { + return; + } + + m_forecastJobXml[job]->addData(local); +} + +void WetterComIon::forecast_slotJobFinished(KJob *job) +{ + const QString source(m_forecastJobList.value(job)); + setData(source, Data()); + QXmlStreamReader *reader = m_forecastJobXml.value(job); + + if (reader) { + parseWeatherForecast(source, *reader); + } + + m_forecastJobList.remove(job); + + delete m_forecastJobXml[job]; + m_forecastJobXml.remove(job); + + if (m_sourcesToReset.contains(source)) { + m_sourcesToReset.removeAll(source); + const QString weatherSource = QStringLiteral("wettercom|weather|%1|%2;%3").arg(source, m_place[source].placeCode, m_place[source].displayName); + + // so the weather engine updates it's data + forceImmediateUpdateOfAllVisualizations(); + + // update the clients of our engine + Q_EMIT forceUpdate(this, weatherSource); + } +} + +void WetterComIon::parseWeatherForecast(const QString &source, QXmlStreamReader &xml) +{ + qCDebug(IONENGINE_WETTERCOM) << "About to parse forecast for source:" << source; + + WeatherData &weatherData = m_weatherData[source]; + + // Clear old forecasts when updating + weatherData.forecasts.clear(); + + WeatherData::ForecastPeriod *forecastPeriod = new WeatherData::ForecastPeriod; + WeatherData::ForecastInfo *forecast = new WeatherData::ForecastInfo; + int summaryWeather = -1, summaryProbability = 0; + int tempMax = -273, tempMin = 100, weather = -1, probability = 0; + uint summaryUtcTime = 0, utcTime = 0, localTime = 0; + QString date, time; + + weatherData.place = source; + + while (!xml.atEnd()) { + xml.readNext(); + + qCDebug(IONENGINE_WETTERCOM) << "parsing xml elem: " << xml.name(); + + const QStringRef elementName = xml.name(); + + if (xml.isEndElement()) { + if (elementName == QLatin1String("city")) { + break; + } + if (elementName == QLatin1String("date")) { + // we have parsed a complete day + + forecastPeriod->period = QDateTime::fromSecsSinceEpoch(summaryUtcTime, Qt::LocalTime); + QString weatherString = QString::number(summaryWeather); + forecastPeriod->iconName = getWeatherIcon(dayIcons(), weatherString); + forecastPeriod->summary = getWeatherCondition(dayConditions(), weatherString); + forecastPeriod->probability = summaryProbability; + + weatherData.forecasts.append(forecastPeriod); + forecastPeriod = new WeatherData::ForecastPeriod; + + date.clear(); + summaryWeather = -1; + summaryProbability = 0; + summaryUtcTime = 0; + } else if (elementName == QLatin1String("time")) { + // we have parsed one forecast + + qCDebug(IONENGINE_WETTERCOM) << "Parsed a forecast interval:" << date << time; + + // yep, that field is written to more often than needed... + weatherData.timeDifference = localTime - utcTime; + + forecast->period = QDateTime::fromSecsSinceEpoch(utcTime, Qt::LocalTime); + QString weatherString = QString::number(weather); + forecast->tempHigh = tempMax; + forecast->tempLow = tempMin; + forecast->probability = probability; + + QTime localWeatherTime = QDateTime::fromSecsSinceEpoch(utcTime, Qt::LocalTime).time(); + localWeatherTime = localWeatherTime.addSecs(weatherData.timeDifference); + + qCDebug(IONENGINE_WETTERCOM) << "localWeatherTime =" << localWeatherTime; + + // TODO use local sunset/sunrise time + + if (localWeatherTime.hour() < 20 && localWeatherTime.hour() > 6) { + forecast->iconName = getWeatherIcon(dayIcons(), weatherString); + forecast->summary = getWeatherCondition(dayConditions(), weatherString); + forecastPeriod->dayForecasts.append(forecast); + } else { + forecast->iconName = getWeatherIcon(nightIcons(), weatherString); + forecast->summary = getWeatherCondition(nightConditions(), weatherString); + forecastPeriod->nightForecasts.append(forecast); + } + + forecast = new WeatherData::ForecastInfo; + + tempMax = -273; + tempMin = 100; + weather = -1; + probability = 0; + utcTime = localTime = 0; + time.clear(); + } + } + + if (xml.isStartElement()) { + if (elementName == QLatin1String("date")) { + date = xml.attributes().value(QStringLiteral("value")).toString(); + } else if (elementName == QLatin1String("time")) { + time = xml.attributes().value(QStringLiteral("value")).toString(); + } else if (elementName == QLatin1String("tx")) { + tempMax = qRound(xml.readElementText().toDouble()); + qCDebug(IONENGINE_WETTERCOM) << "parsed t_max:" << tempMax; + } else if (elementName == QLatin1String("tn")) { + tempMin = qRound(xml.readElementText().toDouble()); + qCDebug(IONENGINE_WETTERCOM) << "parsed t_min:" << tempMin; + } else if (elementName == QLatin1Char('w')) { + int tmp = xml.readElementText().toInt(); + + if (!time.isEmpty()) + weather = tmp; + else + summaryWeather = tmp; + + qCDebug(IONENGINE_WETTERCOM) << "parsed weather condition:" << tmp; + } else if (elementName == QLatin1String("name")) { + weatherData.stationName = xml.readElementText(); + qCDebug(IONENGINE_WETTERCOM) << "parsed station name:" << weatherData.stationName; + } else if (elementName == QLatin1String("pc")) { + int tmp = xml.readElementText().toInt(); + + if (!time.isEmpty()) + probability = tmp; + else + summaryProbability = tmp; + + qCDebug(IONENGINE_WETTERCOM) << "parsed probability:" << probability; + } else if (elementName == QLatin1String("text")) { + weatherData.credits = xml.readElementText(); + qCDebug(IONENGINE_WETTERCOM) << "parsed credits:" << weatherData.credits; + } else if (elementName == QLatin1String("link")) { + weatherData.creditsUrl = xml.readElementText(); + qCDebug(IONENGINE_WETTERCOM) << "parsed credits url:" << weatherData.creditsUrl; + } else if (elementName == QLatin1Char('d')) { + localTime = xml.readElementText().toInt(); + qCDebug(IONENGINE_WETTERCOM) << "parsed local time:" << localTime; + } else if (elementName == QLatin1String("du")) { + int tmp = xml.readElementText().toInt(); + + if (!time.isEmpty()) + utcTime = tmp; + else + summaryUtcTime = tmp; + + qCDebug(IONENGINE_WETTERCOM) << "parsed UTC time:" << tmp; + } + } + } + + delete forecast; + delete forecastPeriod; + + updateWeather(source, xml.error() != QXmlStreamReader::NoError); +} + +void WetterComIon::updateWeather(const QString &source, bool parseError) +{ + qCDebug(IONENGINE_WETTERCOM) << "Source:" << source; + + const PlaceInfo &placeInfo = m_place[source]; + + QString weatherSource = QStringLiteral("wettercom|weather|%1|%2;%3").arg(source, placeInfo.placeCode, placeInfo.displayName); + + const WeatherData &weatherData = m_weatherData[source]; + + Plasma::DataEngine::Data data; + data.insert(QStringLiteral("Place"), placeInfo.displayName); + + if (!parseError && !weatherData.forecasts.isEmpty()) { + data.insert(QStringLiteral("Station"), placeInfo.displayName); + // data.insert("Condition Icon", "N/A"); + // data.insert("Temperature", "N/A"); + data.insert(QStringLiteral("Temperature Unit"), KUnitConversion::Celsius); + + int i = 0; + for (const WeatherData::ForecastPeriod *forecastPeriod : weatherData.forecasts) { + if (i > 0) { + WeatherData::ForecastInfo weather = forecastPeriod->getWeather(); + + data.insert(QStringLiteral("Short Forecast Day %1").arg(i), + QStringLiteral("%1|%2|%3|%4|%5|%6") + .arg(QLocale().toString(weather.period.date().day()), weather.iconName, weather.summary) + .arg(weather.tempHigh) + .arg(weather.tempLow) + .arg(weather.probability)); + i++; + } else { + WeatherData::ForecastInfo dayWeather = forecastPeriod->getDayWeather(); + + data.insert(QStringLiteral("Short Forecast Day %1").arg(i), + QStringLiteral("%1|%2|%3|%4|%5|%6") + .arg(i18n("Day"), dayWeather.iconName, dayWeather.summary) + .arg(dayWeather.tempHigh) + .arg(dayWeather.tempLow) + .arg(dayWeather.probability)); + i++; + + if (forecastPeriod->hasNightWeather()) { + WeatherData::ForecastInfo nightWeather = forecastPeriod->getNightWeather(); + data.insert(QStringLiteral("Short Forecast Day %1").arg(i), + QStringLiteral("%1 nt|%2|%3|%4|%5|%6") + .arg(i18n("Night"), nightWeather.iconName, nightWeather.summary) + .arg(nightWeather.tempHigh) + .arg(nightWeather.tempLow) + .arg(nightWeather.probability)); + i++; + } + } + } + + // Set number of forecasts per day/night supported + data.insert(QStringLiteral("Total Weather Days"), i); + + data.insert(QStringLiteral("Credit"), weatherData.credits); // FIXME i18n? + data.insert(QStringLiteral("Credit Url"), weatherData.creditsUrl); + + qCDebug(IONENGINE_WETTERCOM) << "updated weather data:" << weatherSource << data; + } else { + qCDebug(IONENGINE_WETTERCOM) << "Something went wrong when parsing weather data for source:" << source; + } + + setData(weatherSource, data); +} + +/* + * WeatherData::ForecastPeriod convenience methods + */ + +WeatherData::ForecastPeriod::~ForecastPeriod() +{ + qDeleteAll(dayForecasts); + qDeleteAll(nightForecasts); +} + +WeatherData::ForecastInfo WeatherData::ForecastPeriod::getDayWeather() const +{ + WeatherData::ForecastInfo result; + result.period = period; + result.iconName = iconName; + result.summary = summary; + result.tempHigh = getMaxTemp(dayForecasts); + result.tempLow = getMinTemp(dayForecasts); + result.probability = probability; + return result; +} + +WeatherData::ForecastInfo WeatherData::ForecastPeriod::getNightWeather() const +{ + qCDebug(IONENGINE_WETTERCOM) << "nightForecasts.size() =" << nightForecasts.size(); + + // TODO do not just pick the first night forecast + return *(nightForecasts.at(0)); +} + +bool WeatherData::ForecastPeriod::hasNightWeather() const +{ + return !nightForecasts.isEmpty(); +} + +WeatherData::ForecastInfo WeatherData::ForecastPeriod::getWeather() const +{ + WeatherData::ForecastInfo result = getDayWeather(); + result.tempHigh = std::max(result.tempHigh, getMaxTemp(nightForecasts)); + result.tempLow = std::min(result.tempLow, getMinTemp(nightForecasts)); + return result; +} + +int WeatherData::ForecastPeriod::getMaxTemp(const QVector &forecastInfos) const +{ + int result = -273; + for (const WeatherData::ForecastInfo *forecast : forecastInfos) { + result = std::max(result, forecast->tempHigh); + } + + return result; +} + +int WeatherData::ForecastPeriod::getMinTemp(const QVector &forecastInfos) const +{ + int result = 100; + for (const WeatherData::ForecastInfo *forecast : forecastInfos) { + result = std::min(result, forecast->tempLow); + } + + return result; +} + +K_PLUGIN_CLASS_WITH_JSON(WetterComIon, "ion-wettercom.json") + +#include "ion_wettercom.moc" diff --git a/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.h b/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.h new file mode 100644 index 0000000000..e5ca2bf495 --- /dev/null +++ b/plasma/workspace/dataengines/weather/ions/wetter.com/ion_wettercom.h @@ -0,0 +1,162 @@ +/* + SPDX-FileCopyrightText: 2009 Thilo-Alexander Ginkel + + Based upon BBC Weather Ion by Shawn Starr + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Ion for weather data from wetter.com */ + +#pragma once + +#include "../ion.h" + +#include + +// wetter.com API project data +#define PROJECTNAME "weatherion" +#define SEARCH_URL "https://api.wetter.com/location/index/search/%1/project/" PROJECTNAME "/cs/%2" +#define FORECAST_URL "https://api.wetter.com/forecast/weather/city/%1/project/" PROJECTNAME "/cs/%2" +#define APIKEY "07025b9a22b4febcf8e8ec3e6f1140e8" +#define MIN_POLL_INTERVAL 3600000L // 1 h + +class KJob; +namespace KIO +{ +class Job; +class TransferJob; +} +class QXmlStreamReader; + +class WeatherData +{ +public: + QString place; + QString stationName; + + // time difference to UTC + int timeDifference; + + // credits as returned from API request + QString credits; + QString creditsUrl; + + class ForecastBase + { + public: + QDateTime period; + QString iconName; + QString summary; + int probability; + }; + + class ForecastInfo : public ForecastBase + { + public: + int tempHigh; + int tempLow; + }; + + class ForecastPeriod : public ForecastInfo + { + public: + ~ForecastPeriod(); + + WeatherData::ForecastInfo getDayWeather() const; + WeatherData::ForecastInfo getNightWeather() const; + WeatherData::ForecastInfo getWeather() const; + + bool hasNightWeather() const; + + QVector dayForecasts; + QVector nightForecasts; + + private: + int getMaxTemp(const QVector &forecastInfos) const; + int getMinTemp(const QVector &forecastInfos) const; + }; + + QVector forecasts; +}; + +Q_DECLARE_TYPEINFO(WeatherData::ForecastInfo, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData::ForecastPeriod, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(WeatherData, Q_MOVABLE_TYPE); + +class Q_DECL_EXPORT WetterComIon : public IonInterface +{ + Q_OBJECT + +public: + WetterComIon(QObject *parent, const QVariantList &args); + ~WetterComIon() override; + +public: // IonInterface API + bool updateIonSource(const QString &source) override; + +protected: // IonInterface API + void reset() override; + +private Q_SLOTS: + void setup_slotDataArrived(KIO::Job *, const QByteArray &); + void setup_slotJobFinished(KJob *); + + void forecast_slotDataArrived(KIO::Job *, const QByteArray &); + void forecast_slotJobFinished(KJob *); + +private: + void cleanup(); + + // Set up the mapping from the wetter.com condition code to the respective icon / condition name + QMap setupCommonIconMappings() const; + QMap setupDayIconMappings() const; + QMap setupNightIconMappings() const; + QHash setupCommonConditionMappings() const; + QHash setupDayConditionMappings() const; + QHash setupNightConditionMappings() const; + + // Retrieve the mapping from the wetter.com condition code to the respective icon / condition name + QMap const &nightIcons() const; + QMap const &dayIcons() const; + QHash const &dayConditions() const; + QHash const &nightConditions() const; + + QString getWeatherCondition(const QHash &conditionList, const QString &condition) const; + + // Find place + void findPlace(const QString &place, const QString &source); + void parseSearchResults(const QString &source, QXmlStreamReader &xml); + void validate(const QString &source, bool parseError); + + // Retrieve and parse forecast + void fetchForecast(const QString &source); + void parseWeatherForecast(const QString &source, QXmlStreamReader &xml); + void updateWeather(const QString &source, bool parseError); + +private: + struct PlaceInfo { + QString name; + QString displayName; + QString placeCode; + }; + + // Key dicts + QHash m_place; + QVector m_locations; + + // Weather information + QHash m_weatherData; + + // Store KIO jobs - Search list + QHash m_searchJobXml; + QHash m_searchJobList; + + // Store KIO jobs - Forecast retrieval + QHash m_forecastJobXml; + QHash m_forecastJobList; + + QStringList m_sourcesToReset; +}; +// kate: indent-mode cstyle; space-indent on; indent-width 4; diff --git a/plasma/workspace/dataengines/weather/plasma-dataengine-weather.json b/plasma/workspace/dataengines/weather/plasma-dataengine-weather.json new file mode 100644 index 0000000000..0a8f133f70 --- /dev/null +++ b/plasma/workspace/dataengines/weather/plasma-dataengine-weather.json @@ -0,0 +1,153 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "shawn.starr@rogers.com", + "Name": "Shawn Starr", + "Name[ar]": "Shawn Starr", + "Name[az]": "Shawn Starr", + "Name[ca]": "Shawn Starr", + "Name[cs]": "Shawn Starr", + "Name[de]": "Shawn Starr", + "Name[en_GB]": "Shawn Starr", + "Name[es]": "Shawn Starr", + "Name[eu]": "Shawn Starr", + "Name[fi]": "Shawn Starr", + "Name[fr]": "Shawn Starr", + "Name[hu]": "Shawn Starr", + "Name[ia]": "Shawn Starr", + "Name[it]": "Shawn Starr", + "Name[ko]": "Shawn Starr", + "Name[lt]": "Shawn Starr", + "Name[nl]": "Shawn Starr", + "Name[nn]": "Shawn Starr", + "Name[pl]": "Shawn Starr", + "Name[pt_BR]": "Shawn Starr", + "Name[ro]": "Shawn Starr", + "Name[ru]": "Shawn Starr", + "Name[sk]": "Shawn Starr", + "Name[sl]": "Shawn Starr", + "Name[sv]": "Shawn Starr", + "Name[tr]": "Shawn Starr", + "Name[uk]": "Shawn Starr", + "Name[vi]": "Shawn Starr", + "Name[x-test]": "xxShawn Starrxx", + "Name[zh_CN]": "Shawn Starr" + } + ], + "Category": "", + "Description": "Weather data from multiple online sources", + "Description[ar]": "بيانات الطقس من عدّة مصادر على الإنترنت", + "Description[az]": "İntertetdəki müxtəlif mənbələrdən hava haqqında məlumat", + "Description[ca]": "Dades meteorològiques de múltiples fonts en línia", + "Description[cs]": "Data o počasí z různých online zdrojů", + "Description[de]": "Wetterdaten aus verschiedenen Online-Quellen", + "Description[en_GB]": "Weather data from multiple online sources", + "Description[es]": "Datos meteorológicos desde múltiples fuentes.", + "Description[eu]": "Eguraldiari buruzko informazioa hainbat iturritatik", + "Description[fi]": "Säätietoa eri verkkolähteistä", + "Description[fr]": "Données météorologiques provenant de sources multiples en ligne", + "Description[hu]": "Időjárás-jelentés több forrásból", + "Description[ia]": "Datos meteorologic ex multiple fontes in linea", + "Description[it]": "Dati meteorologici da varie fonti in rete", + "Description[ko]": "온라인에서 온 다양한 날씨 데이터", + "Description[lt]": "Orų duomenys iš įvairių internetinių šaltinių", + "Description[nl]": "Weergegevens voor meervoudige online bronnen", + "Description[nn]": "Vêrdata frå ulike nettkjelder", + "Description[pa]": "ਕਈ ਆਨਲਾਈਨ ਸਰੋਤਾਂ ਤੋਂ ਮੌਸਮ ਡਾਟਾ", + "Description[pl]": "Dane pogodowe z wielu internetowych źródeł", + "Description[pt_BR]": "Dados meteorológicos de múltiplas fontes online", + "Description[ro]": "Date meteorologice din diferite surse online", + "Description[ru]": "Информация о погоде из разных источников в Интернете", + "Description[sk]": "Dáta o počasí z rôznych online zdrojov", + "Description[sl]": "Podatki o vremenu iz več spletnih virov", + "Description[sv]": "Väderdata från flera olika nättjänster", + "Description[tr]": "Birden fazla çevrimiçi kaynaktan hava durumu verisi", + "Description[uk]": "Дані щодо погоди з декількох мережевих джерел", + "Description[vi]": "Dữ liệu thời tiết từ nhiều nguồn trực tuyến", + "Description[x-test]": "xxWeather data from multiple online sourcesxx", + "Description[zh_CN]": "多个在线信息源提供的天气数据", + "Icon": "weather-clear", + "Id": "weather", + "License": "GPLv2+", + "Name": "Weather", + "Name[ar]": "الطقس", + "Name[az]": "Hava", + "Name[be@latin]": "Nadvorje", + "Name[bg]": "Метеорологично време", + "Name[bn]": "আবহাওয়া", + "Name[bn_IN]": "আবহাওয়া", + "Name[bs]": "Vrijeme", + "Name[ca@valencia]": "Meteorologia", + "Name[ca]": "Meteorologia", + "Name[cs]": "Počasí", + "Name[da]": "Vejr", + "Name[de]": "Wetter", + "Name[el]": "Καιρός", + "Name[en_GB]": "Weather", + "Name[eo]": "Vetero", + "Name[es]": "Tiempo meteorológico", + "Name[et]": "Ilmateade", + "Name[eu]": "Eguraldia", + "Name[fi]": "Sää", + "Name[fr]": "Météo", + "Name[fy]": "It Waar", + "Name[ga]": "Aimsir", + "Name[gl]": "O tempo", + "Name[gu]": "હવામાન", + "Name[he]": "מזג אוויר", + "Name[hi]": "मौसम", + "Name[hne]": "मौसम", + "Name[hr]": "Vrijeme", + "Name[hsb]": "Wjedro", + "Name[hu]": "Időjárás", + "Name[ia]": "Tempore meteorologic", + "Name[id]": "Cuaca", + "Name[is]": "Veður", + "Name[it]": "Tempo", + "Name[ja]": "気象", + "Name[kk]": "Ауа райы", + "Name[km]": "អាកាសធាតុ", + "Name[kn]": "ಹವಾಮಾನ", + "Name[ko]": "날씨", + "Name[ku]": "Hewa", + "Name[lt]": "Orai", + "Name[lv]": "Laikapstākļi", + "Name[mai]": "मौसम", + "Name[mk]": "Време", + "Name[ml]": "കാലാവസ്ഥ", + "Name[mr]": "हवामान", + "Name[nb]": "Været", + "Name[nds]": "Weder", + "Name[nl]": "Het weer", + "Name[nn]": "Vêr", + "Name[or]": "ପାଣିପାଗ", + "Name[pa]": "ਮੌਸਮ", + "Name[pl]": "Pogoda", + "Name[pt]": "Meteorologia", + "Name[pt_BR]": "Meteorologia", + "Name[ro]": "Vremea", + "Name[ru]": "Погода", + "Name[si]": "කාලගුණය", + "Name[sk]": "Počasie", + "Name[sl]": "Vreme", + "Name[sr@ijekavian]": "време", + "Name[sr@ijekavianlatin]": "vreme", + "Name[sr@latin]": "vreme", + "Name[sr]": "време", + "Name[sv]": "Väder", + "Name[ta]": "வானிலை ", + "Name[te]": "వాతావరణము", + "Name[tg]": "Обу Ҳаво", + "Name[th]": "พยากรณ์อากาศ", + "Name[tr]": "Hava Durumu", + "Name[ug]": "ھاۋا رايى", + "Name[uk]": "Погода", + "Name[vi]": "Thời tiết", + "Name[wa]": "Meteyo", + "Name[x-test]": "xxWeatherxx", + "Name[zh_CN]": "天气", + "Name[zh_TW]": "天氣", + "Website": "https://www.kde.org" + } +} diff --git a/plasma/workspace/dataengines/weather/weatherengine.cpp b/plasma/workspace/dataengines/weather/weatherengine.cpp new file mode 100644 index 0000000000..f561a3c109 --- /dev/null +++ b/plasma/workspace/dataengines/weather/weatherengine.cpp @@ -0,0 +1,213 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "weatherengine.h" + +#include +#include + +#include +#include + +#include "weatherenginedebug.h" + +// Constructor +WeatherEngine::WeatherEngine(QObject *parent, const QVariantList &args) + : Plasma::DataEngine(parent, args) +{ + m_reconnectTimer.setSingleShot(true); + connect(&m_reconnectTimer, &QTimer::timeout, this, &WeatherEngine::startReconnect); + + // Globally notify all plugins to remove their sources (and unload plugin) + connect(this, &Plasma::DataEngine::sourceRemoved, this, &WeatherEngine::removeIonSource); + + connect(&m_networkConfigurationManager, &QNetworkConfigurationManager::onlineStateChanged, this, &WeatherEngine::onOnlineStateChanged); + + // Get the list of available plugins but don't load them + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &WeatherEngine::updateIonList); + + updateIonList(); +} + +// Destructor +WeatherEngine::~WeatherEngine() +{ +} + +/* FIXME: Q_PROPERTY functions to update the list of available plugins */ + +void WeatherEngine::updateIonList() +{ + removeAllData(QStringLiteral("ions")); + const auto infos = Plasma::PluginLoader::self()->listDataEngineMetaData(QStringLiteral("weatherengine")); + for (const KPluginMetaData &info : infos) { + const QString data = info.name() + QLatin1Char('|') + info.pluginId(); + setData(QStringLiteral("ions"), info.pluginId(), data); + } +} + +/** + * SLOT: Remove the datasource from the ion and unload plugin if needed + */ +void WeatherEngine::removeIonSource(const QString &source) +{ + QString ionName; + IonInterface *ion = ionForSource(source, &ionName); + if (ion) { + ion->removeSource(source); + + // track used ions + QHash::Iterator it = m_ionUsage.find(ionName); + + if (it == m_ionUsage.end()) { + qCWarning(WEATHER) << "Removing ion source without being added before:" << source; + } else { + // no longer used? + if (it.value() <= 1) { + // forget about it + m_ionUsage.erase(it); + disconnect(ion, &IonInterface::forceUpdate, this, &WeatherEngine::forceUpdate); + qCDebug(WEATHER) << "Ion no longer used as source:" << ionName; + } else { + --(it.value()); + } + } + } else { + qCWarning(WEATHER) << "Could not find ion to remove source for:" << source; + } +} + +/** + * SLOT: Push out new data to applet + */ +void WeatherEngine::dataUpdated(const QString &source, const Plasma::DataEngine::Data &data) +{ + qCDebug(WEATHER) << "dataUpdated() for:" << source; + setData(source, data); +} + +/** + * SLOT: Set up each Ion for the first time and get any data + */ +bool WeatherEngine::sourceRequestEvent(const QString &source) +{ + QString ionName; + IonInterface *ion = ionForSource(source, &ionName); + + if (!ion) { + qCWarning(WEATHER) << "Could not find ion to request source for:" << source; + return false; + } + + // track used ions + QHash::Iterator it = m_ionUsage.find(ionName); + if (it == m_ionUsage.end()) { + m_ionUsage.insert(ionName, 1); + connect(ion, &IonInterface::forceUpdate, this, &WeatherEngine::forceUpdate); + qCDebug(WEATHER) << "Ion now used as source:" << ionName; + } else { + ++(*it); + } + + // we should connect to the ion anyway, even if the network + // is down. when it comes up again, then it will be refreshed + ion->connectSource(source, this); + + qCDebug(WEATHER) << "sourceRequestEvent(): Network is: " << m_networkConfigurationManager.isOnline(); + if (!m_networkConfigurationManager.isOnline()) { + setData(source, Data()); + return true; + } + + if (!containerForSource(source)) { + // it is an async reply, we need to set up the data anyways + setData(source, Data()); + } + + return true; +} + +/** + * SLOT: update the Applet with new data from all ions loaded. + */ +bool WeatherEngine::updateSourceEvent(const QString &source) +{ + qCDebug(WEATHER) << "updateSourceEvent(): Network is: " << m_networkConfigurationManager.isOnline(); + + if (!m_networkConfigurationManager.isOnline()) { + return false; + } + + IonInterface *ion = ionForSource(source); + if (!ion) { + qCWarning(WEATHER) << "Could not find ion to update source for:" << source; + return false; + } + + return ion->updateSourceEvent(source); +} + +void WeatherEngine::onOnlineStateChanged(bool isOnline) +{ + if (isOnline) { + qCDebug(WEATHER) << "starting m_reconnectTimer"; + // allow the network to settle down and actually come up + m_reconnectTimer.start(1000); + } else { + m_reconnectTimer.stop(); + } +} + +void WeatherEngine::startReconnect() +{ + for (QHash::ConstIterator it = m_ionUsage.constBegin(); it != m_ionUsage.constEnd(); ++it) { + const QString &ionName = it.key(); + IonInterface *ion = qobject_cast(dataEngine(ionName)); + + if (ion) { + qCDebug(WEATHER) << "Resetting ion" << ion; + ion->reset(); + } else { + qCWarning(WEATHER) << "Could not find ion to reset:" << ionName; + } + } +} + +void WeatherEngine::forceUpdate(IonInterface *ion, const QString &source) +{ + Q_UNUSED(ion); + Plasma::DataContainer *container = containerForSource(source); + if (container) { + qCDebug(WEATHER) << "immediate update of" << source; + container->forceImmediateUpdate(); + } else { + qCWarning(WEATHER) << "inexplicable failure of" << source; + } +} + +IonInterface *WeatherEngine::ionForSource(const QString &source, QString *ionName) +{ + const int offset = source.indexOf(QLatin1Char('|')); + + if (offset < 1) { + return nullptr; + } + + const QString name = source.left(offset); + + IonInterface *result = qobject_cast(dataEngine(name)); + + if (result && ionName) { + *ionName = name; + } + + return result; +} + +K_PLUGIN_CLASS_WITH_JSON(WeatherEngine, "plasma-dataengine-weather.json") + +#include "weatherengine.moc" diff --git a/plasma/workspace/dataengines/weather/weatherengine.h b/plasma/workspace/dataengines/weather/weatherengine.h new file mode 100644 index 0000000000..b38a3c52bf --- /dev/null +++ b/plasma/workspace/dataengines/weather/weatherengine.h @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Shawn Starr + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "ions/ion.h" + +/** + * @author Shawn Starr + * This class is DataEngine. It handles loading, unloading, updating any data the ions wish to send. It is a gateway for datasources (ions) to + * communicate with the WeatherEngine. + * + * To search for a city: + * ion|validate|name such as noaa|validate|washington it will return a | separated list of valid places + * + * To fetch the weather: + * ion|weather|place name where the place name is a name returned by the former validate + * noaa|weather|Claxton Evans County Airport, GA + * + * Some ions may have a longer syntax, for instance wetter.com requires two extra params + * for instance: + * + * wettercom|validate|turin may return a list with items on the form + * Turin, Piemont, IT|extra|IT0PI0397;Turin + * + * Thus the query for weather will be on the form: + * + * wettercom|weather|Turin, Piemont, IT|IT0PI0397;Turin + * + * with the extra strings appended after extra + */ + +class WeatherEngine : public Plasma::DataEngine, public Plasma::DataEngineConsumer +{ + Q_OBJECT + +public: + /** Constructor + * @param parent The parent object. + * @param args Argument list, unused. + */ + WeatherEngine(QObject *parent, const QVariantList &args); + + ~WeatherEngine() override; + +protected: // Plasma::DataEngine API + /** + * We use it to communicate to the Ion plugins to set the data sources. + * @param source The datasource name. + */ + bool sourceRequestEvent(const QString &source) override; + + /** + * @param source The datasource to update. + */ + bool updateSourceEvent(const QString &source) override; + +protected Q_SLOTS: // expected DataEngine class method + /** + * Slot method with this signature expected in a DataEngine class. + * @param source The datasource to be updated. + * @param data The new data updated. + */ + void dataUpdated(const QString &source, const Plasma::DataEngine::Data &data); + +private Q_SLOTS: + void forceUpdate(IonInterface *ion, const QString &source); + + /** + * Notify WeatherEngine a datasource is being removed. + * @arg source datasource name. + */ + void removeIonSource(const QString &source); + + /** + * Whenever networking changes, take action + */ + void onOnlineStateChanged(bool isOnline); + void startReconnect(); + + /** + * updates the list of ions whenever KSycoca changes (as well as on init + */ + void updateIonList(); + +private: + /** + * Get instance of a loaded ion. + * @returns a IonInterface instance of a loaded plugin. + */ + IonInterface *ionForSource(const QString &source, QString *ionName = nullptr); + +private: + QHash m_ionUsage; + QTimer m_reconnectTimer; + QNetworkConfigurationManager m_networkConfigurationManager; +}; diff --git a/plasma/workspace/doc/CMakeLists.txt b/plasma/workspace/doc/CMakeLists.txt new file mode 100644 index 0000000000..e149fa267b --- /dev/null +++ b/plasma/workspace/doc/CMakeLists.txt @@ -0,0 +1,3 @@ +ecm_optional_add_subdirectory(klipper) +ecm_optional_add_subdirectory(kcontrol) +ecm_optional_add_subdirectory(PolicyKit-kde) diff --git a/plasma/workspace/doc/PolicyKit-kde/CMakeLists.txt b/plasma/workspace/doc/PolicyKit-kde/CMakeLists.txt new file mode 100644 index 0000000000..9943d27f56 --- /dev/null +++ b/plasma/workspace/doc/PolicyKit-kde/CMakeLists.txt @@ -0,0 +1 @@ +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR PolicyKit-kde) diff --git a/plasma/workspace/doc/PolicyKit-kde/authdialog_1.png b/plasma/workspace/doc/PolicyKit-kde/authdialog_1.png new file mode 100644 index 0000000000000000000000000000000000000000..55e4d5288b5597ab22efbba5ff4b768bff34849a GIT binary patch literal 35345 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 z zQxivy+*rsPF?=}vjU69$=ojH@DH$1+f9spypggyi9edMddd9R9I&w^|lwDN#wp(u< zJ7$axGvg0`j!6zeWobQH>-O7jduZ&~$nzh8NQg8!8pj#;&_m*=5z=%WUKA>&j9}$G zrA~)mEPK)df4<|6@#DrRTd8n4q@ih0fzT5LuXa~v38X($DJpvyj$5Nyl zLIPieE{{0eWl2?#oJ5I&NaZmnx@k*ir|_MQ7!d75zw6BsUFN&QPSQWvLBw499CLke z%$U$L{XTFdvi-FkecN|z+uompSGKlp^t^t~?7#f@%roZBo#S~u45g1QKx5n1hW1xM zee2e`3|_5i*}A5sx4LC(RR*tCtnV#f*HgZ(x2(CRbZswK)!b9k)U&dwcSU3OipHMe zhMr{!ge$pZrJx&cxc-R45Brzz9$!|vD#Mj4OO8MOSaNQ<@p`FDQ&SVec4faAt2&=6 z?V4WNIc-(v)RmoY{PhE&OT(2O9IoWrxFK|Qb#^5pq0@o|3z`}mb*@zTo(WW zBJM=wjyW2f|t)6I8fSW@KsmRFGd|X*l!%6ppx7;!eB4cR8894=eVKHeCTE^>W!vCaVCkz{Q zGhK5mo%r(0bez}eSN#6+uFg*FbPh6P$Te6LH^NNwo_rFgpuf-+7)EwW3z(~rA{2JU zfC2p{z5KFGn>OJKn<&Epk3Y_}eDaxj8+5_>Vo|u^{rBCgla=0c0@~m6`qgEtPCDUV zj{D9rC!hGeiq&P->($MLmwggPQPG@40TP{{=zT&IAtIcD*itG}Sy?e~z-d&2bncwL z3^?rv=w}WbP*qtWl_9|7LAmmt+_zNBqn=QhiJIu`?QNM+^ZIq|o7w?QM9p7%@kOQv zvM$4Q*FbO|oGM8Wlym@own9n@(%=GEgHaT=zT~&qU!RGhfa}Rs(z2_e3xd-mQ zZ^271EnK+p!VAu)Fe@15&!5LZM4$28bkl75;+` zZXYpX!uathKluKsZlvi2FHb)4gj9anvZAM-d=j>DgH0V6f(y?7-Q-D+KmYvmG{R4R z^5aDd7cx;m{xw$*u31z4`Op0`zBJ*{M=!qkA~I!gDs;(>0iE*-J3v56m*k5M@UbVJ zm@LQVBTE)9hP%lha`oW4+8T~eQ)G^R(S>B%=<<~XFH2iJH+}l77oKmZtCRMDXCV>t zfAgF3ylB@u^$U9HMT_*(_z3#qMT?4xisaPOr%mH}r9O|2AJ5H(Or=W+7=<+%-qK1- zmoAkutdCj0ZY>I}N5k%N=<=l(=W{eEGjrw)Zo}x{(o6qq*v&U(I`Py~pv5)S)pAjf zYp0)kj*h;;`M>+^;}ai)^o1@t#oFfP=boDm&N}O7WL|dZrDAC>CzduyEbWYyx*3^3OTvKcT9umQViU6DOh%yXmHd3tyS^_~TDZnq;4 z)xZ43Sp$A}>cG=}K>DkH`*~Gmx$E^X6raj#QMzQNS!ju!yOrV8R3KBC)4xx8_Ah@? zT~+B;hQj+kcL^A7(`^zQ3$hWoEeE}_F7oA79TefS}gj<-(qnf!BpD-N88h$1bKY z&~bh~!Y*tDYD2l_?z_q7o=grv=Z-2yL!2SEJN%8p?qHnDM0f2EmPEhdhU?vdIk%Z> zhYb1d@yBsnChDZXEyITC5Q%FgQs~&UN!)9}NRA9m^FqkC%d$s7H^1NPLk?SBs)N>HZ#LYqJI&{!1WGOm~VuF$2dF;Jl| zt{Xa3J5*?8T|$>IvP+lJ)KYmog@&RN#R6q0Ef&aCNT)N=GYG>Db?%6cR&&L4xt*gAzvUKm_R2}IYRahP1F?MS z4-re#4H|U8s5|duG$};sty+=jQoX*Wx^mF(&c)v^zTme@7QJ%O`RAdN;wiCsYMlLV|GIS1D;@=L`TIzhbO0U9>LkX4y@`;AUQtnwLMzRh zHH)}1vTz;$Y1`Q1(PBeg?WD&ZX{@jF@)_e3@Jln@xLqF^ z35PCUzUGU6sG>_EQK<-6$J-5O~sXE6}2PlSUrPb74C$L+UMfQHrR)st@@^DTDK zQ0OjQ(nQie?9w?__tfZmYA%iI;?$&^ef%SjkV(kg*wAoqVgQ7DT(2CQ_M!3Rdfl&h z17>FBgB(gM=jNMk+_0{HFirv1U~6=Vd^_oA%*^W`?L_kLr_&R@b~_#GrS=`vOoQcc&ZXk4oO}07|RJ*3?nkz1)0+$Rr zkCe(#C6|KS!FX)$(gHVTB9v9lGocUAND>#S@kqDw0A|>OpbMc}M zhc1EArM{%Y^+xpK;^my0h=AePH;z7~Sbkb@CDTxBvLhKFfq1PlUK_)F#Kr@fI&!NDQmds|Q+! zUGNkitgETf$qL5Qn zxn5yOL>h`AynGOzg&!pv#iFD^X3m(wxL6uQ8eA$*7|2vlr?Uh?JCW|Is;rcYn&5W2 z!;cb6s-2*}G=IKKtulURls#w8Y-?#kms!mfGsf&W0?jgh{3Fahk$>(v=g_ku%U`-= zG3yiN&Y3f7)(b};brkM~k9*7b5ELN;246PfrazRH6c4}gztKsll2!)4Py&1_uenQZ z#ETX!!UCpEn?{TYT@qR{x`e^V!Ivyi>Zar*_nB5^J9E-yQ&S@Yd_zjoA-UmHGbn9!w^nK)qr-Y(tmJsI!6Z6*RsV)|@izNek`L*`{< zqSG1;m^5)BMkaf`Ae74Bdz8d;n1rIw@Q`2s>X+m&!Os-F7%{y$dD0|3h!;fg^iU?? zxuV}+emO_LbCfQHA2l#xmp)EqD4^?9R<2la`st@p4O{|;VUp`@m4q>Mjw^Dhc z3_M}L>8HtbC)1j+Ej1wm691Ih&xsQsBa^lz787=WMm4O{M@i`VoCK<3cCY{V15SsR z$>h-r$7j3=Tj`?}Pk5|BTPXv+^wNv+RD=p#dBqhtn?8p@Q1sL?+eqr2nu$Ej_&?76 zH3y+_248uFupT8RalJ`>*p&KcJWk3V?sxXDeeaW6dAF zfaO~^q42VzB3wf(ic?IP@{C>Sti$29wzl+M?|HgxibRIqx@iaC%0Hqj1&NR3oTU{4?tU)x-q??wioI60@4+sGt^z^dwG*Jcxof8`9053&1Go~rsUQdD z1fu9rT@^I=Qh*3`3Pk50%%Ek^^*fjSbhh`xcs67ywkRUr_x})mR3bnx2=-8jb5hl{ zZlZXV*G*wV@4`t*ye2}N2Gq7yMH|0gA#vb$A06s+uG8<1F{pWvsc>yc4NOf=jnK7B zHCHuY*Q(Xj#qxb;QEUgkvuM6LU2ji!Lw#Keudl-EDew7pU!St!t87XNLYtP0RBm<|K9v5{Itd}o4hq=4LW}!_G;{Ojt~_}Y zOUn4mC&tQItD`Eo9%!s0?Ub5Q`Bub7ziUE%zy}Q(2eWBf?N_v(AIazXl5vA<)oCw#FQ6;41dKRl_ za(jJizt<}o+nKxETHo3WYB%=OZtSheOkDOms^S(x|cU}F9Y>kl!$QrG!(@0OfJ(QKv{R$IlZ)F+NzGJD?9q?NK45-|B?X%|wQDWg+ge#EI)Cn*jVVf?in znkwujvR$Ej75X|j3)+b^1Vk*!Ld2};HQ$jyH*iH9z(Pfrm8n9CxPhm6oris+hq$p! zce*L!Qh+{lmsAx^g-|TL0>t}fwB6L4^b+}aXw%kB6ur98HQJ^?{61n z^&77%y^c08Vr&WVc6kP=lGThwLiUe@_Ubch&l3TMWo*mht%%oCPY&YxvitQ)N$vZ? z+)I`muLY#h_OMVg#|i~42~p6T*;JJiNqL3zvVytG7+o?9XFUot8jQplhI3Zx#3qY} zm=JYK=3Kc+?Kv8w`>Cu*eCnymlP5p==%XwpD=sSHu{G7=dXy-E<;~#ag@v5}wLl7> zPRoxG_RR7gVsZ*6Q1v$#;*BDE`>JJ9b!=r)%cmYwARe0l?;A&|sA!JrUKzV9Wid8m zXH+GD7hfm;16OJq1em4^Hm}m6{8K117D$dj#kH{rKU(%IfFhCDGD#6x^}&!dC+u;?JbA7J%$j4+WS~T`I(l{mo>xJ$q#F2Vc^_aBV}_O{6ax zU25h&lV)gFB(6lU5L%2xorUsdoH||I5jJzOr-n#bIn>gq=$_)!D7tPc!n9~5OGyXz z6sl*fp-+neX4Iz0$&a?3r3>N|SvX5ZqDU!_uib{&Fkhln2PBhrlW#g%uXl;kIDU*S zElYyPrm_=fhk|xF6eM48pvR?Hp>zodBw1_$EAyVXb!*qMHYie?A}C#M~g9LOh=A3e$>YQSSV7;q+!X(>&<~(QmzP%$JJjf59TDw?S?d2 z63a`Z@OtiloKALGqKvbFjUwZmV@Z&D?SducbuI6`N5!bB?oCUVg(Dtj#%~2_nn{!b zNz>A0z9V!SYYt!OeG^jPpa!?009Y=w}%ba z!L6sIMSIC1a@=V+G0u(7d%XG!J3i0P&ucU!NC5oOd#k@Vwk(wrP(GYvHr z2Bk|qBI(xN9!ZHJSz;zBuj?SjB_QID@#|E zl&)M+TC!r*$`z|hK=H~I%S%=)U%8^Vqz#`%{<)F6u~#s3YMhmm)Hw|L90H`65WlAb}DS(v-jjv^ zghh{+bzgSaf~il$zj(Fnh+8i|{kXgEp||=w5-|xC*OnAoAge(NL_$`LmG-C(^al+ zXF2i77?WB)jYGr`t9^E4bChckl(=}Gg|;FvCkl$KL(XdJ)mh2aTq?Oak-~OH(sr)E zasq7=c1!gS<*9XDMMmeG9FY$mYT=CHRR?83Q@-C^1vreP2()7 ziVZgNwFAqW79w`VyT1-8o2^Hv)AA;8mm+)YX&q8hPu@t0sO1!dtx`B|gO_!Y)kGLq>%(z0p#nN6FY zo;US_I#q=3-P=RdncW{`uG4rCu*L;1u44g>yMK7)b)Kw^h9|1Du zMKUOoisFpi`+=)U%HB7e6o_u$T);mCJkhTt;LSIr2I*tfPZ;TIqLnw7=3cl$8%I;G z88y`gClzR?xIN|epcJEXl@NCXj6qF>|Jlt61tfe%0*qNI#D-lq3|hW*5P8U*Bex@PbV=?31#J=2jv}CGGDp2b5 z6!!&m%BQ5_jp5Y64xd|r%K|_*RSAvy1rgfyGBrPr+@FooT)ehiPV9P3<0uI#B27)I zoub|3b)Tm=oeug0)=n%fT28dgsOB2C?Jzl}btr*Eg%MZH3PXqo5Ll#opwMN!nR2jX z;Zdi41dy4_hCG<*&fL_gg|T~O&oF`ZM;4^k*-X0KC(vxp>}`63M5PI+SCp?0_~JguW7fE7;ff|3Wxn?yEBj%-wUShpgusO zF)pw%sS>M=OjUO`g<&m+0`&CA{c&>4*{!Zr=pr&+)-*tinhIjw;dGflrnWuqhL9Q+ zT2RCC=BKW#)a?RT!?mk?6C zr*r$<$X-|44pl{En=y>g;z!TSj6tR`U6Hg-p%B?Oe+)SI0flUIN$!D+NK@w3H+xcY zQkaF!MARG=0sFIMRC29qvK%U6{7sPZQNE_utFvz^nTlfg#p=P$*vp*KBYu$FbqMKt zjjF_&CEk)G(N6K;b*?gF98y-r7+zZABrhvP=5`Gd<5npVaqt5ifB#VF^0R^nQEF=U z=HyVD8KTjg?>P?Hv9?Fbt+8mpo9yt#c$`L)ki_)l25e*ML zxRP%_T4M;CO>V1GT8tB7PG$_bkC)m4DRGc4|1XE_rVC>1HVLA{O?3A=rIMOoD&R!chjpDuIPL~C8Z<-40Adm1@_c;8ux(qe9lA_)zyQ&MEs z_%WqVdCqu{E)R6BOdoQl8n)+2nsN5DN?Mqp!*Xa0_UmY*Dq)UDNgOT_*c=O2wx^I# z?417Ws;9k7JxE|mQPT)4Gulo}Zv%WLJwHZ?GB-u$>o^gYJV=)ZI(72C&(7pkpol1d zw}G-NZU=c$XG#t=?3rh>mMAmk?(29l%a19UF+D}53@76_I2b?BSrSg%Xo!TXruN-hnq ztgfzxE7{~_(f>%KzOD{FrM%nIij*%0i4m(urUfW|Od1F};<|Ok9Gd|aNcm)6*)5KO z=+Yv69RgQmGvf^vZ{fJE3je)0@nf(V5+r}SwaEGK`e1bVY2`|Ok)s(|+sr%h`B8`u z6RE1IVs*&7$*<7K??CY5LnS38%}q_jm<6p>N^gbAp82q(QPOg#>OrSXPXxI$W1{ih zevtSX3lI8qGkx=>XU2GshZH-YPS1}y2iFIQ^0P^o5GA8;(9+UUQ&Yo_fAHfY@(Uvs z6&1pj5U50&-QC^o?d=^M9sF{7TWf2+mXZFRY?V9-G`B~E^bnJRxbw$z?5E;|Z5b_= zFA6_wq>RarwMLvDEhpyeFPph<PgAl9T^21}!q3j7JPYT^x*7U@Yj9UwA>}(^&UeUA zRJWONT{pe=<8gceP7U1e0s|DA6Ut1#D^y5fU%Bu?oejxn9s9j6G2 zh{kACZ~#<96af_+0F6;Z92f+dL}XA#nP-VvFL|!_zVzz1`mKD(>TmCtyXBOv@-TMstpdrG_rluzJO0-C@$ew1(>Z{eT5_)oonu#S2BGD^3 z1up3-FDS@oF;5R}s(-QcegT{V)Xq@**QE>F(rXT_JXJ__r>ce^zJi)wya%zq{2W?1 zC}Q`w=%hXgID9}Va7Qu@36A;~Oo`#64{$jW{|#W-f1{7~M|!;*8XCYNo#3*3ZdFwk z!}PGq38Q9$OO`)pjdQiYC%DYZ&24VFZp+pydJ?eHbEw>O#L&SpY7c7;-4bhK5S=?` zDU6M?HX+`(GRTeW<VlQhd?}k) z;snGPXW&s_&=^e z#wDdCyxxCzEuF;%G&?#wV5Kx)vMKG70H6eynBn{P?>~0*XvLK)w0__X$3`u21u;p384fGmaH;f4SecZRl$el^<7&9X zsJ|8PZ-5aaMn*g?VI?zm=-bA5#^uYGuU@_S&;R=;wl?QbCkl%SVJMUoC))%VfZ5gE z1xyt#6)UCT5>_TAChpm@Co?m%x~dA*5*FVI_&30a5hEiUmujd`Lqla%B?9&6$@%X4 z@7M}17QjkCDXzK*w7N`n)ibWNOEq2{R_@)q7gnCi$iNKm?C5~S#)vaw#E6%;{N5h~ zKSM3eO(iA8wRN?yl5QN-OqP!*D7ei^nlG`+!DUNJi?EW-%1Q##a7o`ze0+S|uGmu< zC$E%W?r3i#bJ|3?ixDG6jOfJwaS6+0j^nv=XQ3XglixzPZ*S%o{k zQ*#rnq?5;rOWA;|uyXhA-8*Aq3@eQoF>>$VlAayBSF%C0zTTc|SF6h|T|$w(dGluf zH~o1R@;HS$Nk0#zWZ!tbv7@7do^fz#9iy%$_TbnpE-o%+#}30vBSwt)1egCBX3fzm zDiTok_H@(8*xT2OO4#1s)^z(BzO#AX+dfdqZP`u+j~e1f-uwSh;=s zwqJ#nPe1kKfBgHu9X@n$7;X_D8TwVJI@{aG1S!0DaX1U_eQ5(8f1b-Q;Sxx}Lx>0y z>0|lcbx5}jB9VZsv$G>7FFQRwEiFB*qOt<;VRVZ>A*SE}O{O_A0g_y}jKS&*8^TBz zTq;YPy_n$uH#RmlIy%Y&RxVkxSl+FV=FN5Z56+%Blk}l0miw3#+FQ(;Ipf`TCY{YX z?I#&3xl0-PRjKpd6-N4rM6fA=~8C8I|_p>P=; z9m$vT!2P3Mc>cMsjvZC+Zg9}jXPC>i-8g-w#N^=V# za#v>uALp1Co+r=8AA5B1!UdAyvY;R$$%^=wUwUcFrcFE-CmFW4w=P|}j zdv--gNKbc{zL0veH!5=Li(|)<+k+3>|Jt}$Vt2-9E{TZPIBn`wGCy?apr*R2Dl4W; zevf3Yk9##O^{^s>=3!Y&<-^-q#a_3X`t-O4^v~B`T7%2Gf=fW>rC?&yVbg!(#@}5y z19LqCTd!;inig*MyEX#!hVP1t{p$F!^z?LM&^@6f3?&~{c4P@N^i#p5vczRCdgacY zJ0rrwGfrZImoqDz36zw_dEqBboWNVPeA%*fYu6GO_v$O^)a@_u)G5MD3L=a!;(c3m z6d)k{=J+?l!d7nBu$~0+(sRiNfBw_!uZ<&&(u(Rz&Ajo(?>U?};q6Tk5#&#J)v7Qq zQCIPbuU@_CgAe|!uF~9s&xt(8kAH*k6OTXk*^S9vXSXaULVdGzx3z3T#FKQl^wNxJ7Zp;hZ?x`n7 zTW@C-d);d4)8iV@KVN-mjX0LOj7#1CUJfuv8zPv);YIhrMkGoikunbluF4XiD36%i zoMn%uNnrnheRR=q32Cs$i2;|_YOcXbt_7D?)lyi=?a(WC?AWnk{d$jCS$_Glyj8h5 z*=1#yB%GIOn8yg$)3obObG_$fJQ+Hsc8!i+0&ZZ{NOl?OKmniL4A=!7KILv(L!S{%hl2 z<#6GGPwlv@ud8!{OXxrtI;hWsnMDNb!dxSDRq}xYg!?R4>FFkUKKAG%L|TQPrq5Te z#)z}t2z`W8PBMJt;fIJ6RYk5na^$eSkXCWif2l@3a@8ojX7y?fF83uR$)TpY686Ru zprJ!jGLAwKLI1E+sXVwHEeYy&&T*-!Pj6~K^L*{4HMsOIT!I*EX#|ZAL^8^U0ptV# zB2ANGQ-d}M14lY@

{>pkS03a4Ez*eE3jWdg__8XJ94i1((7~aX}=k#7IZ4T=V(o z%yIOXmAu5GpLpEZMba#`00EcO*M+l!h@wN2}T#4S4gAxpQWtq7W0mXEzC^OqooWhRF5nzeEGI zeu1=g5~d-aCZxoKy&9%aw%@(}8sT>)P2941Gi`bEKAcMf*n&%4h7?lTTVHzd#h|61(KR6j=6B39T++ipqc9pJ?RwaZ zQYz^Ai?GmC7e8-fRNLj^Mb@@U{likX@a%R}U|ei0ZIK!Pxz(+vK0U4h{qwb#*5J}N zxWuEnmpO_{N;m};Q736(r1P4VM;Ir#1b<)-ViFLmoITgUO46_?VvddW)U57Vb9TSf`5CXsKw`KGjNzBPV4h9pM%?zlMX7f8F( zgty-kW0T$wJyZC>4-N|Ao55gw^x=nS!UrOHT>j!M(A1~|I;qK%o^iSeM8`zI;TC*R zpnu_~f3xCJpJ8iD3w;R0kRj&YvfyBT3yJp(mneW#EL}aA$Y?`+`6z>uMh^>pb@9X} zzx$3n5eyMU1pUKOxA6FO^yI|G#ApCyt6NQddRPPc=W8#m!KK%7XIy?aRAInScUNb3 zR~KAsb8V=W(rSr4PMF3!G);6vrDMWG!qRXlRyq16nkfNjNJ_$_B$C9UsB%iFmcmNB zx^3CAB{(=p#-j69eS7m8ibIi%1e$cTwGI%W;J*UCHjMa6e2h!*)PJL|p}r0Uu+_2w zN)$uvXjqA+iM|Ovfg>F_LQI&5;z1kML#Ng7%$iO9oz!?gTF$f%6TpO$;oR^oEkdQ#D;-N!_%FD~)rj?k|MJ{u7#L52sa&4%ADJdx-At9lmp{v5eHf-3y^ceQZ$;p_{GP+1? zaQ3S05=zRbOd_REoG|giP)UH^N@A82hm{|FG|#Zoh!GX|unX2waQR~j*5WCY@}v!lJZs1W6`!LYnh!efdoR>p)MMi)UtR7)BzA)t5*7FH_NQdr4ZC@HMuP&pr> z9TUI5b+xt3H6~l`?k6JdXI6wPS24H0;e#n&<~(!8jCUtZL|gZ=V44hvU+{8Isqnyv z>*b?qX+ErKBSw5JzQ$!vcJ{Gj$La<)a7lj-BgyH}2A41qT;kFa-ig;sad`tU zSBldOHE9P#%6bx9C9IU$J`8%Vxppl;aLJE}4-4i$vSjfhO7+PnACKs&_V#uGkD)-s zWlM8&UT$tmaxzVpbmM?aSP3!dB8Px5j#b8h9bp-92_&p)SJ`EQPh0PS*0{3p^WPq?w6fiO~4S!rKsKz+lE z1ei6j@&vBoB_EnEKKsnF2*L8@%&WJ4`QhOkr%jnc84jkTC>G>opSRvcyj$watSJ~7 zIw{E)Yt~G6t$!fpSGu8ELjupbi72mpf zGcVj=Rjux>f83fH7}D+TkV(LU>E6dBepYC^q)8D<*19%YCuQ~yBQY_+*^7fB@suvA zC9G5ixHMU+u5k29weSvmt5Zt`5-~fdwx;IpX_wQdO-1*?#Zjno)wL_|EIuL(evmrd3+Yw$Bwo>k5uWD5Q{o@|228MLInC;u|kV(LUX@uhPKb;Fb z<0ZvK$;k)MF7c%XE(t(GjBemaK$bIuiQ>9V7QzLc!b;_oNcHE)1}oXDYlx$z9yxL( zF(H9I_iy@daO=AvH^1p;PbU?9Q>+f=a^*iuPRaLkI`b4^%lA{kp|TKFKJt0s*pYBu z%{56}R9L9|)YjM4a*C>x!wb2&>ZM{JdN`ZAf4jedZKW{!I_Abzap2i!}U()ff2bmTb}HAgR5ot)hS2 zgVn&0ZgR~1i_lO!1j}lU>_MlnQU(t}Of}O{ zv`f_?jwNng6qf|7S$6L1nU>}z8AarCVZhVpt5&(W@<(^Ge?N@9cY;ek2Eu#{>Vx02 zJI=KT!Gn?v?#rnohYzZ&_9Z3`hRbzpzYwRhe3K5!tiz>~k{md&pD-W1DnB~Z!Ucb` zt|7tV2lkrOWUj}h{#p;J+Ah4V_Numu{&5dh14Fvqoihn|FpUgc@@G{(hm;qCD{E`6 z;hHx-J{}Ira3UHmVWlkV2oQysgxy#PKt-=)1Bll|M~Od~YAlnav?>ZE}`)4pUPP6F%|vaVsv-<_Kds+NtD*8>Kqo?~YTY zB}HoWG&>yKVzAnz!4Ai_PRgt=n5HC}^2ZMQ_ZX(0n3#Y84kZPbGSd+( zvO!E(Nt>oLUrG-Td+}(=UUi7G7e0z%u9glb0n35L?ATFVRf)+dZ=28M$dNh$eJ2qsQIOVs1? z_*cgyllP}j3kwTHzFfOjUuJ#5G$qlLAKz$`Cccd-L^C4PlUwuIyVay7b3HEg*ABI+ zY2KA^ZY@YNmUtr>m>0tE+x9bk#f~deUucBG*L1ob&~Gx;<9$ zJbzqh@T#86>f5v!Di`D?Hwjrp0Tg=&ADS&8|AS$5CfU>NMgjCjfl+&^_i`&vlmkOV zKp=h9baC=$$%!KhqO^neb|cfP9=gbL8G&#!uD9!x*Sn$s{ZW%S026>cM$hJpM&EDt zy!%l8?YVE+ZyQix4ST)NX^r9(edLaWw;nhS(F++O3ZJ;J%pjut52$*Qa9kEy7_h=y|fPp8^8y?e5kZ8tCiW>e--j z3Ps14k^n;PXa{Na`!l)i28jE(B8Pz9)6b$7=euJWP*bqSRf_cdLqts!09QjZ(RA4N z_9V9gn*D%?i0FMw4DZPj^rE-8KIpuh zmD%<4TFKxiznc88wbJT|EBkUrdp}f;)$T5&&_SrDt4kaqC6W8l%Z`u)o0BF|+jdYL zWfFD=Z`RjDH*~0e!m@*Z{DIGd5iP9OZm!(4vaq%`ap$m2&B$OR;T_j^Q7UhpqU{%B zXZE5~7Dh%hSw$fY9*gqQatqJqlwB+Ey)L0dxa@fEcSSZ|;HF|NJ8WVY+y$)MZeR!6 zxA|t2y&7;jZI>2wkFGgM25|p`W$1KMJ33$e)zM4Oh9N^gd+V1gkGHvq@P<<=PG0Zu z&hh>u)m?cEu#|`)ek=ehiB0#PF|;7+RnT@ z37<8;{Jh)^E??T}Sph3H+7O)MU-|KhVn;a2t09)Sf*MQAoMwc@ZRKvp=c~f4o|l9; zueEn=^%j9U-NDRd+p`q?!ng|G30s~$d)DYFrQik&sM9}CaPqYEgi~w^3haN}s;1xFr^1j_&C z63S2kj)WYvx_?jjpT~GLyWE@i=tSa3+_^r||3B_I27u-&p$~xhORfI~H0t!wLZR$) zAF^bAxH&--PE5)UIFQHB=Cmd}ho=9P$>yYuX9+ZL@p{8MU)TI?K z&_&Q3yDMaP!@4=wjVZC;W_(;)Lwr?0&mL<0#o{ZbkvI~o`0MLMws7L{ga>FO@%gdR zupRI98#IS>CV2#e!Z#;^QfED}*lK+X50oQoBV4w1b0rpne<8StuR(2iHax4UUr)0) zSgu9pqHXaT-FDd-W;OP<>2rF3`V3nZqzDNJVtw4Vt4$?kG03xXh@6kxggs9Nb&SY_ zKCRC%Bu!+#bf@dzod-3o`Phx_>#*3#H3gr*Kp%{5hVNlB6rtbO4V`)~(lMstGchArjr zR*3RUXLJUS<4*}A5@Fk47dyql8D{0OCBEr_=x&^EtD(8?c8MtJ9#M9TrI?BU!AfAI zsw(|QBcng9I;wD(MXH$Z_||8J@IyL91IkrTv+Gwr2wG2YrFKQKcKi^uQMR;tnI=7+ zts8)9mI@iLi@d)4O6z;Rk1`k+&M(V)yr{Qd?JFx@U+&NibE+!tV~Noud>hghQj#Qp zR2KGsXNnxWaX9UG#QLhe!yzar`M>tGRo__g@A|yqs#F zR+}svzA*>zyw}$=V^QOq$*@3Y)l2R02}5c~H0B|y{Yvdli*@VKvYHs*Ra0{D7rW^J z6^wzgs%W6fuk7L~`pk!G#!45BXj;%q(LHb6IyZiqFfv1HoF}BwbjWfbSyAw1s=&2K zk!4ITEp13%Ug!!#$SICg2ic>22LrZ-+_Bb_(LTr!#?SPSV>ufsRpnPsd~je;Od>^r66q ze+7tX!fm1+<$m-tv6}n zG;*DRL_ZE&I-eVb_A8}wB_)D00`OmHp=uAGg_AnktA#G;i>IjdI}ekyeYo2w08Qr!)ZMQ6L!(B~1aC*7D!ze z(w|3ax*DQQrtbaZk}Wi1OB`GK7!RY_+?>OMJBlnykPdd6&E*!?)ePPVCJ=^~E9D7H zoLi2ZXK7dhwU3r061GKs#*8^_jn_FnpJgWG{igxMEr}l_?!{mMvKC0M7rDBWtQmeJ(%7lf3jfG zQaebR)K4D>q*?o1N0iz6j@t!c?`vdaf2OD>kLSMnoUq;gMJK=L69!UncboyPn|Ixa z2^%p+*ZvgrFB*UeCI36+hyw5@**9y{+b?ADTHxf|^ND2}rtntkcDHyY@Y4>!HnrwO zIu!~JCe!VgUMo011ZJHXY`Rz@H<>|m-ZOl{3fIP-yr;4AM_SRx;V)U~^_#!pWsA6W zIiOijotqD^vAJtGx_#SI@Qi|#=JNQz{D_+Fzalj3IDa@K(4ccA3a;B^aQo!5GMY2D z&q4h~wXHt&i{^Cy8$$cQCS@`*5x44U6HQP1GSkPKj#KqNtcDx~ZGO*SULxkyfh>f= zl!>wQp}=GNSMI4dbJ%V^_xCQY)8Qw-b;-$uYrQ5}b8A#`9@GuEpFwiKV0vZ7*5&hd z4e0cJQ8KG#(Kpite7rp-f#h|)7ie~qfA7;zNEFA!urXR4ZDjZ@9xq9gvP$oHST4ec zJ|bCO07Ns*LO6%+P+<@ObEDb4uf?RpQS8-|#qX#qu$w0nQJ09*0NtOhC@m*W&j~xH zL3hTh(2lgTwFfqT?#P%nB_6&$;;8f>T`lx5&t|lD)TeDg=bDeh?G1Cy&kSb~Y;;PZ z2Tw`}Xp-RX(I+?-wzE%3synOYkJL}og6?s{vHOLA;}V`8egljdBh^CNDTk-zkT_3; z;l7tIOfA1-#B@tfMk@B=*SsHDz?kAidwVI$891L8kWX4~yI!6L?dB@2aBR!iWI}d# z3yFwWT;8EnefhkRQ56A8o*1Ps^z^ZlRYdeFGtoqn(aL5N7>$zlU?@=d`IvyMKr-#$a^>rF&P7Dx= zD)d!M8oF2e;Zod|$gvvvO2o+$p`3*&V?S3hFsvVn&_%yER?jQrBgY7^bxZRA(+o!q z5+sd4;Zo?|;M7!j)uDcFsIwlIQ;^WH0sA#oG}v#3AWXD|fqtq?sJ>+j>^zIP)a(i6 zO#ZFijVQ#*0N8vV>r&LsQWzdiTPsQwoXP)95oa$dn{gknq4XyjT;8g@nL}a@z%WYs z25$B%-SQXgBE?R9pAgiUwncs%v?KgY8gQVY^YU>L>?qg4{$y{0+glr?_K$k((o>SO zGk|3i?A?h9%ke0&mTCe}H#|h$v)KC6C(%i0f2sM(9sF&E5!mgNHw8W#K_GcvJApwO zrD$aEXk}8D95-!6^-nS&X6qxB^HA$;@a-+zQ$6gc>aXPI7Gf36(gC*;#jE0}2x{ev z>4GF`U&=%PDk2j4pyZw6bb)b2Mt*yf@0<`^J|^jx#3g|UjWUSK;UAV+ErKGIfbL54 zvA6P|1glTrqnd84*AD0R`h9CiSYD*n!87%xlhdDGux9(cgfv>yZe)%rOInL3Eqx$~ zKT0{$=qU&enI)7sk40VX;%WHKAt1&VYU z*-VT`aSE9fjDWp=QX-(`9MK@psB=^tb%c2IQ$Rk-*;b~UU$0lfBH^!$7~Kqp0Cg4c zyB`}>Hggr{k$sD+Mj=!XjwuYn3m64BD1lkR1vuBL86<7wMt>e@_kE&pDB8nQqQAuEpk-+I{`(4jM-arzYr)OuA z@f8MC%P8^Y?6D0)x6P8iTVgfV$O2+J%8kq{FkK(_{|Yx<$rO}r0UZl`Z=bKK!hytm z@q6*OA)6iXYylt2a&tuz0nfJgirjd_G4u#H(9w)-2qyw_(IlPQ$p?-ud*8bBM%<5c z)P&AU7U}Y>#cgGld`g{Oi|Mlf07nh{#_^+aYIv45y*U}Y2b>Vl{8R8y6(1^qN}r_LfN>n_)K5Nf&#>3ZM-8bx zlnw4SVS~w6{QC#yBE|@Y~0c&KLE&jkD~@n`)gK>iWKb!xxhjmNBBnyHE1P7jJCbdk>CjSRc_Tj zP~N^(T`|*>0_ns6lGyz9$M)5zFT!v{ZO!&>*Um$96ts5UM8tBpv;nPcoYpqrqALXI>&W8L{`zGO z-E|y|Zu7Yw!2>c?^EoCvBaUGrc`4}p$q&N=0V=4+!0t}_z;pnJmzjx~cr014(>oRb zHCpDYD?P?Z-&%e6-WE>6qUB8jfT+7QORh3Un`pfTOmW*5XQ(aIHIuCVqY1i11B zOvMT?x+=1n&Sqr(;SKN0GSuM400unSI~vidA)^x2ewu_^&KP`G)FH$Gfq$M}Q%*xP zi1roxD1n=vksdA{A*L(5$u}rTg-cVg*W+@#GFvxz+SY)3>Kq_52p@uOoD*38dWs{^ z@tX2&q6q|`awR{RlzrIxKw0|v)5+}W&9O3J$b%>)nJnRP|I*9IAf7exv@&JMd&@aPEV6oID%956U}+H52JjC9^2TUl`aGT zKPz%&BHDv;9*>gvvQXd{IT(rKBGCpC+~RVQtKy%FWRix?9=pn?6i4$nCs2Hm*1%Vh z?FKvT-WkvBoNUM1UtT<4)w(}2Gcayu2{~^fyxnNS+xs)#@8ss$*x*2`*R$RtyZaOG zSRorvaG(%h}ya*vn7yhxsQ*LR{WWoN`_}C|dITZ5u!o%?`8n4lZbOzDZnEPH zCkwsRZmhz21fv-`f}hu6+UVl}hVNE;n-PPzA?N1qxh2z<6Oq(Pgc|e-C2u-N^zSM?ch-W4~3OQ#x^409Em z7|rnId8;a2_zVw@I7ny#(St_;wPl}+UZ8;(P@ZpvRdqPWsZ_{KUXuw)J}$}}h*KA}NXglCwlX+u zD-4#(I&6$`uHZ><+e|mJ8?h=(s>=x^yZ^SABT1KTL24MPyR$9lOOx%rX$EY{$Qz+( zD38rG++djT5#X(5aFY%CktZ`y73YxI#B) z%19aB9i`5+ZG}t9la|H$A|p@U4{jn6KmM$>lrorQfELnx*O#?1! zR7G|-b#=y@BD#Mgo4P@|uhP>esb#luyDIF>20Yh>B~K#p3?s{i*Vn(oD;^lRbeC-Z zVY?-i>u(P~!O^`p?|e<^7qGlBKvke_KkPHaW*cPhK5_M zO*a}H8!rj%>$)cM(zV&svR(tRjvGTpF460KTxF)GR50ZuB=NM3KZ0bgN=r-YJ6@NA z?R6%R(HFqZ{R{R@Ef0O;Y<2Ga6Y87n8P*DFJ_772!+Rn(@8<&ovPP$+n^zoN#G+0i z2Jc+OB69He@2Xg1OA8(F&|b9QgRXBvUu!&f#nph`q-&xp$~+4zM@a0rRG?yK>PV{1 zZ*x>+KhCAPn@QX~slkVgjC73eCi|%Ils{TD!A870tGD#MmV1Wf7h(h#D2h+!a?Zna^HG>yp;nX zh~xZJ45C1pq7}zMfFx3&1i8uMAc%u$P=Y87@esu00jJMsr$#P;5aiAyS?HB7MUFK8 zxquo(@vr=|1SpvRS`Wg16@b!}%0@~R2#0=%ibLrbK;G3(+5gno!Iiw}a=ucVxJea6 zfy5Y!)MPnM$bt>Fv6rU0Hep9i;ZOJMc5zD3OrQ)&53r*m3=Di4hp% z_`$5KEQzl$CCcC<^N=8+0*Ujp4ftTAImC9Ad@bIRL&Qb!(&8eQgGR1+nH0Y`&huMh z4Z0-C26>RbkY$?37826g@v%(1(Lmm0edI68dmA;Em)P-?^?`naILwkP%z;_&08d1D zs85>sEdL#}JkC!}q*OTv8a_xZt2K*8V$x@;7>7HRDGn}fo@`8h5+V{jBG0332GQL0 z#~cAfRe^i0oelX2wS@(SAstCcuUu%ff3XsdRABG?pHNsjbrMt+@R6@!W5ZiF0TxNQ zeg1$OOmA1|imO;LuZ@waCT?z;VuF%^PSoPWHw|1M`C)){!M7X%{ODn*o3VT#fUn;f ziqD3HW!I){ZM7=CfzCeaSFB#*m0Pjz_E2x5UlFR;KyG88tJ|f0QdWsghSC+DK_VK+ zsv0LfYWkVo_g1x#kCGTN;gUs}teqc1A(jDg)6>#p|HNSJZo{@;|L%w8;Irq0HX3WA zjbgMd9`OMmeV8xWzwR?9+a z9yx{3o8PbeCObOX?dZ9@q8c6xTO6b!`fxd)y8ppyc6Ge5T5MiYquntRu~4Jz`|=$> za_0B%;Z9MGB;BeSnelbjLC=j&vSK(e`Q$PnxicbHI7lt;UvrW8$JdnIenSIoxr<=8 z(hQlM`*ms~*hg)$S6e*{r`}v@66y)Wdb#}Qi*>s0!r)hD(2#yLI{Iib2sBdpVpn$B zACp6+ySKF^;QH`0A{L4bm+(iE)8O@PoE+}v)VQvHEIz3wBdxaV;sW6tl$K=5s<+ccc?@5y)bNOi_JkIO7Q8Q@ z?xT+_1C_UTk6WMiT3cEsayspb$Vo7qcL*W0+x^TDv_~CzW~(C zBJrnwe{{08%hxW$!LQ;Kr&onrUaO`JS0xoCd-J9*<*EkI5?EbaDk1cVft5*FUg1d( z)V6czPq(*r7LTI+WIi3OA*&;g##8)DMB0W4-rta` zl<=s>4Bl{ex4O*KRA?@Z6K7^tAuX}1OEN&Pk(2%CGh|8{p&)Nl?S?w7$8^^Ut);wB zpJUP)PUPM9^-D70;f{iqh?U46ibVyqR9<_Jrz++IU}l`%y5!05mdAQb@$Nd_i zf^rnMfQ`^+hXF7F(!+r8wHM058SHKCO%%>(;J{M0)ZNB2sH!V+V82+v`Fq57Wl_@f zN7#3;*Q?Se4lPM0YL^j+kaXeg24h?iDo8yY9bped_kSV+zd^fP7*F4gQBxOEXYbb`9G$wp3SPWa?wbY*){^l=ql<+y?Xj0FIW0URhGC10e z4b#yBXimkvEz(qhjeE(QX+JKP{D{7ndBfZ>EibdmvA03e?W62Y=d--fc7)fU`ME)8 zkY~{Ci%Po4RF@|kFzu%rmnjh;$v#KeM5W{P^-8@>@7Uqo`B_m%B_ZuOh?P~p83wis zdGrtI%Zsmz9(s_eW%Gz=9irQ*PXJGMFH?kLR>fmz+;c4FX(m z{~1`RDcUom>!+9`1%H?GIeJ&v%S_1|@P{&YHrMKuhi8Ub#?Hv7EY;<^i7W!00Hx#Q zE^XfcHHDE=w9M8!R#qTOY9v1~j)}+&MorwUew$F=ANGBDP?Z!&LC@TgL z2lqV_vwy6aGl;BJrmkV?o#Rucq(p{Z+FO)V66<9#_H-*Zn>_rW7Dd18baAJo?k`4F zx92zgPZ+%qs(&^X;D?n96iWVM^rGZ%6eGP60OF&dU0fdn4i#SGb;=Q2_hoXybe>wKJggG z4^(8P#(zOoD3ZsAPLscx70k@dtI?piVBci#KR{{@CvNWqqe;R%*lY%4T7`f5`*!k? zdWA<&_jtQnwFcBWX1$&m{w{zHL{TPu>>wPRU|1Af&hre>$2}-mM$?^R{|c`{Gr?Km zgW_T0q0E#eHW*=yyWrXsUoeq{427BO`vSLNj_$B40Mpf`nFsP>_=t?C{eI4VMpQ|s zNzMTI`P&kUSB`sW6nf}Lb7r%=yt9YBGKeB2 zh+KrUnCxoLulg%*xWM94?y#}r)S(=RZmu9vQPM{Lx!@bvqUa_=<$V*Wprc^pTDrZQ zdkTF3Gw4k1D@FJVsw1I1h;F7pyG)BASyk zI)sJXf4+}cjup$`NQp`n(Hf@9fJzvKl9M;F@^P_a`MW@4wS|j?i;>{cy!6%egNjPcks419 zzvchiz=B>?26IKQLxZ{b7xbDOS1C};#Db+{1UM^c4AUbOocUGQp;f)3;YgbNYYT>x z{WIjAMBVPMs*sZaPlEI2%bbJ3fVr=lUjFI#u7)skoPLc1&`x$4+LV)QB! zC}snLh3l}c{mlJi5z!6+*ALaXK*T<8d)Ex)bxj_rVYLM9NDVCOO>Be4KJpaWN9Xf_ z<0Rn){3#wo^L?}P=P{!4~o5%&amz*R@MLIX0l?~jI{?~cX9L_|cz2&D~ zsZ58Sm)8)*xwmF)=hv>)bG)s~5Cv%aB3mhkr{VsIU)iO$4n)sVN7q+W1YJxzR$#2O zv^z^$<#AKmKoUozqrWg&7K9m3Oz;k0j83ciFHPlni@)4l)b^&6+7&mk)Y8!b6jY4d zk8LxwrO0poWUoA2L4Cy!=F08`v;dFU$Q|i#k55sJu6wb2g-U-4c2e4wad^29mzQ7V z#+1?47VMg=S&Zz<$|}xLH+hY-^jfHftQ3K@eK)^KG(-Z+igr%Ua9S+lN6s@9{r6;L zWv_i7xH@-^=knil8eHvG8m4oR+0DnNnZD$$d|}EOw*zbNh%i~s4&O#bam6(;AGvs3 zfq~P7B2^LEj3Nv=p`&bR!czmqbCubSW7vLY9f1Jp3j1~!`J|6LTnH=6w@|KBNS$VW z7ESY8-;_^^&s^r4UtO*I9(LE6Smv*r9hhtxm4kwp+)@1FDoxRU;&4aNkcoR{?rjvg zzn56MK~iGgulo9HeP?%1h`Gm`Z6=vW^DcsjG*MMk9|rg1g{p~iV^HajKIYV_>&0}& zWAjzIvJvoi&K@o%H4-N;7>O(db@Kz+rWd#H*S|*vIliT--nWB#5uzk ztql*N+C2UvG?^ma5_8J^LY~uhGExsdRoJiiQqrb3M3iHVBwihqe-M-G)&yZkbl)PegUNIg=2hi)4q zYl{?u-Rb*!f?KDO)}`O)Fn<++z*we#@HSq;7=&zfw>h7LmO|;`(9zQ;^xozGrt9V^~A++ zXS_(T0R(WV#cH_cNZ7;((zHg36u9+$N#QzO%0L1jshZ5kAT)`PaAb-PKf1g)*x=9@ zF^WjRT9iZD$D&GFLwU; z|9?NI<9|bpLn9Murk@}p`204ONjgV3NKB|@%z{n}5O#M400o9TR<>1E-gHtumQ+-< z(1UvY3--CCVZs@j(nfy{@8FP=?WmvcZ)7d;cfP*(Dk_fHB|LZStOYz`o=~GGOfp4P zn@$`X2jFU3ZHkWmt9E!t5pcR~p`)eMQ2#VFKTjdC?qw;B81&;!dov2r&2s1!iU4Pc z3x8i2QA^+T>nsz4?s7-Kb!knmcu1GCk_HN#gBcnO?hcQymCJ2q;ZK_0+3Un_zY5gy z6X})i7W(NVsl@Zowt72(o13DLBB?ezS62jZts!l4D`d|i=yNTlP+W{M=(^h}pX@>?9@}AS;_lsXd?4hCic*;93?k)DVwqEzO zlA+in0**(is9P*7EX>O^m2Y_+t1(vJ6YCo53~>DDOi**z(lXe3#TqL3y`H_a-11ue zxlgmo{n3os%1b!VDAz{UJ=wOdv-4XEf2nz5*2)5>^&;W;aV9Bo>G1Hk%e@MNR-}S% z2F?~=WO1SVxyQ!1+jf==R(%86jMP3%avz5j|H>-We#VJuVP6xN5eO8t92#c$aqh)S z5nZ%1=F>(ljbI7VsS)5%@aMJ0M5vmM0lqim=9W0TYptWryXCpxKN<_ucV?%ir_U7F z^x=O7b&n@a;oShROkC(%-h2iAaV`8&{pl#WVW~H1e8s!g#j`%R&v;r6`_^(~nTCKh zySKjy$9y0v$zQ=?WNOv+WWVc^ZLY$WD(u zJYaPfWhZ5gkv;_MJQvK_5<^pLjFqy??CgT&xhX}+SPbCdt{(TtTt0u!>M#T)-nA$kkJh=50C={4QM-s8p02D`TXZ$x-9YWjI;cIC{l&QN31z}p$T*-W63m`9Ri zVgKQJxq_()I%P97dIkp6u&=Ei&V!?!Cx(e-hb`>AoZ4O?Z%MQmlRuTHGftP&t$xl1 z9UNt@vpr8OFVvXJm=H&K?pRtLx320g7W;c0?9$cL)x{(Z&BTb((&g3X;=x2t8u7l+ zpfCRwW*)QLIm!x`v|pq$Qkkg&mM-$=TM_vEqS}PH1?!W&lR42n2#OshG z^@bYH(`@CJ0Nd71wN6GYTJyfXOm}ET_{#N@9{-Dj>P7m< zU3AFe`#6&1aO*Lh5=-o|I*!&W0C{i1;4rw;;#$M@*?6BmU#91a2nLD3wv`o zuS0z^{PRoZE_&4_9pIY4&H!C6j{e}s+Zy(R%j8?g;Gp%+&L``Qi=7+?kp?opE(Biu zYR{VK895gQjJt7%Lw=HFc!@cLtTQwr$EHE+_E)26n-RjmcBhqW6t2qU0Q>Nj+WJva z<)VBvkym^5B|`5Pa+e595%-O9eF&=i2S#SiTbE&_lhyaLvoTp#Oz-ELpz*d0$#cvs z+bXf%fBx_~(1%(RFomR>(vvbol|=-w_syeQF|qjS z>vsnlK{}H4kShbOnqm33wa=BGCl0BMr5(=)e1^sqnE;jLyP@A3AgkVD1*QsG^I-gb z2a0U$ue+$7CpY)=@8sa+{iQkm!p+WV*pYeEZVHd&RbiC;nVpQ~$nXdnS&G=HfoF`HL`Cp5tW-%uZ$tveAZLF*!TWk&}o`LYe<**Ok!mVq9goTWnQn!$6 z$tnQxP5<=Hz4yoV##PvqsZbz?IZ`TdKDY+S#YO-34Nw`}T?{LDst#}_*D!Q&2wT^f z0K20j@eom|$RP&`p@a?RQ4vFf`wrM4Ha52K#t5eLxH8M%F6a9~U@0uZOBQ(!=CIMl z`aiyTWAca>hVrL4f%?pi=&11s6^(I7=}YnHF+rVP~^*bAVSqz2m4$>~o1l8qWD-&|JwAYJ(Bm+S)f; z@Uxkir}WxHZZK&|UAX|W#X=0;3V}itcIjbTnd^`i@K8RTo5E`bl|_)U8Ha^{T=Sq zyqF8a2k3PE?WALCkNH~A#sBz)6Ut_$YV*aa1szli5)mRn4s(vm@XHF4t$sUz#=}a! zezo*GUByCEcrVO%oL{;3a9{!xidy8dRs+fMETwB55gY`)bc?QU-CDe)maC5npbp(WnH;wcQc=xZ6 zr(VhNLYx>E_8FUDa3IDf6jUVnXkG~$gts9~nQ;}VX|>N3>#K9J?Zq1EnnnT`}9 zFm&r@a*&wj|CpB_Kp4`IaBc0yUq%!r(IbhwkXf3c^;^baKW8~Zy$~wmE`N8T{;!94 z;NR`3|2ec_uS4V$|GFQ@_3BiIBs(2P*M1O&AGJ4-@?eV1sZ6JUIQU>v2iUB3UP9$n;W01LQXoU#jBvs(bp`S9O3K+ECDcfVZk3{mCda<8j_Lio2`A3uFx`z^1l zYF*dq>zoSR8DGTLfmLqz8S65;^Y_0F{_i&fNP=%Cg+13vA8|!Oocu$nV$L)<@eMr( zq{!p$yD;2|epPo!nfFL{QqL_PwL~Z!@2ZXJ z?JhDRQd3M77lpIGlt>1(7M7u;MN-0UuD9HGJnyIUJKU}=Hk$1=Dr>hJq*=PcDV>HhCcQ+j8O z-n29GgVZTR?q<6K+w+?LA7x}Obow>r*N*>}uXI8W3?8xSBa*<2kV*m)=n;9jIR(j6 zSV8{}ZsU&%tv_ZDR8ON`Dsx+O)fHXHTR%wFp+7>_VTso3KBR5lMOMi04~{G;k1p(; zL5_qAegxgt=thYG@`n5P_iMtc;spb@dirobs7lmI>z&;UKrGefE zim|6gz0A}AvObXjZlOYhOeL`G zGie<|8E_>{NPq~fb~07FY_)Ke`4)Vl)(9Z=w0LAEc%q#XKc^}-+Hp#oEH@#oY z={uNW)?^R1@{_Uv>TqM@p;OkOJh|B5nbZgLn$^0IOKE@i=#n_Yjtx~8c)2C7O#>|} ztlDVdKAUO>dnssHxd%nCvUa=JQy{}w^KSn+&9IZ&EaWm&wKqv9EhLGs!s-{GJb4tw z@`b&61F_G?QnKDBt#B|IT2p7p(G1=jjl3Mv_pb=K4WrBotuoE;xkF0iUzpQrI63iU zW&U0Y=S(FL%)%szj9+@oM55ZF19EHR<4{Hd&O!2u!Y|M6Xv7s)$7MCSoMGyb@PD{* zbuRfYYQ@TbR%+Ium;biklR1>RmLT`dZ(&<-3UNLh!POZql%3H|7Hb$LKMSyA;A>%5 zuxn)*Ps1rFHj+zB?#orlg~CBL;f4TD5)mM?Ws@TOrSo*L+1Xar2-x0iQ!Q2SH9DG1 z#GvKj;Ss30_2BDOYXvPSqUdFlNQz>1_jH7~Cl5n+8rg?^gjh>X0OA*dPN4`!4W+%Z zmLRZ#?uF>^=r!$Yho#BMK?U>3pV|HxN_|!((*flLStPhN3B}IC+Kn%xo7M=su`{&a6qKBPAq3(J(=#!XYZM$CL;mU-BMcGS#>ew zd9RV0Y`a?5-Of+E24Y!SE6ZrgE%ij!QGVzFHdcLHL+Wc-s?;(ynM*J;68rqalLGS6Hc)A zkYAP->FjM)R$EH^hmTp6*|)u;yT;yAS@}7YKKY{vOpZ6giVmc#RgBt;SoCg`ti%;2 zhI)^=Bzr`yC7JC#iEuDDU3flPGD8UoS}cP{K@vV89b^_=)V9RlE7BiU6>SS}Zc8%W zC?*}7UDY__hJ1-QIITInFC)}_I6j+fwmaZxa!+&AX8CJ;{+^essJ?*Y=y4*o4?ba* zMF5ra14~VcMNeA;vwX6?ShfY=3Uf*g>J>nP-`*(HuQ!Im%gbve<>|OQKR*w=1f$Z> z)ZJ+R6QW}4=5V?`zeIg0`l^LbV?^N-BEyIg`dQOs0#C z=CNP|7Go(j)!Rul|8T@c@38K~^ZTk;#2Y12?3qj?JDtsu1Vxpflt)q@p(&;}0T#h;`#=@>He2T5@mY57>LAbk!Q_d>G9{VkxT7BDSZFTj>HvfN= zj=OnqA6N^)=|zQ-f{w@8{w2l$3vU}*LcYU;q@v7f;lULQYO7dmRbVk8>#%@UHD z7V7Mkma3WX;gk@*-9O+i_@OOL%`#>*F0xte5swU=gH2Yg=hXav@MB>z}xL=iJ9 zex38D4SR3Pi`CJ4!a%8THg22|?B`k0y)u+}0KNe_O>y+EL;hlgFhe@;c`*O({n*?6 zVQSrSNA9A0E2Y3$G;W4b!6V(BqH0U1u`wK?b$Na;Lb4H+B3uzL!(fIuu1zy(_4q8Y zh~H9&%#`g`vKFN`*|Av2mtsBVQ>gk!9fJz9P58bW z?zS7-xj~xE&GrwSe$y8{A1{-vN7LE+p8W8|aSTEx8_S_jn!>*4%kA(6)mCfmCB-f6 zW;W_SYBj2^=?zy@yK7pi+DZX-j9N>Zs`HBt_2us3CeM+>IeVcwW-!6DZKlO}xDKuV z!UvGvf!73v8tpU#DYUY&g8#If?b~m?%H>p5(FWYidax^ba%nF!4A!~r)Q=ZavK>Ai z!SiJYQV8&B4%Sy@_V+ep;&a*}cy#)ZpClK^u14gQ0ANnky6DH2K%r z<`ewhr)(&AH%{xW@w5+PJ9AX?L=b9O)e8gKU&1q>)!JXj0x%7Ex(OUf9TLXwml!|XhoTFUM4<~aX7EExghdxh~ zg8xte18uX!kN{gzn!Gn@%Bdx@T{kUB*oF&k!b8B_IAG0 zLA0%A5{tgyz!^L)rFDxNbuY?z@$%Qdvj>Yae4h^@$BU-MxXA}Z^NebKFE_fD z*xp?}dfF-j($7IHn=J4oCYFQP+>qWz%vQ)c!{?U$Uq!~0@VIWQ`Yv?YFLx7wtd&#E zkAHVH(+ZaWL>ALgb}e+ye!2S`AJo#ko>>em2{&1|w90rAbhqIE&`-*)k9zq`j$OD+ zssYeA!RaQSyL3-nK;Nsea_jEr^Hyb2%C-yG`p*5^l3Aq!&#qzY`!q9U?n_tXlG*b4 z(o_i07gbVuE=vp)H9PObR|K7yBk{h30ufJ1U7I+C zxc5M~?Jj4tMQyS$M=32L<6E)p(v|PuTrV@%LFL7!483l}OMcWd3>2ltC;!yZ(A3yu zznihCsN9qrx7}?0V{(5ag(u*!&Bt?>7{ypqTaDqK(luL*Rmm5v+MdP#&M4`3S!pZq zc#x!8DnJW>VQ4!rzG9`?5j-^YYAGn#n`QJ1EOa8q0)5rgzxB?Sbv(b>jf=(wQu~}y zKg@P|x*Vp>@ZC>7oGx&11GM$kLk|0np0`|(ysx{C2~OLfu6i8$WEtu8T5tN{rc0S< zv5sbPM})<4Z#MzUnx{ljLBC=p34IrfMkqx#4|3hN+W;8_*lQ;roXlhq~ip?KJMB275(q_#o>OkC=2~#*F>t( zR{eaP`DE>8+ntkS?0z9(F>|B=jKzRv-^20>3CREtq7ZNXQ0Ao3jU1E8Vlswyj1f9N z{%v2!iB(iJjrE@kF$v?K)1F#X|WUr{c@bmT8c2+`Q%+xXp5&&QB9xSz*a?}DGu zTc!WIr@C3&`6Cm0?zoL|yJ?BGWkhyI%I=~Xk!t!vkD}KTqrbAg`l&`}|GR)Y43_h}7sF2>)~Re>;IWJf>tAMbMMWf|wx3e5zJI$7Kldz<;_xpI_k(Xk z-)m(V@&^bqZ!CR%Kau=jz#q=etlf+b`*V^5S3(%J`nWzv7)qFCo%{s)8Ehe~McawH3K+@-wNq;Fik4nEk z8X1(w=}`Qq{wUAfA3uqM87>=cEgmyJO4tzq3Qo96#-g&UX7Vg|5Msnk)t)^ClZl?!@ z-qv?xxSze?Pm7J0{z&*TN+q8nnSu|qTIhQ$HNf|p+2Sf@rF_O3Tpl%EHA^#n5g=2& zEO35N#gi{2v;b`g%#4a2b5vcbly&aFji)(qXDsAbmrw9U1y<&?TC6wMLcrH6p#|lP z8on&=L(~B)^Z=skRd}q1%MhR~yAY}85EJk;F8#-PyzCJCkIxwritGXc4xq1$_u7<= zTI{r1C*ee;tGlG#dG>mGXos=;Z;LEum-F1~aemTX!`UN*^l2N*j`-B zEUJH5G(uoiReOJjy^nSy%eodCBz?-kGdUEF(o!GwOZCV82|X?cwElPl2eZaooWUlm zdpdLiky5+q#U#$3NHLGR102lwewOM1W_ta4*^BG9^RMt28UljM+g>=K^^$D?I!Way@FQx^*PQNG1 z{=FXW%|CBlL=a|C_j=Gg9z^-FKaQH3np|C7ix3bHW+nHO2dH`i$z(!AEa^d(xaQJvyKkNAi>^xwRl-i`L~B&TeTkG=H`b%crtXyN167U zbv6D>3ZTosYyc5Hyr-6vljlU2Vt3c23uxVLy!hvb0u>+Ow#!q&w7Fh;Fs)xd1;4nb zrQk09G_u=V;c%mV4&yrX(p9(Vv|nt<16w6Z4)U|T`ca}o2w035fg9rM1WxVW*&64j*_zsT83gUAUsc8^0*Q?@b_k)hwaro z7^gW&Dlt=>Q!AP0cx)aTheEDES91x)haF*g<)plA35~LnDO!sqBHWZ&*8m@fk%;-h zOUKT%T<{!1*F=}St}}Ro@=c>fu31@4kdxz`G-dtR=2O;M!v=@FnBZX%+F@J#D(JJK zR^pXKcO>XVC$~!)T+hEaf2G2FklB8ZCOnsmAf)MB-j$}FaZ{x@@aXSRQtkF$q&m3^ zgkoVEWJ9)lEM$d>L@_ycMwZKZRb8Rw3WraCF2E)s->-MMEkF!o2%InOE*f~0c-Y%J z%*qH4*j?p}706IZmUB4fUvpc7VAmd}mB4lf>j zJ#qHjVhTtoOnqGBay=rfOa%9`ujp;9u0mhS#!uvFoegMZN=vsH7)guaL=t!UAm@Y- zlH&{&6bYM8r}edJ;fUer(I{e#swtK>K#1@rTvb+7B}Zlab!#7pLzS(_&g)_G@0P1( z@v}(gx0wERgNgvPr!ZKDFSx;`So}UnCj}DxuegtA8;B!qWIiXF^@GXl?@#(z_;14^pWT({lxeR61`w% zDoFO_m2KyN+4CQU9D5~#$*Jf_J-0n`(&dR7KYEnFd}wRiWbFr|FzNfjO5`&oqG9w= zby(Vi`$qeKbjoKX2V~Fb8s@w%^Jc25q~Jam51v!6td%uEXwa1heioJb@nyt{~! z$NzdK54xlU7tGwm8-ASu519I9(7UmiySP2)$@s&6irqXYQT%Ny*fI3umh5#&W(Xl< zXEFi`D40>@ovUfoXgR%|mMSCYfrS|*p4+7aGrJzth=H`Icox9(8TFo)jsYkaGV18r ziNR36lGkP;&}sPu{X1DvaG~zh1x8oiMtmg&2w+ zRl>T|;N1`NQFm&=uzH?D7+jeGVZurei!2*bXW1DMe3LJN)Bo6^@|}JddWO^ykW)H! zN&BuQzghArj1ykDKh^q^6LwlZL#-7fIB$fb$N(SUo_|$$@`?#1Gn_tndb{%w)1(91 zaTVOSF2>f0u9f4O*z7%4$?l_<^+H0;zs!~c78HR=&Ko9Egxvv$x}5np-|h`PQMyP~ zx%9he{nM&}-R9=z2ZL+RcH2m?(Cv0BsK4Wa1+*p-?7vnjmKKNZd@Yo1 zp}HF^yLt2-6x~IA^vt<{Wne`zQ=`VLV~@R8bbta8ZQU5F z*h6tyZBL~!_YgLh`=c5@Pc(TwL~#!^bz5JpNZ%G4%8RqmL&144Mmydi{KFj5i)Gxb+A& z2r;@bk-@4{_MHSFPDgRPUJ-F%O1Vg6YhbBoc9?HrSnD4-_jc+%l>h-3pc;mJh!R~F z*xhKj+MZ~?g`u9f}>C5+lP1#huTF9cgr#dB;5m-oi z80FclHMJBpY0r>{`rmC6So$pR7W}ZRIMIXHp)ML{gA`F=X~?d!M|y42m)tmo@8wle zK4;})NtFMc^w5#hGSKY(VPD$wN~Q`tocenZ5|RzlBm|qMFcF9HG8dtKS%rf@O0K0?MjG4p4LXAA|ry=DRxTZA@L|O?P zN9}yUhbk2-rc|uZP}3i1L7gYM0Fs1N7HJgi$G&*B{YTZ3vK2+t+MV_kisH_9JMo!Y z^*J?+2_~uxog0+(Qri4Hi{}!e?dt(mY#xQ2CNU88;S*FQi6dOz&Pi~4gATXLy`d=W zy_uqV%zz6uB*-)seVW0T&neR;es9xTD8u<)M?29~`65Ofed{BEBy&qmvKp~J$U4zt z*&%o4Sv>k%C>$>JrJ+*V+>7&NCpmiyRPFXO{f(C7veHmb=oJ>#`HE4X=8GkxV{y&d z#c+G*+FY^{1ya=UwOgU9Oojz&P-IDNNe(>PweAt~Tz=6E^s>K4OX|@q!hXAPqmvmr z=8Y6<DPfRrF-(X>HXykPMU;L$wDqJWxGE)7sx4=*d+&#()4NCWWBhbF1E#jC8L+(4BvL6z*di|y<;Y1z6)VooK|oM=dd z*Pas%A3%a8vWTxQ!SG0F<4nCNU?$&Yb(2b`OK(m79Ouj@irC&Bc8A^Ah+lrvs7t`u z=R@DGy#BQx`iRSEYiYGMOLF?=N{_poRAbnYTDwhAcSS(@1o@}Wv0<3FqP%WzElOL^#~ zleoN47$6gjP?b)>G)jMPuFJ+fWFT|uOJLRZ1YVToU4JxTe^pRSwc^$Q^N5`!^iUz6 z(FHMb?I`5&w0p;Mnj$i5b!9U^V|zeST=sHA|!mwf@8Zid|SUgHWNm1cD{~LS@7~LbZA-@7y zUZ-D-?Atu2<*c8RCG-LD-<}54^g|ewvguzkpf=|OVO|UBHTD+Vw_{jIIs!4GgUFS> zZl*5DHpt1X9=RdSButdBk=3Hp*5^ywV@rB8ZQKd{ZW8luHNA%t>_|4;&=n`d&e4;j zyc+o6iQ92(P-8$6RhGhjl_`rDhP7lHhAg)IY!pL}U;@Ls^!FdQc?yFFeASTNGYg^@ zr^*(iO?pUd7ur~XI$F%pCVAd=f&D0gt<|`whlBlNb9aT_4sd|t;!$KlubEbNh)Fq9 z?*Nj0QQ8oxV7zL?)$v!XQpW-a5sF-tcDgJ!cGg;plLYdsjFQ0hBps}D;_XIE>U8U7 zZu$+C(ioo0zgslj{v=Ldkf3XANK(|GDp9?uij1#@(5R-vT-@#S4zF`GBpi{A=;`kS z@M2&+AeP$mthUsm#@Z|dY7(g#BVFtVM^LcJ!>4z>l=AZXbmfr<9QRG&6PcJKZbKbC zh}4;Q z;9DeR+3j>Gl9>&;kYElA_nzer?o;NYzW37=962Cw&hrUjawDj1a+{Y2}ZJL z$`f)ki5Vri*^}us^Dj+@fe&oo73$r;0*bSXFnt_URgl^z(Bs{S4i%a@qp{x(E+r`t zdb_gC>$U=nuXqos_PaG=KUREvJK(dV9>p1Jz!uf1f(Sub4#ioh5+nDbH{Sy*WyZHT zv(Urscl3G%A^GNN$^K4p7X3IiSyIG0wJAv91Rfc*ir`AB)?%|S{h-9KEN$gKe9A*F zbV)&Whlm!I5Bd5gKO~ut1X#2-&0wfL2fR+VE3)TkHrbg5HGVu>7;h$+@Y$+j9YFdD z6w(f5A-0*_jwYB&YN!YWEJnz1%8H9^v?x_#tkJYWjz;P10sR>gFkV`;7qh$AhnTAy zH~}WasSXqK28pu%$igldyO|p^uh-eEr*PpVzfT22Ln1F?m5ulYv8+Hx zucOd7S$2fea>>M9;z|`L7J{NV1uGg3g{)QNl{ahFneJR?x~p932N9n>5z3c#I=#7A z9`eWAY=$ZDNfNVYc}=c;i*nX>`ohjDNURv9Qp@j8dk2h&LyM?l%qHQn*wclRe(eN} z`8?^m*ZLwNH`ryeQms`>B9=1%JtRZkK9X(V*hkJQ^RIf}GFaiL2P{e8fpGh#`>L*L z%?&G*116sH`lKD0Z1$>efY4{JDs0%NMF(sA&mFjut2_arLS}|K=5o}^7&&!!&JZK| zFtCS*(N8Dm$+hYlwG$E+gJm|Ms0>)+tzAI6U`aFoRcJg8hK|yzN>M+Sb$*~}7P(W4 z4}bE*0+=jt_7xPWySbct^_kjWP>%Rm?y^)Qiugl5-~=q{BvJ95G=oIjUw!R+x3kGe zR4fO&$Iqz*3r(#vY!0h8c9V$WemdDLBxonbdg)%X2SirsEOmL`95v8fM^qO#%Vdt8 z-IJV|j213o)=y|u$VXn|^oQqq0S&SJ7I$E@lv zJsyqmhkX2O|H&sbBo2EA**XOvPXZ`F0}NoWkv2*BTZR zJQa;0ECZc=GDceQBR2%peGO_9ryFoQ|1YJ}yN2J;%GTD;M+Zuo*adT0*W8F}jWFW( zVWdtP>Mxl^B#8KB1mn@R>v);D1l&j%<0$++QS)U=Cb7{QNMb^KIiqbAQeyLwF$_vnR ztm_JL4&Xi%{DKk@ES%y;=Uga2J}Dp+kLz9c%=CeJUN}`iOJF^%J(Q=ig$yG5MY=_u z)$=enI?Q|Zp_1Ysp9d)064i9uc zITJOf0=dLAqmfS%3nI^W20{D)&5RzW20U83u%!s+yWm+O-3-xi6`r?ixga z;?9kQ$Ye&S2$V|65=bgPIUH!7y>{0jyi8C`saI3OHTu}!C-Wq}_+Xl*tosNU4!f;2 zuwPwVwfO{1?jZ@&PqR);cv35D3&8M)y9Q|>GI^oH?EJins_H9ULhJ21b7ha+z4h*4EbL1)H4h7q94`B?l{8ST>yO?6)Q-a`LFkBC#J{+Mb@=dSf#*f21jZ zYfDQ*imLT6`k3_k{WH_Wd~nBO?bvUAHjbKl#D}~_)9#~6YAxwb`kY++e(5psDkxtjivaPiKw0FC5r98wKm9!A5>5w<_`NT|qNDzsef0V*mQosOI5tuu9p`_zt z&o3ExLf?Ri-4_RtiSEp6c217-0*WZqa2bCUH$b*3M)><^x0s1zdOg@pH9w@nQzidGN zmf@t*Lc@oQnH{f;R*v%iZuN44hCSRm2s`p|b#fw9iyo!B%*epVSo$Y7$4Qw!Vgw{C zC3p=2sCD=^@o_(_pv-G@z5J~0Ee~lTKf6I=aouy5Z{ABLNhoPM!XvkbB5))D!gjG? zrI~_{OEFY}nXMHM&*lW`y0W@DpAwHwMCfI7e%{Q?jI9w3lRe1;{$_jIpdS_p@<6F~ z^n$|h2>-}tD_G2H(~})j5gsb0Kr~8y47hiqn1*FU2JGkIhrIEuirCH8j-Qv$4JGehnEgD)2Cq z+X^m{#bfyvRb%luvHTKe?4B|F>N%VJQ zmH4mCOBF5Y0C^NN4b<7c^EGVUfCm-f2nS#Jr8Ja|HZz2Sj{Nq5YH|WuV^o6ujxiS z%NN<}i6^)zC?GK^rgM9Y`}&l$u%N6;K9XGmx-51Enf+&< z_v-&OxO{|nHkk53%Y-fynPdkB-+OU!22lbFj7*t#grP-4Lwmm3%1lU5)>aO*hKX{3 z`oR4)7@lAhE6c7{*4^D%fEVcXaNzU&>2B)@y#Ga>hpwp)p`%yQeuk~NUW`+VNc()b z0Y$5r%M*gVJ-3Y*6@y|i7>>f|^x7o|?H8Jni30}*XKiisi+qc#ES-)p0q7od zK5~9uk6P0p%P#({l}ehZS_AQfPSmK_*TzZFcmE06K zW)I>Oh;*2!y?sTy>gExE5JkC?T5TpHPlh2WGXkvNVLG2Xu>{0)KD_<| zD-^0omPPCXipdN~HK!_=uvs6H7oYU;oi>x0o!LqAXSK>UrurE)00cU)r@ z76p`HO$wu;`ue@+9`B@p!~{jbVn=Lz{4wDDY!In|*KG)69HHv9z2ofsajrKeUL3)r z%E5AV*{a59&AZQKW4S6J-fcj;j0+sGvxOut;c*$zfNX{`*Td(wA2M;bK97iGEb>Lu z(9`VvXo~MACH6d4J>WF)IL9{>&d_J~6@!>~OF#Qfu8Bd^ z7Dizk8Hw@2BDL!1$7sSp##sdwVGQvl7j_?_m=h9LKMy{ zzoEAk0>d>7186qzcZ7VH{LWOSzxyuyr0cT-e?7IOPV3t7I`k}VH!s7}L~U$oX(?LJ z6%4r%)oWcnscc$)E-XU|IfM5ct^4IV1`2`S85xHMbwhu;khEvkPrtq3+fMZfAiZvn zJ$@i_ZO^j5jBj8m3@ddwQ-ApL<(zUrz>TiSNN7m)W~&o8%t7&SBGT7KDd!HBC{{7X zQc4Ck@6$7$0@l~`Q8@G+Wv8_|7OMVDEf5*hZtLe*)d#$N6`K`VH z4@C~{4k!M9JSAJ+pESNMCAtPLbFg0bd`Z_-7f)`!%f;oS`QHZ9)h?u z694ALGXOj~h!v2bQr|oNt)K&qybEue(tfhcfI!6CRQ!nmjQ`=Ch2`kk^~O_mpyOzz zP9Ku8iP+_QJe~dR`ovN<_v9rK=a7_mq}W`xD7J5>mVW-^XCLL2o0#U5Mnyv zkcDW*T7x-C;f#_@$l}TpilLeoRahH0cZP_yqvyX(ZZMx-C?vem4nVo&Di0IG@zlE- zBKuM?tn_2z-DWzgo8DdBw`sC@spXuWjAspUpwW`?_E7SJZDz$lCrGwufV>LAn+e4u zpVjTj!B`TUOHKUmD8Y80jJRArS(=)^>z(BFFnVsEb zEI#jYwJxWn1_`aG=8a6(&4mnrC!z#Fw#q=y#A%eq;+jt|Igd;H`m*DX_TxYjQ+vl>>xI-2ETugd;PIRy7sEmSu;F6P-C-%7; zggO5?s>ow>LB~tInbv>Zsa20%s;JN9Zvc^*z?M<@(4w?XzWztOLrzgX<&KcsCrwTu zgEdnW2amgWMv0n~%^ummsZ0uy@z7K^iF@RGBe9K8ud?Ist($p-%Dt!tl;~I#q;rkx z+lp<8O8)4<0cl(MPm9Yzr|z48%YWzN9LJsm494_qUzWCejP&4MiXfBJ@{m_if?~&r zFGQQrWndA6DUr!SlgaYz{`H5zQB}-`*{jn_4^vb{M@GWJ!QF?ZKnp(J$>ihQ91**i z&xnWG;Bxo`{;>W8vpOe}6BUq`ihy}%lgqd5@xDjszgEbqbDh9k zzdQ0{3zJ~rH+^hTvIG9jX_nOiImncD{gjO3`#{cJ=zi&n6MQOz1h&Id7d5tYvv>h! z=Vwkb;=%5DIk@!Z2ZRclaL6ycR-;vDPF|zxX4QV$0SnbO4)?^Dn`OVn+k&-uS=QUS zsQ-@b>@7*ugPE(v{H#9hmG0OIM&U-!FB{P9Mkgt$P1@@dn9=G`PKM zf>Mf%R0qk@=@f^xnP*Ok5)lcxCn&P{<}nayNl8ghEglk6MhQ#g)tjSH4dh97{LbmE z)_lA-x!|RvANZY|mKo#Sif^ww>4z!02)#;_M*rp!bz{R1j}ZC>Q}_@Sj2tp@?{C&na(j`DycyJbAvQbdQ&eS|&^7j!?9P{~`}%}gFteExu@=$8NU&kptQ_o4DMhyZA${zyZc|>c zfPc%c%^iI8>0y6GnSm!$g|5!kp~|EAqIrQy@;C;kF`*14VS&MBIL3?4v8@HkDhM0Z zh!aBs>r`wV`6?aO>LC{v>gL1yiW7DNWq$;+qT`t$b8&QVXi0NXQc@BV2WkwFO2#Iq zruNIzG8aeb9i4@z&~GNksHSbe0?CLJo&PFnjV11(prF8EPI6YU89Q2;di+&Un^@1V z28>ZmH2c0kiGK&JXxEcbQHA&MAAEP+$F9=e$rN@1M;%^W8TT(t#hqMje*V*g%xORdY@GKPV_E!-9i@Lqls|;Wk>G zNEb|233&j}_u3LaBM_zyn)F4au~Vdr9wN{v46tZmW_6_}6Zh)w_T!Fm{^7Dg8ygWeZm;Am~ogz#|)xq)w2MOj&0emG8zlm{#WXfC{xW)Q>BQ0TYi76|aK{F7=44Dfh+KpJun zH7Z6Sl?YYo95Qov*R+y$GBB_g{TlAA(eE%$!`PVL3tb zgP~d?lR}Q@{$Bfp$q;~+KzK?O6Z+G$bY_#`@+67c|GLREXeQ~cEBp^5dBkglVvx`K z2;K)Gnaxgwt$i9h1!g~%ke6bCgt)LQl?7GxC_~7^H(1Q(kKhtBmfGS3NU+O-rTLGsJ5TS`AtD^)(plz%a-LeTHNQKDNk!Sf z-q1?OcOV)fZLQ}HqGp#ocR4k6MxcRCq|S%?{;b;2&*P<}Jc&{@bYN^O67qvW`0x!iA+VHL?jU%kbPvy``xUg1)Q4 z99&_j#cr)1j#Ej!vg4evrQ`=+`<6Znj0e-!SL@&mtR0YzT4W4oMdG|@a@&ZE;s=+v zlM-3Zym(V^$~Xv+LUsuw)}S17+sMJydQ6DK9BN5TbBFiRd3}-kM4*)mR=wt9f3Io> zX+UbLJNKkaO55sJRux;_E*$Dd=iOlLzcP)SlhZ4S>K9Lx-)0lNct#rJ=)G;#JiT4$ zTQ3PjE~ba$O1BFks*Jwr?2x1bh71vOjDKjNhgj8Koq`!`q{PI62h5h^TPx2h#7qBj@QpcLC>_Q5$7 zRTULfG}IH9uaawumhPX<=a~Xe0F^n;p|f8Mn*Uf2lYIm&SQOBjeFV+$XyB9&NpqoM z@nKdp41t5Mnl)K`w@tz4);3(?OJlpm7q+fTd{t_)0wSCkXgD|`@W{ka|BqLcl6=>O za*_=~{hy1)3~Fz}Uf-v58}}MuJ-(OWZIZg^BQwX~Tv`2@asCEZ!|-pZZoPPBP|;IG zbL2lKBsFk?t*e@JtXsufa)#4m^I#<%9cy!QipZiy{Q?D8%=~;0=+q^=wV-cs@C3ip ztV;?lffb^7vYYKx3@};^7yYj;Q}Qt^k)y}`0j2Dfn%|oRN%sbSL09%A1;?=wp|m(* z-y84e!8S}{kJelO=b_s8+`w7IXG8?}uJdR6%+M*_PEiocx#MXOSr{S#(IiyucpPGr zW8Y4djZ~F(*SbMc`{ZNmqT#ux`bNmglEIS3oZwZUb#LLu7JpIz)WS_KxxdEYLSZMC zp0=tl{z~WV?X9CyDB9e&|HVvOGJtB3>|$63gvN^;S1Y-zNij=a>-GD_w_ckwIUiqV zT^)!oro?8Hmr*{L)1l%5>duB_gN2inZe|vF&cNbgVK6q#uOaevJR>?u-jq$pLN&^d zEvJgII-%+u^RPl)c#TZ9)s)m^F6xHvUm1y({EqkR@aR(yh{^+3q}i(&HQ+JJknWC!E?ihftp%!Fh%er zB5p?%EgyqFiL%Gvf;otBxgjkO12_>BBz|gAd>tI@F~u3ZTZKvdYw46X3Rw;-F12jr zP}7hAC?}A{!A?hq{%X9%gQ7^pLK&+fjkwiq-Z~7ajlV*@jr1`sV{WW zuGGJAOt{Ttvt~*tCZ0N#`)`6T{a;;g0o7K}{fi=nQrs!-6b)9~U4j&Mcemi~4h0H< zVuj-FF2&v5-QA(sOTX{l|9bDfb(6JDk~!z>nZ0K+^PAbfJtsh_k&yOZ&Ai3|uYQm7 z58l>&H4mK;{`B?eJ}2p;pe#7<5jHGKROT1m`fRd0=w23#ElaOH-E*@OFT{o{2njT1 zxS=?+oK|&b4hhO_;CCBHejkOqc{i6pvzhMTuqTRD5n?j0@@~Wdr}me9{X=Jb_<72w zhv@o5bRtGOB%s2mEflkIfLeg+KFjHU6e0LneJd5`BRl^c3dLf=bkRmRshI@)*G8b+ zdjH#pk4(uNghjcZb~fM@Q5}oDSAP;X`n9&2V_vEvLh0rnMl#$*;{i@r@HXb>qkP9w z$&b*%W^SrzO`3L#<|~w>a=D#SrIGj95;S`uD70@rLHhxn^Bl{i?PDmN&U>hVa~|< z4X@<8YA=|gBOr)C2^T|&@|Q?#6Zz)yy@4Z$1|W3Ho;7ro`QUibc9(0o&U}M^&~~u? zoUupJ`M@(Gpn)*u^fxpr$1KLMCby`n3SCS5?vi3Gj9@=nz?@|DJkvgm@yuSV zf2>YhhBQMrCz+ANc$SR|(`bm0k8r3STNH$Cv^UM0T1IQ^KC-=?$rx*jWze$Tuk#ga zdypFXCalRF)^Nv)B6EFWiw#X7CDn9dY^<+-waV}H-HlxKk&q_DBWQ1WJgKVp;BbnP zk$!lwDo~FZL+b0*{#beh8U0XEej4t>aJ~kE6O&f7-^00{?#S@)@XL!&gAGtov6PnC zBrF_5*|jGWT?oEed?dBj{OogZsw8U~gM;5?IOBSY>&gAuibq@oB?&dF+Z45Nlq{Q^ z=lh#)U42JKM^RDHW|ydfr0B@~g^+7aV4s*r)w|4ND|LuR&EB+{TnDmJn~RjX`u=jg z|4wP_=+k8jmy6)xZ>4_khja3{mo4CRk`}SoiN_BgrkbWEZi3i?f&$5g{i-V#v0Tgi zFvd{y%x?gt_V6&XRJmu}t@NF;c$30hX{s8(HQs&elG0MGj&Z_0S68B939%>2M0eL= zl9%KZ=fCTTiI{G6z3gUU{bF--r>G0d)1q9YBccdbi=IE6Kbgk^ePlRkgs;B|3qlRO zOdJ#!+vn@wX`v?hO`l z@xtU0!Yw1*^dWrxenW$_pJH=93ycJ7S+&&ZdW39joOy-$SCW)4!%~x+ZvvFwH|F}+ z40Ln_4`W>P1cVb+8ZbKLCh#hv2vbV@sLdaGQKxq0HF2xi>&MtA%>(Hfh98p8*J{1F z9H6h5qbEj2?lnmH2Ww>YQH)oya>T6?R{X!b_+pAu1?ckTM#8X}^f1IAP>fR^4-uiF zQ^5%Ak*(8`?zWBP58B!(wKK4Q)rvi&2kT+wCMua2+CwiNA zl@Z7p)jv~1Qm_1n%(@pM&_#vO-OqiPA=e-b<)e#A147Y7o#{f*6M^I*k5hxx%#?`a zAyEInQ{QzV$^XRtCx`#ZMv4LaF8e5pAsS)$e+bR(*}MMBK^}rm_OCei-$w`+DnF5j zG?6#o{`TJxoMhlRN-NA}+w#2GM}MD%g!nzO&(s6K^C{*IWD0^>)i29)46KqQKPx*% z{~As`Z!tB^9qzOEzy~qxVwhlAzqg|6T5Yi}bu^iwS;;ytc^312TWZy)d0pCa)@tzI zxUAYOB$m;OgV}m|e4mUbqK{(rtUM*#}_c}y;P!>9OGL5%yo|=2PfXLz89A(dMz9!d~8Q^wUtP%na*wWQi}DZ z?yEt~!oFk!Dk&$Z_tFTmMk(Oj)L+nQ{KUq8m_RAD2x`~0w|UDz*o~?7=563sD<>)3 zj@keyANRl5zCrsydDg;aob7dvq+5$p^^ULBvjIPTR6d+)*BuI6vsi8hKpMJVALz^j zYunxn3k9$P*3wcPtQ?5bs&}S7-YhtLpmkM5vJbSU>6wi&n4ykjF>7opFkY_Ot+^TA z>yJT(NlwP`eZHlc^w_`s-W9Bn%`5Y`f;I4bcP76QTkUdg`}WIqwQXgbSRzE_y@deB(eCfk zmCI_-$l#vp=};Jax0ONJ7Co1QP&2Y1QOeEC9sHOYS2Cg8Q}7?a6ST+Gk;C5Cij$Ln}@p^WZEh{${_17m2&ihVj6NU(Qn zHM1?#ZCAFG-nm^Qb(+<;z;dLFi}hY>uV=S-Js|WV=&zHNt()uBv~mH^4G{F&!)zR= zH$=rx4#%G<9KfC#gXqW16t~@zNQz4QykE3o)gbzE{ znQ}e%cj3lAe`3wyKckMKUI8B6D`UxDu303 zDTN0e-qR(0(NeijQ^Xtk%FG2R_Z!fWHj8Q-3~bcZf6kD_i%~$|ipCq?LHp@3i(Wfl zJT&rrkiD*#ns^P|j+I{!<##?Z@7F&E&*ucIfk<2$_vK#4u>JmKnltoXR4nB=8KrKp zZ~5lu#}L+8%|HS)#JkKu)IF8h6}o>jas6))4A|{wVmnV0N5m6=@FRx=r^{-_6b!>7 z#xdC9H=eTTmpYx#{d z!GixA00?|w`)b~um-b_J<~IG2*=RR0+cFK`D34NI!+mrS#MJB7e`38h-jt5--E5%E zVznLe_WE~v1c`f0J;Hlo8v|~kz-FM_lMofK6S;Gz5I$R26{icIjVpD7pvLZZ#$&o< zQdQt**>fBt6zD7y&|ua-g@2pIXW=O zoU5WyO5K)(p%9N+auq*QSA&kixy>?;h%E=;c0_*>x;7QikTmPocsQBw)4;f?W^u|! zjR@*n@75Ikf;u?wJk5M^QIo(OcthrkrbJEeqXw zn=@&7Qh0gZT??DGsM8mH5++K12gG7N)YdJ?7STH-#dUIjTvdu&gVpnWJfMyeRYk_L zW#t(h2@H%20xQ3RnIG~h$jjqveVe^p8AR{9nQ~W_8-%U34km&-&LdYXK~|u&WJ8RP z*(qS~5ZCG{+(AWu9R8T+ubP@^$V_k-RqwdP0!8nQ>mWS4%aA3+i75!+f6Vb286K9U zK3Lk*8nHw5n&XHp8j23bvP!_~YGWnf!bvRzVs4Bw0$d|OO`VP_QR@*Km zxz_VX6tiX@uQocwM8mjW3hyNL9eS3vy<9~hjw6p8&Z$MH7$?&2*4wDZClt9SYNh36 zC)&33`Ls`V*fC)IAz=b&*BiqHM`53jI`vGm;&I4kB=;(L-yJ6}|2*yv@`?GPL-b|J zI=?9;nE?-waj_5j9Of+0dOH`_voc$8JC!(}qNd!M3LnnuHA`#%1FPC>VVpQImyk&z z+}gy)5A(=;nUGR|3I~s)MUGF;IgK>%(QvhAj}fH!Afdjb8%CX=Q_waYIGFSlYH z#u~h;q~pt^Ua)~SUN)e|I)f6(mZ+bRfCC)xQ(ZTLt82In;Cs%FBQP-(C zLLcGmnYHyOoquiW!^Wr;i!dC~zvjqO+vfvDXdG2+>TdJgcgh@bc|beYArk z)Xl-T(Rj3Qa}^G_3pN(_d{P=xVYzE^-oNj^&+5${vbBSb)*To>e*OfauP&!>i9*i? zP?BCm_j_dzp8C~PfINx9F(ZNN?H3noJ(n}^EJ8RMkg_`FD$`Bo(@#~(TFlct9V**I zoQ6ubP+@;&B#XQR7mQSCb4htWWUk#H#H4T$QFk6uDU=tClMV$R$_UW-YE^9QIwSNW&fKLhz%v{SUBd|y8n$RAQsrto=RbecGSOv!;)=<(v5AC0$3rnPC}i@cQRrd1w^US^cRxDK6ktY{ z3JQarvW$~{n4vaQO9A3K>ST;fV?ob{Er?FCa?1JaHc7WCBrUi1Fxg&j-@#J6rXO;e zn5kk&hauY+T~C@xGx--w!u(DUg{tOO!2AXL^HKP%f%f*`H_+g0ajgJhJKYGLy^D}7 zb%N~T)Q@k-nUjIbUNbvY^n{ofG&y6LgNl+`?jqR&Y2;PmA$qs!(|h&3CTTnhV4;JT zsUk)=5YJ#nNLn%993RQ~;1hrZoyz(5R6gWw7RLMOOOj?i_I)20#J};9aif=!09tzf z4e7i;2TbMv`_Lmmh8NR5x?ZSn`A-6OQ%|`@2giT9d1fLFXTuY)fuBqP5H*&7?V}E- z!?=Iyq~mX%f%MdIQpP>U%0Zvfv0eIJu6PyKjPe_E>B7W7`X6Y2DGtYpX%}RPmxW-N zQnnm>J?b6YAfwI{Ei?1py~Hldj^;(c3^k`IfgXwA%O|&d&Cpq-IqM!!F(21jJ55D$ z9L@CGkxeyY?g)b*XY8E6dkS(s;bTFPbhlx9z1m z3K5l^vQq0N_rpSz7E$AHQ_wy0_uUpjg4$WL$zHlEWSz&7hQ za1EP~t0J4-y8)RUTXwaF=k6|@(>`K%H9)NeQP8`=&Zfs461-&0v$L^aUoq)diUO*z zN7PF=z~gwaTCS4<=wx{fBw_xkx`870DBo+mSAX!;z26$s>?et!mBnlPyhqcnx z8b)71A;{8-+5nMQX)zE=rHDStP+^KF`vDjq*7O8|U-nPZFG&t8LimLr|Bkpo5ISGT zIdG%u@@c=@)AYP&&3~hvTb4+T`|E?X|IOJLB|(_6(sQ_0qSTEYCC*P(>D)z=K zj3!gx2|cmq^ivfEG1?lsN4{Bp3ca3ou70NLh;Y&$wm$(on$&8SYFO-5DcMvVZT5CV zEiuLS=dEFwk`dw*((vYB!OBS-^4$;*X7_%pb?)|k-4Ttb@rj8shxW)Sn}K`2u+?=) z+=}PgG6%~Y%9F*fbX(Rec|%neY=+@sk1vWD+#-_PmA%a_$5(`FnP+D@Ln$mC_DMX& z;;o4w7#W!toLoIf9N49TpzmK*WHqOw>2jk_6*#m2IdDsjpumsAN3K=z#HF)1HTj>j z)h^+R<;dg)D#)bwP_W)FSn*i0ZrxZrWH8;wkLtHhWr_5UC7(XBChEEL<&30SuYD~k zEaVgZuMQLmgDNx{Q*B%NDxf1ei9L6F$(0t#$D-0`a+lw4M<+BoF%~F?9_9>eRU0d{ z;YLULUg8X?Tn-BQ>yB+-X!Kh6+rpiY;D@kwNM=F{6u_?xaV%PLnc@Y?kM^c!^(1P! zg^@v^BNf5|VD7B#;w1r+q^Ww`AbXbQav@cmnnGNi)Qs7$wYPb{Sv%5wQH(P6$zpJQ;2G+K_Y#v`^b*monnxT-IVxD;Xii}YH5{A zB_CSlZW^x^s)v2Aq-$ufGw0P3a-q2{OPe6alAo3WJC!**=Luj=@H?@d+i+=E%GH;} z<>+nE%|a-z2RJPhy_HFsq%gytKNo=D4rS%Mk*fbyMrdQ^n`Mh+uH;9hyE{?v3moSC zh!YuSFalM6Hie*}$Xf)=Co0!#@jpV222XJzB`rUXKbMGtmS*chh}}6+E*su(q%sJP zC6d#@oLj9~!}J0rL0dG=q#fTxn^>JG2EyUOfMT1S5ZOt~Qs@wT+FOjxMwrwalVq@#=-WvQNt61Hpd| zrwt>#*=Sti>-T*wRz+Ld+?cMgpB07P1`JuKpj$@edVY;$J9oHGhl|^P{Xa1A{~I3L z7LFJQWf1E1L-}(htNf&J@j3M#U)uLc|BM`r75AA#Z@3*!>~jX)mR###xX@E-v`IV9 z1`T`v%!KtY|zE*ciAJ=n64P)>B5dogZ{8OISR%SaZ z4bv3+C6o-ozpPAdxZtS9S0{b<;|m_Eo~bi^PAOHUwEbW^&xJpH@RZs7PEUmxC5hL! z{dn;&1VZOdAJiuqM^$ow57=FBgG=c*Zx)bI$Xww5)DwIrosj)Bq9O_o$)kzFi+0)) zF;*r=&xkT>)sM**_>H?01u$d3eGz(}O23J-WHA-eyUyhLa@FImVPjJ|fc58Ws0G5e z5&=*~BBzWAd_=|uy^qx|BA@rv=Qkc@m-7Ax8M$pl% z7&!4Jq5Gwg&y5^1sX-2hjD?p%_v8E}#SHN~H-+qHb?h@~xea|Kzf@0DS24I&;UDtV zbjma>O2*~-#<`=S%A*=APXrqz%ArLVE9!%Sntn*Y1b#q}ktQSmfIv=YN-qB4!-4mH zrl1Q{^l~rkuKUEn?#sIGL;G{v&_&WSE`OSkL^6aR(9$IXzxUCjl+dGz5abLDKZx?bCuGn+ zi2S?%5VM6b1p{(t0O)@wIhF%}QChq9Lc++##ry`!vgqQCl2pm>m}uNBSNgC=HN>8Y z({(|^;(mAeL;@n{YXnqm)A%y-Eg-?>T(+1CF01*mAvB0+XtKlq? zRExeH?ZZXdwy(79cu^`7eo|-wEw>ZQ$RaVZZeCF~aH!QBWvs8~P%h?vP%leq zE!M(XGHzWnmbb3pHrx%Bb;dMf(~^(H7=!2$M3!lDazR<2uqo{_o$mtjB-aUNN>7NxcUQ*IM#dN3{R0<9*C;aaB;9dK0eglsFZKob&G$R3I1Y8$X9x@*4oXw;}+xZR~m}6&l0oq&zC5VL@M~^$5QV4LC- zE!fP>!RYhcVl9uuX%yJ(?6HJk9TS$SqEz{Cvuoy5{rH%dpKp93RUS(0=z*{d`a7Q; z-j^9?hU*LOdGP5m^YpmdeH>;6ALXx`(?B~F4bA3CZcpas91g7Wi>B#ee=Gt5jNyIH zF{ahq2W>Jv!8kyr<*{ibWIY#YPf&9DP?)tAw;t^DzD+MBpy94iqULV2= zrT*o*gyW3*)_3;S7VThpO+x?)bGqKk!T`C=yibIonWN*MA2KGA@!D-32{o!+Wv@Fx ze}s)@m#1qo|M#o!#43?53rBSbiHV5?@N)P<>ki!?-Wh~+?Q0w43yDxIW~B{+MdJIq zg4^I=I!sIkZLae^E#myKSHdDCz2LGh%id_hXD|5eRH)~Ggw!)Nj)(NV z*0tm8!1(=u@pWK%Na~@T0k4cxHu%PYDH`FblEbp<6Vhnk$ha}y?_6$Z6&lKK$#iKy zBO)fmxa>@g7q_}YT@b{(gSdR)*^_;rJr3(2^wXz9CCaP$Uj@<>B-df_1+rrm5|afg zN;&B3Px~j8RS0^BkIl9wr@c0t0i%5QMO_(uLt463mAobfg^p)t`dtUw?LO0gAk!{Z>qiPI9}1!$xI%us9fO(#`ou<+D;Lv$c{- zjcG=eO7TPGcesj#k1W6W$S68VPm<9Pi77!xAWcSU;LBP*~1b8RdLWPkY**Bu_mP z1^f5`R=7Di_s{yGiB<-BGmPV*v8Dxmy~HFl-B4z>=*eLQntH^#L1^z^+bAS2fZ(>C zS%&2NMfAXEc+;*IPYCB&LipB@#buSKB{G3h5T__IF3uRY(SEI)vo;Z+R|<^A*B}bi zrki&ROH=mQ<>HUG_pVNPcz%}X^d&o+qd%LY@a>}apmTn{_)#2Ou4{1eU}EiRZQH#K zr)Tc8)E6%%I$xpZ%T5i)y=6f+XX(k@|-i-SodR3y)n%XT&^{AJ*BNHppo$1I{16 zS55HYTOp)kS!4*a{xk<2E*5668Ct6iU=xwXgkSTMMs zws^-oBfh#Ofa2Q}KYlgTC^A}TxKO2KG~A9~sO+(Kaq)M z*_R{2?TR^4abh6l$aH%hYGB#e>ZvBx{R)cVOxurYw>Yw`I;^#85}5)>|TF-t8+Kx@HOwQ(c-Rj z#A|HWcP?e_!oFYFd-&VV(^Shg=qn|Ogsv?k$-57y?@`^dfHy^qz0m$Sd=^>F!h%@x znTFPA9uRz@d?6irsQwh%ykBZ^;t*HAL1Omqr22a1G*2aD*S(_61L=r@4u5W%CV-Aa z^?u6H1na+}XI8Y-l&SvEax^PEyoJ$PRb4s?=uU0!9x>|kY@`Q9*UhO}N}`M7_1aSC z@e(R24WaRK^naE4RlQ={+~$o5x$izpcef_}%J z%wXZNrqp6Yp!KrboFA)L@$@YEhpBQNa0LRej}R>Hjh1#03zdy9xuM1(U5Bvzo6Q<3^_uscd)o5FOoR*KMV&Pg!t9-MMGud0gpr0 z(|gRKw2ZX$^xeK$DJd!Y`}_U<-XBD6S^gFI1+_q&f({!|2CR5A?3|1U8x)Rq2}FNP zBMKt(qWTBM67}-w85kLTVDxxjysC6gFmGgI`eEr+i<;y`hJjSuQaoK#u_$>!TmV?- z{k?F8^jnT-pIwwlWo2op*xw{Tb^wHS+t|=x9Q77}h(rHwav}FCw`H1NkiAebTh6_Z zy}hrDAhu~r62nXh7f5d3L@1L^OMoNio@kY68PiWi&j*M=grP?a8Kg)@h@vQJs-i_y z78VhK4*k}rEsUchGWNc!&^h}}!ZiUSNM+;QXW(Ye_hHR@qr63^)B9paAQ&S{HP%%Q z*bWdms&88)qH)2ud9Prhi*w*+ab z?mfqCWt9}%eJI?;M(4ikzMF97UXe{u0*3y?2hvJg)23djhK|VV>XfJy%X!fzDu}&=z5t*LAoRp7 z0L5qhNrKIbiRmu7)~6eETVrUa-!t&8x~Pzzh!WPDt(J+YtZbiUw7I!?{40k9g&29^ z;Gi@i;YloE=A|DXY=qFtYH?bG0ukiyKC=wpO&uxT^upsP^RHGyfd76{=wbpOD|}mt z|2q*Aa*2<=)6k7)?$|Y$XlVTyTz4?qx7MYU{!+onoe>UVW2=EglYu8N!8dI;c+CZ` zYd7L<({2*Uoi$AIbU^hVq{H${Z$}9%&%yZol>_|2?_&uG7yuMvup7w|>DiTlyWr*X z1=w`PObvNAUAt|}VxE(=V7 zwmPYfRR$J~H4a@<-6F&i`7kmAZT>#Ci4WYFbbD+xDottTIo(A!?9N2r@UG|-J&g<%Do%fsWD7^qVgu9C{!L$J`H_gpv5IO8Ko}jN5Fyi zuFq+kwT?4{%VBOgHz|5%o~a?W48BYW&%xQ25y&`zS( zYES=C573tjtdO+#z{6sYUM?CvdY~fP%YJJOZa>Bji>saDLN4=58b4-_vsv$H%F)_Annh90lkkzk~LRM#_AfRzewuTFNHhx^(amw4baNn_2oAov`gul9y9%k-%*5 z5%7+w3Uf{*Q$E#8jc9fDjW?TF4AFe@&+xJe^;zaHS`eJ{RkJcI=aEXiIjA^zlMHV> zG`QtktBK*Kmw*`Fuo3cO^vMls)j@U-itmu#hX_0(pnTye&wqJ@{opVL8<%>v#7f8N zA?IE!Q|ain?Cx;!*LL_5X|z~HEt+li4^nQsYQ+Y@Ch^GuN6%|XJEK_sPVKls+Sc6I z-@&g5qWkXmx7`Q*?{B@<=pVNT*~Q}sg@rSlR-G&4mX~84D@g%p`XwFAwki|x*cB)U z171ITBQ8K10<^lzq=4T1Ed%w1wHkHQ1Z1$-EV(Ar8lJ|`|CAKSKNGD;z@?W-B~9$> za{-Kf@GoTaQR?&0iGLSmes8Q<75d=wO4@xJS;I#&hry0_h^N);+G!_SZEoR$5wV z-2I$A*T-Z`5xV6Z!TaHI4TtG}Y8}W|xk?y*Fs#ylhaL0julcORguw{uTSNT&iQt<9 zFG>3kk6o@W9Yk{~+#(6~J`Z)U=OzoWz{wBnPNSY&;XlqgXhns4*>pXp%g~6`Ga37v zSD00+elHp=d<3WV1SKyTzyRi=Md9W8@rl51CTwpx{ieF^bF{cyIW3jHWoT!3>SE;k zS)KMLha7ptiGwa(0josR}s+Q*EVTRblQlc6y?8965)nc;w0a)}X!f7M@`Fa1=g7QU9WeNOu$_^G3% zvR>xkLN?fb-bqIwR1qh_r`q6yE~-+vArlOt2!HSZH2#)z>Upc~c9!4!BC_Z7VMW=L z9voIl<@m6WNt(Z#f?1T-L6VGK^Uf~a&P|Xx^Zn2^{>y|yqmg2P>NIP{^Q`8>BWHD# z#if9vWD^mMZg<9N&r_GmiVBUpX-x74@r>7l{8A>@@_C%ns+W%6TodE$?9i7ui_1

Un^z+iM;`R9vaSanNKrJMKIt4_xG)(S?NHa=+w5Q0rCk%Uugoz1WY#gw&~F$& zz?|6hcQiHqH|BWvN(Q!D+&eGyGCy3z@YX%<)2--0H?W4|`T1^Ml%k3=gE~1T(n;jS zB_*4vbl=ceNmo4g7%vBPPyN=qI6Tu%%sw{GN}S_gc&(>j+@bUD;lDV<#l$#TO~!w8 z`}7PJNrNDSshz-2;B4_X@PMPPKuJp=NMbi3*k@aS>>KLoKIIEh-ibh1N3B*mss}?RV#o>!asm>;*mkk6kWU!oebZTaL zF>p8V8|s|oZt3D}=({(r=@9a;6jzc8+~`(Y^&c`Ip>-t;ZQjm#-bHa#Y$Uoq>e z({Xlen%c{^H=D|i>7}LO{likBx2*@S{oAYi^Ru(Lo3itBdo(onS}k^qpFhWcTD7=e zkmOmKo0QKfWuDvqwHZ5Bg9#Kq$w`>>md&4DxAzkhYmX9#n2GJFV1lW1!ZtA)j)~3* zGdz_hB!#R%-u4Eq*H(M6=#kY0=)ES~w0rXei}LspjpX+34y7-E^zJ&_%ggJQ-zGhJ zM6wWNY;5dus_NIDKM?rs7qF>a+R7Vi8?5f-^|wD(;h2uH>Y-ay?7{ag`M-JA)8}Ov ztb-V2|55@lO!Sj^AMWl_qKBJUOskr9iJEoK>x^DAmQrp!T!b_m8JEE@Sm>!8YZ9X*Th#qw+TxO+QmOR<*F_mM+J zga{Q`(*#k%iYw-!vw!@kd+R}4%xP%g+^^C^<;tyBL9Xl_CO)4k9J7*kWt=u=my*Iyny z5@4*-@9@!*_y-nqjAaO-@x8s1=_D!=rg@Nm*fxH|RkBOhKED6Owr^eC>Tu5|gb!w9 zlvPkzZ-0))r=cNJoqv7aH?Lh>?C?8VE=gv9C%CMx#?z?Ktav|OIpo?9j{U3H&to8E zQ@^cj(+;bL_XYIG@LS(}b_ZZ*dQYP1L&+rn-^?#%PpG=a>!v9&o=$rDQeHLjnS-x3 z#b>p{R8un;GhULMAk&vGoDpQa zj3jk4Evb58GzFzr`ky9*O_zy6R^xlgjgS86c}gh32A8^80$x1xK%6}Q=xa|kJU%{8 zbcvu`H3ubUNIbT-7CR%EKC!vA^;9kGN*)pmp`lNr$_+uYt*H;|^B2MyF5CJUmNmQ9ZYg z;A?M^4E=O1I;Z~r@^ajz{DB^yJ5BGvKka(zAVIBuP5cH{Q9_&0v{>2)td?1WtUC8Y zgzBaGZzt7}tamO6m}){_%DmJakNEtauvGIbsJ<7q=@t z^!4@{X646?%$Ut|l(G(x3bGkH_a8>md4huH+!e>f=*=_e*#v)b=|iTBSz@|wZXj_f zDJ;N8qb6T8V#rMDMUDI35lXx_H5F9bPW)LoGG6IUJ+(Z$^eYOcoPZL`)wgj?oJesm zR3|MZK(ByN$Vk`H9D~NJ)6s*asNvP@cf=XT>Zj0_B_-w3Lob`pL_Wxs)!fWKJzGG$ zZ$+zI6N4w~pZ^J1|Daw#^~K~7lD9>1ak2ZkX;7-EscD#@;hul3dx40Ot`1RQF6}Y9 zBKo(GCP?jkP(^@^B!I(kcMKGf{uZf38Yn)AsX<{6o%pDFxfQ_%gQ<#6A+8Gr4GoJ- zGyDfZ@|%&SIIgeSP`Jk3n}mjIzHC9bh_!U##Oy`Sk+;LdD39xSr^Cik*7(Fo&sZfD zSYT^^48q8NS5>F+&MPXz@L|K~-N3?T!9&D8cOYT5j;^(oqA;uR=3MQhz`S=p32!eoH!YSxiiPR$k5wCoLuQF>5@E zY!Zo+kTJ$mnL`PY_)hgD7!ybn+ZHiOB@nV9t*kw2RmPcKehT8N!| z*S#hlbJ20}^ZdqY)p3zR8L1xEF_AS_OfdgT^&APM=aCi?(<;t7;q` zA=OGQDwG4Qd%LDPnT8;E=}*Dw8zdSo?srf`88QGkGBA@sL0hhK%g%xK?-}OTmzA*K ztMP3zHOlt(`N{|E1p$9}!cI}SY)%fyo-G9Z(46voL`>jo2t_Q3Os^>Y4!}PP(7;cJ@BNYiq(8??L?dcxI&<%@@bVJC$(udzaOWO3n&>{P0Vo=Ij zS65gM3N9aCE2Dl45I3Qd7dF!(^doCBZFSC4rJxQysbuM!AJ43eAj$Tqefj&8 z@?p7qrQ`n7-G5qBTn!EXCpi4`&gGUEUl0tw_m81LcryW;O%aT9n*|oViL80ICpg55 z{R`CfQ1&1QZ+Jk)B>B#DRnPj!(t)yhm%KfDWka%G8?b@z@e&@IO@fSoDiyu0l$<|P zfbyLCkS(*~PGfh|nL>&gB)>-~kusKnNgCg(ps;Wdg^D(tEJxtKv<~0XazwmIYd5%;50qhX z)H{4uq3H<_OjZ1xoef4JJSa#_E15)5$DJK0yx6TNN6{~WbJo)Q_QyUeS(*&Y?Ip*? zdI-;eWW6&3c3z6QldcC1@9DYCY)m-Zm>U$|zD!dC98k}H0I6fr z)6!CJjpkt8hLaf}PL)7t-W}x!iybRF`DwIBXlMgXZSBo(H~Ze(O`un=u#E=0&zr4Q zCdYHwb&l>%V$MT~UP6tp!94|h4qA|{v=7FUvd?3)YE|28Z7way4>*ax+phW=w^^>m zmhs1}0fAAhOvZ^BBF;3+UHZL?`asR7zMJ1^{3+=MuU{YI~95A|! zHLOok67B*&3xr8k}>b zhNyo8Ukbl^o3FdgTYxtETIYvUR07En^m91fDFd-GHCakTEXau|{UR3~bbG5b_zNd-haFF)?k0k7{p?{c?4HfJJ+y{X6;+$H>pAo%XgOc zH!~%rLnaLPFE>bp)YX>11Kp$ssr$vKf01}L zyH5rsDV&zda%pO97L+UQT~qHHTc=qHk^qs}narW82U=G7c;vh#|JHkEjHuGKz|DGv1B zIJx2=#{uNK&Y-El{E|1T`~7R~2eYB}6>kaIr!uRKo7b|PCIz*)d1kBU;~m_j%Wrew z6kJ?xXUf?8>VqYK%gxnyU_YwBqidn@FvrAbmN(wT7Dj$0Vezv<&C#X~FR#SKG0EJC z0Rc{AMGN9n`Oppk#Y2is4i7RQz|v!PqWq~(!<{KFXAFjcWi5x+OX1v2T*H9;Aw0FM z>1Y4Uo5u}~<3{EDGsHK-Y{4X;?!fHoES2!GwhZ7YaNgNFS>sPx{t1lY`rC_T&{3Hu zSjFMi^WVTUK~FD=f-(_137no9;w118t~5}WmX_CAYM-1egLqFih#jp$NZ<`WD>7(2 zJ1-5Q#C)O#O50V2a$Z{Wb>tbqj4$V`SpRLe|AnTo_DrAxb<#U+$>ri)T+&bA zm%%h`JEP^^C3LZ5utVC5iaF=)+om5rjn`o}6v>?6_Kbz|6Q;%)rBa9#!w|LNq`5s8 zC#QG@Z+2GN4R{n{DliUy^U5SkzvZT@N;~F^Nas(KaaKZ~E)Oz-slLwPxei_9r(ZS7 zZ-3Q3qL#qyc2?>S?d>7J3{l5#ji1G#c37A*tJ}+m@-MFm%o@(6foobHf|oFN%I5A_o(BGC^{wEEtJ{R$Xy%&1GmL!fV5`cH0(*#g{+%KCBf zkejv0Jw&#Uf`;vUjQ-)vNtb>IBdrB)=pPg9|9TK{{zp0cpTm^~l)GZ$f6(xP?9r3U zqoP@+(A%RJ`J*G`GRFX^QBitVeOD0e2h!;GMwbCjs32i%;iYlAGFND+$@%4!#ZnyQ z2z8Iogif%ymzbD@Sa_RXm_z>J2@T*tLA}UlWN&vMLu_Glb2=b!Z)|I6bS-OS zWi4%CZeeF-a`wg@D*ylhR(e!ebW~|{Y-Iodc$@{n!3lsc35zy6lZWiK*lJ8GS6`5nL7Y?0xsMEXT&yxG3`tDch2H2idv_t&bb#7=pXC1 zC~DW)cc0o-YkhlF)!zFYcI)WbkIky9X?y$6pM3J^r=NcI*=K+0tNd3yqj`ihry_2~ z3WfV$K0|Gxr~6<2zocHF>fc=c`q#fo%6}O>@I))7wf$R*bWN}y_rHj7#%y@MOTu|c zQxGT&DTNE|^k?LgPpVpX44tt0#Gl-97!dr!AAY}U*M}eO-1+ufZ@sl+M@F;4rj#Ty zNOtp`REC61KKkxE86ovlK6>}vci+n@c~VsoGVj0lp6GkB*(;Q&e(?StA;0s34@gya z?#vYIwAw2ldfF>AB%8Y=PqUI{|NCgyE)@CbqmLy0PgO@g=HEvjeJuHp`9GUcb>#O5 zDVmi((2Jzm|Ni)gjQsJB{GZLJ?k#^pCG)30Nh&4$k9_jUCz2w6MvcgY(<`F{5o?po z?h;f@Trx)N4*Zpf9!<;a_l7DdqG#lurk=Z$c%tnlk|~f)r?pAlj&M6jdrgD_uYXn4 z5|BL>A_n7Fk!xKlo*k-K(?uwDjhsTy3tR`;<21&nM5NsFL`416Q$#+>N0Gdcf+Bff z3Kd~fN~9&}!lBQvI*dq=7UrPxvEiW600*V^l8JG!w;|Fk;<3-&G#wx+ePp|h#OY~+ zLdq=)kwQ3#V3VXC2Sq+|Rm9;Sir@}jp$kVmOw!*wLKjkkH+qE`bws^aJXur4fh(W_ zRd%QdP~s>gFRAj&gO}u#bOJlODf`$zmm}-9#33m7c27Zx=$5)<^x8+RD4t03x|>ak0M_U`+^x#!@gi92|-miB(y-Z!Jrh9_kuy$q(|Lu){Yl3@gSRp zp-?3m5*Ciaj}?L%cS!1#7!;QB8VqWWXo*0Oq#y_u6bvE-LC_kx+P~`*q6k~ol_c5-I$$V#F&vDb zuppi!N3N=f1s&~%g&c-C$sg?K=^@pO{7obhL?xpfR7i&^-LYj$adGjyIdi5=nKEm} zj5VuQZ{4!RHbkLA<9J9&rht&pwUsamgCU`?&s9eRg>Io(ePnd(6^g3YA!P8(LQxnG z3@Hi4hL(n!*^@^NzYv?TVt4b{CtqW~)DqCffO>YZFtBh4Qs{Pv;7?Q~J|dP;LdZ_E zP*ucOU;=`O8hJs%Aeu86+}_bqR$4lL?p)k?_RN`U*R0vPb!$F@V#y>qXc=N5g9xd% zZr)r}R5bVX*C)U7%FG!vR;^sQW%Fi_T_hHC90B7wj44CI$o>wEZKkKYyA%S>ol9eF z+O)B=v$M6e6%NjvK7GxqRaD4_pk(w4lZxgGH+@I1e;!5~Ur1csEAl+2$u&vWNl)2EZ-&Yr`{ zoS|`WlQDy0G@c>p?&>NoD$2>0)hk!>sW&LL3Ih*g7$O^e8ya`_bki<$3|}9rYgcDy zX=&;Dx;mN?Qxj(sL#K!9YHL?6U*5TWI}Nd)LPAu^5qtGR#UMN@DL5oV9m#W|Fi#ZP zxk5WY_Log0Bbo>d(=;T9!W6}go3Rs!9=U7=iHx;#X8e7$0yfiI;UobgVW621Gz%kU zIBe$x^MPRg(}J$_AsF;fh*04Bf<}UYnL!{}URD-!=M^h(`Did`GwDIG#S792TS`hI zm07WD+4gPQ+(EHb7#QVXbjVGJ*U`~Ia|%RST3T{emx}|nX>Dm{FG@PMZ`;1DL$s@N z`+M)cLv>qQS_rV&GaeEu0SlfbR3?hpjt?xD>{}E{nns}+Ol1@?bA@IFIk0kqn8lM2 z4Rxd}iiCiO*jUU>m5dg1Zb_tkuVB-Bi>*Kif8H5!ju8T?XrqNzMYQE@r+6b@myBr$ zD*pB%1dVM6mx)mkGonK9>AJeQq&veVK+(ToFpWZpTw7C%H=H}*i-hPXkQ9rAcvogPF^I_YeOEz3j3}rTHRj z$H?eWBOiX~H&mEj+}zg2AD(vVDLQag3~+4|&@e@LBdz zpRa!9%gR1C07Pv2BX%}$+_$OMCkiIpiqvmaec2z{Ji1FP6_uv2c*s%qLfnsSm zvY6}r_uV&k%oqzn)PtZf5Ep#w?cLMv7;0NqTi6?%Ck~@dqE|qC$ zXz1$dQl`x=-8QRa+pOY_nZ+G5iaK_F{AZtow3MGAb;3ZH0_!`rbx;FDI%&az1r7D} zeCnZ)aJ03#naKhw7uW9YE+R@IqtN9-t`*-o{4l^n{shn;ebkXRTz4(jk57?GeTLk86ZN4T z7A;ud(Xf+!vTlZ_(h*l&ei^5UGoSFCoGEyR-7w$_&)`+QwvZcJdY~X-1 zs0Qhr*MB$QjPH{_d*FbI@-nFmAt8~hGHuu1GgPT{5>@b*J;dhrw#)!_Q`T z@h-6#^uBxVeRBMG*_m#@UgelL_N%W>RSxn|Wo0EM1*S&~A3kZ)q~X8)t#sa~ks~no zMT-_)cIhwJnRWg7^XAGyCQqI`ZQ83kE~ZN=%gb@UV~#oc`RAVl!IWQo@gT~@g-{~L zr=NO?%=f={x)&R6cR2X+i!Vy$SFBhL<0ifQvKtCeg1R*fRjA)^wU>fc?E@V3gid}AwJMi4wB~@DHQ-HQ=7Xet0O*h*H&cGe zB^PTDCh%5dFo^FM00SXSh)5MVG-7=7(Nz!TA(k^UGgifg?wbz!?`VT*!%wiK@9` z08^(VI`m3t0 zTCs8k69gQW`qMsh=F9hzDC+ywGpls1ZN?@%c}T8|{_jUGawd{PE+T zAVYA({uSAnhT8`A!l za!{<0YOq^dYpWQ6tbbuEs2T*<;j&)g`mB?Q60yt9!a+GE=SY6D;o$AJ|H@rWX4a3w z%P+r-hI;goM>1|U^bX49AeaIEfPKrjSY3ii99)QcDhKHZcP#}ELCI0!Ux4}qqt4x%Ma95r$zo44FDgn)|+8z~nX;R0hub`Va;UIvd>7;#VZFga} zs~5+z$)D({$=PvgOkz?<<4_H*6XXfd9v?SW4la|ea=tqAAgwBcgDbAMoJQggg`3R7 z%ZP*>fR{9g%yJyp%Ry?(#Z0V)j}YlZ05@{@a0%W4Hzpw`g)zI}LvR5#HuS}b6M_0a z|L8|t<}uO788u^Rmt9&_S^3DYVdR-Ld*3lvcZ!@mm6xkBc|Mgd<+2S2rNT@K;(Rhc zC=*b8+SZGn=bjpW$Z z8)bfw$-x}+gQVH{!8HxttLr;guJ07oU|_8dTWYE*Zod9%Dsa_h7n4#Ms^qeTe#vWo z&?@cDWGACh9s=Q%;GirYlPA!7_0?BB2-*yPlAZP2lA0f6m(3eCG;vt(UUS`i_L*ns zo=gAz=QFZ0b;=aF?S$ixlhL;fNT*Jo%&1v7C_Q)TDJQeZ#PFQgUTaqlBE1~M+*#v? z6bukveDTG(bLKD=efzKe!@Y7UG-YZ-j>*~xvx>SKhFD%+&T+8pM~4l2>4g^_d*l&} zb=0T~Uj`4p5f41~?192TOkWNHC7E{I(#t_yMaRX@KKsnz>}chZK{_rL4k8W*ohMRU zR<2sLa`fm?3*LBR-rPCgJmwgJM+#*+Us886k!`wC8sU{!USSmfn+G4ni|{BJ7c*`J z8Nvl{L+i;L=&YGDuf66PaeEIy%0b|Sa{6+7rUDke`Q}3pK1e&%R8?{O6HoXKgK=n^ z;ULFn|GkywzCBJvX{c2g_$a!iqKxW>pe*OCM&N*8+$oWcr z81^#pC+Dk+9+EOBmOljLfKNDxrO_Z*;Ph$JuDSYZX%JZjrt;F!j95E*Imkz-bRvM$ zaq%-xKQ;J<>)GexL4#}n=ng)3ibJL^X@^&*PGJg&=@&l5W~!^Im@Ai|$@^-qW5&4V zN;!9`afG9fKI*9_$KP=Mb(9af=%UQ9Nyo*GDhC-@h&wl|ubcAnL{fF(`t`MxG}hO- z?yO$C*Zd&j+@^-u+j_kDK@u$yywJC>4yAu0rF1cO|BKocc2ck(W4w5_d;8O0CPy9l4V3Gfa**zO{<-IlIqFF1``sKoR=S5XjLG*qIxmj$^>#xb|HK%jkwbydh@y5>TLhh?;$mwz<4$HmuXP$Wm)xg&Agz3{> zWgquVHaeN8_awpyDo-Qe&jSXW$?OtLnK0pbFiK6B^wf0&%AbGkSqecU(UWpe0pu}J z1d1_(rpITVltSk3I6gx^M#s7iqauIE{3dOsM|NC%nJZTbD{nrbB#zANtrh|opGzI4?^||ZLp>n>uWG^Ycec{i3y892oN0x*1 z0@p2R5bhsP4pNRSY2|sYR5^(3jt3Fb>07pJu>qiR5FBvlmbXrV6?v8h2Ql3VxInL^ zc~76_C6bN zC-0^T_ht*Bp?A-X_t>&`^NpscE;rlfz(9zmsNPSfi>HXP#w}DZ37h(*_E5;)-{@9G zOZaEb!aS#_CvIBCWuNY#ZHv0i?sbaoFIU};{xJG@oK+F;Q?)~toOY|Cb{_gj)jAQ^ zkM(u6dAc+E8~=^pDQ&Tc#MrVv%a#p|4f9@`L8^6VYOJSZV^brQsi-IiV*E*xgFQKw zrN)|?8m#9d&59TR(8rx!F(@M2dQ>+^ruv zs-8eOK7x|j^`Up~p{qC^x=WQ3ToRQ`4kl(dP%;@6w>PPL3Li8h$k6-gJhDih4HN{S zlGhSZ6{VFGvkz(ys3m?KB-KJ;9Eo>EfwM;TYNA~1+pCdD9QAu=Z~Qsk>h!g10BRMk zsi^&^MX8NBR;SkJf=?}0Em`PhtUd4%UV|_>xZ|yii>X7kx3{I;xwVB_xqjNTB{M&` zK07~H*V||zeO+6{| zgWHue)QK-jR;Hz;nR9W;HoLTAR!PUK;;l1_x61rrjDsb`MR1V#76=jy0w)>UQ^=|i zJ17dRYB~E*3D#?tE?&e!`H;WbBKw5qDFeAQ^9wS~DVAqIt7}lp9IyNj~;T zt$KHS(~`t)>i#s7ux0YzY+0FQ3!Np8z(o`W#yBXqkMoQ~NpUetGd!cmU1_XuGH37Z z!juJpdRncN9M`peF?-g z6U8lECPuKR;*n7|J!*I}Vj=Em-q9HB;)pbd(yuUFR=Ndhp8FiNHKO2C_Gt?z*|%@; zr~8Da^E^IMV%5U2Py(ta(ooD}&@7%i(}S*il0?8qipKG$8iEW;%gd8IS{b?TivE>- z`pZF-)i>_30=FzdZfI&^qBOK;yx#Kcu>2r^JVGzG$Z2yVt`+PP0N;M4VeC^AAsQeC zn3xsg34LdxK6@0xVuXwqj`khep%Bt?M?{ZfO0?WaYEQ4ka@huVmUlT$GfKS>_%u(_ zFZQXr1PoY?iZ#3>H}RM>Z;W7?vpJJ)BtyDs!-h?n zCzzX19#?8=Y-BmSsiD59v7xb{o?C$$>(@7kuCGJu*VnO-&(kTaprds))hG|HvXqla zt14@%D_Pa4uB@oaD$fH|Rg_nim+=gCWqD}@T2{*Yd6;J6S|rd z14CD>ScCSOAEvkt%^GLXS~O#;#p~9VphalLl1qy-hK-R+ip`g0%pT1+LSERKjze0D_WG2#n7A8$hxg%w;*8>$jVOMK&t^Rr{}o5(E*3KQHILtjo0B$YQ-J^} zSZE>5BBHIOrM(qx1swonGuoa_vydUr9Wz3n4s`1la3oyXj%FV#23dfs3+>!4C9adUOsPkC#zf zf{N@4iyqH@5Yjx4VE5s_WS#B!m$ZLFVAM#c?;^hsr!%^f^W3c@+u6NP1v?HUtD^lL`Jwbc z+!w*0hIPpo;v-4dusfJFkUkR7y1$#ZdeCzamfOw#(@@jgh32lqyu!nN15reIXN@@L zkBbJF==|1bn2aP|w74@3$21?)nx{mGX4ULmfE0KNNMXp+wngk9Hx$cjKU(%_-V6Og zmr?;B(og)Q13>!`Ny-8D2>~rG4nhv@e;Ou3LftGVJnTCVb6I3$)GV8ZiD4k3Nci~y z>wcb>l1pn!b{?FXmUQEYDEoZOSyk$3&xY`4*+&MAjC}3Y7wHTB3J(A^O$LJpECJIT zJP>H8nq;DF`Un|KcxWKl4nq1W^Oyi#2^qwk#Mvqn|`4YH@Zn{LGpJ;Tj8YB zcCZp^RU7en;0xd{#y-2|mh%}wppVSxpo0;OC&x)OqH_pDj!c zmeb*(F^m2Jkx%WY*)w0u#1IfQBa$sheT9J-(F9 zky8GCZz>rlbpy#giVI81KB^zEX)|8PY4U8S!WL4m1U2OmP5-S|;?>9I?RiN?Ra=8p zb+524+{oGs+ApMDuW4_h?pt208E9S%#vz@tE-evt0y^D|G^ttpC&|}qM1^9W4_fs; zGM@hTQ_I27t_K7Y(-5@X5bVM<5FGR>cO? zDm2iGuupb2(>sYvxTgGB0DtL}KZ*OGRl~jmOS#wl0Og=SF05NS_mWlkW zVWNSf2^G<>laN91C`nDAVbTyB)x9-iH&Hthw*=9=yQu-DLnRMeX-)qvCFZMh=X1;| zpy_sqBQIyF)YG1FBR;F_V1hn_Ji|WJurGbh?mzy#08lmVo7S%nad01K^2*asAgY>C zb2><*Fcc@E=9Su3-;PF$cJm$7BV#4?Z{WOpZH6T(hefE90b@0uJUNKrf8wd zGRTfR=5cL`xC>`?1aso>_ z;Z3K|mZvp!kaPvyyj_LROr$kzsu^7ki+6{9 zeBr|S>4B=bIB0e{2xw^AI*me;S~w=L80Vu}WW-*hluWB#`K+l2 zdEK-nraWIw9w$^1&)Y&0g_>!lbgO2vfi>2oTGGW~7onczT>yM(W$5wARm^e>+p z4j$A%)D9@LQ!NuU&6|me#WeoV+_*H7de_f>hg|3 ztE>4v1b&V|esiI$tV|#X86`I4*Xa2j`mI~H@{7!Et*!BuXjhZ?OB<+Su@E5ILc4?E zV+4h1GKLG&<@Wgf)bwEsuXB=mDF|BLrTZw2KO64#&)!rI!R~)Q2W2sYVQ)i213$9N z^dB)G)Z+(cN=r+DAU`6*CWV|PKYGtEU9kxVmoHn!Z!srl8*^^Pqr%i^I!(p^9Q&FN zQ!-8gTWHJC{Q0HY|NYbTDg3dygJRf6B*;f_kOkz03m5QXg9UvZUl`g9JtCdVsJh3& zit!87KDjBGP(II(?2FF$E>Rp`)O-3$R5r~==F^n2v(^3|N=lM7*K^dfwy%LIv`GVb zzX#>(dQ7JmWHwggCVEH%`oDsMP_VtNl|^CJf~%^k;2lfD@*AE-MMd0$#sHZ-fgv2^ zCr6Vn-fd4l%Ez!b23ZV zH3_`o&a_6lL6XfI=bb@bf9*jts{g~@nSNDuWDER%c<*&*9yNoBni!LAHBpl|By6j4w_qPE5Xl|e)h5pYBXH7cN}Ofm>EiZUvw%$lK-m9_d?ztu1Kkk$G0ez2k5 zdG|==UM^R!@77tHyXw}dQ>UnN>Ua09VV42luyOD~{5ROA8B4gmy&X+Zj9=L}wWg+q z?sLYAMHwU>TNnru<`52IAj3g+q}Jr`J|FD?t2p6eN0Y!YjM&09dfHkqGDwVy)UdmuwC>C^ly~mohQVrIjCn18`?KC)H6N|??9XE4P9Mb zO`i^7Q4ER47VhckA*?tE8GAXH?*<1wz)g3WlcaI->s{_;$E9;LMBKoZd+gI+qAxjN z3*hXG6LGn2UH<_ydYs{)yX?RyzFbp{t_R~}>sn^RUNWdF7=w#c_r$JnGZL1fJ&91+lrbzCH zJGav$aI&1PRkaG5-T&_l$P5R)nuDSM(#cd;U)R{w2+mOYa5BfZa9LTIKv1^x`Nx0#qobn( z2uc^B9AXp`5WARpHfgD;MTLbdt^3jE-v;M-VLhc!;{kE2NQQ1koZ_H5=d}=&DKfRS zwLlP!@9%w6V@u0Qd7qg_Et_9J!?yM|AShjg97-P~00H437V-Z5`;Q$xT3uDu-PQG@ z&%X_x<`goFxfysF@r3+KIOxr`upD?#fPJSbFDR9v}o<)8okPt0DD1$)uN1uNKj2JQEy&MEm zpsKm4spfJG0?rr)`TmFRISPvkfgl7Fw@UR@?jDesSe)l*0BlJU%EmT~ zf)p1Od0PwDm)7U;cmFsDeE=P(19l8h{LuH6F%Lu{Ayj*NTW)?%dU{$~dRlc&HAKVy z6>l*t{(y$8oMTNLxo{b?shK5%y(}D5Mlqja5ku0Ln3%}Oh#^4G-+!sRG@s6&=j1;) zXVxsz2d`S`IRw~mF?-g>@4P+r?3vSElA#h0W$0C<(vwBit>NGU#KC`gPW2O0m;HnB z3&QLc6gTW#)H-{)>i zRu)^8K>}hI1^9)C<;nuj0y{jHlfyp4mN$k1zr2X~Ke z=WQIMNQQ%+77ubz2#CH1u%H#H9HhhdL67CQ-FFZV*_Sm=AdV z{CQMDhC~=6CIK9*ukVMT0|ylZ*`*UraNE|cLx3QXl$5~hGIHdIF=Iv(h}sdUPCfMd z-w~6Ql|dL^HH4Ac+RK0Z!y5#Yj2!W-;$UQCI8EZwM;?CprBTO^9aS%7V8HShUw9tQ zjvf7S`1b86#a4Q*|LeNXyqd}-8+tAc{MIy>}*)LWnt;bCLPjv=?l9)0BX z@#A84MQbhz4c$C*#tbq~PCl%uE*!5-pY|@v-WdN{TIvzi)HM&wTB>2ZomK30tEo>n zYC!jV?WHvwyw@C*&c3hj+-AWWoPu9n14p+UiiQ>-!aEKE=gyst-5qoMt7GZu>BOKd zfm`tyfxygr^VYUb^pZfa)2gg^3i%H+4Ug@%$p z;Wb}`Sla;a?%Fk9eDu+u)m55X(4NV2;=~DrpMB=(Wy_ZO`z@IygoxJO&asKS6>NQ*^5`g@4hpY2dgQhhJ!_g7v$Gv>((u_)rAW_ zgW4JjP|+WL@IExpICXNc>iYJZug8q;Ef~L8{L5Tu=O7PD!$FsB#~UrTgG9Zz8#!Vy zve@fZQ=e|sfbRL)OKUiIPdV7fKzv2tp`Q+Z;q{<%-~eifB)sj4WKQ#=D@RmoS}*3uqGGjGnE-?ZhWR7g`(Bk`BYO66-|LB6Jt8V;U2 zn@xb0d2s*!!V3j-KIjKA7A;yRUp3XG3Q6^nlKwtB2YFZ;4i4>hOk*G=U!Av$HTCIE z4QQUPy|jjd_l|==5CmcnLnXAlj)6l=TF8ZR2le;_WI#}%P_Rfm93(Ng6nnY3x#>b- zK}%~3`_Hjx1PD5CP(hH%hEY*b8`iHMG6a#;t5@-^jC%0}`Ju+3;H%%_&+Qy+ZftOd zgEV{Tb5!qr{GL4o>{3%*rLH=B@Bm?6baj;;TaxF~Pd!PbRrpC-yLJusl=Vhv5S(%* z!zZ73oJdhFTyb+ zp%fCKpMm&GWn}}Zah|@ABK8dBS@3R<)t-Td&75}4CGu5MU8<0c_4TC8KHaYm78Mog zAC|hs&~C@uMN0l(>+P&!uUk!hx>Ez1=c_NR5yT@L{+vTrI2}Z=b%&&6Tc^p1km5H*C3*L7>w?1+q5>bFEn1r~pk0-EY##StBC z(m%+*%!R6{>mQc7#n5g?1@4ZCp}$W<7Kggk)TbLYpnJad(i#qWoP**b5CFkD6hM0M z7}BM84|oX&;Trq`g#^Sv=9BAyAZa)h3P~n=Epb*XWky$Iv_#Hw$OwrH5Cg&R@Nh=o z+=L+V!Fw}%&5D^btdYEPM7}v`5@EEhHz!WSrh9$-YjL}GTbtI^D_17J^`_W;3?1lU zK_LSJ0%#1_Zcjb&IQ`$yLC?Y8yal=ymB7#~c`{z?w`8$sg{UOlf~Eq~mwf)Ym4o^W zZ?v}30Z$AWVtK6y4CF_S_@QwSm5qvJoCIqX{e>ot`X*`g6wp@}Pkh=tZ_5+GbWlxQ z|FG08hI~7QL1Ln#HDs~Xt)@PQSOfa!YcH+gpvO7L*hgna2LiBwAP35z^bBGe6J~%I z-4Ja~X@$U!uyhWJfsBTTE=d5)k&-Yei6pV8mYh<`pg<63T-&#A4-5=+grLtqV;_{Z zn>S5UH!^e%;*;8)?BEUtPE%teD%%aq0Rd6vFlm7xx*-}OScH0veh3PI9jYQR1QZPc z9A8B@vFi6bArH1-Cw7(Y1v17*$KR-WzfB%4hfc5Lw^OfNaAP5RUBsfF46I)n1 z2VozI9=wyKBQl=`6cSH>c&_4Fu9E%QH*enjmqm+Y?J!H<80w`Yl*fn>BSxH&;p8Cm z!dTJ8JODU#z3)JAJ9Ow!P*6~CaPSu)A)7XBVoo2Q2M->^l9kCcVh;1E`Uk8JAz1dSLm;uRc(K}^=KC@%+pxOjnsz;45a4eQpeV=SL} zTTFVwZ^D87`@_Pvr=_ID+C}z1&Sy^dwb2A*|$ji&4=kDZ*6BjR*u%-u-xu&{0KR=Jzd8~p2lyDHD z$qZ^Wc~JZ;Sd~E%$e^&AGl)J2VL$%(BUv$GAZWyh5wGB2S663cMR{T2h30{SUg}-D zqRUE4Z?v}HHw#eWSEI3^Aul&)-@YWg#mF){2&mT8!9FxZt1<}t=o~~JWcG}jGsrl} ztXZ=%P8m(mh!GZJBg|entA|azpBvcNy)SN+19XfOn!rr{mY9MIDh>_tEcR2`>E)*1;IdcYIFM>iw zbsHM$S=SA!Bn>1)F}zZHnK_0Gr!bdL8NYloL0TXv!^FT*x!PA81cIU-CM70fhO@Sx zJiWPE2bKgJ@`8Lc8QvdZ_l3{qeDiBo4pg@qb;#XC)LgiY9zCs+V0$Ds8dX+LbTbzpo ztsM5oIs0Qg$jcHmcME3E@P0`|SQw>xZsdrlsK{gKX+!&u3GfAP@wF3>it590Y&jVIQAb(2#(jP}UA}D2tc?{-v${wYWAcEDXCCP18dh4D9uCK1z8x$GL|x z!2A8YXPzQ_t+{Dn-F}VT3A<{XkrCmul~ews>5>m4Jj<6^{j;y6^VHg;L$~0NeL7`G zK72?u^4#1U>s`d#puWtSf{~#!C0V}?&-*jT*;0PAIg5M!*T%7c-(HQH%*oJNsuh6& z&R^?lRkz)|5xBhHo&A{6FNba4#_#fARjux>f83fHaOrk;$;9Vj8YO@hOtxg7+H^aJ=`no!n zGjkbtA9kUY`Y-jvflctLelO>^Dv?tI4I_hab5noAxLi|13lt~p6DLdv!3q1uFG=vf zPWeCllXA-6-PJ`$fb!3d>;KU&jUvq6X;g~3irf6@gZFV5!@VSiW`q9{GDlXg4i@)H z_+rwRSyM1FbfzSS$V; z^Vhmm)fghmo&DA=TeuxzyyVMW>{YD_pnu$h)qqR4i;jxAYbHkSp$sbr=?5&mc#*Xt z=!3Yxf`bIW9QH5NBOrS-0Xy-yCHru~M1i1kIim(c_iN?{P zhpL7~DDO1RA=ny%mlcPa)`oF9E6br_@Kua8^(w3P9!GnG8|vyLaY=FUsgoxp%zwct zs!qNZ(!OJJ#@rB`vAi3$f=Yg$?B?oiHU=8 zkQIvJ-IK=VM45FQbfzQ+4;*kPKQ+JP|GL*-yJn5OCUu$XIjFzZ^{TcDudBVPt)hS2 zgVlgbx4U~LJ`bh`n1h(87^*mZiI0y5a58&_&Osn3o6kWyK_OuW2tr2D1UcZHd|F3} zIRt_~|NL`taq*TdTS`ky(LibZo~W0!0War=4;`W*eKL1WFH>gpGe!Kor>3S5X0?o! zgW`l8BhHQ+hsLRt9i|X*in)*j+d}2qspk-b0VSPcl@%4vlv!UeO-VH6m+pw$Jw2qS zF@UYdH-^Q^7GCXv7A~N7$|BQ`|_|q z4l(MzCv0t;XgA6~JGhys}7vWt(ng0P?P%16$d4;^Vhms)pViCwd@qicEI!J&81_}pKe);7W=4nSpM&{<6$EO1?oQLX#f}&_V+3)3i_Tta~)5?^}^72W&e%^oA z>*qZ-*2+PwRGhGLiWo)}G>wz5{A@6+q5QM64>w!u5wGVU!wdHDVttwQ1=Ey7Q~rj! zs~FuQM?Nd=^=b2GPG{L`Qj@u!gZgV-t!gIuv;DcamA!RPT+y>G3JEY+W^f2HXo5Qg z7<6!VC%8+1;2y$2a0w6~xCM82GPnf|?(PJ4!kzridG}PkSFh^ce|BwV?WL^#x_5W4 zZ`T$^6etGZpYX$>%bPR511}o?Yok4kK>wB<8rIBEnw5I zut?EuAX3}xGGmdDIAbb$x2~xWF7>9)1M3C!o;)EwJ~z*^h6}XAMSBLw`V@?C!+kU$ zI9uAGdXeG`4U-=@H+{6v!HbAxng1afWeLr}e*IGZbM{=XU2_<38C4itIA3?zsdGzc zN?$Lwo#Woc*cd{!8}&hdqGSdVvkg_(F(qFqhJVd>! zP#o73b!6zy&^Wgjk8lQ?9ntP<%YU}P$^cXi~x-4rRoX7?X(wOTAtqqpb8BxqVFFM!4-7T~r}QXK*b;!(Q^#;CA0L zarCReWfjpUy0oS#ZU$JdHo_qqAvI#LZ~WY4bN$WOtQ7e5N(C+W7oGLXJlkzq8Nut|)LD1wG$XZL9d z;j)WK#YyNTNzetLmrqWLo;MkKC@FCp?paK`gAa5xo&H=tT=R~(sf=CXi9Z&>uA{~C ztKuX1BtGV;%2I2`wRec_E!SF)Wr}`^sRnC;%*A>JLIFAPaiU9@eoyzjlC{~{RJ63T z%Qs>)C-1DXM2+1#amI#BM)q&5XoGK2u_)^3L*{ryFSam}nQi-HR!QXqA>cnh)XlD7!(}$a~#ST zl0vmLh3Y%$GC z0{Bua2e!X&n}TZYgm8Dar)U_rcp;}@Z%pX1NVq~2(J_Q55}5~7IkBSmwbtPcNC)&{-oz`|2D37BbO#nscvX^h$=CzQ-9SeL{m&$*5okT2)AQ+So0w9Bn>CfMvbcf^jdUf{VRym}$Oa6|nTGUUH==}RXGjw1SPr|D`V z`tlz0jCt~_s6N!#_L|$4(jWB5vls)p9fdU6Zhpyu0o+0RN%4MSl9@w8$Le=nP_sLMyhNvjLVs8(`gGCjNHbz zM#=lw(pD1h zy{T3mgpie*m3TzO1n804yOpqSiX>wZ;&Hn4AYv0A7fQk96<-fb@4-b@lUBcq40W2S zG(5uX{w$=(V>2(7BV`ckD+)<*6F`f96hp~39v zH*oBrvbSC_dYN&fqwNu%Et7zE=WT<+uCmk(RyobY=aSMkE+k_ph2Z-RKU_|CoPyA8 zZMN|&@azl~)fCZ6>tcuD;$xdZ7!*2=YiSizP(Y&^mrOroNppNV;eD|~b2?rKqrvMl zB|;oe1R}&xNz#W2ESCmI02LAS=qxsVkXloQiVT^t6r{e^aGBQ8v&Bd)p49k!nmS#Y z#PU~9ctn>vRU3Ag-|maU?{;dEHp8ltIUvUWajH+C*1Gf^uSY(uxvsG^olNfvJBy^( z;tZ>-r50v5Pm!qNs_@ZlRDtLsV=IR3P%daO5ONEKFf8EJIh^eT$jH06IIH5YX}ow( z?X=S5SX29k6PLJU;_KI@pUSB`YN>dIY$=@n2kVIhk+QYG+(xDELVAC?4zLwPsz9rx znq_)=gD=;KEpgAv^&8?$2V+J3J{{SVd;^M)a)=q#Ig1336~R{IW9^`RZ??!3Gq`#+ z6?wV2nhz96`-|}J;5v;?2&$HXgKVwR!D}fOJ;fVQYt_aQrDzH)ir?RGmGF7ZTf(5* zRNn^kqybl<`>ml*snrK#?}jR-;AjsV?xzxm+1Q&U44Sr&P1U5dd(8e1mqdmBJ_~0U zoBeVc=S$to`^>d^fF5}BJeK>CGaw|VklSp;ZokwhtdZR6#hQOh zyKmd?Y)H7?yy|cN8_t@B-+PoT@!(+w&XF#GpvS^HysP&BB-wh+HnR<{S_&ZSqpjs5 zk@b*0kF4HfdfYE#+1W8>n6M5uHyp|k*=bE*PWqv-PF9B!qLGV@M!9;Ne;zj`v8p1<^AAChtHiW93{oUohP&LY{mbUQHlrpj zu3+WbbTm@*4TU69hV)W!U-Y-VqVI2M>v^XqCe-lhqq2CJt>BM$(iHFOT~P5hUc^LJ zX(1LEeeDGME&o&Z{f0JYn?bXz>@8t819^CYWI->2A_dusXV%q~ED!c^IO)xs<8PGD zZq4I&Yae^hQF36f_xDL+sPTBKgs6=cG+9$3!heA3Q|zoPT$qiKRHiTYz(LHg9S-x5 zqH@GWE_X;hDvJ57T~*?|6e!UOqI^ftw^-+Bxve%0eKL*S2Yc8_Pq`l_8s?^QU6~%3 zAi-9$!yofW#zo8O>Uug*JKy%RcD*8-p~h(%xGNj}hEk8&^!KrPNSPuZ7|)j=gKlYI z5v)6p{eEL}^9%5GRKGC|jr4MhceenfC@BM>ov8omsk_*A?hh3kOlM33=$T&wkIroozoqKI$snK8jMFf;%yaPi{Z<7(#jph30b9i;p>{ z>VAVz9G|9}Vc};~*5WjuaNRKy0Waaxkq5h!){0}Ylr6qG)pX$d1}6VKKqlQp_W4W8 zx7I81y$Np>Z)b_gh)1%97f)Hxp6`?O9!o98GCyaEsY>@%MP6Nb1l--~nJP|%&2GnP zQyQf#7TCC*lu>A<1*%iVIIq}tSdz0Dw-9)XVB*;gsDIl&-Y~4QAKZ|-ef|BrfL#x| zTj^Gf8xLH~Je~Q7w=^sAo1B#(U2CF6tuKoWfyXh6c$4kiI5oOQS7H8A>r|I5tAtz~ zdN+qMNh3OdM3}iI2a0>TF$&s-L8~m7gEQj_*2cA zZyT<>4pnwDU;5BYF@gw8i9UOIS z`o48?ytZG^RDL3&arMCC{x^mLf}o1YCdoXY#+e6RGEooCXX!(DAqI|Y!rH-gzAWnu zIP|~PX~Pdijma_r%Z?Mx5@YgQLQS98N9e*`?Mqq%j+tKPyOTXP2h5bkayoz&NzlG{ zJ~n*qd388D)e?s+ld+r#5DWoOHmnc}`qDteTPLC<S^7m znHSna1Ye~;63&VR>c0Md9JeI2_oq5IPt0}X3iK4R3vD!z4(4T{sXQgE!?TK?+q5s) zY)bL-uF@4$_h+C6y>&GFs2C%4ei1Eo*o9IiG-oRm)+B2IcaUDwI6B*`z4mKQi+m-Q z8!Yfdxeo0y;qdULqX`%FqMMxb8{TS+^sPZqLP(p2uo}IW;*}}ERTVcudoa;soc%_D z#4HcY?W`i0#fXl$7({=g5z;o450q;Z{_#rwB}AOuOe#NFiKb*beqGYAJPokUdrpfL(cZhL zX-3x#v*Eh<#eS`bl9(>E$Mz<7<|r~`Z4HQ>AKg^)G+UU55kneXnmytn`&Zo&tjLeq zBj+0D)Md1L9LZ_(8qZWoDSD1W6A9F^q@Hx~Orio7wa|$~JIA6AhXG9AY~-{rG1oma zOLuWd)*F!0JZu0_->VscnaR9sG{BFHaWcqD!@IuU{us$wEDSFDJN`OdKG}f=imwI+ z_(S`uF7NLziHjd9$(D~B0PznL|CKUbfr^XoC5Kelnn3gype_WTnqwE1NZd42Ayc6 z$RY9`e@<}+AmbWgZNRfec2KTr#n1Ef6ndcwOw-5$f)n?p0`Q6=F#0&6a zh!S3H_j0wr!I3@fxkQ;zkx)?>Ip~}Q_z;Dh$wDYfa&EpAs_Tsi{4yj02r)y%|8uL} z4;x!pWsUOt!$1(h@KeQS2==dNd;^-uk&8X5>>L!d+SliiXELw8GKHUJ9d|$evF0WHG>9Y}-Cq-N zcddbd#pKAZyYL${kn|rUQ-;wx1Iy=4{xsOypMxe9Q5aLMj7r#@Z=MMkASjt(K z!-Bk{6=7qt^)jrhU06vB?D!da_{5k%e}+l+HG6++EM1Wv-QgM9{z|C6^nGHhYO9Oj z4P6z{`iCiEGP+*Q7;PleGIA)%k@dv%Iv zrD5i1W-bEzn@XH}eKUn9mC*mSfqKH~-bgV*cn*SXZ{TB~U?mc~c0__=z4jEg*l*gNkv>>O>`Dia$ z({7pvMD||V8HC@9>>*0UT&Jipb zBo(wpz#60(18&e08K|WYlWlwlYIzv?LtR9K76aWyfbC2LMmLOlx#P-;&%#z#l3=nO zjGZbJMA!cUZ_CS9mDM9aCDRcLEfP=JT3b8Oywb3i6!Sz9?C_zzQt0+m3g2$RH`HOy z6w?t|f|ITbDybf6s|aD<4YrSXa>VSv6_jf?rQ78l(uPy4%j7n@HWvoO8trbKJ6yg7 zV{E_;d!-&{iBIiWPJ&829%Sssr`(22Js@6Hc^OnyPp#fKYK+xtt>TTG1I>G8AzM&E zq(%e4|DFKFTi%H;>+m?oU-tw8x_lrsB``GJoLY!PGcsd!gyDY1O+XDc)-L5`aZogO8BnP{xP|vc@x`~r<=0o#j7V75&@LgWd z;-zWGadxjZj3-ck{wQ80h zb3ShNo%?buV2b3}2n5#|3JB4a_3l7r=G1Lv8xc7>@1m8y01`+dtAuQbldour3+9$g zEnutRhj75Pk7{CME=I?cw1rh_EU!$gvjXbN=#rE~jZJrlFX?ymuW_O_7PTDEMhG6K z$tp$7%zg91zlky(#Ovx9kR@`D68`-;vgHH)H{lpl2>^5h12QR{a%lI$b#9-uaXu2T0r1GuPJ{o^2@x(f+-2x>4eGu zKXajQw{KaV&}j$_uIF&Zh79q{)vyCG7d1&IKa7EwaXEX~*x2wIp_?Q;-Y9Z0A*WQ* zDU5a^@&R6|4qhDodpJ?~YpcY}pnLc$I3gqIUSr%1;#Y+E5)S$j!9M+{axo$~GHft% zY|s!l@;8{EyDqLDxCB`zrX#o5^0eIXl#&u)d=idkd4JYzg~D;&Jux+2J~W7%iOCds z2}Xq~@*h4na|or3i*^RWGn^Kb@o@&ucU21TvHuBq^o6Cu#m8?+Gg#5;>Px~7-PaC) zHIKs`=%N-52x2$i>ypB{vD|I2)nJW7ka6wX)T1Pos6^%7dtKJ zx{0L}@H)M%Ce;K%Iw6|^=d~I+s^BAe1cM8xDK^iYoPCpu{`z1HQ%WjgGvTimS?EZi zg{Y2hD!*yav0Y!Ct;EU=N?A?ca)-ABp0|)n05CMriJ5t`g!>S>QhRe5c5iLbvr@~K z-^L=EwfdG)$i{>vWkAIYp)gM0lmi+aR-!)ER1gs|cEf1KlZ5vLo<3r)Tu)6du}H5E z5n>Uu{A`*anVNvO=Mts0a}@3=xd)8d5PdwUS)W8zFQS;?h99vr!kc{V)c>d1v+QQm z&h!Z15kSvn2&8R$mP7c((bxDvQs3mXwjLFHy!$L9iXG_ z2axZ4&Fw>g7BvVKcn!V;&jl`vIc=1k%##)w0%3wHc{D#LW;q zT!VrO6OWehujb2C^Me2r{)l}MVa)YEx?GatpYEnPQ2n=K)I!g*#dE?ScxP0_AJ*zR z#m_`NPM!)6ATgV_&*M__Lx)2I&8uw%RJOKJ-|<@1+bGm`NwcFBv>W2ZQZZd{4Rf^Z zld@lX$;2%p1B;eW97?BmK;`9>WPhbA?;S&HGn_*4!SiQ+S*ya=%Zp>{4}{Cfg{yzQ zBBQ*U-4piX7Y87ZLm}vaz)2{QTO9C3Hp~qNi6S3{v`0mOA_mx}Lc;P;T~!di{xIW) z6G+G(X}th_qEbUbw?I6%M}<_H2SX}zYy$#8$YLh{?nldDH=!8;HIK$Pfq}cE?V+^) z?*<{VfOS`5vX&VN2lTN91r*=wj>X~oM}zuTKnQy0wYj;cW{mKUBHCvM_BGd*x?I=e zWYX4ss>j*15LPmv=QITTc>Z8KCiIke9SGW=oJ39XDGC90eUjR$OaA@zct}Z$NFhI1 zV<1aY;WN~4`I+moh^Vcj=_qqIy}C!WxOte(R<+j_(?1D+=$1(0^}Q~;gS$#dhVpTXR|pV@HHsikt-a<_L_ndGUM6Vm9&fI>?XId*8p>14k|u?{#+Trb196= zekUr`b=7{ZiQ0WXzIQ_HtDIb=AY`F_a?#cEsinS}Q%wO8xJ^rUXmQz7g$wS#dqq4t zTl7NoJ%6RZvK2EE({JYuNz3<&ZVnELevjvevlVtym!`h8T<%_ojKlyveSJJ!q87jT z$+mRz8{o-GyZyq&*uItX;S3MEijDK9!As|<-m(#Fid*@Oh;QNH{_Vo|8lcOY321}Z z(b{;n<hB>=HPXbTig;bSP&FUsR_uqxJh*e)bm-kU4~Kcj3X9!f9*65pTf;h!0_tF z3KsWHostt71%jshFUs|!t>%(UGS040?@e}EF`rmD%w$;wN&XoW`g#`_aup%pPgK*%@w>q`9Ej17)?WuktrvC{J?Th2!|mg*U)}H zd~vn8-YomP@8!>3H{1OerA~)Auv7c1>yG1X&#SE$x9Q~@qra|246XhPcgCSe*)SwA z0P-5HApi9y#y_BPj$T3k2U^o`+|uW0q^hdUc|qc=k{F?wXWR!I=TahERb;83^|@^k z>`6F3Bxmc~-I?nZme27}naf-I3gi~Q%?44)V%?U7t-bWd)Xm&z7ej6ye3!N?`T3#a zeQMyOMWvLQD& z_mqZ4!o^eeNF*`4D!85}Levcf3U)i)E{wN?Ba-VB zz39vFtrFCL3EIqGf{UcELjF~Z{|0Yuv|p}NR!tI(mKGHw;`{LHsq@ukEH>!rdJ$Ob`qgX*USs12~+<#>5=#ABidd$piaA*w~}G;>3z9kLb8)Ucmo+oS@i` zH*#A(v;9TyF$68-E#eO7)D*D^1{UxWj^7yb4iaKG&7QXzSK!>Mdrd#tB@xsp6&x@%6OoP)sTTtn9`3mRrQW!b z>6qUtxQKhwUi(Crzo>7})-Log&2&xpv`Cyq`+58i1H!-Y<#Iok`{vJLf z9>n*-$>?!*it^7KIGmQ{IJ{0xTW4H#FocFqKb6$FHUHK%lM1wOlRfV+ewc;*$};Q0 z%*z?aRvK!gd^swss^K5Got-}is~TYf-55We6Q2?cvkVMh=I+Dz`UkP|P7KbxNQl0%BBD2Qk-ozGh>vpfBUEk~)9?a(`ETJN0jr7ukc^ zvxGalCy~+NqL=U9t&JU&$UkBX$w!{icbg^gf_rB1T1<_vI(HF7+_bs_-MUuwjl>E% zE4QQ?3n9}ryU??W{GTO%4`-`0{M^qrMvoWcRIfz3_TmYD?jlM%@T0VPeQLZjwmg2Z zQfAeINXbxyzy5*RChtXHLcw*1(AvTL+5NZ+sQ6<}6O4l6?4FzcU(%pgo&w(`{pB)J^HG`uYRZcJ;Ov zR3TO7=IQil?hDV-kZ_aVJabCR!X;H!#XH^^g~C#Gkl8kTY?k;0|0bAk=y1V4Oh`5k zU_1nqZdb4=AeK-E{sP&2#X&*L7d;3uE(EU91q3~ipQ&;?nh?Oa+(K#p4JA(RWr>zg`$_0sfE(mP zmb_HuXGFzpjnBltHqUmyfB(K;Ts(Lepo->%C*l2jlHu?8Kb;`Aw|_uRMwXVE%9W<% zl9HL}bA8EZXG`#D57E~i-Eh#WEBBPsjW^-{A80R0MkJZE4O%3=T=~HO%-z_p#;aEP z2^S^T6-%CsEreGEQPB%og}rokj0dJn;%{-f7n=adLF3zhS6W5W#BDb_Qdwv)&So$_wEJ5&$Yl}qFD>`-Rcn^n36 z?Lr3g(7p>Kf6HQ34QOu{@}H?dM$m|{oU`Xf$2ri^Ic`14RGJgqEE?V9_K~(m&1?gD%;H-YLwY8?}^R*`f&D+_xEx#QNn-b8{&@5;_hl` zJE}0f@p4QQEEM*YYbcM~OQur#jGoCm&N&L8gx0PMd}al0H2#DIiS559I@SEBHsLF? z;qUL4q9rg{9tP<&_oX}U?adc311%iolY=5e7up3IIX}sq-mV@m6stYz{Tuz(f8o}a zNzyS?gxSHaEK87?z}NY%M#smuW%tVDp}XNT<(rVYU8Y!osIm~EWnCKym4D!ciUS(9 z`tOxO0TcYj^TKh6^z;8^S2^EYyD_GIPPNVfBE5o?lRGol`+;6y+r4=C2=%g&HT-bg z23Ay()8E~VRCs;*Rg7rcILWLBrQBrtkFFgTb9Z$!A>I;Rn}7*Slxx%MMmBPOWStdL zBoZ-IeKXLGmGxbjo)VvJJOHELd)$KOpl;J8kn=lWpK!$Xz|}c2f)?eOBNe z5}63;Gk~6mpL*bbi2YrBa_!v8?Z(%YcO!Z*FWUTI0%f@}q)wJ+bO z{f{Y@NlgtlDJt$jg>PF}ha<2<(wh|fZ)iI=a>T~S20x*c+H4t#GMp^d%ek0pt(l8>r&JVE`&BlR&iu=cvFS%$F}P_q64{8PtE; zU#O5n7S^~-YEKB5sVtjaz45VH>Gyp;`CH&C5=2GuSSv@JxiR!>urCj5=_ewj8H56b z-0q@?7Qgtsi6+7|0V06L0r61&?=Gh^&}&AW$9xK~F(D!PcknDB`57H+nu})q(k5*b zu!HjCVY099%csaAV~OrafI8z-L2*?WlkI$BUi=m~3ser%9m5d~xH(yFZD?rVqj?C8 zJFC4#r-V`Ax7VPfR_6OZXt%2aDN~dLnB-34AG0C13IRrl^Y{jf24x{{#c{koD7`|1 zzV-R2Mm(1vQc&bRq$YRHD)$#MFbZ92`}$>Sg0IrpoX`+A2oVp4Q;kdJsD{zmHfzLu za${#=s##j9(9?ZSCiu?%d@JjNLk%4sPeeAI_aRlQg}%N0aH$Tv6gm-F&Nq$_skwYs zwgi7)y8Hdw-4_Mq&wBxaA2)I{SCsniLJS&{{BJT?=<;nA8|nmHH?A(n-=xNGRl-Vgp=@b;Ousp*JW--@ZK z4fgluSJHsJ26MimzS>jY4E!_ve8mDongu2UQZttCTz3hWH;e*Au)RMVpZ`#J_hBh8 z{g3p}SvXMM({U}(w?hQv;m~RK!GC6s-}Lu@0TwytqhIZecgFrKF_65qv#4^l)f1%g zTi0fF^%JwcR}_;>J6VzHPS>GHg8IVqfj0*!nNd%A4EUX!Y{1EKMf%m1TlE%Oe<>o> zaP6B(-C;I48Xtlwys5PXIi@8ac2EXWS}tg+a|n9sTmH(J9N^vN@vJ8!C#I8`3AUeU zQn1Sug+((yBJx&xvqUR(ai_IZj*fRxZ=H@2qmHWRcvOqZH2M-sXMPni07bilKWz7Q z>R4MXk&3YV8l;{YX8vXQntTm$Dy!`3+$Fh5Ti6?$%+hP?;SVnG=vm+x!k}a6hcX3E zN=iyrQj&)o3yZSHE?+)O?qdD9;!xMn*d58Vw6}-lS*n}S=zE}{nNggf#>6ON5v{4- zeJ|WSLYxIFQ1tLHGSV{|Jp$g9CJgK4?cBMBA08CXXmb~;7}UF1_IrAM^zcX*eZH;E zLu6+!8J0{fh(n6dPUZA*h`@q&Gd`;nISvYP`?TB-uL5$;`r|J?JIOvCX&HOARBAHu zl3M6qZDB0p5fUO2$#@!Q)Ya9iUe`Gp^Qj~DBBP^Ya$oGw{StTAK$Z4e355*BBvan) znBjOg;$*sh?^z=DHvRyfCH~yfijsr5Y1?19MYzSStv#co@7UfY**-D~yylHO2gC)5 zqIdB~uxNe$igh8f`iGb#f@nU^#WnQG{zmdd_P6J6nWqErLW*(qEYyWPeDtK(f~KLyOU?oc@3dO34`exm)-ZhI?MmShM!+ zqM({;{FO~|@SQAwIQ&!(!)O|nmw zq+f*{S%v+Hfhs=@Azmjf2^rwCZ`QMn!S0xm!p!jM}ST%ZTmk5%p47?PbDlx(wxa`S)V=8U(c5DFxzM7EcT?S;kviE zK~3V)US58SYo1jBs~F+>uXm3*-Wm_^&o?zyYk3(1oPedtPgFC^lu;Pe{&9~h)(ga! zo2#zoWE)OqKAB@frxiqhG4!7DFH=-(9OM?$H9+2W7T$VPP#HV+<<&}cbs;gU+ri#a z1n>lF2jPk3X9Ogr}{C ziTJ%Ljg#GYaaj=y5QmulWBd#shegn4!L|Xg*-7;J?qq;5k}i@D5P<7u_ta73E*KkA zW#~^UoMwKL+x6moCCB_Qjyo4#sB3QozZVsWI^sJNQVIyEP;rx~tCs*=hm+6!k@N6K zZMMBv&3J(zmW4y8pYDbDajDr^1s>#{DerHB4GLQoA~C70BPNhzQbT|>BCzOa)dqh1 zj~+$^O-?#NNE8-B>15RIIE}{$?ji=%Pge*M4AV7;=zV|DdiNJI4Q*{STHZGyXM-aH zCfI(FLGut23>`M}+&J4{aZ|FXnahZiET53l;y|(g=2P zyS175`rz^pnqT=aJEY2d)J%hGMYmJkmb+8T&b;^!xF?Is73YHQ{?t-Pl&7mZSuieB zG)k}e;=RxN+4sobK`%f?T&Rnyh-Sw0$~t6ORzPIpX=?I3tikHHSh;<7Xu+P%wyh1p z<)7EN(-#4fC_Y#qO6Bnfh5UxMOD*K&AW(Rky_C0rL!zm!&EDqFQ1EcZEQ?#*GA;!D zQ(|ZM;J{Itpbh+r^7^Tg>b(XIO2Nw2dZnKq$}~Kg-&cKzApy5(fsZ=_ozAhe_QZng zILur7A(1I2!p54^^BB9Q9L=MlA%*Lw7`tBExOsHz*lGid(N+(C-SDdGOjBx#i9Pr9 z=nNWrjqmJe%%2#I-k}H=68Tn5z)j^pi4clUf=~NCGXCWc;3&_A6{Grn}xeIUlpRTrEUZSbv?dBDg=Q!|H zkf41w|K(KgLnIZPS6WcFHIuTQ?Bh?sD~9iA*#1Hugnx+sYs4++M~HV|r>L@|SRpSK zg@tD@kn1WfJu$dasjR$UZcewb;4zr!aa9c`8+NMmgAR2Hy3{arx)FkI8z2QK0-vfZ zY=Z+5h{~@;J$yGCccb3^YDssKI8EH7T&|s*G@;1QxYsA<<>7Mk{WEp&6-=P%{`|Ln zZSC@7H@C=%yPPo=&{-oZoM7D9wxHWWgi&!49QtXhw7(aszmEe~@p-d<-EMiSQ~dE6;P0DHYL|lrwA1`eLJ4;} zedEY1lN%PRgHXROpYzKrE3pIvAa7VNo|(39Wsae2X)GYsX?05y{k~ zvOrO$VPMIX*MEoTuDM^P%3W|+KH}^eQF&OBogs*}B)=C>fG{;F^*d7#ui|e1-pBtB zFXC@b#lZWBxA1I7Cf^`OAC~b;VLLd zTfbLt7%F}oS!??&Fnmd*30DCi1&^Sx|GD*}r@{1juQdy5W9`%PJ-wNs6M2ht`=l7x k8bjvvE@m-S@D#O}`_m^c@T&~O_eqiDq?9Eq#7%lWN&vMLu_Glb2=b!Z)|I6bS-OS zWi4%CZeeF-a`wg@D*ylhR(e!ebW~|{Y-Iodc$@{n!3lsc35zS33E{No>z@?S(G(Y<(a4!N6nq?@xdFWY3w!;M&+ZOJ9mDNRq~{&B4l=b@PVk5@9GgsR6pFcU&!zN@IzA7-McddyRG)f zN1pZw9m(b%$Lhn16fsek}Qq`9GUcb>tI-6wS(?7)8?T ze}Dc{M*jR~{?BGq50t;4lKIPDB$X2WM?U@ZQ%RA(qDJJz>6KA}h_y*(UkR!vE*T@X z0)M4Dba$Khy}24GqDSN&rXIVLc%uC#k|~f)r?*Mnfp7;%M@@tRZ+unM5|I5CA_n7F zk!xKlo*k-K(?uwDjhsTy3tR`;?=;4zM5NsFL`416Q$#*Wqev>GphyZ#p(1Qb3Ef2x zGlWmgtT~2AkRE2I^0DQhs{sy5>m?K7U{6QHd&Fa(yJ-eMRK~~-8%g3wv#I4i3X$G$ z5Wyx%Jr0WObydXSAd27)UZD#|JWSG09AOA4!5h88j5?y;E1s;W;=mP9fhs#x1Snw? zl9yEZ<-tpGN;-j^-IO}^&t=d0Enx@>zTHDmBD$q68NL3IGm0nDy{=HkD0qcBUZg6~ z5m#{yM#STw+FLI;C}ONV>>Buv4J;6{i9U!}hgA+nqaXGCL=IZc1UP6_IjG7W= z?Z;p%bRR{&2H59i1aK*%!YBk)-H^}%(FTK3NZtzuWs?zghgk<+#KePa_6~(e%8;=4 zDEwF6Iw*qWes2j}_;jjL-wh zzE~t^A*jKij);~B1W9@e!Sul((klpBBUk%(okA30%esc(1#gR5S87fk+UQjB?PUwQY3!=FNqLh4bdjnLK&&%<0qDtX{ok z^Jd!-y`36|LqamW2nk(V38FALBoy|!>WHAwEflK{%#OW6QS~~644&Cr6ovysN^-@9 zmWG`*arww=njYAPgEs5BbHf0z)qx4Rm50e z1cHb$@>IbfnivdjYi}0ZmBAFl(wIp*>p)@A8}-qq%0s%0X3$ z5j^TiJlF>m>RMs)9N7rv0UC<9l(9hcr=^mj27fLeZJ}R3_7%2O%E6oj6y6yL^cYDW zAV`5XnmWocdvGN08L^bx8IJ{cb++I8Y%AVIjpQ18V5HS zGbu*H8IrEf&ccF%M7FG6xsp%4NwHNJcns4J+34HUxU0L1eqmtv`dn@8?C2;iF0QSq zp(`;paW*k@M!2TBdgb!v9ox3i5r-%wM5P?DXFpU7!ncz2A)%@xDHjS;qR_4t+6l6M z*+ep;k-#ucLvkogQQWv0yMXAC%Vv1aqtm0>RRfl8`&ESb@t2gF%~#Pl^+R!4B9`R1|2;ie<~T zZQbflimk%HAP0j}ZbH2F_IA2cAky62oY-7W4z#AFxrthobZpzYZEL$|XUDb=cJ83L zEzQjY*whS%gi63(uM#Q~MeM)_E|?rV6q2Svq31$6Yqo2J_6lZ`Ak?h9NyMn;bs z`S@eMr@@Tkrq)*e@SL;H(uuPg#{t9dtr8jF7lh(+GL-JGz`p<$BrHI z}ywlp?20)ad@XlBO};%x=J9oxFHBE!iP-Ci)ECbZtVb*rK}SKoDXY-^~mk1iBG zJai}niGdz>?3d{UJ>l5dv5eezzw>SC6S`UsB8+_>P$>K47^y&mA9?7Zabw52fuUV4 zv{z1KCo~xA=Kz2A$Rp1@{j|G6lz44Xy)Kc49P*>T``!3wo>52D2$96jmt7#z&Rn6Q zh=&pZjdvp+NBY?BfB)RG&&sANv|j^BRW@DS$3YX;o&4SJ&{uShk38Mf_Lz1e{Gko{ zEdE4ATn=jVm!5#4GIr8hfYM@>kwpT^eikRO$^6u96L1MU9f#BLTr=R{kO76e!9%)Q{eSK$V zr!s9;@z$9|TW1!w&nRr4UeLb#pxw^7g4GSTRWY;gTO;{FUR5{n}T*a{Hj$#8UfTaKX2}`He4q@e3z^ z=i3iFa6eyn<{4+WJJ}Z|oPCz~#=LoR`4d2Y;t9v!dh<?X-t}ShQe) zN5c;4WZeu;WgxD-=4uWTXFlzFr;Zyt#tRP_8Y+JBN3kOIiP>AVM zdBTKOzVmGgjvq66v~Y?-R-^gbi!Zv6Jl{?pZ-+tM?2~x#oU_Tl*VRS-tTWG){l7VJ z;^IY%bpLnXeHVTO!Crgq)qVRPGe-9B0hB$seZ`&^y!+08{{5wYZoBms?9&4uhvt6u z#MfV6m~?3DM8<{24;?y$o_CLT^r%t7L3{7|00y+@4~GnS5c_XzY~UOD3h>9EONY=i zNdry8&Uo;_2gCvwy!)<%fph21(LulE=9_SYJv{*+JGtYI+p(zeu$v` zz+C@+{iI>oKbD3&_%6_6u0lExc0s>>@})Q3ctcxT8-pTll6ZkQ8`dV3meSIa{{7CQ8KiS& z|FPeBKO{e(f4{QQ5@`$}A(5;yZO?%#RI%$Ms^BlXi%o5lJa2!HONy)Ka{t|n8XWqP#BZgCscZtQI z4-Fmq?D+9gnPI?QWuG|qTW?KK4)RfXc{wHprbi4PK5^p2;eYsp4Bn`bBQWRUQ5W=P6QOL4!GPCD`BmtO+GlwWbhK+461P$I|YpL>qX z4}Wm37aMMKIQYhEuSw%qtXK}?Ccg288y=zp3zSyj{L`mTrxPx@_@X6?7BOe1{EpiP zRaRDf@eBWilf3Z!^VeN>EroCj@gn=b_8Q$kszKtyg$tywrcRwYbLI@zgQR}|=-|P3P=4i=S7;C@ z6W;Cv4{gSb8S>Rj7BALAqb7OKylmMr`RXZCrf|H{o)?~bjl*W%7jip_g8I357TQ`qx8h^BY;_)od6c)oJA=?$#bF>iy<etd)(_MaQV)`ga7&T(>VVVCsJX^gAZ~#(s?m%;Bs)}h!NzO z{!>od2w`Xu$0ZyjRFQQ<+CzY#>rkTZ`rwi!OUM%m!mmF(^%O%!p(~x)%7hOz6@U=_ zc%4AV`OMSfvM?P&1%{No8xLN3$&bm?7;$?5)YZ`;4NxYsf0=O;PTKvCAO8#)f+OnF zpR#{D6PKwa`(cy)$jJbZ(G}7^atJCQ{(!GeIyCIz?z`?HjM8zT&!5g7FVhA32eTIr z3jTBo%vjuY=bi3=J~U*Aa1hk)bvP*NywZ67iY*3OED&qK2E+pQID?MCV6<{j_!k_M z1Hs7oh+E{8gAy@~fBI=k{^Q>-!=!2O;5+V+hH*a5bnF8Uc;@QHgTtSCiu}+AAH>Y~ zRKAo^6E|_!1j#4|)txKLORxUVfjIJYSN(G7;)U1#>I(9tl@;aIo%4sFeR_mQ&|}Mz zMeqLNe_u*7F8jBiEL*(LV@qBf+}hsG%{a^xyRyO@gnUU!F&Tz-)~s1om6gf2SohpE z*xKCW@vgSI>XjFtt*fc_%9(BxNayM=`V^yKgj{|2p`m;v;o*h-FQ6vjs>?yKLYhIf z)|M7A0=fQ$t)OZUoQF$#g!8j5CQ8IEm4$<{PmYoNCd0vd?)j~IHJMdE3a`25YC7u4 zC!WZ-*?sp?E<3>t@CWQ$=Edp~EaKoov{O0AK)BaZ@IX}Xr1xSN3wb6or4LuII)p(vJoyYXQYB~O0FBQI4vh0U?f2(heMt~Oyj{W zC$IcsPFm9=b#-;mJd^1h)-(+h;5R+H2?}{!qBdI=swCr~tgA zLu8fXlpYS!T25wSEqsK?AOg6N!-q@o4!AK1IVgw~1(^}#juU90OlR@Qb1YA~=Chb>hVWp~_i0}Z(T>MKZT3{7&`!nmYbAGAupv)IXO zl!rh#BsjP|E943E-g@gT4}vzspJZ44HpkWnsj_MP`bKu^-D|F!FTU^s!*kVt{&IR& zrc9p9u$^}5DKh((3F(wclbAIN2W8~WKI=?2Ss0%4_SDmpKI@x>PgWoIi_4%B(Ea1e1g=sc0)vU1g`m7_ypAcuc+71EngTCkU>3&kz$qnsx{0%nH+NPSa$j9TPA^B|u-sdJ{`u$83~U`wm^Sq->bP&R(Zxi) zClN-_csc=p?$_^pR+nJPE3do^MrjF)o_gJY@|RzFkwOqj^rReA0C`Llfnuzn>Hb+K zrI7VI_RrLh*|A=SQIS7neUrY@y}k3!e7U4R18%+fX1)mCp?YQ9T_0eRltbJ9)#sl# zY2rkh`tO(hf}PMgEC&k*=?acl+VjBu_sQ|8^KC&ES6gY25hj9OZ za*%RtNgK~|rOH8MUp$DILEpT2vkd^1gW!NWx4i2lSdnLGa1hgd1sCYCG|#Hv{^P8E z=btZMdeaRz;HvnAOq=x93-1LAZRy>)xjA}&OyFVM=$jNsfs!<4*3?OlKKR>_PyDW- zzLrf`JYv`*l+2noSsDYr)7&@Hov1vU!rK4Vt?kT%7#JLqySD*>TwCA;HJt{x41ggPg* zefnXK@-id6WkDWaLU*fD=zJ>rk*eMULH?uND49JUdAHhRl#e8%x0}gFAdpltQtk$c z_%F&fDIzG8R4YJ1l#JvNeR@w*RJZ_Gq&^u`StBCkQZNad@>07i#Q8?alei5dB}qC8 z#68%ayy+Q4`%Noo-=c1}dxK)e%T;HfozYi&Pz!O*Mr}z<%l(u>wZmPiYOAh!tL_xt z5%@^dx>8qXM{P}Y3U@BUovCmAH_JeQ-FBgbmyv3^xjo31+ALevH`LF2dpfDMp|PQk zk`0XwG^VVq6o|>6Bx;0%TxzVUs^ToMDJuknoD=YJWc8|5+fCyVfyiNVhT*;*8kE>; ztT?P(zKrYfj0b^!*OrilLUf?&IFi$G5ObxFi=_ zX1`5S`5qyXs`p$2>w_T?qGv2mv*2(p_|jlez-O_To{XMf zdGGtOe6)xpuQNE!xS;e|pqTJ74qsfvXo==c1t~WYt4Tieb(*maHb>-P&g+fD9!ut( zA}?uZBGe0lPb#DVU#?FL`$CMf~ z*2(z9wA|6WqaoOZ5oveD{w@Xe2g10Jl)A<)~Q0i!$z@ z5YY27c?y6VIe8V35Rh-tI{JpxL%~5yH@=P$*~gabL*+Ems}tv7-Wm@KZImmS5C`q1 z`B|t#L__;g;Ov@sT22O{ImxT_^WVW@qIFmE(riP<7#uAk9VVh$qIuo48WLMMVV^l0 zU||I?Db(4GYps~-H9~i?(#-COQ8=9DIxn>^HV zP{^S8`rPjn0bP^5#@2E#)bwihd0*O zH8#{Y)YowT31`cg?ye);R-rhQ&oxb;3^N>k#a$&sN}pv#u5 zSe8kbFI|i-Te4{Bl0~FEkaSd!swyTSDt3awh+;w*66S=iCdI(eRV&t@J?4igu0ylN zS+Ew(*lOXrwMA$Fnz7{K!i-^Kwh79>nktkN=7JarARjY_`r9OiX^{TFV`4h7ayLPvS{E+P^u|& zFkH8r_JVTsigWZTO5n1xT{m>rfI`2Y7hO73*`4=f850c8mr+}SiUcOPxGiz6;b5b(F`Ncz6xK^7qBrTd;!D*pMBknR)|us zZ|ozr>wQrgs2+(NjB6k&>w;v-Sl zuocWTkUkR7y1$#Zdc<=OmfOwzX{c#(p}FfYRd|>;5Ji-C)`$~-Tr`=9?r)6-$SC4O zi@W1+jPo(BDJ4iWtG04_>E$69i6Kw>7O|7uT(P{4qh+7wz0fb$sZ;=nz#I7w`_w&4zTwG(a`-s%I zr0E*jP zQdtI;@ntiex>k;XZlB#N-Xqw!IJ)Nw5uB6j(@y3gf zWhj+&cF-j+)vyo}DY1ss^I|$twLN2#qT5wd+kBR10yXPOxl?fFduY_717tXy%DqKt zIe7T_7y@Dli)aXr$z)8SYIANlY2oOZrqmQZqLdgoY9`|Y9wOCGJW5I`4o2$25%pQA zYJC{TCPkH=Hj{gSG8as}5>(5h8vom5goWB^k4rMD+8d;*2ZVLGJ6T6T#>FF@W<1!T zRI_HFc`+EZA8TC}d1{$jjWnu;dXW?jfG>={G`u^?!6YWeA!vso)o=zv#T_P%MvLYR zves(J9~d(l>sPf{bixO-slG?*xTSSk%SSh&OaMU;k&;i^R5cIDMl|i;CVp?Ysd_}- zk@COgT`uZr-;%o23yh8hpH;B|wF+Ht39wHplSCsvYtMn8FPe&fg?-R!fPJ4&4${`o zh4R#kSK_T5*b=`2d0nAMog((4nAL;|Dk(0&QD4_zqE};NSNeO{ZYr7NRIk#Aqnb3Z ziHd*-&A8CCd2b2K6q@po#{X6eqpc$A9}61m2fvU+(qNz4LJ}RpfT|^kZA5QxaE1E4 zHdhkTUV29+NpiQSZ(6_p9Ox0i!H9;JQS$_%s4_9UPVA{=V(1+?Rz)24xtWAbB#c6n zdVI9_(@ifE$|D{(y+Rf8y;c2FBVHjJ@dgJE+3>D+eJheUQg%R9qR3O*$i+U1Fk(6Q`ERtIOf=O|$p?s@MuS98t+m-rrC~Hwq;f*EXkEw#7y1$TbOB@X zrhAPNi2*UNsfDIIrN(z+8xX;tIXW_d-Ux?#66Xbm!(*R~_#_h`pl3M&8w&-4vCz-e z82;jF{q%@Xp1(F6JVIz_`#O$7lUg`Nv1s@hu*eu9EhS^>pY7Gbj0Yxj({7TAIIivy zA)41sTVl%7YIHxjN+Nk%hzQhdFQw>J?SFfdT~saU!mtZa&+;w+zA*A^8qly;9_8TS z2BHlW6VpD-pi?tZu^7i6nv6>$(HT?ned8)Y$F^$gC@Vc&eoi^)7HX4wf7EtU#UfAA z=<0`+TxJjjqJl3a`+7aw4Pr6sedtjRel9f6AhB0WOx#2g*RX9SqLz$E433HZX%MMS z9?IB!m>RiQZbyB|Qf{c7!xu zIsVmN-ldyn(szsP)iCnx5T_6HV&IFLU!LsKk39Sx<=_#9KVZizY9S!+j^w?PyqS_8 z8s!!=UK`0vA{kksVfZ&OTQIg?9DnC&oVBYiINWKj@xAX>Ha9=j>dyW7zFu6lm?`+fj5ft;(Vxw zl$DinhXj!zFCpgz=0K32U0JhcO+$S>&6hp;$Q~|+_ITzZ+(3|KeEj7A2A@*;|FGni;ICEKO(~>g&Zb7 zde1Liu?Yv4FI&n_XUR^&G_;%P-;iPLHB82UlcdjEMCPJofC9GAlt-v!E34*TKiZy@ z$7M?_=`8H}HYjc;NYFru?F)Ri(#Yz;FX_Ov%L5A32~jtQ1X;81rc=W?9X5 zSLtERreU07PBcxIre|LOd1yXnqvc1zcThOkc_dDU+ge+=D9p9sii!$&$E9KU4bOst z0`5U$f=r&k5DxMar~G^}5EOxf@64ZHQ(cuu142fY+Mwgr_UD6q40vO}Z-XULXrQUw zR5c7WFg;jmzaqIMxqj})^hUfxqTL(jok3o{_TYi<|Fd_d-&Gaa0{TmAHd ztNqkJ?}qa9JxO_|gqyx~*3MmZ&rqjQ=hW}+y{l@Mv3Y$W!}SGXk1gES*GF1$5HkMA!2)kM=zDH@)12fS7r*v%FFP(>qaorGYYenRoUuysseaFe^DcYow@uA9@l_AXIBpCUjX~d zU}W&+zk-8|lIiW~!6H^m@l;e)&?2BA929*}AV`8up%4g?@COdM*f#X%Al`(wMKmCA z3y7K`c_ZH3PIH2b<#e}tU3_*1d4_{NAO+(EnGFKwz@Nn96bQ0uMVs8^9S#A3^5m7cMU^7YNF7KL7Z?e{^+q0YPyQ z$|goJ39*YAXOo_mcCM(1sde83{2SmpFRX|3=lEQ_ERvy{kzvn4fuIbLxqSID5Jcnq zU&qkcvhp(C=f69ao@9fD9i1IOP+Wv;N*^Qv0pTDP@vdFF4j(#Hb@3t|#%}`t4H#@r zAwJB_z{^O$aZqew*@&J12SpP^F~k-sDJh}5LoMP13ONUh7zjFXP-%idFf}zbB{@0I z;T*)i{wCnx03$|>_%8>6Aah0N@g;QXg$oz1T)Fbk|NkduFUQadic5-tBlwfp#UvO= z*wx(yB^3u11jRWB1XEH{cI?=Zot=I8(j}BZAp1?gzX3*!80nXTU;BeFU8I-zF^vt? zm#PtPMmNY;Uw_3`bgl>pLO=<-M55JUHLLz&r4OnB)j%*QDQU-!?OB<}t1ea&VS(@r z+=Lq$F=8YDWq5FqC^9Y0*UHLD>+0)(AblWTg2pe2k8lfuxCk+j;b2Qki$IXg3PBR$ z9He6;F)=YAJ`M<8th~_C-bUsIf<}xO4*E3*fs{;}JAL{TIKu<@1?c+X=GmgNjF>?U zgn)Dt(^Yb<`5F+Uhs4T3*`TLDaQpV{+hSu41dSLmG7LC~F^sv&2CDjcdupy+t~h@l z)$iubn>RkcQBYXGArwG5Nhl?sO;?*bIyðJ)5Y)M`oz@wgHa5@NS*H4rpn#E71Q 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 X1HEx%JN?k_6v9Y~$%>W>>-qj4`9bX8 literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/PolicyKit-kde/authorization.docbook b/plasma/workspace/doc/PolicyKit-kde/authorization.docbook new file mode 100644 index 0000000000..5405926ef8 --- /dev/null +++ b/plasma/workspace/doc/PolicyKit-kde/authorization.docbook @@ -0,0 +1,112 @@ + +Authorization manager + + +Manual + + +The Authorization manager is the application that system administrators can +use to easily change the default behavior of any actions. This page does not +aim to explain how to create new actions or define new .policy +files. + + +The Authorization screen is divided in two parts, at the left we have all the +actions that PolicyKit knows, you are able to search the actions using the search +bar at the top, and at the right we have the selected action. +This screenshot shows the main Authorization screen: + + + + + + +Main window with source device + + + + + +When you select an action it's details will be shown at the right side, +the action might have an icon, a description and the vendor name. Next +in the view we have the Implicit Authorizations and +Explicit Authorizations. + + + +The Implicit Authorizations are authorizations automatically +given to users based on certain criteria such as if they are on the local +console. These authorizations are read from the .policy files +that the given application defined, they are the defaults settings of the action. +These are the valid values + + + +no +auth_self_one_shot +auth_self +auth_self_keep_session +auth_self_keep_always +auth_admin_one_shot +auth_admin +auth_admin_keep_session +auth_admin_keep_always +yes + + + +You can change these defaults values simply by changing it on the combo box, +the not bold value is the default one so if you want to change one value back +you can select it, to make you selection take effect you have to click on the +Modify button. The Revert to defaults can be used +to change all Implicit Authorizations to it's defaults values. +Note that both Modify and Revert to defaults +requires you to issue the PolicyKit org.freedesktop.policykit.modify-defaults +action which might ask a password. + + + +The Explicit Authorizations are authorizations that are either +obtained through authentication process or specifically given to the action +in question. The default behavior is to only show the current user explicit +authorizations; if you want to see others users explicit authorizations +click on the Show authorizations from all users, note that this +requires you to issue the PolicyKit org.freedesktop.policykit.read +action which might ask a password. +Blocked authorizations are marked with a STOP sign. + + + +The Revoke button is used to revoke an explicit authorization. +Note that this requires you to issue the PolicyKit +org.freedesktop.policykit.revoke action which might ask a password. + + + +If you want to specifically grant or block a given user of performing a given action +you can click on the Grant or Block. +The following screenshot you see the Grant/Block dialog: + + + + + + +Grant/Block explicit authorizations dialog + + + + + +To grant/block explicit authorizations you have to select the user that will +receive the authorization. You can also select the Constraints +to limit the authorization such that it only applies under certain circumstances. +Be aware that explicit blocking and authorization might self lock you +of performing the given action so be sure of what you are doing +Note that this requires you to issue the PolicyKit +org.freedesktop.policykit.grant action which might ask a password. + + + + + diff --git a/plasma/workspace/doc/PolicyKit-kde/authorization_1.png b/plasma/workspace/doc/PolicyKit-kde/authorization_1.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd9a864142379b7a92fda47de4d26756f4f7469 GIT binary patch literal 101385 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 +Authorization Agent + + +Manual + + +The Authorization Agent is the application that is called whenever an user +wants to obtain a given authorization. It's a &DBus; activated daemon which +uses libpolkit-grant that in turn uses PAM for authentication +services (however, other authentication back-ends can be plugged in as required). + + + + +Authorization Agent dialog + + +The appearance of the authentication dialog depends on the result from PolicyKit +and also whether administrator authentication is defined as authenticate as +the root user or authenticate as one of the users from UNIX group +wheel or however the PolicyKit library is configured (see the +PolicyKit.conf(5) manual page for details). Note that some of the screenshots below +were made on a system set up to use the +ThinkFinger +PAM module. The text shown in the authentication dialogs stems from the PolicyKit +.policy XML files residing in /usr/share/PolicyKit/policy and is read by the +authentication daemon when an applications asks to obtain an authorization. +Thus, what the user sees is not under application control +(e.g. it's not passed from the application) which rules out a class of attacks +where applications are trying to fool the user into gaining a privilege. + + +The authentication dialog where the user is asked to authenticate as root +using the password or swiping the finger. +The details shows the application that's requesting the action, the action +itself and the action vendor. If clicking in the action link it will open the +authorization manager pointing to the given action, and the vendor might also +provide a link for the given action that will be fired when clicking on the +Vendor link: + + + + + +The authentication dialog asking for root, swipe finger and showing descriptions + + + + + +Authentication dialog where the user is asked to authenticate as an administrative +user and PolicyKit is configured to use the root password for this: + + + + + +The authentication dialog asking for root + + + + + +Authentication dialog where the user is asked to authenticate as an administrative +user and PolicyKit is configured to use a group for this: + + + + + +The authentication dialog asking for a user of the administrative group + + + + + +Same authentication dialog, showing drop down box where the user can be selected: + + + + + +Same authentication dialog, showing drop down box where the user can be selected + + + + + + +Authentication dialog showing an Action where the privilege can be retained indefinitely: + + + + + +Authentication dialog showing an Action where the privilege can be retained indefinitely + + + + + + +Authentication dialog showing an Action where the privilege can be retained only +for the remainder of the desktop session: + + + + + +Authentication dialog showing an Action where the privilege can be retained only +for the remainder of the desktop session + + + + + + + + diff --git a/plasma/workspace/doc/PolicyKit-kde/howitworks.docbook b/plasma/workspace/doc/PolicyKit-kde/howitworks.docbook new file mode 100644 index 0000000000..90e4d33190 --- /dev/null +++ b/plasma/workspace/doc/PolicyKit-kde/howitworks.docbook @@ -0,0 +1,53 @@ + +How it works + + +Overview + +PolicyKit has a simple way of working, but it requires some +design changes from the applications that want to use it to request +passwords. + + + +The problem + +In GUI applications the common way to gain root privileges is to start +it as root, but there are several security risks in doing this method and +it does not allow a good actions mapping. There is no way to separate actions +like package-install of system-upgrading. +All the users who want to use it must have the root password. Another common +approach is using sudo but once you start an application with sudo you will +have all the rights the root user will have. +If for example the GUI application has a dialog to select files that dialog +is running as root which means that the user might be able to delete any file +on his machine or even coping others user files. + + + + +The solution + +With PolicyKit this problem is solved. The application in question +just need to separate the privileged code into another application, +often called helper (which will not have a GUI), then maps the desired +actions into a .policy file. PolicyKit then loads this file +and it can now authenticate applications to use those actions. +The use of &DBus; activated applications is the best if not the only, +way of putting an helper application to run with root privileges. + +With this design the GUI application calls an action of the helper +application through &DBus;, which will start the helper with root privileges, +and informing it which action was requested and which application has requested +it. The helper application now calls the PolicyKit agent to see if that application +can do the given task, the helper should report if it could do the requested action. +In case the helper saw that the application didn't have enough rights the GUI +will then need to ask PolicyKit to obtain an authorization. + +When PolicyKit receives the request to obtain an authorization it issues an +available Agent, which might happen to be &policykit-kde; if available. After a successful +authentication the GUI application needs to call the helper repeating the +same operation again. + + + diff --git a/plasma/workspace/doc/PolicyKit-kde/index.docbook b/plasma/workspace/doc/PolicyKit-kde/index.docbook new file mode 100644 index 0000000000..6171a34004 --- /dev/null +++ b/plasma/workspace/doc/PolicyKit-kde/index.docbook @@ -0,0 +1,88 @@ + +PolicyKit-kde"> + PolicyKit"> + + + + + + + +]> + + + + +The &policykit-kde; manual + + + +Daniel +Nicoletti + + + + + +2008-2009 +Daniel Nicoletti + + +&FDLNotice; + +2009-01-25 +0.9.0 + + +&policykit-kde; is a &kde; front end to the PolicyKit +system that is used to manages authentication. +&policykit; is a toolkit designed to allow unprivileged processes +to speak to privileged processes. It does that by centralizing information of +actions and authorized applications. + + + +KDE +System +Password +Admin +Authentication +polkit +policykit +policy +policies + + + + +&policykit-kde-introduction; +&policykit-kde-howitworks; +&policykit-kde-authorization; +&policykit-kde-authorizationagent; + + +Credits and License + + +&policykit-kde; + + +Program copyright 2008-2009 Daniel Nicoletti + + +Documentation copyright 2008-2009 Daniel Nicoletti + + + +&underFDL; + + + +&documentation.index; + + + + diff --git a/plasma/workspace/doc/PolicyKit-kde/introduction.docbook b/plasma/workspace/doc/PolicyKit-kde/introduction.docbook new file mode 100644 index 0000000000..ea94ac1313 --- /dev/null +++ b/plasma/workspace/doc/PolicyKit-kde/introduction.docbook @@ -0,0 +1,28 @@ + +Overview + +&policykit-kde; is a implementation of PolicyKit tool to the look and feel +of KDE. + +PolicyKit allows easy and secure password management, it can be used by +applications to ask their users for a password. Each application defines a set +of actions that can be executed by their program. +The application will call PolicyKit to see if the user can perform a given +action, if not, the application can issue the auth dialog where the user +can enter his/her password, root password, the password of a given group +of users or even swipe the finger. + +&policykit-kde; consists of two applications: +The Authorization agent that receives requests for authentication, and shows +a dialog asking for a password. +The Authorization manager that is used to manage the authorizations, it is +mainly used by system administrators that may want to change the default behavior +of a program policies. + +For Qt/KDE developers there is Qt library to allow easy integration with +you application and PolicyKit. + +For more information of how PolicyKit works, it's design and API visit +PolicyKit Library Reference Manual + + diff --git a/plasma/workspace/doc/config_update_tool/extract_config.py b/plasma/workspace/doc/config_update_tool/extract_config.py new file mode 100644 index 0000000000..702a92507c --- /dev/null +++ b/plasma/workspace/doc/config_update_tool/extract_config.py @@ -0,0 +1,72 @@ +#!/bin/env python3 + +import os +import sys +import xml.dom.minidom +from xml.dom.minidom import parse + +######## +# This application loops through installed applets and extracts the config XML file +# Output is then presented in mediawiki format for copying and pasting to https://userbase.kde.org/KDE_System_Administration/PlasmaDesktopScripting#Configuration_Keys + +# app should be kept flexible enough to port to a different format in future + + +#set plasmoid installation path manually if applicable +xdg_data_dir = "" + +if not xdg_data_dir: + xdg_data_dirs = os.getenv("XDG_DATA_DIRS").split(":") + xdg_data_dirs.append("/usr/share") + xdg_data_dir = xdg_data_dirs[0] + + + +root = xdg_data_dir + "/plasma/plasmoids" + +plasmoids = os.listdir(root) +plasmoids.sort() +for plasmoid in plasmoids: + configPath = "/contents/config/main.xml" + path = root + "/" + plasmoid + configPath + try: + dom = xml.dom.minidom.parse(path).documentElement + + print ("===" + plasmoid + "===") + for group in dom.getElementsByTagName("group"): + groupName = group.getAttribute("name") + print ("======" + groupName + "======") + for entry in group.getElementsByTagName("entry"): + name = entry.getAttribute("name") + type = entry.getAttribute("type") + default = "" + description = "" + + if entry.hasAttribute("hidden") and entry.getAttribute("hidden") == "true": + continue + + defaultTags = entry.getElementsByTagName("default") + if (defaultTags.length > 0 and defaultTags[0].childNodes.length > 0): + default = defaultTags[0].childNodes[0].data + + if (default == ""): + if (type == "Bool"): + default = "false" + elif (type == "Int"): + default = "0" + elif (type == "StringList"): + default = "empty list" + elif (type == "String"): + default = "empty string" + else: + default = "null" + + labelTags = entry.getElementsByTagName("label") + if (labelTags.length > 0 and labelTags[0].childNodes.length > 0): + description = labelTags[0].childNodes[0].data + + + print ("* '''%s''' (''%s'', default ''%s'') %s" % (name , type, default, description)) + except IOError: + sys.stderr.write("No config in " + plasmoid +"\n") + #abort on other errors so we can find them diff --git a/plasma/workspace/doc/kcontrol/CMakeLists.txt b/plasma/workspace/doc/kcontrol/CMakeLists.txt new file mode 100644 index 0000000000..5867bea673 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/CMakeLists.txt @@ -0,0 +1,11 @@ +ecm_optional_add_subdirectory(desktopthemedetails) +ecm_optional_add_subdirectory(formats) +ecm_optional_add_subdirectory(icons) +ecm_optional_add_subdirectory(screenlocker) +ecm_optional_add_subdirectory(translations) +ecm_optional_add_subdirectory(colors) +ecm_optional_add_subdirectory(fontinst) +ecm_optional_add_subdirectory(fonts) +ecm_optional_add_subdirectory(kcmstyle) +ecm_optional_add_subdirectory(autostart) +ecm_optional_add_subdirectory(notifications) diff --git a/plasma/workspace/doc/kcontrol/autostart/CMakeLists.txt b/plasma/workspace/doc/kcontrol/autostart/CMakeLists.txt new file mode 100644 index 0000000000..fa673b22f8 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/autostart/CMakeLists.txt @@ -0,0 +1,3 @@ +########### install files ############### +# +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/autostart) diff --git a/plasma/workspace/doc/kcontrol/autostart/index.docbook b/plasma/workspace/doc/kcontrol/autostart/index.docbook new file mode 100644 index 0000000000..240f79ed02 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/autostart/index.docbook @@ -0,0 +1,158 @@ + + + +]> +

+ +Autostart + +&Anne-Marie.Mahfouf; + + + +2021-04-05 +&plasma; 5.20 + + +KDE +System Settings +autostart +desktop file +script file + + + + +Autostart Manager + +This module is a configuration tool for managing what programs start up with your personal &plasma;. It allows you to add programs or scripts so they automatically run during startup or shutdown of your &plasma; session and to manage them. + +Please note that in this module all changes are immediately applied. + +The program scans $HOME/.config/autostart/ for applications and login scripts, $HOME/.config/plasma-workspace/env for pre-startup scripts and $HOME/.config/plasma-workspace/shutdown for logout scripts to check what programs and scripts are already there and displays them. It allows you to manage them easily. + + +Login scripts are .desktop files with a X-KDE-AutostartScript=true key. Pre-startup scripts are run earlier and can be used to set environment variables. + + +Note that you can change the location of your Autostart +folder in Applications Locations +in the Personalization category of the &systemsettings; and set a different folder +than $HOME/.config/autostart. + +Please read also Desktop Session and Background Services for information how to configure the startup behavior of your &plasma; session. + +Some &kde; applications handle the autostart behavior on their own, ⪚ you can enable or disable autostart of an application in the settings dialog (&kalarm;) or you have to use FileQuit (&konversation;, &kopete;), otherwise the application is still running in the systemtray and will be restarted on next login. + + + +Migration from &kde; Workspaces 4 +To migrate your personal autostart setting from &kde; Workspaces 4: + +Copy desktop files from $HOME/.kde/Autostart to $HOME/.config/autostart +Copy pre startup script files from $HOME/.kde/Autostart to $HOME/.config/plasma-workspace/env +Copy shutdown script files from $HOME/.kde/Autostart to $HOME/.config/plasma-workspace/shutdown + + + + +Disabling Autostart Files Provided by Your Distribution +The correct way to disable an autostart item, for example the printer-applet if you use printer from time to time, is to copy its .desktop file to your personal autostart folder. Anything of the same name in $HOME/.config/autostart overrides the .desktop file in the default package. Add the following line to the copied .desktop file: + +Hidden=true + + + + +Files display +The main part of the module displays the programs that are loaded when &plasma; starts and scripts that are run when &plasma; starts or shutdowns. + + +Icon + + +This column shows the icon of the program or script you want to start with &plasma;. The icon is extracted from the Desktop file from the Icon key for a program and is the default icon for a script. + + + + +Name + + +This column shows the name of the program or script you want to start with &plasma;. The name is extracted from the .desktop file from the Name key for a program and is the filename for a script. + + + + +Properties + + +This button is only shown when you hover the item with the mouse pointer. The button (only enabled for programs and login scripts &ie; .desktop files) allows you to change the properties of the program or script. You have general properties, permissions properties, a preview when applicable, and properties related to the application or login script. The default command is extracted from the .desktop file from the Exec key. + + +For a logout script, the command is the path to the script and can not be modified. + + + + +Remove + + +This button is only shown when you hover the item with the mouse pointer. Pressing the button will immediately remove the Desktop file for the program or the script or symbolic link in the Autostart folder. + + + + + + + +Actions + +On the bottom, you have the combined Add... button to choose the type of item you want to add. You can add programs and login or logout scripts. + + + +Add Program + + +Clicking this item displays the standard &plasma; Choose Application dialog and allows you to choose which program you want to start. After choosing the program, clicking OK brings you the properties for this program. + + +This will copy the program .desktop file in your Autostart folder. + + + + +Add Login Script... + + +This item opens a dialog that asks you for the location of the script you want to add. Scripts set to run on login will have a corresponding .desktop file created in your Autostart folder and will be run during Plasma startup. + + + +Add Logout Script... + + +This item opens a dialog that asks you for the location of the script you want to add. Scripts set on to be run on logout are copied or symlinked in the $HOME/.config/plasma-workspace/shutdown directory and will be automatically run during &plasma; shutdown after the user has logged out. + + + + + + +
+ + diff --git a/plasma/workspace/doc/kcontrol/colors/CMakeLists.txt b/plasma/workspace/doc/kcontrol/colors/CMakeLists.txt new file mode 100644 index 0000000000..e6eed59f93 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/colors/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/colors) diff --git a/plasma/workspace/doc/kcontrol/colors/index.docbook b/plasma/workspace/doc/kcontrol/colors/index.docbook new file mode 100644 index 0000000000..c82287a578 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/colors/index.docbook @@ -0,0 +1,367 @@ + + + + +]> + +
+ +Colors + +&Matthew.Woehlke; &Matthew.Woehlke.mail; + + + +2021-04-08 +Plasma 5.20 + + +KDE +KControl +color +kcm + + + + +Colors + + + Scheme Management + + This module lets you manage the color schemes on your machine. + It shows a list of color schemes shipped with &plasma; and a preview at the top. + Only one scheme is active at once, but you may edit schemes. + You can remove schemes using the Remove Scheme button that + is shown when you hover the mouse pointer over an item in the grid. + Note that system schemes cannot be removed; the button for this action is + disabled. + + It is possible to filter the scheme list using the Search... + field above the grid. Moreover, you can use the combo box next to this field to + show only the Light Schemes or the Dark + Schemes. + + + If you have an Internet connection, + you can also browse and retrieve user-created schemes using Get + New Color Schemes.... + + You can also install schemes from a file that you have downloaded or otherwise + obtained, as well as import &kde; 4 schemes named like "*.colors". + + + + This documentation will sometimes refer to the + "current" scheme, or the "active" scheme. + The "current" scheme is the set of colors and color scheme options + that was most recently applied, &ie; what you would get if you choose + Cancel. The "active" scheme is the set of + colors as has been most recently edited by you, &ie; what you would get if + you choose Apply. + + + + + Edit or Create Schemes + To edit or create new schemes select a scheme from the list and press the + Edit Scheme button to open a dialog with three tabs + Options, Colors, and Disabled. + When you have created a scheme you like, you can upload it, reset it or save it + under a different name or overwrite the active scheme. + + + + Color Scheme Options + + The Options tab allows you to change some + properties that deal with how the color scheme is used, as well as some + options that change the color scheme that are different from actually + assigning colors. + + + Apply effects to inactive windows + — If checked, state effects (see below) will be applied to inactive + windows. This can help visually identify active versus inactive windows, + and may have aesthetic value, depending on your taste. However, some + users feel that it causes distracting "flickering" since + windows must be repainted when they become inactive. Unlike desktop + effects, color state effects do not require compositing support and will + work on all systems, however they will only work on &kde; applications. + + Use different colors for inactive selections + — If checked, the current selection in elements which do not have + input focus will be drawn using a different color. This can assist visual + identification of the element with input focus in some applications, + especially those which simultaneously display several lists. + + Shade sorted column in lists + — If checked, multi-column lists will use a slightly different + color to paint the column whose information is being used to sort the + items in the list. + + Contrast + — This slider controls the contrast of shaded elements, such as + frame borders and the "3D" effects used by most styles. A lower + value gives less contrast and therefore softer edges, while a higher + value makes such edges "stand out" more. + + + + + + + Colors + + The Colors tab allows you to change the colors in + the active color scheme. + + Creating or changing a scheme is a simple matter of clicking on the + swatch in the color list and selecting a new color. It is suggested + that you save your scheme when you + are done. + + The Common Colors set, which is displayed + initially, is not actually a "set" in the sense used by Plasma (see + next section), but presents a number of color roles in a way that makes it + easier to edit the scheme as a whole. When creating a new color scheme, you + will usually change these colors first, and use the other sets to tweak + specific colors if needed. + + Note that Common Colors makes available roles from + all sets. For example, "View Background" here is shorthand for the + Normal Background role from the View set. Also, setting colors that do not + refer to a specific set will change that color in all + sets. (As an exception, "Inactive Text" will change the color for + all sets except for Selection; there is a separate + "Selection Inactive Text" for Inactive Text in the Selection set.) + Some roles may not be visible under Common Colors at + all, and can only be changed (if needed) by selecting the appropriate + set. + + + Color Sets + + Plasma breaks the color scheme into several sets based on the type + of user interface element, as follows: + + View — + information presentation elements, such as lists, trees, text input boxes, etc. + + Window — + window elements that are not buttons or views. + + Button — + buttons and similar elements. + + Selection — + selected text and items. + + Tooltip — + tool tips, "What's This" tips, and similar elements. + + Complementary — + Areas of applications with an alternative color scheme; usually with a dark background for light color schemes. Examples of areas with this inverted color scheme are the logout interface, the lock screen and the fullscreen mode for some applications. + + + + Each set contains a number of color roles. + Each set has the same roles. All colors are associated with + one of the above sets. + + + + Color Roles + + Each color set is made up of a number of roles which are available in + all other sets. In addition to the obvious Normal Text and Normal + Background, these roles are as follows: + + + Alternate Background — + used when there is a need to subtly change the background to aid in + item association. This might be used ⪚ as the background of a + heading, but is mostly used for alternating rows in lists, especially + multi-column lists, to aid in visually tracking rows. + + Link Text — + used for hyperlinks or to otherwise indicate "something which may + be visited", or to show relationships. + + Visited Text — + used for "something (⪚ a hyperlink) that has been + visited", or to indicate something that is "old". + + Active Text — + used to indicate an active element or attract attention, ⪚ alerts, + notifications; also for hovered hyperlinks. + + Inactive Text — + used for text which should be unobtrusive, ⪚ comments, + "subtitles", unimportant information, etc. + + Negative Text — + used for errors, failure notices, notifications that an action may be + dangerous (⪚ unsafe web page or security context), etc. + + Neutral Text — + used to draw attention when another role is not appropriate; ⪚ + warnings, to indicate secure/encrypted content, etc. + + Positive Text — + used for success notices, to indicate trusted content, etc. + + + + As well as the text roles, there are a few additional + "decoration" roles that are used for drawing lines or shading + UI elements (while the above may, in appropriate circumstances, also be + used for this purpose, the following are specifically + not meant for drawing text). These are: + + + Focus Decoration — + used to indicate the item which has active input focus. + + Hover Decoration — + used for mouse-over effects, ⪚ the "illumination" effects for + buttons. + + + + In addition, except for Inactive Text, there is a corresponding + background role for each of the text roles. Currently (except for Normal + and Alternate Background), these colors are not chosen by the user, but are + automatically determined based on Normal Background and the corresponding + Text color. These colors may be previewed by selecting one of the sets + other than "Common Colors". + + The choice of color role is left to the developer; the above are + guidelines intended to represent typical usage. + + + + Window Manager Colors + + As previously stated, the Window Manager set has its own roles, + independent of those in other sets. These are (currently) only accessible + via Common Colors, and are as follows: + + + Active Titlebar — + used to draw the title bar background, borders, and/or decorations for + the active window (that is, the one with input focus). Not all window + decorations will use this in the same way, and some may even use the + Normal Background from the Window set to draw the title bar. + + Active Titlebar Text — + used to draw the title bar text when Active Titlebar is used to draw + the title bar background. May also be used for other foreground + elements which use Active Titlebar as the background. + + + + The Inactive Titlebar [Text] roles are the same as the above, but for + inactive windows, rather than active windows. + + + + + + Disabled + + Color state effects from this tab are applied to interface elements in the inactive + (windows that do not have focus; only if Apply inactive window + color effects is enabled) or disabled states. By changing the + effects, the appearance of elements in these states can be changed. Usually, + inactive elements will have reduced contrast (text fades slightly into the + background) and may have slightly reduced intensity, while disabled elements + will have strongly reduced contrast and are often notably darker or lighter. + + + Three types of effects may be applied to each state (with the effects + of the two states being independent). These are Intensity, Color and + Contrast. The first two (Intensity, Color) control the overall color, while + the last (Contrast) deals with the foreground colors relative to the + background. + + + Intensity + + Intensity allows the overall color to be lightened or darkened. + Setting the slider to the middle produces no change. The available effects + are: + + + Shade — + makes everything lighter or darker in a controlled manner. Each + "tick" on the slider increases or decreases the overall + intensity (&ie; perceived brightness) by an absolute amount. + + Darken — + changes the intensity to a percentage of the initial value. A slider + setting halfway between middle and maximum results in a color half as + intense as the original. The minimum gives a color twice as intense as + the original. + + Lighten — + conceptually the opposite of darken; lighten can be thought of as + working with "distance from white", where darken works with + "distance from black". The minimum is a color twice as + "far" from white as the original, while halfway between + middle and maximum gives an intensity halfway between the original + color and white. + + + + + + Color + + Color also changes the overall color, but is not limited to + intensity. The available effects are: + + + Desaturate — + changes the relative chroma. The middle setting produces no change; + maximum gives a gray whose perceptual intensity equals that of the + original color. Lower settings increase the chroma, giving a color that + is less gray / more "vibrant" than the original. + + Fade — + smoothly blends the original color into a reference color. The minimum + setting on the slider produces no change; the maximum gives the reference + color. + + Tint — + similar to Fade, except that the color (hue and chroma) changes more + quickly while the intensity changes more slowly as the slider value is + increased. + + + + + + Contrast + + The contrast effects are similar to the color effects, except they + apply to the text, using the background color as the reference color, and + desaturate is not available. Fade produces text that "fades out" + more quickly, but keeps its color longer, while Tint produces text that + changes color to match the background more quickly while keeping a greater + intensity contrast for longer (where "longer" means higher + settings on the slider). For Contrast effects, the minimum setting on the + slider produces no change, while maximum causes the text to completely + disappear into the background. + + + + + + + + +
+ diff --git a/plasma/workspace/doc/kcontrol/desktopthemedetails/CMakeLists.txt b/plasma/workspace/doc/kcontrol/desktopthemedetails/CMakeLists.txt new file mode 100644 index 0000000000..862514e80b --- /dev/null +++ b/plasma/workspace/doc/kcontrol/desktopthemedetails/CMakeLists.txt @@ -0,0 +1,3 @@ +########### install files ############### +# +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/desktopthemedetails) diff --git a/plasma/workspace/doc/kcontrol/desktopthemedetails/edit-delete.png b/plasma/workspace/doc/kcontrol/desktopthemedetails/edit-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..9be3fb20fc8f2f3f15897cd6acac18b23c3033e2 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4foCO|{#S9F5M?jcysy3fAP|(%W z#WBR9H#tFqHQ<1U$3flc5B}dca`oF9)fLlInhPV{#2X&U7R5RjUI^gnD73UQ^|?}d qZpFdHZ2bDoksmzyGBnL<7#Ko1MLhra7dQhAWbkzLb6Mw<&;$S*H7vjY literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/kcontrol/desktopthemedetails/edit-undo.png b/plasma/workspace/doc/kcontrol/desktopthemedetails/edit-undo.png new file mode 100644 index 0000000000000000000000000000000000000000..6080e7de9148bd6ffddd01a20e7ae23e8b7dae42 GIT binary patch literal 374 zcmV-+0g3*JP)&Lb46BLL4Uzcdy9bOva{WY{()d)l~xc0 zKOuh*NxD4P+6aQBrJZ#YTiIskStLjZyUDtw%x&+?;mj~^V3Q_s&UKt~gEo7&mqCF6 zaNY*IwFEj8kqcEFw*gB9bPxCd4i>5aOn~RIEFbbbfBToQs{SxN{+EcHsj8@|#BsbU zA{p=+MNyun>8&0#~8cjsx61Yl| zcs2iZU}&l7Dg0Cap9Kpu>nnM+zyHJI|ApKCOZ%^K|35t39saNRAGvh%aQk0e z{SVjI*Z*r?-CeIYYo*ti`9-aJ$|DW`)r}Oyk z@bKv1@aX91_#dt8U+(Q6{QvZG_i}e{e}3n3=J(~y_T|d&qubv#TmQw**1fcSRs4 z=5nn9^2U;~8oaVcGBQ5DOB-@Z8&*x5Z$bnGCRRBl4#mS#Y~X$7(fu6}hDKrEU4rxU z0=onK!c4pi)xEpLyxU4WRMp*+?A@YZ&d&N)(S~oFbVhRc}GGcrpLR^CXX3W2A!NVb0Y3~5n&O)rsh~r?_K-_@a_ML^z2Xmn@%e! z!pKD>NXYr4qwr@3a`3xeRh!^wGNgNuA&c=L*7}5 zaOss4jO}TfUX-naUIzOs8j>v>eGlR9c$IO(#ze^&I#x-ymKTRY3aKJEzTsJ)0Sxu4 zM6c{PxQpI>y6hsLRK`R+i=*UbE2eI|$*hl!Gv?+lrja8`pqcz7-5`vm5UNBP`<^3p ztFeEcz)qaIIC@x*>hRfLzKkqr*zt@!DLKd9Jk&`Kv^vc_U{@F>;qt%uRFDa zBWcKrzJVk`T1Bdxf9eIBcw+OZ@)T+X4X`-UeK`1)Ek2->R?GOI*M-;$r*#9MK5Cvr<2TPs;5g(J@zYeu-*6fDqcH2pD=NkFQX_1T?fA&FErA+6T>J<1$} z#eih7tXSj-^G`r=H03iYc~fTX!%6yNLT4OY@xN93E6mmhpS^K);T-f&nTmgHvAGh0 zy;AcOpFqW+WlE)GvPC@L;PlWE>Rv#S$!JQA4z@BrA=4V8w>=HFC#=UfGNv!djbdH= z!nMPk&`DTCM0O2=c2~O#th9Z zKfy?6aeX@W4i2Wh8%BakejL3w_k7>?w?M%wYk||CmuG^nHS>ts0 zwe)RU4}TNdg+pOrV-_(C97c?UyNHdqK8@MUqOD79Z3~hZuP`vU0FE*CJ*)?(4w_9OjN+4d(ndF%XHqvxXgiQE3uJ} zGPJva8&b|aXhVqfnIZk`%s@6hI;3qi%Bu-w$@}Zj@yFXJQBl$Jn>F5B!Fs!RwfS)Q zJr0SsS&)f?d#}VTIq?1`Is)SP%TE}z!cG_O^PzL9CD0ZfT@5v5rga~26USTftZUm} zM;s$WB|R;q)Ih?O&b;jK10OvT3)c8NsUV+|=qEu@K-!NM{jkAHRpU<55PDF741Q5# zj8w8JP&(i?QmW7nuT4D-;w&vBQB{r}vfNCk8z_|41dSJxP7wh1$%Kq5iTNQF08uBC zCEeaS{}=+#GgII0<_-$-S%TyfFzzA9I*2?FK=O35GA5bmV*6FkJeM8mZcKeq`?-Ij zl; zX$gLwsiPGs1X%)9X((CVp;3C9sn~WSix;hSJ~aA#SI(SxUQ)Ozu&=+jW&1dU2NX6A z5{YaGdmmrlRhB>9c`3TW1c_yuj@1?c81dX3KRjEj+52G~CwX3dTDt|F$e@y|VS1Ox zkm%@t(R^9vcQ>#w{0fO`hTKk04IHq3Ah*a>7$4C%65Xk@+L7s73#f~bSOkauypW(6 z2*Fn>;cA-$TybbTd!7*aTMm9qJdIA=VI70+ZpBe3iPn^cShb60EqX{C{l-40dw z3>yCheA>#3%af#ERG*rj(6>fxd@KAE-1LMrOFD9_lvr%%No^H>?N^z6 zIUlPN`M$qhLo^RiAvXdV&+jtN2c#oKn3kt@hA&0VM5kIU<*AY8F6hUrYA6Kc`L+Zq zwh4HDr)-6@*R(2z@mvT5(EUOj9|p~#anx4)dfhOkq}bzZtI++e$LS;8)-t-BR{F_v#Ky(TB z)!u*Gpl_;(ptFVTba(tAClHByMhFoG`st^%0eL=b^<3`MFVXt+h3a0Eg5PQIsI)Ud zlXlFea3Oo{B!{^mZ?{0R=?S~%@8n;-Bmq!!g{SiL8L^(>P-iC6(mO@^ zIZ1j<;n~NUAI)&(ecF5v z$H0?|s&D;)30{Gib}m4BS0Y|g8z6}oY+1fT3y&@f@wjWb8M_2WfPOTZw+-Ud-0U6L zjN28vM`VLwCmhN=Oegv7p{*ALv*9f#%+_hcU3wJ-JGy}PaF^gzYbBugQ&-rz2(TI0 zOd7(-T>WK0rZq(C3h7sS>?T%7XH^Xy1947D14R<5X9wWBW&+O9Ui_U89?GD`zLUR8 zfx%M(D@t(RvMi4*TcR}xE~A=iUm+Migw;<^y7v@v=7OSW0}cdV%6>vOn#?h z={>^oOhiqvucc^0Rs&VwC}2MLZA=B`){`H?UKxuaF+0ww?VjPJ?s1mG-f*5FS|Ba= zQfb`Vbr;)>wJO(Z5C$MZa&GmA-XiE*pH>qv=&pfjw$*(T>dhm<&YX`3kPU};b`Mld z9va76@H*KV0;cg4LELL`9`Y|SGSw)(DI9aWK9(X9;_PD!a#o+)5#2bJG;+W%SXavu zRJeIwskMcEGhIXzuz&o>-HL?^%>|gilZm$Ior-kWtb{Vw>n;7k;)Gc=72Y@=1z4;VvSme?A}rlUFX>NomI=bHHsI``i~!e!OxSdRR0+f&S2} ztx^nVGI8idObxVmwtRuQwYHH$C7T-m@jEDTOtZOn3?@akuWo&MJ(FYQ2h>OtSB83< z3%)l<{jJNj-%#e_ME7mq#4f@pBf#h)`j2C|P_}>@k!tt~=Vb8L7r4ZR*i5e6{j1-A zu_z#(w*Fjwxk1tIS;;axMh<;5Q!jd!T>HsQSeve{aD;-J2w?!>*Tu>tKN|T~*C)qL z=YogPHcl1N_LCxGk6d=yUdnc_UJyjlF2ArS?hd7z(h?L}8TiV5)U@&<5ZN^ya-U^= zcwKG-`E}KU*gcTsV{C0ki9ias+}`CT_)N7v5YxaUWd7;{UBlj+febU}J_CYCE}ydB zy^Za7dwjoG;J*1sXkttvq&gw}+uZCx`vq#_g}`%Mq+#yyGN04M+W3?saYc~{a(I}! zOoqd(JTT@GilxeYS>Wb(XTCk7Bj#yJ@3ZJqx9>cy0XN_mcy15P9qUepvU~wP#A2v= zJy=%cm&tG&*NPfkMiuHfXzx8lwczKDS^G5kBMNdErHw$pG`AlKh7%U>Z9mgXdvx!t zUM{q5)IiU9|LyA{G#-HOxYNs9Z!B28x?l9Yl=PGg_gQ3dDG2g*aoJ=B8pFRS*75<` zAMHBqhM{smZLu@vZWH(-64?wXocqx(XpThuOjT*y&FgdwM|Wqx**5+1R;;r_+_488 z|F_|TsGw_(6JuAv0TC_t=Yj8Q+5F%+G>SA}S&*lAjnauWqe0EVTMau~lzGvR5 zqLc zxv9I3-;eGY2>lsKP-_yVLqd+QjwDH6%&(<}lJkH#%I%?8WSI6izC7$uy=F{0FBM4^_pEiN9MU%;^;F8^{kZzC$3%E(_5)4rqo_I z%EPeotot%uM;yNhIgZ+zea_sQxRnhBpnECRyR7x+q(F-t1wMf3v{z}0$mQiO{H*An z!2@b_``__N`{(_zp|X}U_o5nnodA-rhK0t^ru+mAtFo6sHc-gQ@IVZ`-D_s0lA*Hk zL|6s6q8B?hZ>0+f@fux?(x=6$$WG1c({_CL?>qvbj=Fb%Z&XocZ-T-LR zW!mjh-wyBKKwe;)h`BpC^nFw)NwvDO$>KcY93%L$i{kg}!|{Hwa?{0Ik~iY7f9JQL z6@sAat-d+hS_a;pShMyQPXu)gXD}0f)33#$eC+&0XT9zOgji&r5!Z|FrfM@%NOLn- z<9}Tg37LYh!Y}%Vdr1Y)0{j|MsG92}u2-)=#)@0`~&W8MY zdXN<~2un!OZvATX{P>5X%J{ZnvK6@i5f^N1&>(lTtlAjrl}GrHES>X0hh8?idplxq zZuKSXci-=?wyD8dPu6!Y2_C2B);xDwoQcMnjeFnIFLt0-Y}R^A#>wW zjFc^3OyQ-*QTQ^o(Dxe*pOo9;tnkwJ1foJek@dj(tnIZSwvo^p&Jvvv7Qz|jd3{*8 z0OtO=Y@)1PB(>s&%hirvO1fa72uV=)V}xMOuebYF#u=)VjD;i7{SBeR;`LTSt`{!Xscvu;SmaC1Lu)O}xRPwFkoo{ZiK7dX`oY4m4EjJMmc zi2Mvs2!_OMe}86l=JJy=aBwAy;Nu0$W#EgPu`NTYPnYk!0*`Hl_sIXCC8C}PHGKMa zJ$M_(glE0ElBj*bN9(;?Bocf8!z8&VET$?Ox&MH(K)GPdi+UcM;8=gdU#+>qQ2JUU zgQ10rvbij~SMoFLMNK=SDa4lNVJDZS zcl-k}7<};@*Nup`OGZ99kWTodB3%#EM@%deB*n%+cHqr+#`Ja`mR$eU3D7FN@@1K4(J46s0m4`hPD`$nP)7JhP*m`nWiUicpI~)m?qyn`kyW2S!qS z0hCbpapjVtS>^tTBam> zllTWgl7zyq6*8jEp(|VYy}6;pF2E;m3_1pX!m6S~1$(wRZZ}k*?;yiF&Cfs*f|q9R zVRiIyX;(|gt?uiwT#Z?)Nb`4Z$0(7$r++@p97MBoj9@=SsBIs{w2mZi-i;qV>;MfHI5^vY&SY0p7vceDi4fh|1 zddNxf0>{hBdPV!5@Oie2hGmRI?|;~Q2J(v*dN7G3T>SSk{!zjkQH2ELIifV@gbM$_9z zuJuMs`#C#fLmf;;%Yx{T7{dO$2|ZWK^viu*b@AGg!oT<#AN!w=4D_!FAU|Xvu6kk1 z;-1iBA<(;M`t|@R7@yQQ*T$r~*tjst9R_p%jFSjuB!@$rbl2zUaqlt_qCK$Nh>@7y ze6O5t^1?*Hm;#&rX0BWB`$FpZNlKQf1k(-*NR-?^wgWyMAaQmmjfWYP2< zVk0s19{(960J6W3Z1d?_69fM6xYs`l5PQq2WfcY38LBFUY(vAFL_kLx@FhWPT{Bc; zn>`t-E-1WdCi`f_W2^#qIJ15GO6p28J_NXNZ+I>NpQ!l_j$D zJ+tIg&cmOZ}6L87o_7 z@~cs^moMkokLFs9J)k9N?YZeC!_PmZeXu<;cci%5U(Ymlf=21T0SLJmD5J7klk{2~ zE7CpxHebh!IaM@2S1mPY@MC{+c-dj6mx*u^-KovD|LI){HedSU99e3WO%E+O3`b~1 z-k}rBlil#Iz-t6BvxO=FP_L&H>eH`;{Tq&5sk!Ll> znA;+acfk2c&6a!hAM0p=CnEhHB>Jz`)Sp_LLJW}4={e`A)X=`_gVvw4HlnKY_QNl< z<$3K)g{b0;d}m#~HKQGJH=YKlk=~krN3Dg{PgNczzI7LuaB1j<)hCIxY5wHq8-F}{ zarbbfS?T~2TvHNqZu9v_KeK0%&3xoG$$YeH0vBpX*)JiW@Q(rh;=f~4;3n;l#z5nu zuq^kqatEJqN@?s^)f~>a%ung~bP;?2`^+DIeBjz+y84N9_Fc5D5|Mi1c54`pcAR%@ zSgRWBiI_drokbC4hi8gONgj1j&I68=ET_A53^EWaC2xxL#adcgApD9csQZ81925%JPD1z$efZ1O~+sgYE*!0U}$Y`s*eN~?ThK^8mw zH_w!H{XG#}MUdF1Sn992pB632WLNLaDg4*oP-4`UapW+bbOacvk!7kzy$UmXF zXWtu-4jPi$Ze-r~M`RVrVLr(jfqdtjNhvd*9WipPj1sU+iP&4pqc*p&h+1RNh_a1c zwW)TnZF`J$>kXU>9)ZQJcnoD%s5i;SFfIn6vLjUgMB@cd6meNWe-6sD73lM=HwKpE z1A5VGKloV`BZmJ8O@ChIDeBy^RP;ECG{6?tg$wF&8!ow?z==8wq!7HNhCPaYWqX~0 zO*u3`DW|+)tz?!Pzrr*kb5%G@*N(P?_=<;kUdMq(9q%9@JE;0h_X|(1*C{h8e$ZZl zQ9+fb`|OZZFo()!oyj-AhS(VUv!w6hc&+WXTZ8f1{3n4Q0A3ae(Dw1(e-?TnASJd} z4Gk^Kn4m;FqqZpHs7TJh!K=)$0SovZ-0>ysx zf8hTez5a>duTwX*oB#u~5ltCVOYfR)pSo|)%OIg;(%>7nYkRZWhoTZtY2dVUEG|;g zB$3&~G0n;uIyZYd`aIci7sKdRW8Kv0pQWx(et8TEjXSg<{C6g>;D?eam=1KaL`p|@ z(suZJoc4F(mzBqo9t|enO2EHkW8RtckULoU0-7YlR4kqGtsqy)R~5Bg1Th*NTRKWv z+IX?R8E{CQ0SUYhzc(Gufz*@=vhGbsvddfqmL%Es$mHu1V&!GfUGYmgtgbAS`LmnNr4hTGo3U?JCxujRez&DK`H$~PL7kPiMgdG;m3M#!Xb*B_L(^FW)I0eQtDj*(B8n3M|z)73NHyOJ{&oAz37CwMh z4nYYFQE26ApolAK&Q86js(rLRm+vR1G{W1lbjs!O;Gv!Ogq4!{5YppZ^mGcaR+LH* z*;<4);cyVQkb5uCttu9$9Q^pWY)n>8QIb=?Ws(^tWvJNT&^xZh1j?j8l=|pRg<8bT zNs3>6c00l%qfWdxv9l^r|91AEuB5-}_iwR;{fqL2rYb%p+520*B^S3a8j)X4kyLCb z8UIeaHsjo4ZUHx4VXW9+rcplhZR3`HQ7l*es4P1jt@g6@+vnMG?RZBsf8QdO zx$sN8|2*atR?NP}VkC_tLFm(J=O(G_)o-gps|r5#V%Ttl!!QTX5K!$|1sNCiDV~L| z1waQAIn6MhIUmYOavy9^3+1&oE{ZY8#xb{6D6rH1L%Yf@-iy`Ef z6Zl&(R3^dh=W%TEFygXitoD!-yl9BUVv~=IlD>3UOvrOua9kk8;EnlGVsdRnP%PsN?`o7Fx5ob>i}a?vi=iI^QO^H%Sx zErHH}ytwg{EXa^aQQsZ~yIvU5E!%6bqg}IXr3DokLwmhx;MANCWIBLQN!}s_)bMN% zjZ0?o#aj#RSBo&vB15dKCkVpnrwT8cOC#G(yUzR0<}>_+S%kVXZWWbtf!>lFhY1>V zqV>)N@PF}3{nXL6m17+r3xzdhwvA3dtlw3bj7;6_Amfi^kWOi#MV?`02&Kia=s9SypLRJWAx zcZVd;?ON0#T!KvR0g$@ANykz|4*h`9H<+kr5w{M=>jz1eXYNasT)EKJdhfO-CwHCH zN|8f~KNyQv)%BUUvW<2^)CkLWIT?a`F$~{D?f$ZKXDD=`^ z{(#_4QRpL&W7N1xY<%a-cF70nR_DaM0eMg2oP@USwo?JX89htXxp>P>y)%L0r4uBh z!@d{eBop%(@pz0{J#ubR6fX_OEzcb)I;9{E+on z67-;H7u=RJW)=WYl*;LEJk7W)OpzC&nl7fyR?9rbxAxNqN(bXfwRR4XPwj5lH`X8d zZCrZY!gudyF7z9J-jj>GJ}QD%GMWARil@Ah#F$dZNR+%7*Ev$_vW;ut?Dzfs`8q^> zdHq#B9#jW>n>O4laMF3Cuz|FV1UEbBg_WRFxKBg_99DmQMTq>{M4sbPV6a$XXj9N2 z4c4fOlh)0FVpHbquMkKIRPoXmD|-A7o2i%|h$y^axg`s&*JM_`77h3^Tp2W-YcK^LmgjzvGRyAk z%Wf5rIr5vxjI+1~isenN6VEgF(}`581+IVgFWv-BGfiB-6XLb~#*K}tgRVUPtcR?& zpQ-g*hE@lUe=bG;`SYhB5B_B#{7};WzGcPapR=T6S{B|oo<4#7F*)P`nn#${AhN`h zNZ+b3r$xj)chG&~iBe!~k2iEU_V8b1>i2Tb6o&+o0RQU~Sg91t^!n_TZU4ZNaw6FB|rL;nDxWtS=+Szjh> z$g2G{xfRo}V|Aqc7ogRAryDrx7+)6j_IkP93_g-rIfk+^IH*+qQ3FiZ7in-R^?X-t zBIruI2!Sh$I{$rBK3`)4WMEkyv|gZ{Y9dkpw_X}&^~PV6RvM^}>npz9fJVH>i$HB3 zlTOJpi5ChUsQwApXy+L;b4;z({_vxG;WDf7sODYZ-t<|cHpTU$+YGUc4#HzG0O0?D zLG~#S&!C<8?RA*p`{4A z3e`ZT>Kq%j<-QaOAO6><^K^+;5b2gWf6K2hdszSy9K{_KAIyIgivd|$o+tjPL|n2> zJwrXa8IUh`!IS*g(y3YdE^H$Nh1WA}CRXCChs6VLdJB}~ z*N8q9++wA)1^+_YXr>B4_GtwSQ+;dX!Iji(P80)DTwI!AB-pUly4vplxl4bD zuoxi}XHWfcp;W7P2HKmefdPn%U=+6esZO6L)j>C+L$i*R4}IY)+1seksOF!*p!$$` zx0a4)9VHs^l_K7u9RSC>VYhEr#m4gg7C_;XNqmobDy2px{VSEcule$+!c^J_cwUD@ ztPK^lzuG!BRIC8|8FdW%My$$HQH??1AtR$nTXr4`PB2o?6nV#6SN1T9<+2xA?y~o< zR=i;bE1%8$D-NJ74THkBd-N`;+-^mDLm9pu5PBVav27Tdk7O^8LvSsXIK`iaj4gHg z<1-r-P+Z}pGZLSiv7ZtCs}A+G-OyJ(_le;@lW%tX51B$mdEj1f)QuW{P?4B8sO5q= z)@GCeu1E5O5RnX;aP)=JM_NA4ei=ux{Nc z2=t^_(fOGjf#&9gG?X;VWgvyVRAFt(p~-=VEk()sMfd$jGk8rWR3qfqz=N;?#Keh4 zGi;1Qx8!q;F>x*so;N2m};;z;=F#pL#rvn=B z8c81A*~JpwQ)WMoE_2RKAe=XuEze-6(VwZN7~Xx$d)whXSN7JZ5DM|kXx zGGS`GHuY7cf_9>OdkjG|6_tphv2l!;ZRP8^CNQlnd!cxdFC9dOAodyUIjPo~`H3T& zTP4)<)hAI8)&p*{rNoe#-oIVq48zIGT|s$c)0gyWf;l;PREUJaV0j(&6G4v1-b~SL zLcwZLv^kcYbJH&BYVp@^e=9gF&e(hL^4Dl%-UV442;No=#PnxoZweLbnx;%2fbJor zlvupQN;58ho_?n5r5pdztc`QV*o$pGpkO{>Srq@ib*+h3lC{vOviv;3WphSU)be~W z97rFvJNp2=e!AM8@dTRT+hr$dMxWf!Td%(( zh;@wgi*%w^1V?>R1SB4EhxGstc{5{Wb9w(10`(ckmCE%jJ6}Xe@F(U*nC8HDz<$5W z`jFoSaX)u-spOj#XiaaEP>w0lL_qn+1mZI$ID2rug+zzrBl47d_!UZ$G ztiDV7mcDQ8xw|lgCIQk5nB(O6dH|$Z01uLTY<`@cifFAdY^5bR?sW-jdKvMAv|?@E z>;A{ttKMJcjk;kW35 zRLG~_G8VV;*u~8wRg!mpYWdiLT~bKc_~JlO0ybtOdm4h)MsL;QD8m*A)^PGR{zcZQ zPyzBfMMzmL4z!HUs723u{zL%%*2cfEya=Bl2g2EZmhA<-`>EPBXf|`e;qD^BZtMA4 z!yl~5Bs9!Y*o_y^!gVs?ar~~@{hz@#t##Si5-r;E_Gi@weWUqodPC2BA6Dw ztnd;DESzxg{PU!UC*$!M=V45HDD=x!gfTBnH|YI*?y~C)ffm-5UqmCSsKEHui!6L9 zY>Dp@>@3oI=J$eu!3miYvQ4a7Z%B?Q*x89?HT)OVrYCqVp_o~9TX5>|zx+0Sx0m+8YDM$k9}mA6_7n-K**Uab ze-v23#=b#obwtnwBnj?utUsf?90P?z8H#7ay7^+FewC4kMMmG991B^yn-iGlH!6TF&K*!6?O8&G&4 zyCsCT_Pi(Kn|FFw)Q)ko)c}4J8+~^jOQ*^Ow%m(Nxmb)d+uak4p9LiMw!0W_gbRLR zs(r$?_iuhtDsWZ-=#JLV)GW6!BJUUy1b)sich)v6$3WgH#G^cK9&B(P&IGa^*}Vhh zU01^|=#%Xo7_1@(CZ!}I(cMN48fu20cri8yP#haEn1Eu5Mz*9#L$LfdhN!1Im__P4 zwXr`pli4ebn)6u1W~FM@9DGri?(|lmuro^{K*ca~Bc{MJtmd@n2oFTSHTry)WJ7Dt zo1SY>4OyHkg`CK7V@OOf?9zH9Y~?CEU`Q@jjpFd{4c^u-*``@l$R=tdFZ)9#@E4O- zfPhc(hK(q7t9sXgx}FTnl}Z`(`1^+v#Ygsy+8P$CrXy4uH0@oL$n3RAL|dE5L)Fj> zu=&>i&^f1xZ}; zK@x}ZGb$ox3xgudbZnL2;L#RQszl}RE&-}P`exSqLx3QK0Hmki<<*2k=ilC?y{g%< zKF4D7jceZETxwutVDtA*O_-a2#0Wi>d}A^p2MrbQ2mrHD&r?E>ZpfYuNllP$PX26w zpc`n+lg+f<;u~y)VZQ);D*i`aBpkaGOk_E)ivEXVoRKBD!bdO zSYep?^M~Q~<(!URT5aWrk1H#d7t2dZGMYjuDjx+3fkf(G@_>V$W?#`^HJ>FFMCz-R z)APGhpAXI`91iBML^BK)a;ZvGZ+DUL?s6gXLrV=*Q>E%Ty3@L5i@|wljxj-JmG!O8 zVDp&5egTxZQ|iZsG$GURw2(gvOQg_EsjtNT6yrF`xN>^=gitk=Fzs$c_#tdQq`13o zqiCl#agDsiY4`> z!}SQqmqxw@N;5eva#>g#FF)_WibuIj76QLO{iKYBjW`rX*g490KEC-bAOivp7E~Siac0R&wlr=Q4s~0 z6GJ6ZJf4by;S;h}$=ZQ9`q~$u;w0KEl+cr9q2ePRV3mM7mzq4UVKT2dI`WI{U;{^1 z@^$1N=mRJe7sm@h1fb%rpAp(&Qlv3mCD6R#oPL6BZKpv{GBaY6f*bd|;BN8iFX>WQ z(_`18i&Mzmk$*L+#hz$uW#-7>oxrW1poHQxWDE`$!~g&#nt^7^Myy9>_)%`RzrCx5@a|Qd z<4Pv5G?Hyq=lwJtB(8dIwm@K1FrA14ySF zopy`pZc4b~8zRf!O-QAaJN{-FfH5Aw4Sp)XsvtyZ63%+i(v@JV+^}KXv9b4BIK(cT z%4O7<40g2C*5ao*Px0c|^~h~`VP1jyO1~KZE!J#a80J95r7;cB3YvJ!b!zRTkJEcK zie{NlW!_3QV5C-1D`p*4s2x%ED^@%;WwX2E9o(VdQ>{%*#AX3pE?;!@$w#n!&JC?# z)dW8kSkomRDdXfNi#y&b-SCjpnKuB$+Dd#zl6lc(T zPa;_1&ZcXvs0T=%hN^l)i-HRj%WP9#Cx*JHm0I@Xalw zRtVdsPj{<;{FIFwaMsghY#sOaP;+8NLC7M-dR+RO&h8z~PSHt^2oOZ}ko#J8ZSfcLj z2pHYNw$gh%|NPfMxw^Kdsw#bmw!osI+ME)kPs&!=1Ej6?jz9;#>CB2!wgWRY*a^sg zkK89nzzMAa&5Yqq?AE*7pa{N5}bu9aaETJSd5V*OwyfrcI zXPiJ2UOlC((GVWk<5bAs+x_G(r7x!Kbxm~@RF^u}mt$<)z?<*}nTBGO#)L~IGL{m& z6-ylJ<$~DDT`%c5Hr**MlV{2wApD=hfBZ;iKq*@({>)BUUa%QI>wL54*G}_FjW{)5 zc*vN&$p0%I2@=B%0>k^6gh{hH^_Cq^6w9UlikoSkhOmN=3xpXB4kQ15`kb#{eF(2{ zDE)P{F~RD=7L8!m?;nouZBi=W9|isE2%w!eW-nBc?-I21$BpMl8%5Ux-|%eHoU*AJ zbv&st2%IzDK6>Gs@Me3vcc-?~9riqa9in>Bgn9ZG2xI9X;42!oNbf5dpHNQJG@l~{ z4HYo^$h(7x?P-do`3aVD%U4-TTs29ot$h?CwI{@hn%Ek;C*%sqzgsQ>@B*yA)7TpI z_H*CSNZP!TQUGvg+k@pY>v|@i_mjc{%CttGVdU8La>iEZ>y3MTh4m=%!)#AZrXUxlJ78&*2ZPvu&6OQ z3~hBqPC05lJEsJopKm20$IFZ@51}6K0}oKzn@@zVM;Dd{7p628jxX1XMSB5d>fBGk zRC@BdM4VXA2rMKBrqD}IlHB7IO5HUykU-D{LB6OLP|9V?e$+$ge8;?~nWGs|tF!8Z zO|aGbLkLXvGR1OOed?OzW>GZIEHBO3uW<($-4^b05CEa#IZ%~<8 zHC;tIXD;t1txIurYsHpx#qZ!n0KH~doE;_q7x+j9BKUeTlruAH-FwRBD@DnI$L8%O z+3N1Jh&hkidF*_^`NMv(h2<@Je@>(J>V@);sSM|EESY{B(WN2Q*WDaBY&Q~q&z8+2Zwa0)4uY}qB$_l>1T>uLfvbH7xtulJ1T3nqb z@kJ&rZBoU_@8L;E<^9;eWDWpM>R>Hyx0nK@zsdmwUB>s7T|2ZTVilwjw^J7{xzTNA zFtf&}uh83~J#0S)lbhv0OA~%dlJ%}fV%lFw5HTyf^KZ|m!)&n>sE@))1rJ1XQ>LsF z2{j1z3|vgJ>F^`-Ag_RHq=^;@v2^w?nY3aPV|DeMM8~9e23a%X3tw-;^Iu>2>H$Tv zKeAyKiwph=efjqUFj^@t@T{e`zDavKUF)xHHOivh^wH}wWg*U_+^G6;W{>j&&%#zB zB@!HKlZrs8ol5TE$TEIRrvq!--nua=I^J}ofDtTFWHDzCT(RXcdzXa+!n*+=8TSbm zwcos~Dk;y$tSPAKS`#V4a4j#!89^5gkZ7jY1fya+$>8j z(66HF=yGV^Z2WNc=i#?vEW$09ci~oOEf>P z{hn|EscPNla*3+*c&97VrQFg~Z<^!%X00oL4Hmp56KGVCn*Oe!9q|7#c2-ew1x*6R z8QftQ+zBDLI|EE`5AF`ZU4#1!p5Q@-1P>aV;1Yb$0KtL=cMY)o`?9-l`_!kaZ{O4R z^zEvas;?n)wS*g5_?2-}ZSUsvmb%EQ9&Y@i6TUd?!^Lj4@7)?7j*^L)?ydIr;)-7a z7LZVw{YxUP9;@N31u-vlfV>un_$IX3&coB?eUg+}>p zPLC@{r&*;r~M zsMRAzoyTE$XBqGz8mrKbPQx$QGq4C~J`Ep@`g3(GC*@a4f`eCCiJt+Mw#iEVBX zkiW;C-_H$Oe`HY!_JNzrw$A}xd)9r5q_=Rl=Q9HJi|VOlktf`v%Ak4L$%Q7aQjkWd zM9wBb96%fMp%4vPVi;j-6a_sK=p7b%W&4lN%JFllM?z$BoM@sN1FxqH__wstBqhlM zrH8Hop-;mlv!ydsHoJ#i*0Q7ydq2NIo`~GHW%qFv&~~$N16lCxuO%jT%6$Ft>U4}( z7&zDL2YUM~($=L@Bh>UBcEsPmR_>~=#Z^udOr1PAn{M?kww2QUkWrB_f>}1KqdU;i zC@{x5!_KTnRYpK$)`eMKb8M$`h_#K54c)jE7o{}0lAh^62Ms3#LSsuh*stoY3_&Kt zVJ2U@PNE}Hn8Fl0eOms=YxLtQQ;f1C=hjJcUp4>GZ;Qr@Mf-ve=dp8F&roT(_9nf% znX$F6rjIlB{_d|3K~>+XzP;1i&M2BD;A>+vVQ8S#MdDP%fqEy((AOu7Yhho7DX3bf zn?;ORrb{Ri>q@>xe`^tC#zd3kD;}jtN64T$Oqr^6Nh;W0COL;U-~nRuNdB-t+kp#l zXToy5+~6fv9#-b(G2IS=lYQtS^qUym#b82C>JiM^$8(NYB^oL!ym9JXmazZjI}3|X z7AQ)wvZY9_X6O99*Xn8@O5jm<#O8X~Yd?KRhLO>$BF?t~|0;b2*Ba**AgS zPJ15B|6cn6cHo2}^tKli+6F&PG8&UUdE{HzyYi6BJvfSZu3Q?d_Sn_HriF z@bBA3@9!Fx?m=vM$%r?Bjh+wo3wRKp=)$sN2~H&0R!!h%0=e&Sca!zS&$~PULWn3| zLZ3)v-i$3^_M}f|_b}^^Zm*^Ro)5}79doh={1UO3tmpWc zhufihi#tH~IwX_Fgi~IN6cVMq^*X_7EMBYZ_{I8W*o;mspQC!4U3F8naX@r;-{|fh zgjjlSLZJCw!SN%0&EHvtW2xo>sNDstUkEDKO}$u`;*w!o=(wzDNaVEDYFSX^Zzb0Qysqi8r zLQ;!5SyB1VXSe6aTu}<2jXHYx*EO^^-m8!1o>1~_7*5KL$1LO} zVQyW&zPX+py?>aOaBb`p+!z1lbR^K^7nZ6b}mzI+)u`yPnuyGQ&=6{b*Q5rtUy_R;+gQK`~pFn@=W?f?c!*cXkw<_ zUW@sj`}<2=-KoMcMLuRn^>-s-$Zs63v+-baM}Fz9Yzdo%NI0ee`QGhiZM?Dq+7yEV z6c9rC`%~b@D2=E34z|PQeehptAY_Vr26mj~=kDEo@vYME(>!%#AqvySme#Hx1Kr)| z!(6j8z9foU5GxE|M`_X#PZUy8IO+Mz6!uU51rWl@V^wZn#Mn_xk4(VZvq6uHX?QGC zA~GhqNslZ8JQFF3J0KjZ)j>~4hY=WXn6QE4#bisDk9=j+snJ&oQGJRe!pfL!O%yJp z-1nK!@-SC#DQPMQjv?l2KABVrg9`&x(AD%84~)AD=NO%R`qHLHbY87ao&3pIRxBQE zz9tW)w(v*N)39W=!fi@z)N1?H(Lx+ovnAF=;-8)B{VF=peb4O4CpBog&dw0uhPzW0jqnKQukn}kP@R`!J0y6 z3%M5@vy1G*)rT91uxq2~a)ql~>Lz`0&N>n_<==LfmC45@P3JGZsr$^kp-=f3nv7!Man< zrCHwTa&}^h1@flEsWM=iFkrI5sYF!juct9_IdqW|2Pr5s!eJ^+ zeFFaF8_@U}8fTchI5Mn24;+AJi61bP3F z_zF%b)~#+*^X|8FE|Q4aZ7!%psM&?GKIOEf6>0WYa4!hZ#TgCN3aB+ zX@4;}k56GN53lOvLy}1PFOP^l(2X1@->x;iKpI-Krs7fhUMx&-NtppS8OU0%KMaI{ zf9kM~3aCW@$j})_UghjidYVvZnBelLF7WO8Fsai&^yt53_B{+4P*^d?f3%0bm_M|5P2376e6sThEtc-5%oC6m-+D zG?y4H8AhYQbIm^jc%P*n+A;lJ3Y>SULN$FeQVL+%$%i4kxf6b=te4QJF3 zHqW&-Z(D_F$n*RmACitm+P1ewk^wkW(>^N$G)8R?^T{SUK0!rvKjNztrByy9A`-q) z`P(WLOv30rKE)A)5hWW}JDM1;Y@TGg@&pnjik%$?n(BHlG@Gn_|2=#aqOT>7pdhn| zBAVlFVm*YWwHjEt%8v+BZ2?GnD0|J8&}p~}$n4im2@jcu{IN8q&xt`Xghxj~n+=58J++r_Z{0aqxhi~)o*VoyzPV8UMR{f;89|*u zBJjtU5+|g#3*xc|GsLF36fWOH_(`O%xbh_j%pEu-JD<$9?&#tDHDvJfv@&*mQ^F5M zQSsRDEcekD*IK<9Pa3SZYW3Xi_xm2&F3#Vcrh@c%A9C;Oo6#R2hr4X~hbh!m!(!8G zf^7~^flZg+3due!F>&xV0-a4;j$oqx9YALBJ!ua~iRZ;MiWu_XA&Zs~i}|r~6-m6; zz-sFeMu}`$_wW2}G&7T{GtlSGvj*W>QzKkwhlG7(p;a^=naa^vEu{6;lq|>n4-Hyr zV1wb1kKe9B8`poJA)jjSS8VpzsNRzX%b^gP|s`{t|p^q1D;2~DO)RKnVF zJI;AWNAIFkuel!>xjXkhLESYj%b2jT$KVI77KG8KQInBc)#UGrkfIDutS>3?u=McW zo`hcvv@#x9Mxh5}=jYp%yUUXW((iqRK;s|he1kNqlNKDo%fB7JS@kZMEr#Jc?|VB1 zIgKA1)fiWX>SHdpxeJ%a_Pj^u*mB1v-k|xtGm`1A_c~J(&O^6@gAF8$0raIjd=AC! zi30H!SexHBdQ+gx{5Tmrgc`l?=u9UVy00FFt_#YI_yO5I5;HW6qnp0L;e6ZW!7uiPy}?7+wWcpjW@ zclyHXRt;lC8rPirnqp^8eRDf~U1%!s#|JT@$|qb2yqBKqYsg~{tLNHR?AQFBm)cxn#}XVZ`}^z4WtChxHn?|(;or_5CA=opWLU-xrM)A^

0sZBcwble(>A;$rnl8COVYU?D;{6t+p?*l)nfkNHmK7lrGX zp*k0;oN!PPfB$nS)R?x6Gk zQEp_HBu78-y5mo7)kb4<|3=+0BfgQ>#(Zj?xb5o2sVzd zLh@g*SCudBPx``{X-F6?Qp2T{eDrY?m}ns#!Q=b`35LNEqgI_7ub-o-*Z%l~6DMMB zozuK}d?@UZK{Xx^4=8y%7Lgg8K^+3_H3zo&0rNq`QE4Xs<*6 zo63}QWNv@6JvO}*0)rlY+io1#*xMM|re1ltE%vGM`W+NqUzULHllQy!?j{egh%78@ zfjMm-3E%N@FSTyi#_wwjg?#m>*g#|&O-7gHswpOQ#I5dutHA5p38}0(v%WNWl8T*j zl_PG3{76y)ll`>%llX3H?b9&y63$L~Mdqs)7@)KD`jtN)3`vo+u2wUGtm8uSY{#ub z+s{Y$HJc)Rq;*zfb`}2Akw62G^OpyzDgitL2gWgK@a6S#e|@alpQw8eMRp$=D)p6c zhZee@Q#eKfP>54o#JrsFD`Lk;2#~wUymRS8i2$e~jv$Y(>pgXp5(NiAXIIrTx|Oyy z*?ZrhcD$7G8`DxO)6$1zT2dyNA|p%`3aP5u<0FnhwLhc44DupQw)jg!cH4K@l*Ev~ z{^c40M|;a((VC@m0}=3Cw{YJ} zo&PJ!kT#gdu4w zPn8ty7Q|z8PF3G##e(uzxpurWZ`x=y%V!Qwnti+ZT6C5j{bXxCvRqxkv>j-mwMNt= zasl4n0*tt;e3k-j0~#+}m=g`2kj)bBuKe$=zgu`o{^eoPdU-CSnH*!=QhdzLA_GYs?dd#6F;#7D*73*3EBuHuvqKzL1W#Tc_cEFo^~$o z(IuK^@+K*Qcdh=srVA2(qR#=%Q|ftG?UZ&AP{)Wdn;Q18OLN+Yi7VN%YNc=BD`57Z zM^D5AjjxJufymMZTJPQ#;N}y&^pL96@K<8=od}TvmKbY+V$SDNX}a^^r<#u(Qc4UL zLptwHD*8Pt%_bhoA+=0{^h7Vi2`#jTSRn8_9t(zx7VkSi*r30#Ocp-A))P7Bp`ei7 zGS$I%R?g2T3dbwHL2yL*$q8JRN^@%2%@6cinC9o60dPagFS$is?&o8?V4F`4sOT-5 z?c>4qp(1A1S(V_Is)@RouIAW7>PLEN{poy>99xITX3Y0w+gro588LF;fi(GQ(t zH~MB%^){It1N2V%={7RjsnWL)QC~ypz?}VK{Y}A8$+5Zz0vYDOXvn#@K`f_=g?j3{ z9bZJshN^&?+-te+6cy6Y+?wnxsgHgl%MLP#qW$v5DoQ{-r{FBXf;3!ZWi<1iesX_v z&L=pysY!cz(d}4vyhy83^iqa7LY0|VYbXlzk%LZG7M?8C`DlnvA#iuj`G*}347f9Ug*=!4(z^RN8dae6@~&s3=;_KYQas`Ilqut z*M`r_$TICo46=TnmW0YVnw=Ki5^dOVCTNEHy6LSGL@cIZE-7OOxVitQ%7Tk!^cPn#EK z#C_DjF=M#CO9hR$3@N~+gD;j_@AMvJ;A=?c3=pPo<@f^F{>qx7$x17?AXF#$(AMu) zH@7jP&Q?sgt05nM{l!c(x7ClQ58r)2t9{iML%8SiP%-Y$36|594@jv6$WRqQoWOtQ zg&a!K_^Y*ewuoGf=;br}nKq|3c=S03Eord{GVevieL}8yKj<5Ue+02ln8!{An3U^D zmf~(Yb-qhVV`dz}U^dI#f-2+Mf0_-4tl8`sSHFkACpUr?^lwhqo+}CkD*!M=9L~5u zOzQcS{(hddfM<;q$3LMT8?&#knAJxHiKK2hZxO* zPz4#GU)x%6WAOLW$qBWi0Ve)<5upN$M+?iP1J#ajM3))`EbI=wjEl3wexl*i$1u6p zySu8^#T*ypDY}kzF}17^{~JQBTZ~ns)Hg^h9W~M@J5^FY`+`Z3`> z9A~vqtMWs;k!TQX)$N<0eXpP8m2TEKOuBM+Vry|sbub#qChI#Cl3quM)?+E#L4S&m z$(y%``FJ-o{Kc$AksYTOJC*D>1sSS2HO#r@?+Jz6Dgfk(Bkv<7ItvMMha5?hmTbW3 zscbazeIVqKVr?-!7ZZ+mm5$w_G2QqQLMSXK+P|;_QIad>CJP^|wi7X5oByMYVJnsD zgtX>=zsB_os0R*P2>oQUWaSu+czurW>>a;=u+0M49y)w&{~1qYLXzHBD)V^#d^a|R z`(lLa#^*>BnAl7cWs5#eRp0$!hC(#BsbFYU|9Tz;d#7|kxMigi`%~03Ny>$U0_*P% z_mo#esJq5!lp)$;lV5GlOI(j`l+pirs9V^AU{VEt+KDQ6R!| zC0s!)>Ekx__spIRt~(TCBqu!z9Ti-5{Du~*aTl(uqjr{)pT%LS12@AX-|1qfRvq8< z0FJ}Sd@X|ThlR8=P8Ej1J^$#_NFynPBJN2Ary)o+uX_m6+Qvt7QDK+HcyJIUU2L(I zjcNi^sf@U=t-X+l*NJ)@QL`tVr3zQR5{fD;&o+W%iK;X8babqX<+l^v^FeQ+o5NsI zg%J`qHk{9#Xwwdjdoc{jj32hCo#bZ@3rd0lEe)6!7MDKY>mSV7SBrhxOLKbPN}O&f z@d}kPF_1knka6^5ZBA`1XSw859KmEVKkA;JHcYHAmBZhwFU}3qB@Xkr&jUaOg=j(B zW%a3MiP9ZK0^&Cg1tRD(va_$7%^N7?3oLAU*)#I{xH_FIIeSO@LiyzQC{rafK`YRU ze%9LM3HPg94p`yJ(yA}OC;y<_DoJf%&k6UMKCVtq3l;&P{!ZB&7j7H|VlZCLtXr2< zXTp=t0D_`k`N7}DrYJ)BI?-XS`Q$ndm#x!#o5?3V`gHx2@28x7D|TU=9v2~@A9~t+ zS>7bz?EH3@w6|oqq0Mdfv=(T)(V5$0?R*}R7Q^3_fTB}qZI#lB5_eHy6^|8fZQ%+8 z!e#;6T2p@|l-#%wm-&W0lIVvID<@G%lH4|=$y0_k{0(^mc)eV&b zOUuDaYiO{9-e3FzkF$zV6Vzh~KzzyWB3#!DOaj_rFF1md>&9U`UmpfwnIQhP873TC zSV)9P2R0~y5pOKdHvB}JYEyGq@1vh%2ja^y((L7*SP1m~lmWst;Zv$e5s!p+Aheb! z;o?8K)FR4n6HqfO`b<3*M7crZM)O&dSClmyoo6~pkSLzny|{9pAJb*0bm;!yjT{xW zN0`Y|4IrGPS~kV0yh>2QLm7dkx3DHmP^TG~r(xR$${vJ^>KJp$yPg~+$zHiJGNSzV zpTkz1D30Bcf!?)#1gF7#&`HlJ7%of&%k!gnXKy$EMMU?#yky11`bxSyQ>^y+Ymu_* z`EngJtJ}Mx48oX>NDwkA4Dx3(1)`0kn&orPMK2=s;PRXCqOXViWr6w2k6Ae z_QQ+0BmKGB`f%Lz;E4{6z^GZQ$3TUQpD9XdgsiggS5r!+-F1RJ4tvL1Q&bL@vkn5F zxJze7LnvkgiLzV<@*CpIeYAmacrm)sj6~tH73fh!6CUk6VQAs?%?tKM?DK($=uOVW zWD(InoX}z%=g#&$ZLqrzXcU7m6h>HJ-mkkW z(#YW%ib{iQW3xw-Y>)F7abW5yOgSp{bFyAQWorBd4K;WqcD|zwJY@aFAashR&60UU zaw)vm)w`y4i29>T`gS-{4g2!-R)<4I9jo0F7(Rml*vby4j_$ZGcxh-cIrO`W8Xq3J z;-`rPuL7zm6;K>=!{o~lsM`dGO9c#G?&1-4M<$MF>AsL*TK&S$pK)Mexxy~kR7JIm zOdukT(&;!RBidO-jXlcR_JR+>OS&KzDR3gQ^H=UQFFT?a^0-&U=S13;=0hQ>(HAJ-#2Uhef<<9M0yA6394Doz~$QBArkQScxYbH2fxEc{l2i@pvrh)y48A_N|<$53B zwHWYe+@N7`j1xNh@6gaWVCRU<$Nb-4HWRDDn@*QykMaW@?II?f&0rC!#lVvJSG5~i zz+_Ex$tGcd6b4gj5~Q6w>}XdR{`Q=3ZwhI1(m=B?T=vM9%D7}ksb+RUNgERs{+1*y zJHWVGdjIBma&_^l$;AfmR-gQ2-LVkIfzdnbi#8vsW?HqDgp8&*fG zewzl@fx*Ad+V-ILlI8R~Wpbk~8Lo3n#M6?y?qk0aY^cf?sBk`rP)}gK83xc6Y(#DS zhMPM<^clI|y`TH%+J?m~82(8;s1hwd0&c^`vSld`DiWIMaX=%-!-1t`!bN^3-bO-2 zbus`92zrHUX4H`3qSDS%$wtST2Z)0W9YW9#1llOW4?Pi1Vw0B0v4mAzECo|?LmP>! zjXl{xj`%k3^YJf5KG_!8vJDkyw@oHbJ0PQhXppD$nH z;Wl_?CKTc4#+*kF_slzJ4cSuJcG+~(d*Azr`r&Vrh3L8rou%%dRy&s)DiQDcYAZfk zg>-n3>24m#f@lVM-5aAbA*~RkXnORu-7x8Z)#xbF27uzyIA0_Cfn;<(5SRU#j1Bee z;#@-lL~Z$o?JbSoPN?H2HeV0Pxy_j=fMg(3E39j}%1Jjn)6tvEoL9n5<)CW`rv@}^iJqt3=LRGCSz;&$|Te2NE?&p)YeEP=NAvV5(}B?3Nt6m6fsOmqQcaZabOdvkgr($K3^U6hUu_HAmNS%Uy0KW5<}DL10KMygoT<`iJ(THkIA*HkU?{yN z`Z)b?B4=$@^N~vqxgFL8j=oK4Bh9Ry-p+~9d_$l3l@H{SoB`ngkuH#W?ixSbqc#YEa0KHt^u{=@1ZA=p2xucpIm2hNUXK|ZsgPtv^vD8&SDk+SsG zsj^;=x-#mDQ)j8tm+H#!d2R#@Xi%3TQUpM*F1sLwnlXPIu=3(j?`JZGSCZ>Qq_vQ0 zw6Q7KrR7B$n`iZzrw04Sl{J3SCm#>)Z?LakMMRq(f4-!7ttyMY{Q^j3HsRsq6<$vo ziY7(jkR`+x(P#ec6##4p$MD4m^pc#ggj4nNDiXr;Ro=q>f~Ntc2y9qbiRRxgZ?zS{ zO7guWohaC{S=xM9oxPRCJ@_WnN7tdKUjoJJH(EGr>Hp+>JFpz~HhtZVbl5WV=Vhmv zfAr9?KO)L&5G}bci~PVI?H;TER>U9^J_9Na6;flQ1h5;0QT1S>^f2^A!35O&8X-s$ z?7T}TiSxbeQ6D%GD7H3Tvqg*WoZP*k$%}>s%I9dO|n?(ZLk=F zV^odRulH^z-xp?SRvom9yDj>am@WasZSK^$*26qQgO6osrB}Xzj$Lb^>RvuJIv}Jw zipz6ubj1^& zTYh<>=>*J3mte>J!WGKK@bAnKH3%n`H^WA!x&bP%zw97}O&uR>sGPsdv~N(5_{Ya1 z7uCO9zb+r#BJCaBIX5o%l?3je9v+mLXbfwwX7%b=hGworWhU>awy(!?_e*MAIA+~K z^`e+ZMnCf9@70M3{PJ18I0~eh9C-Z48wAR3@_{{di{7q5$VMx457(n^$4L~?9;UtD z^r4e;-j3Ph4F<0fe61^yit4$k>`r%YVKU&k04r2lnxmf}bAIv~*Z*BLwQnyHDFPX2 ztdf8)4kSdg^!JPvkB8IDU^K81ySIv6eBGJpG4NLVnj`ks#XHF{OHi)T@XOUg7#)HQ&4x0s$C5-l=@Dp}v;D3z5bJro)Zub< z=$*e&u&wQOOmYfDtK`6=qIYE6ScXPWc}5qk8F*^p5$&S(y0GU*d)@imn}5`|9RR_!^U9#IRGVJBQ~R81FUsT!x-MZkP)}DU(bvom#qX1S zJ~KZ#y$=Y;=C8578-vFmPT{P?9EgBhh%(P}4!ByA9Au2wgD+r5?v)iAo)s~6s{Qq< zcZ@!wotM1zK(BzkUi-H2?H0!K-!Tnatuhxw;-(D)pku+zu+K3E==h|@w6jK<@ury< zQ3bT|gwhxCKUhefK1G3vy|`>q#tldO9`fv~*8(tH%C|87Eo;o}9r;O5CpD#xW9sllS zlDM>kL%?|Z?zXB)Ai*oIpC46D#*!ewq;H(V0xkiy@{ysBcGf*Uor6~gXD})))%8Dp ziYRZ(UH~JgcJUh~cePQ%O?s6%T614l&S;Qstv%w<(MbpA!{hq(*iMSCHID`yEuUOK ztB%uQDZ8zwIn+LKv(qR)ka4f0F?7wiLN;PP`=Li71YqkL(_gSaE>)zU0DwAg28kJ$ zwazay9UWX{p@-3X6REk*^PfepFme~~WYu{6%^H60KU+5AikRpZBmH}g!-Ir@flcO# z#bumb3sB_Y8#(RvJ(FPwGtsmNL&hj{;^wh1Oa5`)xwz?S_iEZkN`#G$FnA{{NIe0H zptSAhqM54(vLNO&6Mhx&Gfrt43adS^#D6VUDT^CpXWA=cKj>$-MJ5YLCGyUou(iry zj7_l1AgW!p2}lJpObWeM?ivAz8M)Q>v*!xx3N%33tzRlY5p__&7fvGNFE$@2S^fYg z0fq5I0G?&&*o{J@{0KlKEd_9vFkoX5dPA_SIUx-1Tnp7F$AbCA0!}p6NLIe&;KygA`b-$2$S3M4FEXB~QhtG+o7PlgRvr5V zKrIXnanN6AWk6z;arT2%%uOWz4nIR^VQz#lv4ekIk)p&~$SbBzxv>_9cZ!MqeEic~ z^2v!tF|&#i3NHnAkq}NQ3SBe=AmOFViq)aN)TZaN3-Swp$s_4OHL}>9!Q*P3A&CAs*ZYLrQk5S*3#bFdT zC$IU(=vWkKZY(37k$t-&LP{Tl2HaNxe4P z6^JEz?exV7LW#>so`+bUkb~539L5;**y?M81K31~b;cY@YjjQ`px0#mR_gRmqk}oy zHa6op=*rgC(GEa#1EyL7cgTGfNukKTfmRnEO6>wD{*|3>!HZ{1MI==IU3Ri^wcgIr z8Wevb$SpG+!TM{kKZ0`7LCv+w8od)IVSw~@KrW|Y$D|;)-91KS&C^vqFL5)LuH}1P zvl)&77iG^aU6EtH~QvQ=V8R)xMw z<1=gI-q*a0&Mmd#Ahh82KlO;SP_Y&>H7wh7PLMU2Z_A3Oh@2zWH z(5=?@MO*SPLO)?g_ZE0nEa&B&bL?WkyFOTmmMY~(-<~oh9J;a}Mp9l2nbH^kK02Bl zHM)uIAmZBnzTTD^Kz=>X5|g?g6`LDq))Zm(^((V%;qj3<%^Ro_3N0QTgxSkcSjRz)tBAIC83b>)`XePffI|L|A4Rlo@7 zf?i!fwl0qeT?!PKm!PS+e;u(SSx|OAQ1y;vtUAWI(Ywz)Re&IVy1}kpU|XQX`EJmE zk(*U^XJ^YlAdTlf(#pop6_OD0s>U}w4mH0B9ber`qgjZVsbhjqxEtUYK~xrp3;+?J z)M%;fk2HVN^(gZwP|0c62h-py7tatDZ>Cv%d&+(1ro2-TVNGu58kV}R=ZPyti1r3Q ztvV*{VPQ1?FM$f;GC#E&Kq4&qd4oNG(xQjS$P&0e;g{Hry^flLhn@swf0H+wyJA)| zlxLVibiAO#6s;wXOmE@ebM5wH;)jq;ltPo)NcU`oO(W}(VGUZ9ytSSj22`LXZgc*- z^CC`!g;HE(_m?Z1b8{)V`}2uN0uH~YdvkbLD#G-gc60}VPx+SNNIV5 z(;thuJy$`&1qDG6dF&6F(z!=KCI(n)aX|Be_xsiM9DbuX#HauyNBG%r9t<@|Nzv(Q z7Uypb{r+#EKbEXy@3(bHj|7`7;R?J&{m@s4z>vdmkETr$B14O?styrtk?!b@l2C1{#G3RQH(uL?ca}WC~-gWO+@9$N>)XbS(?IP&9L=bygCVA8?nS|svqCdQ}1!MH-9R7Vb zW-K+xjIW^D=qD+n7xeslG~W*38MEb0lN>%_y{?s{H&68ojLIwO;yzT)9-5Rp28FE9z3euvp13F;MoMHT;Kd>QWn}#)c@Z%D@BDv@iT_*vf5g3l z!F6A@llh!2xYhI$^=_sAvw%+V0hmmBGFpx$0#3(HNHvmE&n!F;tJU0GPkh(y7V^MH z>3o{l-j{9t)cnpwlRZm(U`@Q732Jk2%E4G$1hgE-BhY7N zE?yS@EidlcSSvm08yN*3;|+{9_HeYM;RV~9SoZ62(wMzFLb6hMRH6||>1mo2hFHxl zFMaFNp9`Tq`fTfMs63!8{M)-o=Xp7-M7`>MS)&e4^;C7_N2|h_iUaCRt=##MF@E{V zb81QbJ-Pb#C`8%@dbRe;)iM^|eS_aM&TJ*pG<`;6fD(7sHsr$T-J{~sgph`4u;rhI zj`CLFtbh1+#EWBQO<*j7DXOe1l%PavVm zcKreB`vML2CE|uZ>CA<4!LhBDd=J8D#y0$$KXfIi<>{66znnF0p!}1g%lF6n~eqS1j2(34OECX z;8ouzBp-LNpcwB;O5J($ldwJ(Sp6pkw^Ey>4O_FtaL0PX28_t|^+)$D}bB^K$F3YX)8BJN1m{>pAA0wNJL=bjPd9*Uqe-%E$ zr2PGlnv}*ATqbcvV5pRraHBgi?{u#%kK@Yr?C(8|LW!hQve?fvh`pg_WRE|tk66b$ zWo>wrBy~9{78(1?AwbKUp`;0(ib5C39`+6I-SQ0nnNps|0MXtu2KjMb8}_B%C1CLZ}MO*lo*?jK+cxuwl*>Jz%904c=I-+&UT zWyTp~>1OCb;2Mg@_lD&~@rH#G!avq%v2ObF|Ni$EB1)|V#qIj=K5!(64*HFM*#qid zm@TEC&bj==^_c^vWhG4sKU#dvZq5wcwt~{~KI+SL%K|N7MTs}zgb6=N@W(ka=+g2y zdlV0^35)i+ynB`{)nIzsnHHp1Au!9viuF<*HXpYhb{}lrxQVac)J(1~Z`|U$=dt3I z!?t8L<`hqd6G!fqsNhm$I9QSD1D+V!fZL;!tdwsIw&Yg2Hbn+`>%;00uXsy%m-@x{ zRQ8uxgy?#{g;Wst8T0-rZK3Dap@aPXzy~?A4X(JfWQL@RpRHlRb)*N}H6T%hES&ys zTy32%?OLjly9$&e%3t02!(YPI13>2m@8170xqoJN(*qARQuJw%&PRLsXJM+TkLdKP!6@>R&pWtaVRn_x?3qdnWfd_etFCSKYj2mRXf}V$-+7Zc4l7=+Ipq3#UASC>oDOP;6I%VCX5H_b>I`@U zeo?m!L;YQL#Lese@DpfzmSA*K@&W3J@x$ORmf6J=Si_%+r#P6tD4O_Jv-A%CuSU&? z7yWO;c_3PeZ7C1Ldr|CIlUM%B3!YI#mI950keKH0%Q<8KCZk7XU@9Sm`9&-bB!c{} z1pFWQ|Ku+*NqghoI7pggp0mXV6w*C#iBp|LmN(ITb&k@S$BHP_$@Gkrzi`F>Wn9T{ zQ;u;Tr@mb!CkyOs^BNk=yeyzsR5O}&?U0xRh;8svSvEe63%?6h0(Pk6Kjo-5?jmkr zjADYa7YX(>*u4ONSb*?gRE|8|HJYk6F-&(KXRs_$8 zaSby+XT2M?la_UCEZ(b{=;Rf5IyJkg4a;9Wu9X?L@|96f(2`^jXnS>^< zKZ|>Tfg#t9q9&gfYvIv2s)?1l`h%+7{kYGDHQ)(0G~o@Lrnzb(SB= zec(k0sRyNS{!Rl{VmlWQH#HN4G1klCFr{4{#|CfJNsj>0>BHX6%R`m|orGPzZj9!^ z>EA1WPpTccy$NBT$96%+`3d_DGI9XHnmDNnaki`@;zX?#ijnM+2xVLzZhYaAj?`C- zw80qOlF7iIWj+1L4AwCUc?1JJzbLJF=gkZQ{O5??q#+umr}MUIr0j zCZrg)f7jZ)%Vh?cy6oNA`LX6$Z>0=0ErjIpdBJodaroq1(59rjKa^xSR2w{zQx29a zraSY!bhF1Oe;Sbn6-RH0&y4esomR$|^}E9y7|hQdCSimOoHZ<>rr;P#9z$Y69%|cK zq|znXsh1m)FTU}fO8I1Q*X!E=d|x~~<GrXVO1$c$o*kq*S>}0g!aoibIGl}W?xHpugHOJs5 z3Y5Y1tUKW>|DMoyqU?cqAQqq404229L;`u(Ge7TG4aX&2mqABPuOrx(?P;>o z^eAm9KwzHEAQ*l*O<>>NIpav?)`^>jTK>@CwA@I)b@3}>VS z!BysF=DxrlW^}IaMS%`4_zlV6pRt8E94$l;L;KxwPY&p6l#{Ff2> ztG**!_{8D-K1mv}6yJ#3C5|mMABc@(hTDwCaBDp?P6A=>vGl=wEpwCdBeskG=1 z8m75?(d0c@Cmv5ToX5I7?ZcD)y5>Z`_ugMM;3KnxfYQ5ibaAvq8gqF6MRX zcVGiOe@>(Uz;7sehn+v=PS=%i8DEJLtcc!X)9u*y5kzz{G7aD_`E^<~7U{jp6nre? z=cb3m2q(Yh0!8#R{ff|}lH+B^PF^0dg})=4dVf^-jw5I}ne^Lc=i(`yzwzW3hZXeW zWUGg2Z7auvI^ixL;U(4*3z`@NhoDQ4^wz_S#r~J`Gq(w0*QWJ)(7cl)+ z$jgeJY_8oGm#MFSrWv){q4GoPK7|%ddy|SYZ^0smY5fAr>4RrQ6cAYfg$gEca)*Yq zSp6!-PmmIp)`a^!bXHH`?)`yFr25|GPSGz4kx&vqX3{NX_8D#ReZ=B4lZy4B#!G{; zM7SS)IuZ`tc8h0tp-;zui!Um1fR6%NwN%(l(dO;@O)7j(zxow{skq6hJ7je@UwC?J z3O`$q-s;+O^|0@M$!E;XMh)^1vTt+^;FX`Lu0Q&-=1WEaX!i%uU=|Lj4wl?x%j69=%8_vz`i$)zll{eOZEH#T6f;KeIanWk@~x$a1+A7*MKM z*kV6;urdW}(4yt!^5_!Y^S!A6Lg8*`1r4IVtd6w&aOkIF^(8fzdz;e+Lu)DR<&-$I z9p4*YRx5Pkft7I72FNz3+}Hw?ZqzqQeNU7rlM^c*pTDtpGp~9fgm#3ofRJU>;b-pA z^-nK*ux^~|-z_<$2rQxD@QGzPpw6~EFqFM$ozW5$7yRAD5VY|&fD#)vxs0@yTc){r zFS~!uHery~xbZnIy52&DuM;tOC4%`EgjEDLf83W)3`Kn|_n%Q;`~h)VImK3m>?`(wL_!=ZzRGspBFSiyLDRgXxd z6v{>z9tE&*41Mb_5PG9_hXHT@B^%d$!FApu=zLP)%Q$rO0P!jEb!=z$Xu#V$05hk@Bbn2+rq4KVS$m8X+!9n$X_j%>-nr-|G3@mL^?kaG!5{XUA-MwBUqv`Da z16HsJCEm)&(ekGyqv`rtu@e&OxbiEj<7neT~-qUw2clN?>@09AEvW{iQ3Ju;3 zJKK0NkBn?f8Zyss6FqZALdEHks&iI+sf` z)F$u|d&!Fa6TPEe45qS!DVBlO3+YbW| zqtvUlYV&(BArJoqS9QrNoS)IYt0|xDDePG*lv=d)_iB0$o1IvSJ1csp;P7@V6>>r) z+}?7i`RKK#`f`otdxo0T{yFDIWc}G5hFHhwX&mf%bOnistP zjojdcn%Z(TqIbaQ=(BBmgcA4u+>i(FNZQbqkuO-wZllmB>d1@Ej4l5@;5jZeA?%=_ zOPiZ3rcUB4@_4qf*JY#>#eNLLO}u;F!Y&Wj>c%ySl%&snyu>Pykt^%^k-Phrk3;Xi z$w~}+dv79)D^Q;_V2kg1=Eg@-*|dgq_gcwbq@?>pvc@zOe`|7;J(IVx+<|#wpmK46 z7*Sc)#m!0p;byi6ng?(WU@WeE+zU;b-ad-lw$hRrdHuWPAa-5RizZ62e68O1WRF_a zUUR9eeV+o~suuT1_BAnX6eHcn2Y=1^Ng-DA_RVjs_Hn`Y7S7JWjoGdX@dq+najK}$ z4N*0^nz{&%T-P_T&n(`O!Em+$ok(o={(IlH{OkgT$!Fq?HyW@PJWNuI0fckC1Da&AK}tEgOTaeDI_G5U!W&e;2+eo-}IC7M$vQ zKrW~*b-mh_lMr_&Mnd~&wJm2Zxno6Bx>2>OH<>}|4*%(BVL*?Km2ws?P>dRXL4!D%$x~8iG zPJ6{cuGriAYh|6Qlr>JfUdJX+!=qBAtX#T*+7$zO{KDiV@>`58JPMA?mqIg@Yk_i8 z@lYyIXS0v2k#`G%W+b&2tZ_b-+((#>J%4PKXBniJ9JJd$*5^x@6Ej&+}dGaPT z5otqG#-Y~>`sagZr{$a_3xCKv-FjM6aTF+eGk`?A^qoV|Mj)a(NCxFh2bcN&=+Up_ z;vSO~w-w88Kwnlc?-@7Hc638ZnyfyXZeqZeh)uPYcbWoi6~8q96GYB0;4jK0dB`g) zmiLL{xEk=I!e_Px^ZiLzQ$v?%N{}>?p~uIWQ|9tmn4RpsKQBhm>)Pxt?x%Jey@;P2 zFD_j;9(#?#NQYj{GxivTKD^7i9Hqw%uU)+T`gA*?w84Q}PCI|pD+}Vn?W%Pwpm+Xl z5%!43V0mYunP`&^y%R0B@33kIff!B&1sP&>yYUPvG59rd0Y(UM)6=;}+LH#MdJd@( zJ9*Bgz<}uK8bMi7Rjz~~p58Gc03($x&|An4;BrWt?pb8^X9MBCksmkh-qF8$E#2uU z_nG9*EQ1?HV>ITxY1{1#_2FlOee!OoL=TO+b-67isBzKAAsw;U+W||nK*cT$?NtSv zC0m7A?Zr+LA4423wC>@@V9N|I$EulnXf_v}YQB2lAz$|IEjCfF{CGR|OAT3$bT52< zr~1br(N2;IPzRK9;ZMe(ogJW$kP!P;F)u!%3_7#?zL{WPr$lnMC!SVn>RmmYXFj$K zTU}zazybaUd@7ES!;scXfJ>&<&MAGy2g!xQLO7*9g<$$6WL^l6^xb} z29V!vLHrZ}pNdurz{eqgfRow9_9_g%^|W*>BdVNZ@;yd-c;K)QCSUokn#A$dkx44F z^F04yKIziG42I6Vf;&o<7gttmr2ZP}nxRiz8!c*0tM^Ys$W0mS_goSZ!{2IDbg>u` zCu4^DuJv()5^yc`*=%+OHIXboZ~v8Y)t~&kBFOTz>t)$Qnqnoy*sl$F1i13oglDMmEWK;-=30?kreh+ zze%j^p<3t5I&Yf1&~S~)MRQTi{N6}$#?9e~xB2GPuL^?J4Z{6d=4H@Eo*v+>ZQWNZ zq@b#vEi~rW*`??ZQV2$X2$$Bj+$i=cT*?tB_O%ZVelLXYGj>h+bp ziy*(3-8%OP0&GQb05Ek>eoH;|1x`b zuVh+n?u^3Pgepxvy6jp-Wt5h_0C?gXp%DiaE0GjXi4_>v+Y~-?6XJUEW`Wmyn>6qa zr;6HT8t@=%q*JP(r3xfJeE7KfoqCp~mAejS=ieiDD|d@uUuu7Hyz(F6mQ&6Ae0>Q_ zCGa97BBG=U>66h<*rqZKq$93Uux8weh$XIFR1&O`>0awe9W@6)zU47!! zx4)6;bhC=~^=9m?;dMWXKhj)n-REGDsD{NWQC$0z+T?V$eN|DXGAKqtS3x7LDbzfn z6tJ-q`6p}&A!;tnKzf!Mn@+?@l!zCdaJyKFKbO#xwtJ^SPjBSeiav^5&ch=qkiJ~>~ zApn$kj263zxE)r>+}}_OM8ltZ*}N3JUmMB9L%>yXHHbc%j+(*&1A{({Hzs5C!{^#U?g5$Q< zh4aDCoa`Sc;AY)ZJ+I2+h0gS%ptGQM>BqfpLn@eK9zpP*6k7;?gqA5vGqPWRP^!bkshz`5(TBSJ~f{02K;8{kGfXA>TqwBckd2gQVO0}+mNs#6ev#$rV4#o z7mxiI6~!|C@^>VC@TIN?o*_Z6e;>5(y4(W;*)y2=IaK0WELu;EYSjw`G-89@Zm*86 zXv^-pV-*a#R~W0G|7Mb5GPydk(#m?sRiKvrC*ShJgyY?+2$E?2U}+rj^e~eku zO*;td`$l?|3uwy?+G!F+;*$*%VjVsVG$Ot`VMOstD9- zL_`=9hrOxM*7B!(TVUy-6=5m3_`-jDZYMr0NHOny|+hM7Mwtw9&~X&f(Ufwyb0WPcugD~1AYPAupr#|7NsTBueW z703zP_IgwF{YtIYFJ!ZG^d!}oDCt-lwmT6Sa-ZpN9o^S?c}K`2R{DAq<+u1l4E=*7 z5Xn3hEfRkHqH|Pb<6Y{Mu^tB??en=%Hx;RFG~)FGsc0T!KS4WCN~eDof2FO6Ja%+0 z$&Mz-I8c`U+J1{!e)@xNe_65Yr^m3$v+IRUIe4KV6yA?8vl%ya zvF`hX;Cr%r9P>wwc9%UcWOHvHg+ZdZ-T7fxw9|zFHUkhL`49&&fZ%TqLGjUO za41%h5-=4vk1+s$vLkP-$ zbG59(cRwwzy64Z~2=(}P#7k!23G2V%wB~LChFa@LM-5}}ClvXs zws&pGJ)0nIsNfm0rv{`l;wcB$7?7o6_nZdau5uLsQ1Gl6BjTXD-pEgNQ7)Xx)1|L^*^nTTlGKue2Fx zE&K1$C@>=kkMXW@5BVtnOVtBd;n?NJ<70r&d4#hCB|OfO(hkLQWx{6A&8WVBc-P|F<9A+SLN3$*eeGCfP$WYd z#-{?_SstX6o8R2(KXL9p%n>)VAN6P5~y;*Y7 z(vL^u_V%TbMS)#7T83gf7AZiwt?NVdoQt{MN=aRD-7jCj`$!;4qv37ct#f-Xa5m

=0t**j;fwPyu+5W`bvx|xIj$X=`?%xAn|God`L*H5^a#{o)t%}mBoG=?6l3g$to&HjMLG z6mN!|Kj`ZI`t?RlSTHyYVBXvSXLAUfYFyhn1SwA&MiA5c1Dw_WAI@%;ihE z_VmNqif>D$vIUkMe7mX>#f!vF;IKOy=5vKF)_&s^J|68HsE)CJ-!;P=?i9-}%;QdA zdn8B~>J4*^$F@r^d*cd=dfVGQ!97V9e*P6cD*jBYrEBg7AMcD*xWX&Gq${Bsyv}Oy z;i{k8PnN0DrDO7Qk*^*MsW4!=@Px^s1@NthDhd1|r*$6GXs5$d#py5s)GcmCo^Lcg zmDG8+hu-trzWv!Ks(*7~zxqyOqw9~BcM89K->9o|U*QTE9z-;Ge6T5RvMx8J@?LU? z1I8$mW=*><7tYB`_v7Ts`O$!FCH^5%V=K+M3RM6=9gS4Zg^&Ress*X|tx&<42_kcH z9Y)6?Utj2GaL<3Tkx$GnJOMLj1W+xIbuNn3-Mk&?QbD#&q-M?vB>XT-?R^9VPDUc5L+E z&v(5C?Mr_5Js!^eJ2)hUqSJ&M;OO3Y$^74^^*w!ZF! zY^=dTyJV7PECRw&Gao8%K1wVnk;IWz=F~6Kles+WS|sJf_;WdcW_7H46q-Hd0;eJB z#f>w@%dw=MA)rEk7Vvhqcz{O0m7dA))j^Ma_?>52Y*3L7sCsqx(pA8hu^+sIdIh4A-&i6~SezB;Sujv?765ed=3x7Y{`cqWU2ej?&!#e)I} z8(#@{T9g*699i7ICLYQraA0RR|3L6IRxdpVL3=NrwzNOJmt38MLRX0mQj&hzGJ?T~ zy}SHfoE5MR-clW{WD3R3eF(1Z+()2%4Gd>w1$Ci_O3`>pDSqa?3PF}cA;FC>e5suc zHVII@RsI;#2r~L>mp>PqjzJF=p>I31d z5NUWDO_?D+1h6u6eG_*-fNSHL;?#Rt)grwOE)g531cX(mfIXW!f(D1RK&Mn(GGJr- znw7Pg9NBnX^*c3O0Z`!hu6SWs|IhS46;o;HTsMb$V}&=Ez$$9BG%43Md7j4FP^y9`hCpRZRzph6!FaOOKX;q;2*Y7 z(9uD_KB|TX;GR~brR}_VEU+G2_|Ee=yi^2e!RYYut?(678^X3oDz6y-6OL80fHRvq za*aBXOo-c;975IdbWP*s8jEKceM&c18lLgu8d)f|Cj1D^OKSB!#?T0;e34I6)f%DE zg~zr42;p?6KtZU1xGk}N25_F%m4Yzw#ukKI@_A-et@yN@WF=D=NlH+4V-HO;d&l%G zbUBnM(Dmuh9|l_646?BoKqhC5!Ba=R@X(*zM!l#lu3I`SK9VB%aTSwv+Qe;LA>p z)_a~<_efFS?KKO_*OaVuNi~D-au$$&L^0tQ%mqWgPcHLbpQW)3gJHDZ~Ok(Sz z!T*C)kj}T8JFx_}*7+tqIQ{LjySafqKI*xamVBS~X<{%r1C~=tEKo0XBQjCA8yy)Ot8{CX0>7yt~_3yocwwI+qTVRjKcIAw?=2sU#f$|e2 zSs~R??d4haMndtJeEHduLgF%!mf;^ZG$u#t)Te+f!|3YLN2P5isL>Koer&L#FVCZ_ z9Dlw~lU3(~6EC9^bP!F}fg%V{-2(3H@(v3YM;c|e3>uu|z72MnjBS}2h*Yowlnxc} z3uC#w+2ar(PyTW=gkvNkY77@d)cTPGv^@hy=@ee2oC#HoJa$KJF8lg%sFJt|3Ih zM#~PL{%uLHD=7^D+9(R4(v zpw?0*Gm8#-zTETh#PJ+qclKrWeb+7p6E~Yy&!_O6O$(!VOy02p6w#6AMyIwPVOQj9 zl$@wz#IW6&Bc#V3WN2uR{5zQfxW#6*W<-~kBe=fS9=4^^StH2Mc`IHoffA?lwZUZf z@6TzEN8-Q5d*t)fzMalSMMNvMpUeyy$5}-Z>oA|VkRN_!*v4(J%}AlFl`uBn_!y7- z1RD&<)+K4ho?1DwYLGZH>W<&*V>@c9pd?V>e`-o*O$#~;gB}}C}2BDxBY-WeBJZqVZ$+L+3 zJ0(vLdBbmQ(uaG(IMS=Gmr^0zay6uRrqmEwm8F9U$T8Bp@fvMS-f;9_^fsBdUuP$? zE{{-(3R)ajRq6nc8+vEal?~>4kQ*a9M5_6nOr~fAv9MtK)2jw(Li0Nq*q85VY+gw@F*j}T5iY%IMlQv2A_M4VlN;)p>vB*Eu#E>)JKNS=lJ`kIw6jZlhYX@an-7cBEX z`tMCL=^$s(W5|RIDsZb-<)HF?P_Ob&*IBEGEf(FBLdKIqq$({X?`P>3kd>49VgngN zA(`xMN{ci^ioW-J7H)(}5IW6Su`K#5g9u+xj5onXw1k4yeA6_IKc1y3MQ}rk$T`I5 zpMEK9OnNDZEHg6bq6uvH1UPYWjM2|REVhVj2^QI42_GQ;Beh{Mj^kHACq? z?a$C7*iu?RFnM&7Q;$Wn2x44+IBme9KkSaSQfhPA?;Ifm46Jn8fDUU$8P~4S&3veC z23NOL`qO^IBdb$sZ#FZD=pIm5vYVKnka#9 + + +]> +

+ +Plasma Style + + +Andrew +Lake + + Carl + Schwan + + + + +2021-04-09 +Plasma 5.20 + + +KDE +System Settings +desktop +style +theme +plasma + + + + + &plasma; comes with multiple styles. The &plasma; style defines how the different + components of &plasma; are displayed (⪚ Plasmoids, panels, widgets). + + + + + Here's a screenshot of the &plasma; style manager + + + + + + Customizing &plasma; style + + + + + +In this module you can: + + install and choose &plasma; styles + edit &plasma; styles + remove &plasma; styles + + + + It is possible to filter the style list using the Search... + field above the grid. Moreover, you can use the combo box next to this field to + show only the Light Themes, the Dark + Themes or the Color scheme compatible themes. + + + + If you want to remove a style, use the overlay + icon at the bottom right of the style icon. To undo this action click on the + icon. + If you hit the Apply button the styles + selected for removal are actually deleted, so you cannot undo individual or all + deletions. + + + + If the plasma-sdk package is installed on your system, a button appears hovering + a style preview and lets you start the Plasma Theme Explorer. + + + For more technical information visit this + page. + + + + + Get New Plasma Styles... + + + You need to be connected to the Internet to use it. Clicking on this button will + display a dialog where you can choose a new plasma style. Clicking on + Install in the dialog will install the chosen &plasma; style + and after you Close the installer your new style is + immediately available. + + + + Get New Plasma Styles... + + + + + + Get New Plasma Styles... + + + + + + + + Install from File... + + + If you downloaded new styles from the internet, you can use this to browse to the + location of those newly downloaded styles. Clicking on this button will bring you the + file dialog to point to the &plasma; style tarball you have on your disk. + + Clicking Open in this dialog will install the style you + pointed to and make it available in the style list. + + + + + +
+ + diff --git a/plasma/workspace/doc/kcontrol/desktopthemedetails/main.png b/plasma/workspace/doc/kcontrol/desktopthemedetails/main.png new file mode 100644 index 0000000000000000000000000000000000000000..f7d6e2c0ea47d5b93b3176dbc082b10273fddb60 GIT binary patch literal 31266 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 z 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 + + +]> + +
+Font Management + + +&Craig.Drummond; &Craig.Drummond.Mail; + + + + +2021-04-09 +Plasma 5.20 + + +KDE +Systemsettings +fonts + + + + +Font Management + +This module is responsible for installing, un-installing, previewing and managing your fonts. + + +Font Groups +There are 4 special pre-defined font groups: + + + All Fonts This will display all fonts, both personal and system-wide. + Personal Fonts The fonts shown will be your personal fonts, and will not be available to other users. + System Fonts The fonts shown will be those available to all users. Installing a font system-wide, or removing a system-wide font, will require administrator privileges. + Unclassified This will list all fonts that have not been placed into any user-defined groups. This group will only appear if you have some user-defined groups. + +To add a font to a group, drag it from the list of fonts onto a group. To remove a font from a group, drag the font onto the All Fonts group. +Below this list you find buttons to create a new group, remove a group and enable or disable the fonts in the current group. +In the context menu of this list you have additional menuitems to print font samples and export a font to a zip archive. + + +Enabling and Disabling +Users with many fonts may find it useful to only have certain fonts enabled (or active) at certain times. To facilitate this, this module will allow you to disable individual fonts, or whole groups of fonts. Disabling a font does not remove the font from the system, it simply hides it so that it no longer appears within applications. Re-enabling a font will then allow it to be used. + + + + + +Font List + +The main display is a list of the installed fonts, grouped via the fonts' family name - the number in square brackets represents the number of installed styles for that family. ⪚ the Times font may be listed as: + + + Times [4] + + Regular + Italic + Bold + Bold Italic + + + + + +To install a font, press the Install from File... button, and select the desired fonts from within the file dialog. The selected font group will control where the fonts will be installed. +To un-install fonts, select the appropriate fonts from the list, and press the button. + +Click with the &RMB; to open a context menu with some additional actions like Enable, Disable, Print, Open in Font Viewer and Reload. + + +Font Filtering +A text field on top of the font preview allows you to filter the list of fonts. You can filter fonts based upon different categories: + + Family. + Style. + Foundry. + FontConfig match. This allows you to enter a family name, and see the family that fontconfig would actually use. + Font file type. + Font file name. + Font file location. + Writing system. + + + + +Get New Fonts +New fonts may be installed from local files, or downloaded using Get Hot New Stuff. The Get New Fonts... entry in the tool button (located above the group list), allows you to install fonts from the Internet. The fonts downloaded in this manner will be installed into your Personal Fonts group. To install system-wide, you will need to move them to the System Fonts group - this may be achieved by dragging the fonts over the System Fonts group entry and will require administrator privileges. + + + + + +Duplicate Fonts +If you have lots of fonts installed on your system it is possible that you may have duplicates. +Click on the Find Duplicates... button to open a simple tool that will scan your system looking for fonts that have multiple files associated with them. For example, if you have times.ttf and times.TTF installed in /usr/local/share/fonts the underlying font mechanism (called FontConfig) will only see one of these. So, if you un-installed the font, it would re-appear, as only one of the files would have been removed. Running this tool will produce a dialog listing each font that has multiple files, and the corresponding list of files. To select a file for deletion, click on the column containing the trash can icon. + + + +Preview +This displays a preview text in different font sizes. +Using the context menu enables you to zoom in and out, select a preview type (Standard Preview or All Characters) and change the preview text. + +Launch the application &kfontview; if you need additional preview types for Unicode Blocks. + + + +
diff --git a/plasma/workspace/doc/kcontrol/fonts/CMakeLists.txt b/plasma/workspace/doc/kcontrol/fonts/CMakeLists.txt new file mode 100644 index 0000000000..a31297819f --- /dev/null +++ b/plasma/workspace/doc/kcontrol/fonts/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/fonts) diff --git a/plasma/workspace/doc/kcontrol/fonts/adjust-all.png b/plasma/workspace/doc/kcontrol/fonts/adjust-all.png new file mode 100644 index 0000000000000000000000000000000000000000..fddb4a02babdb2e855e8e3c8195bab34befa4e96 GIT binary patch literal 19197 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 + + +]> + +
+ +Fonts + +&Mike.McBride; &Mike.McBride.mail; +&Anne-Marie.Mahfouf; &Anne-Marie.Mahfouf.mail; + + + +2021-04-09 +Plasma 5.20 + + +KDE +KControl +fonts + + + + + +Fonts + +This module is designed to allow you to easily select different +fonts for different parts of the &kde; Desktop. + + + +Here's a screenshot of the fonts settings module + + + + + + The fonts settings module + + + + + +The panel consists of different font groups to give you a lot of +flexibility in configuring your fonts: + + +General: Used everywhere when the other font +groups do not apply + +Fixed width: Anywhere a +non-proportional font is specified + +Small: When small fonts are used + +Toolbar: Font used in &kde; application +toolbars + +Menu: Font used in &kde; application +menus + +Window title: Font used in the window +title + +Taskbar: Font used in the taskbar +panel applet + +Desktop: Font used on the desktop +to label icons + + + +Each font has a corresponding Choose... +button. By clicking on this button, a dialog box appears. You can +use this dialog box to choose a new font, a font style and size. +Then press OK. + +Check the Show only monospaced fonts to +filter out all non-monospaced fonts from the list. + +An example of the font you have chosen will be displayed in the space +between the font group name and the Choose... +button. + +When you are done, simply click OK and +all the necessary components of &kde; will be restarted so your changes +can take affect immediately. + +The Adjust All Fonts... button allows you to +quickly set properties for all the fonts selected +above. A font selection dialog similar to the standard one will +appear, but you will notice checkboxes that allow you to change the +Font, Font style or +Size independently of each other. You can +choose any one, two, or three of these options, and they will be +applied to all the font groups. + +Check the Show only monospaced fonts to +filter out all non-monospaced fonts from the list. + + + +Adjusting all fonts + + + + + + The Adjust All Fonts... dialog + + + + + +For example, if you have selected several different font faces +above, and realize they are all a size too big (this often happens +when you change screen resolution, for instance), you can apply a new +font size to all the fonts, without affecting your customized font +faces and styles. + + +Anti-aliasing text + +To use anti-aliasing setting, simply check the Enabled item and select the custom settings. + +Placing a mark in the Exclude range from anti-aliasing checkbox will allow you to specify which range of fonts will not be anti-aliased. This range is specified with the two combo boxes below. + +You can also choose the method used to create an anti-alias +look to your fonts, and how strongly it should be applied changing +the +Sub-pixel rendering and +font hinting. It is also possible to Force font DPI for +the screen rendering. +If you are not familiar with the individual methods, +you should leave these options alone. + + +The ability to use anti-aliased fonts and icons requires that you have +support in both the display server and the &Qt; toolkit, that you have suitable fonts +installed, and that you are using the built-in font serving capabilities +of the display server. If you still are having problems, please contact the +appropriate &kde; mailing list. + + + + +Fonts DPI + +Force fonts DPI: proposes you an alternate DPI other than your system one which is used as default when this setting is on Disabled. You can check what DPI your X server is set to by running xdpyinfo | grep resolution in a terminal window and then change the DPI using the drop down box. This will be applied to newly started applications only. + + + + +
diff --git a/plasma/workspace/doc/kcontrol/fonts/main.png b/plasma/workspace/doc/kcontrol/fonts/main.png new file mode 100644 index 0000000000000000000000000000000000000000..b455948576b55e2a617b1878c40b61ed03790a17 GIT binary patch literal 20694 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 literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/kcontrol/formats/CMakeLists.txt b/plasma/workspace/doc/kcontrol/formats/CMakeLists.txt new file mode 100644 index 0000000000..36db2cbdda --- /dev/null +++ b/plasma/workspace/doc/kcontrol/formats/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/formats) diff --git a/plasma/workspace/doc/kcontrol/formats/index.docbook b/plasma/workspace/doc/kcontrol/formats/index.docbook new file mode 100644 index 0000000000..1944627436 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/formats/index.docbook @@ -0,0 +1,63 @@ + + + +]> + +
+ +Formats + +&Mike.McBride; &Mike.McBride.mail; +&Krishna.Tateneni; &Krishna.Tateneni.mail; + + + + 2015-05-18 + Plasma 5.3 + + + KDE + Systemsettings + locale + country + language + number + currency + + + + +This module of the &kde; &systemsettings; allows you to select customization +options that depend on the region of the world that you happen to live in. + + + +In most cases, you can simply select the region, and all +options will be set in an appropriate manner. + + + +On the bottom of this module you can see examples how the settings look +like and which measurement units are used. In addition to numbers, you can see +how currency values, dates, and times in long and short format are displayed. +When you change any of the settings, the preview shows the effects of the +changes before you apply them. + + + +The Region drop down box contains the list of available +countries and will initially show your currently selected country. If the +selection shows Default then you have not set a country +and are defaulting to the Country set by the system, which will also be shown. + + + +In case you need different individual settings enable Detailed Settings +and select the country format for Numbers, Time, +Currency, Measurement Units or +Collation and Sorting rules from the drop down boxes. + + +
diff --git a/plasma/workspace/doc/kcontrol/icons/CMakeLists.txt b/plasma/workspace/doc/kcontrol/icons/CMakeLists.txt new file mode 100644 index 0000000000..ef088d054d --- /dev/null +++ b/plasma/workspace/doc/kcontrol/icons/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/icons) diff --git a/plasma/workspace/doc/kcontrol/icons/edit-delete.png b/plasma/workspace/doc/kcontrol/icons/edit-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..9be3fb20fc8f2f3f15897cd6acac18b23c3033e2 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4foCO|{#S9F5M?jcysy3fAP|(%W z#WBR9H#tFqHQ<1U$3flc5B}dca`oF9)fLlInhPV{#2X&U7R5RjUI^gnD73UQ^|?}d qZpFdHZ2bDoksmzyGBnL<7#Ko1MLhra7dQhAWbkzLb6Mw<&;$S*H7vjY literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/kcontrol/icons/edit-undo.png b/plasma/workspace/doc/kcontrol/icons/edit-undo.png new file mode 100644 index 0000000000000000000000000000000000000000..6080e7de9148bd6ffddd01a20e7ae23e8b7dae42 GIT binary patch literal 374 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
uGbTnOJ(a2*Fnk_%om%;0AgiDwIZ=Kl0W zN^6QP?Xq!nA?sgPQ!)>;&@%juf>DXx*MAzRNkWeDtJ7hv<(>FCi95yc>m}g4 zS_TnN@Lv6^c;c4q?!3(VlA)4Qwl|S|mxLLs_jzE{S8W6AH-aH29fv!;aQfo=4l zHr(xg_xyH`(~SbN%lh*klo|@?=t3*X+oN3TZRHGFZ2~!dLFEH9vE|*KN#Uo)E);#G z>65E)j!BK8a$FfyvBh6qPEnq*nEM3h|UM8HcOsL(Ra>m2lIFK{QI2} z9Q7Ao+!Jeh7J!~(G2;R$1VKMgTi>S3RT+187@O2m;K;vFQd zPf!fpw2~+$r}-k-M#Q*k_}|mLSMb%jDZ}2!8He*nJ8*=#o>SxgU|Na1oi|}T`!IDs z1(u)P`upa7A5mFV9+`wRR0CiO6c_{|#LPuI_8j(2o?E=Z0A+}1FhfP>aNYKJ01%%> z#IGxa({w*a@Ik1C9t%F`4xew8(+G!A;i_fjBdA-17~w2pk^BSSqRpiv> zE@?4ig`kWs2nwjruTG1A>mV7oo4b%uh&FvDwO|+oU9}Si(jk8!TMHj zW|Sw(@#|hAKmX@QWG$cr%^asmlIa;waq15}Aq}m^?HS1o8DMhjMsnXB)oI@Q$H7TR9=r6=VnAeAs9du798KXYt0NrM6V&_7|@)vcU_$GfgebBb~2*(CEB~_K}MPp_*Pn+ zy*iNHEIu)H1=cVeaf27~B!5Zmsl%d&yVHiEWcfyDFW~`CBS&M3ZOH~PtRGPW8AuGpZ%iQ7lWig{f}QV) zaRWZ$1%~P&>TMPzgOTF&^)F{5QpxP02hlGHL7@N(*%`?!cGPLyW>y!(3@j=UpCJvh zvv@69-N{EXk(fio^2@WmU)k~nA~G6)im($}!I?24?U4Y?jxp>|jt=4!eE~K2Pe^EQ zv7?M(Vw8!a6>-EkN+RwnK1^l|E@lE**^ti-yBhz-s6J*dRv*$-PvNhusx>YG^d8Ab z1>GpTsU8BAbN9*4w78jkK3n>N7FhG$^{c6sIJq+GnQTD5gfX?_Kpby0e;sJ3L~!HR zQ`SGJio8PPC~uynvefv=^B_vKPLT6-D+Le>f!M3a?|s%6ZHr@5r9smsf`gkq=lDeT z6va@>NF)uIB%)R(4s4=mw>9vrjqDhx_xT+{>LdVcAuz&(>1exGDn8XhCx3I`EJDMH zL-sw_yI+SI*ujRfvw!!^>^)T8_`jkV!??e`xcT)7z9jPeTc~bE?>gv@yL_Q)v)w=tWmxLQ(mB52^uz*EB{GISV z?F3W0mYM>!o8aX?R_cs%8dE+9foOgprOq3R7g#of4sx=K?!g6%Y= z;?WT~wtchVO%zWY@Wme?LE?Yox0HIxlP*lV5t04%tDB17aHD9>uedw>FIg~zkeV57 zXM&E)yc|w+XfbjC$DlJNwLiy7?c^Ik6tJq`?F_i42&fU+fH+VD)~x*D>x_4CGJOWk z0uDs9;`dP2xtB6ZeJ3kX{4{Zqo%HY5s7I)w^g<~^J`24DNfiFu=EU-tS{ULbTNA(f z&>McKu}`2oHr|)0>ynv#iQbXAl2bqE6-*3}imlv%PP$FCl^QbrLRKNyMPo67&7l-$ z+BIIPXTKbt`d0P}Bs(7yiGzYmQGEDf>5HIF-#tRLfD!NF%GJCP=_PDX3FsN>!?rOM znw1X|y3~WC^8$|OHfu1#p=UQRpaL<^E{8Fcq+;gyUDzS4f6?6VUW~qe<3SVGHKzN; zIfXLghbNO;(J(;=;PneIeLKc;j7GB8fKS0c73kB;4H%-#O)KH@q?JtQXRnbu7r%v`J!h^zY9R1(g5`zwyyYcO(1He2r!G1T%h{5)j2$5?Va?K@*IM~ zGpdh%a%qaK-dYo$fRyI20uTV1Ygc4}&v>oZ___xuh%M=@)4P8ieF9z=PAppYD;7fF zKK;MH4pY+dgO;_Ls0e^%z3QAglW$ai`zqQIT>70kzW9@cbmRyugT?bz(;An42>*&O zp8H?@#UA@VJwB>s^q|UFEvxCLr4vNQEP{?`22-yM98|Bhz4YL>0hAbE~#v@&>!VyB`3)p@mh1wOh$LmKu z{DWyXGnN9z7XAH+h)AE1$E+L|y@Rv|c-e*9*L?_=JaOCy=H>%AsdOYA$Cj0xZvdKa zk$!f^)#$>1*}hvZ`x?_it36%F0@Lb1taKG=16}v z7x+$~8?~^BWRqIv<%oLOQr1dA9CY_B~Iq0K4?wft<$NE)REexm4zap}OW{P@XXlDgh~;2n<7Lxyqy6uozGq zxWMNq2GqlEam*RH&t-of{AHH|{^pLf`c>X`EO}l|P*o5n&bYNdXF=N2e|z=>BNC$% z-~<2y!%R;J)q>?3Ll>TT&^OCH`DB9)coIrv;`w9QyZFZ1W|#>wYRQs?$#3hft$SH? zlUJ;gEf$P4Ije+_ApSwDND-Td=$@{A9_`YSc%N4#y5uB?gMp-Aw4{l-E_R5FG*!(d zVP~SgY~a5Ha)d)X(o>4wH_=457aqVb!Tvtx6VgOWsbErVcxlP6>v^Mg;@>hJl~^*MQu_iWZypVc*D z!XJNd@eOx8Jk$H?rV2dsOxzv-W%g1_dmCuXPMNNbt#cP%N9Sy(BI%Xs^2v`~?9*zb zXKRM$({3Siw1)JX+2tqW6y((^Zq>e(f6uS$uWv5w!7c+TZ*+ZD+gzyRdi)HppGe$1 zfvf>>+=I^a8vrxe^Km{l-RRC;L8$#scK(Iyh_k^S3&sf%r_uNl7s63@`&Jm%h8 znTu1|(q#`0zN5!PKifuht_Feo%Dh}_*VgW%<$h0S{UZTDP2>ufsb9UySZP(gXR~w$ zOL16N+3zp)mrc+6%5@qO$Xy$H<2j`wP7RdPK-eg-ZIVNYFMrV*S{2}rIOPZ}23*^~m9ShD;m!OQ| zeCzuO@WbN%(e-Or?&!%$82D?f#~`^PJ0?+vNYg7V|H^b8GDkbslX~_u?a%yEzCGCs zeV)|(h@h}eMgFdGrY}>`9T<%oU>|QplBka)p1Lhz;HLH*NsE3O?y9+vCTD&WMeKC#(tew$)`vJZ~$-gJ!GV>j!%t^VJ0{W=@&MuV`dTLNP67 zo(=q*dnrvN>VSZa8}~e5Ml!dYt_=gYr}h3#kEH>;#S_C|M7GVn?~Z66H4gA?qs2WK3p>$aegMBr=|zu3Rm=bedBIJf%xYcZ%+d zUH2^EHw_KGj7OS0yTyRO(;ovu@4y;O*q#s1YvY*@P8xb6XID~q2|ykFiyy-s84{d{ zIDN4b8oh9&A9zYks;j&i;#}AV17CVX$1T0&c%`6n|JsGp&PmaG+0*4R65U>@gb;BM zqQBlLm?0CtEn%a-J|t^A+4=Kl`9VZ^nHH6Fult*I4|^Y`Vb8iEa9N2nLj2o#uQgGY z9N;~&G`8NCcT?jv-1mFqyguP7$lZwZydH(kyr!nqj>TFh_f0s_ZKyWY91VfUt~Qhn z9}Pq6Dho}HhMS2ojU&r9@+v%oNL_QPe^Jin;@WAE% zl@(hm<|9JQHdWAt33fJgpmrIVvF9${ym9IwOYm0aiY3UobMbKBA?0Hnq@-rgF`=(Y zh1G4lr?`1$%)YemK6_iUfwTS!4cj(<~uR@JLDk5z*q`m{7IOBd$6 zX*a(I6`Tr;SpsXHp@M`u`;Bz?@3;iJ^kmyKpA~IywvN}=KAa&y9fsbp{WYCnL~I1v-oRcD?~e0xxJSqN}s3yBVGStF~K({yrlII z^H*?BCYYXbbL)V0ZL^XNE5c;R;nm1^PWE&%rB|~ku;{=V+tsKq7s@e96G~kk=cz6i(*M-#t$$UylBZO zIYqZv>ykz_EB5T4bx2NLx{M&NS$C|piI!lxZbvQ_L-SJQ!p*0A@?Wh`4w|DE^P`7X z58JG9T4H!AH+`052^ruV(1Rm?>c25FYFR@28M9oUO5et?R{x_{EIvXS_o5#k(@u;Q z`+p>5a~p5&*Wq09rcDgaHeSy_>(l(Wd@E~8cIta*&`fyiZE^Ud;{`s#3)nBhuPYu- zEUX3|ug^Wfg`_d?*b(9r0|F%#1_g zDXLA1W5mMm9mhTqW2MK&Xz%}tMVnJXwMjwEb4Oz&9VLS2OzTPMzipGO+`V}znb_EW zOm=s_2+05P?_l#r-|H3P=7;-N{_e=9j)iAahAbq(#2<>N!l`#x771A0{j;WG!HfnS zU+!Hyx0_`eTdybS;tNc>pAH1_79{eSaYe;puLaPj_Sios~MJOvI--Wt7 z@GFf|TY+W^20JC_t532|ft7``UyG&2B?J*3#tPOiLfw5(1>;TS5=PkuFAptSMgdZDm>|O+_nG85uekwCAna?*gbaxqPe8;fksaT@5O-HD*uSNw3FaDPP- z5=D%c29W2zZWutbgepoRdxs+HJ%I<_IH!K;dFheJ0c&YM-4o(aH@ z#koRs@o(IhnTQ8v;e z+6w`4#>>}>32gy}dq&6SdlN@{x(@7zKjw~)7@pm=50tk+o z*{ew%Ja8?J@^%f9th1ENYA?n7mIw8&lBtZ_e(txHm8AlT_}BJyrB0ckP2Y}Jc0qxw z6V|!q!O7upSIBkPWy??T_gskl^hf*XTr>MW?A)W(lau@RT!F)a^naSa$L62E8)uHljm@BUcU1U=qrb6^2P{cl_AFH4HpzcB$f?&{_`czzsYfz6a? zB=jwg@sdFEIV-9Zba53WK8O3xlWzRwcy>Q&XCV#(_zMAA=lo>TusDT!?qa5gBDG%p zI=>B&RY)`hBl4S1C;}tyUPTRIUyq^PcBNjCBC_;!9o_)wy|G+z86MoII~vauAWgrL z?D}jFK+E%ALN8R?~fQT$#j=n>kxDub@HsEg%ZMwuDV`tz1!gzu0Lau%o=6sOQ#?T&I@Q+`Keuct3*d$p-jCB5zY}hcW*S+!1>iN6kImVye5HW|P**V}^Wf zzp5dY9@6ss@0XYoaQ#-ld2k67ftx8~+@-;lkh_w zB=%WG$yf#T@cAnYREc4fOi_JS{^IUyf;Ow*!Y^wotWUT&9G?H4CRPYy_N&lg4IfiH zDmK*M`BxbhdMnhS=y*SA^ygnW%Y_eUQ7aAq@5k6pimIf#(>pLx{)xz@$BX`SgeI*Z zQA_{o>7`d28{n-cGtl&wqeF~BQnemwXL364PNxeO-;vV3mB95IZ*pLsQG!dEk@)uHoo^btKZ;g zOyi3XbSURz+5~T4OI|$-ZEJ)lWJj}0RhLHVw4G4%>HDcbx$GqkQi~(HXhbq5(r1jt zxD6g5;C4yW0*^LCx8_Gz2Fw2l&YPQ&p60}_8;RZ33-*v-T##zV(`;zfJJ{fEzNuJ@ z{aaxBFDPs6pg~yJBTFoL;CcIhg>=LvE?MvANwa@l^uQq~n1g~|6X&{ZllISdxAvD9 z6z6p4tC6{m^iUquW?=037y0-iD)m9pq(>?^-P?9)CJK` zzYu7FA?rI0gWCfCf^O<@##G5xaJ<3ky7T$>%}JQSF+pfI-6r>q@=x_CAjcvr>VUCl z1g~}${C#StWHb>rPyf|~EGfnI(rROUfOUR@!$KGZn`PzIBEy#IH5RRwEQw{V|=jLK(=di%q&!wt>G{YFhZ z2JX;b1Hp}XEm#fiF`)Rls8k3%;-~~ER?|^4_&mTfp@O1phrp%vG(X*d8F%%3Ms!1@ zSs5(*`gp%zUd1<+^CrY_%s2vRpyt=YIUI+~t{FBy@vjp&kFY*4?P5o((M4javSU_z z7E3Msa{B}F>?;(0_sb;0t9c*r&y5wa5=_3}oe=Yw)yQLsd^6kPpXH8nK72i!@xFD5 z!Q@2h8LAM-ll6)_F|r82u)JHV6QfRcBLJxC(Q7tKyEi6=GSNCI2HcC>=X!q*1FIJxwYyU`l`H`Kn&-UC%>h+CH@_Yx>EpWHXx3BFNiQLE| zGT;M()p=_ziQj*pR|uZTeR4^-yY23B7R2XD37m*8bSTF=CCPX(vYlmyh~jV}@(ei_E zQH;Lfo8t0w1{(%x=V(gRGzN)aGV!s!uFt96+XzPqZ#eySf1~@u-R|VMx{oQXZl`CH z2w--)r8%3Xt6zGMxrD*=tiGOhn|lk~!K3d+AB(xV}YOk8I_{? zy&dT>LEDjk>S`#|<-?q#*;lv!3Jd#x3FK(Xn20p3DV|;6n zv*_eZ-st3!q`qD>R#0^R!8OSt=FsiC-1@fUjA^LDDE7XFcwMfLzea2RWU0s_0^#|B z@9}~FDX4rG3qT%X)B#7(zD=ESCQ?2SsCpWcY;{UZySEt)vwQCQ=Z_CUJrU3W@lt^* zAz;p-=v@m$ZNUdjP%bEC_5J$qfTj5~T##6Qc!v9l21>3^Di}F`zbXLZUSV6_$uU_k zGOPr*sH^R&B1pVKqgS6;niK=?7@&XM8n!EXDJg*8 zC^v!Ay7_5ex4FD`KYo|BQNh=}7A2(b6E0l_2)H2Tl^73U>J{;kgDO?1X{!_(b=5UW-!*)PbK@QgHkM^fYLNp&MHa_p%E7x`>bdz8j zQ*#x+HtQzn2Hu^*`3yJfSnGg+x{e)vfT|L&sGtS93GF}sOLpzpyL?IbpufW`XRc1) z?o}TKFiB4Yp-^)S%mVS^(kIrTAO7_uyW4b71+l3j_E)?Tww}q!dd&~U#zHf&x@1Lu z8U83%LNo?L5b$pF?)pr{EG{DXq7qP|WB-QBi~q4`DMiYHgl*DR7?QB{DZljFN2b6Y zlSQC|FONSqwzo%0_7HT=xi6HgzSnnnv19PmwPXHqA>RRjzwUB-?-nl|cfH7rpBR7<3vclUaQO#} zZnd70JO|w#dS&=Ju>huR6#pew5+f=(?w?>kqJ*lUB>x*+{10FrmTD5J5ZcVmV>)ov z)Rz&~*`Kvs%(AEK@JnNV*T5K`w{8sm4Ad;}Nk#H*>Xc6)h1>^0{EVEnYzPnH==l`P z*A{G+ME&4E)f+H8N(v%8r{yFtxwZiu_Z&@s2tf|Iozp?BZKs5fXVOjFa@L;EHB|Jn zjXJ6#0tRx02Thms$CV791Rgt@{G&z$Cw}mWFTVE`%Oos}%_q8Z?N-E{E$UPvMNFk! zi&2w5l>gIyC7tIT9eMiJPrB@x;h$jer#vD`KoOl-MwzTNmQp4~?00*;l^%|t*ippr zm14SLnDJx-ZNmH#6QeK$DkSylN*K?h?&72|vFqb~q|t2A;H)4vzXRTbuxAL<%u()W^jYna+=UvGQ?GZg-m+jd(5rG zg1gCwr_dkky{1MAt+b?}Fg!W|h{o|A143nnGwwemCAF8#t5rrH%D|tA5 zo$cX4Y8Gl-ZNJV%v7xQ*QXeg|H1;{tYkjIqpQDxFu>?`WROlcPFEu%-Ls*HzsBYkj zok?v?*1{X@-s#FSHFRHa2S<5`0_c|@ltkh%c;dAnVHyhnxGPwZzCl+=0zE6&GOv66 zEuF^_uk&3fQ5v9VlgWzH|1<5vySV2@Tywq&08^=mK>d^zCt)jLdBCi;R4x{YUnsw5 zz4TUG2GNMW6fM6&w01yK?{pr$R?<~LQ`GP4FQhj6_LpFB4?(Whnh^a^Vqv>s;)PMurX7S(G-wGCJ^g!L5z~IQ*=OyRILUMcf1W9!Dy?FBpyw>*;tGK&9z;TC4q8MRJnY`JH5Mo;GmQ(A-p0d5TA%`YB6r-nc81a@Rf+vm5<}uz~ z+CK2!@xI2TFirrvf44&3UDXI^OEfU00cJn&L3@e-?hXH$@b9OieU3yW8c}=|JOuKU z>`^v-8>OwUKtd2PT}y2@SU-M^n?fL*h#Iu7_hj}{r*&aUOjUY$+LZI9|Aq~b(XEL8`x^Y@?z;5{cvwM;P{e0Ko_^>%eR>4rU+uX zjH?^Lawhjf@#hLLy~EEBv15rHU&myXIMOuSvk?EVsa;v5F*veB*q*<9v6SK4xDspH z0ns0PcP*cKWLem3zV%0Zw&f>a;-2O&iO}XW7}}!IH$!Y829Wr3!-z{;HpMPz>|_(9 z2!@;4N)-jEUB6wV4nTC3e#Cr;NL}~|IenQv3 zc_7h!MU1#RN+g8-oO^f;ZW4#)4<5Qs3gSWGuDy?r5|aOkB1NXGAIM;`{|7UIy^Bic zX#Q425x+0lnwn?Wll)hYTGNL>t?yF)UT08*(0z%~^gk|}H!i(xzuC3-O&W^9bN_W8 zuFO>Xys>N1D#|Vv9K3K$CL&Il1fyRoh_ZA7 zP7c~E5t4B(eE{d*ooqRV!;$v=x45pQuib680q?1ClV}1fMhAa>AFL}k7B%E{#J=fM zsySd|04y#(1KAMVI3pza2Wh}-PXws%x+CqavBv*saw;WY<2h?h4+5UAev+Dd z1dBI}mQFeYH-vE1)f85qNXQ*K)3ZJD?2>_T3_JE)&e|5b#i1-IKb2bH8|JIT;~9~c zfWF`Ss0Hs<8M>zeqSTZTYm_}g)G>EG1>sCM&)CjBP2qrqTYqu)x)0QAbkf_B6wK%8 zV7>qU$NdB=f(_nspGnX4=d4OaEcHdc_GE@8LM}{H9#kJDQ^Se-gjya0E5oDN5ueD+ z%%KOe(a``E)-AV|=Q@6VSngRSZl0uwao2Rc%bcPqhcVo^S5s{xIBtoi_}&^;aC7L7 zbfF@CI91xyv#Tiz`U@X}T{bn*DB^gb1F3i@Dd70nLFW;QJir7U-q}r;gW?X9TieJr zePi)K487tmsA!YU}@O8X;85VJT^27GGyq_{IlX)JS z+0LF;uKvvi&I{E67cyFM?nY|M1B(f7l5B}X=)X;CI zywxe4fV2t8$HLz(*a(qg6Ebw185KN%4|DstXxcOX{n5b;HH{W8Ac2^cH~!nvn{BC9 zl7?CZZw-jrjB#7}iF4uUGt?Lb==$cIbTs)0M_%!mF4M0M3A}YpU$I zAw3YHK66&RA@k)UCAU4>w2$)^U_mb(gVBp`|D#p^Pb}g8(6;~Oz z@nie{-TeO=bo)lpi2B6b=NU+?>EAQY_^$$jj`NPyhJum5?IFqqGen#_ZYPw8{f9K~_Q*a`LUB&Z?*CcjwRsrHTDoRZ^GImzhD>tM3K?+2xeUN_l36x(mlnVEVx33X%;_%PK#%YS z`>fXc12g2cM`pJt&g8MQ{-%fiWbIJgzW_Xf<=6!!mJ2PW!!7?g1*bNpE%i!uZZlbd zc0;mN9H=+Ta6shppFb~l!Cg@4Gm!IO50-1D=LnH0TI_=mQ>**fg~D>-63%Jm53$e- z52)dbnF94Ye;-Y=sgD-_ITq*c5m-GsUD!_=pSg3J`$3nIvulnp8Ky&3Vt7bU#LI~$ zOdbXZk>o65;rOnS#%+zJ5@tB(IS#7V2}jn6=bqxn8e8nYDDw>McsxH0!n%mTcNnaE zd!2ir&M;PY5vrL!WMb`V*;^b2JH9Qsv`zA=Q7yh~WJtFYMOL&%-WigY1aK^g3xzji zPl;MCNVu^OUgZ5NEh)di7m){Ob5VB**n_W(dvW|znko%*T&)pf}P7_ki|_7U9IM-(RbBHb@Jm;XHv{6h}y|0$n6E*xv&=6UPw_z?@y;_$TU zX5s`@A^pae)9e3h>&oMyirRlsvc_1)mSxb`jU}Nh z6Jx9ySyE{1OF~8VeVgGLW>AQVu_Y0a5(#6S&=g69ELkIC%kFo3-_QHsZ|>*JJ+qwW zJm-17_bktS?sGO3hchBlmHyihwS265SN}Ag-;I*hgaC$k@d)del8boMlU|pqKIww@ z><#vhwV$dX+Uxy%nsrhB46uP-fQxv!EiR50Xx;6w@sx{v#f2guNV~=jc%Ey6ds6zR zbHf0LG6IYQyYqU)1F+dXA}j3r!*a%zyq&+?>D`F-2#Qy*_f$gU2D{w1Q;vR%Z2p?H zxm01vg3b_^90!sJygxslA#{30u?hWb$TF(`zj7dg+I5>npl0JWLvEUC z`{r-}KURb{^!|kXxg{@DsF|KX19Z?>Q-iJ`8s8qjSFa85Lyn5PINzW|}<< zdV2r@i$<$Igx;RfEshUg)RH)355&&TipD}M3B1F20# zPn*ZBqbBUNk756uAnLo-DhJ2*SI;GY4iPd)7KkbH|9m6Nx6K}YaN?Sf?q{6aWPO^>;3V>@Nnc(d{ zQfI_^8+^cNgU5yU=q2dQ;BKvHt943N#CmuQfgdHAHfw$w+tk+D)!%Vhp(eb@K~Q3j z>s~iK`9NDiy~B<*FZ{e-&|NP)y)yXFW!U}N>p9D8jneRfb9f?&wR)9>?RD41rmJ=m zQ{}a$T5SwQ+7@2ME)+?_Hg6^#piF+wTLv^q0i9$b_aBbnkbs*Rf3=nGWX`tzDBPSG zcZlB%J@3{OejkS-TdvS`gV5Z*KIyzi)S<7;VMnN7xLC6|(t_(JnL zHHG>+=_{8Tx=UZ4m=N=4l1oKDLH7#QjM_tMv6of2-)f-j;d7Fs!fqv$2)8C3B-%_Y zR7;#XrMF@2Eh7n}R*G99y-Wi`G%II8@^0!43TB(!rWViMZ0uJV$A!P#9o;XbGPvGp(1tjg-9*ef1Oe! z`)c+LItY@VdeDyg>rH?_j&UtQ)K_GfBWxw@52q_!Zh*l^YW*dIZR-R%EDy_jJ{}oaOE$COl$nU7w=s^lDI3x>7%wO^T z8<>cxk9}?vDC?OpaP;m97U-%ZtUZLvPCn8pRdA#DKs`Xs+AZC5gVwS_*tWCV8H^nF zmc+u^gyS!Q7`KDsdjl4AmH#(w{r|>9TuxY{J(b?IaNMT(wjio*2v+C=&coSg%hog$CMpuf0IxjAY z*JCI4Lfg+gX()b-9~LRAZ4Xo{@3V@AXrl@ycPHokp|+nE)W?v=E}ryfbRfvnzgiz$ z1(BzP@)x~L&sJ5w&C^ie1K(7-{=7L+fe_QA-$l}MohBX%o=cg4xmny`(Qt?)$uYx}bSF zlnJz=riZ$=M8D`}8(J=91HG1_+tS=@EtK?+`eQ92i}bB>)zID19=Kj=ddlT2Qv=TU z;;bqsv1e4~DX-6L1C@wy;d`t!&i1I1w&ONlF#!^?dK&=NP=3umP=`{~)A`236zg)g#I@YkyxOx}XM-tg zOQw!0+z-R7Lt3t_Sa0)m;Y#mx`K1jACq18Djre>v7!PiG`@y8pAsQ;2VR9}fJ_z4m z6+s&sX@AONu)ht-FecNblbiWA8vQ_xoHJwRaKlfAHx%F@YwquyQ&n7_ihP3g-iY0? zz84lYI<*=hawFpbbX$r}r5&0BA3)YFFp?)7TGn6Mz9qaV#XN|E=!c1k22(+6cdD*J z==w3wm;(W9{aPJ%G@>)~Lyhza@{$FA^Kk54H4s> zH%LC>)7d=ke^P`8P`cC*Q!ktLY$^>k@`KY|5}(I!uVvI)m%ocX$ZL%r{%nd(q}y_2 zwB=!gN?A~BK0j5I^q9cO>WZuU8UBuw;AC`O$qmyw1rA0@SScj_Xi!%)@E$YeIP@?v z<`^=Vor|wS1bgiC2J!4~z;i=BXuVl%@>d%(3{YvGhoYb^Qa~aEJpJ#j9S!a5_?K|x z*y0JnLdT~gdf4o0m3H{Se@_~{@XI-v4-g}8o5BI_+BKUR53l;ePVc@95A~VLPw{K3 zS5em2U8KMxk*Ly+{&$4yLrtJ9If}V`krReI4?S(ZDCLe*{ibe#`TF3=`l#H{@03@9 z>612KO2lx$K(3UMFk+TDV1h=a&;;Ik{uDG>>!im7v0j&MP+iD#-q{JxPgrphsP2&! zzv^@I^2x@jZ`TUymNUQ0Y!ov7o;;?`HV}O8+|%j@O(VlJwu7h_8 zAex+n0l&DL9)Bn}dW37q`Wv+)U6e3zrs1M)#4MF9^kP@aM2*s8((>O^3C8vlnYY$M zA>q~bL<5}MFKCUwRVMRiZ+Cc1A|Z(?3s_HUSfiXEmHzd*_; zX`&*Q?@m37`a2UN(9_@fmtK1@G;lXwWOO9x=+4~eTRJ;z)^_flJCnfO9(Vbv9~a!a z$~YN+zcrXgc1dNM%u32|yc_B2sKZuOy82vy#l-5{)@&#mWP}>AuxW6w9Fi9PCTXYS z*#u1cQ9f)WJ$)Re@a+=+aXY+p+Gz<_At`%Bk83ZTPohW~FV)Us@^_f+zEDa#=G*8w z2lPPTyeK#reRl3i=}pC=f?X8R;jmF^9OK#6FvSJO)ljT9Qo`j^@&*QT}WZ&orf z?>K&}NTPHZ(YNW4sw%u$=oNuJ`z^oUt-=>0UT*&8=nOwM-nxGja6>bL{+RpS9Y$E8 zlu4>>EzbuY$FrkzVg|ShbA;*#wWMLrD_Fe8nexra9_2F6r;Pi?8EoUtaAjb_^`6|2=iL#$?fUF3Ogp z5Z-67@thc)J9(~?T{eg3x4cp5LW-=F%Wbn)K^GGjrYGu&v|t{-=lSuAq)OKBJdTpX z`ZI<3JEMcmY3oV8>1-wM!ug;kUp}a!*YBxPnC(U=zbM`{g)W9(VvDaVW1`h`4_QL= z@U&_kC!4IuGcenc+~A-00y^PAkkT--5yN(z+cazf}M9R57MLuN#dVjIZ9c zzR?YD<%13NltSAU^Iu2Wv$u+rO)dW#U?(r|G3s`()x~UfhsI8%`(sP4*u)k+RI+1{ z9s!F_B3T^9In*8=AOP-5*%W4;AIOs}$Yn{YC>%aKg zepy?(K)$@mxdEFN`!L#ZIC@Z9`mEEl*Xlnwd467~EW&)PdF&oEX80>jN$an`c)^@E z%wMJ}Cwk@pFpXsrE1_Sa#Xfg+_7*W{?+{Y>V>46WWf3C-SHx~YR{1{r?w1iJE@c5R zsLF5HdKwd1hu2f3>86sQ)U8)--eirg`AIL^gr>Vn-}Oe{CXCe+&l9G*Uvc0_=}sqn z;F%{hq~%O9U-f2f=x<@AB#R`?BJ{PApIu%*(X#WJd(JeX-)F*2O#Imyk*cbTTQL`I zqxnAkjNRKiW2)359;N%23KDA=M-#t|RuPgq9Y;gP zK^R~sP*_6;@^RCzJwlUaHmlN}u|yw4whC2b>c5{bA?J}M!b*qI#S+dRiscgD9zUMu!1H*u-c)RQd+=*Jszn zOcz9{!=*fot|)46u#Z`Sx?fzc*GroQwuV)AHzp;>{px}ely`7Kx+`!_Om%nF2M>&w z^)pLsrA&FMhQC!nJ%^H)RS|NrkUmy2141sK8`@x(0YPHHa2Aiqv9vTL0O7&=3PZ0eFW)oA(bzH|h31Nic^Eb9hNWlr_hYbnNgy{X zmxi+R+)%;}*-$DNQQjV|a~Ylc_8JazAG97n+kpm?RJE@&qL_i-Z}Gt%M~!|cuU=>a z40TFOi+qg{_*jWhb$S>Rh?;HCDTK~-{e8OR7}j34()s`o)$lE#btm93*Ez^eZD>Yv z{iP~Ro!QgvdIipZc4t#9me0Hu&G`Ct?}gIY0i%F>YJxuTv}#vWN&!S~sy`rRj0sk+ z4IJJHk0*v{u59|1dmVjVe@j0aTn^Pmeczw8kADA9O&fGun;VRWh|*4RwYs3D}2?UvKky;;0R9xpw`|-P73bKq!o; zZ0pT7-h8m>MBO&wCOZc5dsFp(KF_4dB2q=^)g#Zi$(n-$%yGnpZ~Gly?&Ikk9rxH< zdmHF(YeA?0Db#TI#e44`LJQ^GZIl+8x+}H;5FnVt_YrA+U;K>&vftZM(M7e35{v`+}gUe zx21Sg;6$1CQ2!%?(r^u$SJRe3iC|Pw&oe=FO5HdwR|2AsnD34{xl@M$w}eskRs1lI zR$;AaU2|j4`K=0~WikcY{;VdRMr1|pkLp{{-*=PBmX4G6ch!*CY){YPOL+n5k|;&jA?Hkp-n7NYjcK*yO|QiFa9x)t&uaD~8%Q!VoTIX?xk zX`RSPTg`MXSH?~wz?qM=Q?hki&OhtxuXTSTfN&m$px+%ad zZq;JUm`P}*lKW7_XSG*#UCeQii%!qRnFW*1E?Rn9J;)|Tp4Vu$jX$dVtgeE5>Y2`D zD(t`s^$|L<5Ez${5Hq*vncuMSoXJ?6*IirfgD4u;yk#y0JAqP%Su2&|PI$AgVMyj) zf_5IUTk<&hUdlP+Q=evWkES_L@Zn@w1IOE;o350{6N&-pr@e0u`6v(IJlP}awTd0a zWS~U~D&I=9L~o^UdADccAbhIa4`I=j1f};YT(AJGZ}XD%v$sOiAc${8X*K(Xg(pEqJZUzZ`igKGja3s9}KnRGgd+t-w;(s*xEsL8Raw z?ZB0dACWIypWb9H(xpbE&UE4)ZNL$0eEn3MF))9%a1iGi+=d8vCvxjxOHb!-qp(Ju zk;{H&=8=y9Lt(|q(_SS+ajqT?&DBHBbGB45v+<{0YG0QJ`VSw)J+`wg?H~TtxidOC zy0Ea&+e@d>sNW_h4HlC2Zrs0Uru@TTVRhAGd47I-req)7a?)>5S*d2cc^FDhKk+B1 zU}5_iv32xe*L*pY-s%_SJBzMiKNy*LW;+>lx;$%|fz{gDRy$CcU!HpUhs(0GXbvIc z^3S`@+f{Z4jnmqnG~71uN7v1OXiE=@7dSZn>>okY%*4AmFKr-W{;BB&0{@j#6PN5pa!*14`zkkAnYIITtKV*Jof(bBORYOU*)JYpe!LPnCVv`zJ0zG&2<@)yti~byCjk!nT1G!e$AwqpxYD z4u$EdRb(pk-e0Zv1B2wU)fJ`AGZ@+hB^0Zm4pGR|Lp9j9F8KaJPb0aDsDy5k zs6`u)v=p81=UrB(a4*=E+{BQ3G|Ah>y^k9pkkYYm8|`iI&ReLZ-3T>J@a#_0h?uC|Vu-~^z!Ptx5 zVl5FCHzA3;$a>dR7T))e_tN9HS>lMBFBAV}V>HHN6n#S<;--#=5I(@)sm@>ZG7fS2 z>zw(e@j%kSTK1~%^?oI)LK%m?wD2eHvFfDxsF5)TA|Z$#)dpCp$6<7g+lX7}Kfxh| z&O$+TZ#_tvQl?nsP@RY1y<;aN1YL3d!H+(FM~0MZYTv?G4H0OM5$!}h>A%YHkyYPk zD++!G8L?(1N~=rwj@Gg3fJlK#I;Ad#`KL*WE$r)v$LJD@S%7vC`t{`94_u;A6@(K$ zS8lY9mgFnueV+0dZBP@xSMlz4Kw^d5E*IZISRD9x4I+O4cSZEB`h_o1@5Ii_2}m(L zWwg#KX+W`<2i_S;%kc9*0*6VLSNzK-Epno^i+=nFTqw1+?=-PZy_{D+yTk+ z=)MB=BTSE(TY_B!7xaYfthN%0j@HZ|W&}Tii9``$OeV%U;zlM19=!#C?}$gl4+4EQbsqdW)N#ce_hxhW@#RiIm3+hvnEm&PMZa`^SH=UJ(F`kGU+l;DWnuN zT@oirN`(9Ig#l%VZ!7Z=OY`Fiyy6BXxx7!d?C!@I2N$xOG<;ONXGij&SAYf@DlR9^ERKrz=FT~>}R_eT52qO#x z#J4vC%y*92IJ#!(6bssbyIe8^YZ9Ujs#x7A8p53 zxm768lnq-kyih@!6zT(J37s&6j3*%+vTPM??~kS*9P3HEW|&`W2ckTKh9mQnR?uiw zX@w%ZeuIs}mcZmG1DQmLG5j?{w^SJwUy%cEQ6scDNj9!Gzo-75!~Hz=QO;O%oJ1<_ znvtH6k&Y{`sNkndN$w_P?q*D-htBm1dso%Yu&GK2SK`h9aJl?`xb~g%teJrnw*+Ye z5Od#dIxvYHGw4YUy=n!)T%a|idaAf)#*<(Ct7X)BW9Hr;c{4@CQmr_17_OcBq>(u& zT{59voI}P(pT!ItUS0S5i~%DuL4x&)dhvXQVEP{-C{YD&5>)y+Nk)Hp(B}S~_aJ=M z*j;ea48(Hp$TS%hq1!|HNbYRE-bX48kq%qvd64|5m7#jEcR3KqD`8|}MUqyzc-LXe zHr^aAl!~)be+(gXiBIj%3|l+TIhNQ-WRvh(W!eRpqhX|Y>4ZBs1mvk_ z;8V3vApgc1bJT`1K~hS}$n>cG@~QjWmoUc+({?it<@bKN*8RTv8t-MvdFsJh)qc@O z5u~I;qM@GV8d}XqPU1?@n%|id`N+FHJRG-XGNe8kOZ$I&usYa?G@nUpChEDC?3FyF zn3sm5Z*zIGj-8euDy*RnaYPj#x&G2o%H>|Bl{`H)Z`MgJ?p{fd02H59_Q`#!+@15U z`4d^nrGHc)ViF|teAAvpt6u&YgD0|5DVfY^B}6@vNkt;&JNJ(wv(vqjKJFP%maIUQ zym8nY67r~ zWD%OdZzKX{R2Hf%7C28Us<)RgB1 + + +]> + +
+ +Icons + +&Mike.McBride; &Mike.McBride.mail; +&Jost.Schenck; &Jost.Schenck.mail; +&Anne-Marie.Mahfouf; &Anne-Marie.Mahfouf.mail; + + + +2021-04-09 +Plasma 5.20 + + +KDE +KControl +icon + + + +&plasma; comes with a full set of icons in several sizes. These icons +are being used all over &plasma;: the desktop, the panel, the &dolphin; file +manager, in every toolbar of every &plasma; application, etc. The icons +control module offers you very flexible ways of customizing the way &plasma; +handles icons. + + + +Here's a screenshot of the icon theme manager + + + + + + Customizing &plasma; icons + + + + + +In this module you can: + +install and choose icon themes +choose different icon sizes +remove icon themes + + +Please note that some of these settings may depend on +your selected icon theme. &plasma; comes with several icon themes by default, +Breeze, Breeze Dark and Oxygen. + +At the top is a preview of the current theme icons. Most default +installations will have only a few icon theme available, amongst them the &plasma; default +Breeze theme. You can also download more from the Internet from +https://store.kde.org/ or +install them from a local file. + + +On hover an animated preview shows a random selection, common mime types +and common folder icons. + + + +If you want to remove a theme, use the overlay +icon at the bottom right of the theme icon. To undo this action click on the + icon. +If you hit the Apply button the themes +selected for removal are actually deleted, so you cannot undo individual or all +deletions. + + + +Configure Icon Sizes + + +Select one of the components and use the slider to adapt the corresponding size. + + + +Configure Icon Sizes + + + + + + Configure Icon Sizes + + + + + + +Get New Icons... + + +You need to be connected to the Internet to use it. Clicking on this button will display a dialog where you can choose a new icon theme. Clicking on Install in the dialog will install the chosen icon theme and after you Close the installer your new theme is immediately available. + + + +Get New Icons + + + + + + Get New Icons + + + + + + +Install from File... + + +If you downloaded new themes from the internet, you can use this to browse to the +location of those newly downloaded themes. Clicking on this button will bring you the file dialog to point to the icon theme tarball you have on your disk. + +Clicking Open in this dialog will install the theme you pointed to and make it available in the theme list. + + + + + +
diff --git a/plasma/workspace/doc/kcontrol/icons/main.png b/plasma/workspace/doc/kcontrol/icons/main.png new file mode 100644 index 0000000000000000000000000000000000000000..66a14db7a0f37fcb0239730faa2cfefaf0a5c8e8 GIT binary patch literal 34341 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 + + +]> + +
+ + +Application Style + +&Mike.McBride; &Mike.McBride.mail; + + + +2021-04-09 +Plasma 5.20 + + +KDE +System Settings +style +application +widgets + + + +This module is used to configure how the individual widgets are +drawn by &plasma;. + +A Widget is a commonly-used +programmer's term for referring to User Interface elements such as +buttons, menus, and scroll bars. You can think of them as the +fundamental pieces that are assembled to make your +application. + +You can configure how the widgets are drawn with this module, +but to change the color of the widgets, you should refer to the +section entitled Colors. + +The top grid, labeled Application Style +contains a list of the previews of the pre-defined styles. +Each style has a name, and a brief description is shown when you +hover its item with the mouse pointer. + +If you want to configure a &plasma; theme, use the Configure Style... overlay +icon at the bottom right of the widget style icon in the grid. + +If implemented, it is possible to change almost every aspect of the style: center tabbar tabs, draw toolbar item separators, draw focus indicator in lists, draw slider tick marks, enable extended resize handlers, define keyboard accelerators visibility, windows' drag mode, use frames for the better accessibility, use buttons on the scrollbars, use transparency in the menus. + +The general configuration can be made by pressing the Configure Icons and Toolbars button below the grid. This button opens a pane to select further settings. + + + +Show icons: On buttons + +If this option is selected, action buttons (like OK and +Apply) will have a small icon located within them to act +as a visual reference. If this option is not selected, then only text +will appear on the button. + + + + +Show icons: In menus + +If this option is selected, &kde; applications will show small icons alongside +most menu items. If this option is not selected, then only text +will appear in the menus. Changes to the visibility of menu icons will only affect newly started +applications. + + + + +Main toolbar label, Secondary toolbar label + +These drop down boxes let you determine where on the button in both toolbars the +text name of the button will appear as the default. +If None is selected, then there is no text on the toolbar buttons. +If Text only is selected, then the button's icon is replaced with a text name of +the button. If Beside icons is selected, then the name of the button +will be placed to the right of the icon. +If Below icon is selected, the default will be to have the text +of the button below the icon. + +These options only specify the default location. +Each application can override the settings used in this panel. + + + + + +The Configure GNOME/GTK Application Style... button can be used to open the configuration tab where you can choose GTK theme, install GTK theme from file or Get New GNOME/GTK Application Styles from the Internet. + + +
diff --git a/plasma/workspace/doc/kcontrol/notifications/CMakeLists.txt b/plasma/workspace/doc/kcontrol/notifications/CMakeLists.txt new file mode 100644 index 0000000000..b347972690 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/notifications/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/notifications) diff --git a/plasma/workspace/doc/kcontrol/notifications/index.docbook b/plasma/workspace/doc/kcontrol/notifications/index.docbook new file mode 100644 index 0000000000..b533dd9323 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/notifications/index.docbook @@ -0,0 +1,209 @@ + + + +]> + +
+System Notification Settings + + + +&Mike.McBride; &Mike.McBride.mail; +Kai Uwe Broulik kde@broulik.de + + + +2021-04-16 +Plasma 5.21 + + +KDE +Systemsettings +system notification +notification + + + + +System Notification Settings + +&plasma;, like all applications, needs to inform the user when a +problem occurs, a task is completed, or something has happened. &plasma; +uses a set of System Notifications to keep the user +informed on what is happening. + +Using this module, you can determine what &plasma; does to communicate +each event. + + +Notification Settings + + +Do Not Disturb mode + + This mode disables all visual and most audible notifications to let you focus on your current task. Settings in this section let you configure under which circumstances &plasma; automatically enables do not disturb mode. + + + + + Enable when screens are mirrored + Automatically enable do not disturb mode when you mirror your screens, for example during a presentation. + + + + Enable while screen sharing + Automatically enable do not disturb mode when you share your screens, for example during an online lecture. + + + + Show critical notifications + Whether to show critical notifications, such as your battery is almost empty, to show even when in do not disturb mode. + + + + Toggle with: + A global shortcut you can press to enable and disable do not disturb mode anytime. + + + + + + + + Filters + + + + + Critical notifications: Always keep on top + Keep critical notifications, such as your battery is almost empty, always on top. This ensures they will also be visible while watching a fullscreen video or giving a presentation. + + + + Normal notifications: Always keep on top + Keep normal notifications, such as messages, always on top. This ensures they will also be visible while watching a fullscreen video or giving a presentation. + + + + Low priority notifications: Show popup and Show in history + Whether low priority notifications, such as track changes in your media player, will be shown as popups or in the history, respectively. + + + + + + + Behavior + + + + + Popup: Show near notification icon + Show notification popups close to where your notification icon is located in your panel. + + + + Choose Custom Position... + Lets you choose a fixed screen corner where notification popups will be positioned. + + + + Hide after: + After how many seconds the notification popup will automatically disappear. You can choose anywhere between 1 and 120 seconds. + + + + + + + Application Progress and Badges + + + Application progress + + Options in this section control how application progress, such as copying or downloading a file, is presented. + + + + + Show in task manager + Colorize the window in the panel based on the progress. + + + + Show in notifications + Show a notification popup during the progress. + + + + Keep popup open during progress + Whether the popup should remain visible for the entire duration of the progress or automatically hide. It will always be shown again when the task finishes or fails. + + + + + + + Notification badges + + + + + Show in task manager + Let applications show badges, such as an unread message count, in the panel. + + + + + + + + + + +Application Settings + +Clicking the Configure... button at the end of the list opens the application settings page which lets you configure notification behavior on a per-application and per-service basis. + +Use the sidebar on the left to choose an entry from the Applications or System Services category. + +General warning popups as well as startup and shutdown sounds are located under the Plasma Workspace service. + +Applications that do not provide proper identification, such as shell scripts, can be configured using the Other Applications entry. + + + + + Show popups + Whether this application may show popup notifications. + + + + Show in do not disturb mode + Whether this application may show popup notifications even when in do not disturb mode. + + + + Show in history + Whether this application's notifications will be kept in the notification history. + + + + Show notification badges + Whether this application may show badges, such as an unread message count, in the panel. + + + + Configure Events... + For &kde; applications you can also configure each notification individually. + + + + + + + + +
diff --git a/plasma/workspace/doc/kcontrol/screenlocker/CMakeLists.txt b/plasma/workspace/doc/kcontrol/screenlocker/CMakeLists.txt new file mode 100644 index 0000000000..38aca9d7ce --- /dev/null +++ b/plasma/workspace/doc/kcontrol/screenlocker/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/screenlocker) diff --git a/plasma/workspace/doc/kcontrol/screenlocker/index.docbook b/plasma/workspace/doc/kcontrol/screenlocker/index.docbook new file mode 100644 index 0000000000..58eb4ce75d --- /dev/null +++ b/plasma/workspace/doc/kcontrol/screenlocker/index.docbook @@ -0,0 +1,53 @@ + + + +]> + +
+ +Screen Locking + +&Mike.McBride; &Mike.McBride.mail; + + + +2020-07-02 +Plasma 5.20 + + +KDE +systemsettings +screenlocker +screen locker + + +Using this module, you can determine +how much time must pass before the screen locker is activated, configure the grace period for unlocking the screen without password, and choose the appearance of your screen locker. + +At the top is a check box to have the screen locker Lock screen automatically, +and a spin box which determines the period of inactivity before the screen locker should be started. +You can enter any positive number of minutes in this box. + +The After waking from sleep check box can be used to switch locking the desktop screen after the system suspension on and off. + +Below that is a spinbox labeled Allow unlocking without password for. When you press a key or click a mouse button to end the screen locker before the time in the spinbox and return to your +work, you must not enter a password. The password used is the same +password you used to login to your machine. + +Locking the screen manually causes the password protection to engage immediately. + +You can define a shortcut to lock the screen using the Keyboard shortcut field. + +The default shortcut &Ctrl;&Alt;L provides a quick +way to lock the screen manually. + +If you would like to change the background in locked status press the Configure button besides the Appearance label. Then you can change the Wallpaper type. + +You can select a Plain Color as wallpaper type. + +Alternatively use a single image or a slideshow with images from a folder +or load new wallpapers from the Internet. + +
diff --git a/plasma/workspace/doc/kcontrol/translations/CMakeLists.txt b/plasma/workspace/doc/kcontrol/translations/CMakeLists.txt new file mode 100644 index 0000000000..f950fdb3b2 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/translations/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/translations) diff --git a/plasma/workspace/doc/kcontrol/translations/go-top.png b/plasma/workspace/doc/kcontrol/translations/go-top.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc84f067807f8fb553b5c086d51bc0e9739379c GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@I3?$8F6>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5ln@Q@332`Z|G%=D=82Oh z%`B}?pE-N%_=%jn0%McIAf1d!-tI089jvk*Kn`btM`SSr1K&#!X5=$=k^u^`mw5WR zvOnhF<2Z zp@UmEtjW`Yo#CsKio+`B1y(mi4phw)+VCRP;YBOwfh7y86!W|kPKBRfV31qKBW|h6 z{~KtwYKdz^NlIc#s#S7PDv)9@GBC8%HL%b%G!8Maure{UGBnXPFt9Q(@KczOj-nwq zKP5A*61N8XT8-O44U!-mg7ec#$`gxH8OqDc^)mCai<1)zQuXqS(r3T3kpe1W@O1Ta JS?83{1ON%Ocpm@& literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/kcontrol/translations/index.docbook b/plasma/workspace/doc/kcontrol/translations/index.docbook new file mode 100644 index 0000000000..456c25c813 --- /dev/null +++ b/plasma/workspace/doc/kcontrol/translations/index.docbook @@ -0,0 +1,81 @@ + + + +]> + +
+ +Language + +&Mike.McBride; &Mike.McBride.mail; +&Krishna.Tateneni; &Krishna.Tateneni.mail; + + + + 2021-04-09 + Plasma 5.20 + + + KDE + Systemsettings + locale + country + language + translation + Language + + + + +On this page you can set your preferred languages for the &plasma; Workspace and +Applications to be displayed in. + + + +The &plasma; Workspace and &kde; Applications are written in American English and are +translated into many different languages by teams of volunteers. These +translations need to be installed first before you can choose to use them. + +Ensure that you have installed the &plasma; language packages or translations for the +languages you want to use. +As &plasma; is build upon the &Qt; libraries, you need the &Qt; translations for the selected +languages as well to have a fully localized &GUI;. + + +The list shows the localized language names that will +be used when displaying the &plasma; Workspace and Applications. Because not all +of the &plasma; Workspace and Applications may be translated into every language +&plasma; will try to find suitable translations for you by working down the +list until it finds a translation. If +none of your preferred languages have a required translation then the original +American English will be used. + + + +You can add a language to the main list by clicking the Add languages... +button. +The localized language names of &systemsettings; translations +installed and available on your system are displayed. If the language you want to use is +not shown in this list then you will need to install it using the usual method +for your system. +Select one or more languages in the list and click Add. + +You can remove a language from the main list by selecting it and then clicking +on the + icon. You can change the order of preference in the +list by selecting a language and clicking on the + + icon. + + + + +Language and Formats are independent settings. Changing a language does +not automatically change the settings for numbers, +currency &etc; to the corresponding country or region. + + + +
diff --git a/plasma/workspace/doc/kcontrol/translations/list-remove.png b/plasma/workspace/doc/kcontrol/translations/list-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..4980ce171015b938a2f8bfd83d057dce99afe030 GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@O3?%!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8Uxs6XN>+|9@pQ&38); z`hg;hN#5=*3>~bp9zYIffk$L90|Vbn5N6~vc9H=KvX^-Jy0Sm!;N{Q|_iNU21qumz zx;Tb#Tu)9=5Hk4Ro}k3Y5Xr~H%2L?c4wO_aag8WRNi0dVN-jzTQVd20hL*Yp7P^MU zAqEy!CI(hU#@Yr3Rt5&IP8~8q(U6;;l9^VCTf^+>RUJSLk{}y`^V3So6N^$A%FE03 gGV`*FlM@S4_413-XTP(N0xDwgboFyt=akR{0MX%B+5i9m literal 0 HcmV?d00001 diff --git a/plasma/workspace/doc/klipper/CMakeLists.txt b/plasma/workspace/doc/klipper/CMakeLists.txt new file mode 100644 index 0000000000..57e3088c77 --- /dev/null +++ b/plasma/workspace/doc/klipper/CMakeLists.txt @@ -0,0 +1,4 @@ +########### install files ############### +# +# +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR klipper) diff --git a/plasma/workspace/doc/klipper/index.docbook b/plasma/workspace/doc/klipper/index.docbook new file mode 100644 index 0000000000..c80ae4a6ed --- /dev/null +++ b/plasma/workspace/doc/klipper/index.docbook @@ -0,0 +1,475 @@ + + + + + +]> + + + +The &klipper; Handbook + + +&Philip.Rodrigues; &Philip.Rodrigues.mail; + + +&Carsten.Pfeiffer; &Carsten.Pfeiffer.mail; + + + + + + + + +2000-2003 +&Philip.Rodrigues; + + +&FDLNotice; + +2021-04-17 +Plasma 5.20 + + +&klipper; is the &plasma; clipboard cut & paste utility. + + + +KDE +Klipper +kdebase +clipboard + + + + +Introduction +&klipper; is the &plasma; clipboard utility. It stores clipboard +history, and allows you to link clipboard contents to application +actions. Please report any problems or feature requests to KDEs bugzilla. + + + + +Using &klipper; + + +Basic Usage +You can use &klipper; in the systemtray either as &plasma; widget or classic application. +Both share the same functionality but have a different &GUI; and show the last item of the history +when hovering the &klipper; icon in the systemtray: + + + +The &klipper; icon. + + + + + +The &klipper; icon + + + + +The &klipper; widget is hidden if the clipboard is empty. + +To display the clipboard history, click on the &klipper; icon in +the systemtray. + + +&klipper; Widget + +The &klipper; Widget. + + + + + +The &klipper; Widget + + + + +Hover an entry with the mouse pointer and four icons appear which enable you to +invoke an action, show the barcode, edit the content or remove the entry from the history. +Use &spectacle; to capture the barcodes and save them. + + +You can search through the clipboard history by opening it +(click on &klipper;) and typing your query. The results are updated as +you type. To clear the clipboard history click on the icon at the right of +the search field. + + +The Configure Clipboard... action in the context menu opens the +settings dialog. + + + + + + +Actions + +&klipper; can perform actions on the contents of the clipboard, +based on whether they match a particular regular expression. For +example, any clipboard contents starting with http:// can +be passed to Firefox or &konqueror; as &URL;s to open. In addition, if the +contents matches a path, similar actions can be performed according to the file's +type. ⪚, if the path to a PDF file is copied to the clipboard, the file can be +viewed in &okular;. + +To use this feature, just select a &URL; or path. +If there is a matching regular expression in &klipper;'s +list, a menu will appear showing you the programs for your selection. +Use the mouse or cursor keys to select a program, and &klipper; will run +this program, opening the address pointed to by the +selection. + +If you do not want to perform any actions on the clipboard +contents, select Disable This Popup on the pop-up menu +to return to what you were doing before. If you leave the menu, it will +disappear, leaving you to continue your work. You can change the time +that the menu remains for in the settings +dialog, with the option Timeout for action popups +under the General page. You can separately disable the +file path part using the option Enable MIME-based actions under +the Actions page. + +Actions can be disabled completely by pressing +the shortcut &Ctrl;&Alt;X. + +Clipboard contents which match a regular expression can also be +edited before performing an action on them. Select Edit +contents... at the right of the clipboard entry, and you can +change the clipboard contents in the dialog which appears, before +clicking the OK button to run the appropriate +action. + +Pressing &Ctrl;&Alt;R shows the pop-up +menu to repeat the last action which &klipper; performed. + + + + +Clipboard/Selection Behavior + + +General + +&klipper; can be used to set the behavior of the clipboard and selection in +&plasma;. + + +The &X-Window; uses two separate clipboard buffers: the +selection and the clipboard. Text is +placed in the selection buffer by simply selecting it, and can be +pasted with the middle mouse button. To place text in +the clipboard buffer, select it and press +&Ctrl;X or +&Ctrl;C . Text from the +clipboard buffer is pasted using &Ctrl;V + or by selecting Paste +in a context menu. + + + + + +Changing Clipboard/Selection Behavior + +In order to change clipboard/selection behavior, select +Configure Clipboard... from the &klipper; context menu, +and in the dialog box that appears, select the +General page. Unchecking Synchronize contents of the clipboard and the +selection makes the clipboard and selection function as completely +separate buffers as described above. With this option set, the option +Ignore selection will prevent &klipper; from +including the contents of the selection in its clipboard history and from +performing actions on the contents of the selection. Selecting +Synchronize contents of the clipboard and the selection +causes the clipboard and selection buffers to always be the same, meaning that +text in the selection can be pasted with either the +middle mouse button or the key combination &Ctrl;V +, and similarly for text in the clipboard buffer. + + + + + + + + + + + + +Configuring &klipper; + + +General Options + + + + +Save clipboard contents on +exit If this option is on, the clipboard +history will be saved when &klipper; exits, allowing you to use it next time +&klipper; starts. + + + + +Prevent empty clipboard +If selected, the clipboard will never be empty: &klipper; will +insert the most recent item from the clipboard history into the clipboard +instead of allowing it to be empty. + + + +Ignore images +When an area of the screen is selected with mouse or keyboard, +this is called the selection. If this option is selected, only text +selections are stored in the history, while images and other selections are not. + + + + + +Ignore selection +Sets the clipboard mode. This option will prevent &klipper; from +including the contents of the selection in its clipboard history and from +performing actions on the contents of the selection. See . + + + + + +Text selection only +When an area of the screen is selected with mouse or keyboard, this is +called the selection. If this option is selected, only text selections +are stored in the history, while images and other selections are not. +See . + + + + + +Synchronize contents of the clipboard and the +selection +Sets the clipboard mode. +When an area of the screen is selected with mouse or keyboard, this is +called the selection. If this option is selected, the selection and the +clipboard is kept the same, so that anything in the selection is immediately +available for pasting elsewhere using any method, including the traditional +middle mouse button. Otherwise, the selection is recorded in the clipboard +history, but the selection can only be pasted using the middle mouse button. +Also see the Ignore selection option. +See . + + + + + +Timeout for action popups +Set the time that a popup menu will remain for if you do +nothing with it. + + +Clipboard history +size +Sets the number of items that are stored +in the clipboard history. + + + + + + + +Actions Options + + + +Replay actions on an item selected from +history +If this is switched on, selecting an item from the history +causes &klipper; to display the actions pop-up on that item, if +appropriate. + + + + +Remove white space when executing actions +If selected, any white space (spaces, tabs, &etc;) at the +beginning and end of the clipboard contents will be removed before passing the +clipboard contents to an application. This is useful, for example, if the +clipboard contains a &URL; with spaces which, if opened by a web browser, would +cause an error. + + + + +Enable MIME-based actions +If selected, in addition to the actions you defined +a list of applications for the detected MIME type will appear in the popup menu. + + + + +Editing Expressions/Actions +On the Actions page, double-click +the regular expression or action that you want to edit or select it and +press the Edit Action button. A dialog will appear in which the +expression text can be edited as you wish. + + + +Adding Expressions/Actions +Click the Add Action... button +to add a regular expression for &klipper; to match. &klipper; uses +&Qt;'s QRegularExpression, which uses PCRE (Perl +Compatible Regular Expressions). +You can add a description of the regular expression type (⪚ +HTTP URL) by left clicking in the +Description column. + +You can find detailed information about the use of +QRegularExpression regular expressions in the Qt upstream +documentation. + +Edit the regular expression as described above. To add a command +to execute, click Add Command and edit the command +in an in-place text editing box. Double-clicking on a command allows you to edit it. + +Note that %s in the command line is replaced with +the clipboard contents, ⪚ if your command definition is +kwrite %s and +your clipboard contents are /home/phil/textfile, +the command kwrite +/home/phil/textfile will be run. To +include %s in the command line, escape it with a +backslash, as so: \%s. + + +Advanced +Brings up the Disable Actions for windows of type +WM_CLASS dialog. +Some programs, such as &konqueror;, +use the clipboard internally. If you get unwanted &klipper; pop-ups all the time +when using a certain application, do the following: + + +Open the application. +From a terminal, run xprop +| grep WM_CLASS +and then click on the window of the application you are +running. +The first string after the equals sign is the one to +enter. + + +Once the WM_CLASS is added, no more actions will be generated for +windows of that application. + + + + + + + +Shortcuts Options + +The shortcuts page allows you to change the keyboard shortcuts +which are used to access &klipper; functions. You can change the +shortcut to one of three things: + + + +None +The selected action cannot be accessed directly from the +keyboard + + +Default +The selected action uses &klipper;'s default key. These are the +shortcuts referred to in this manual. + + +Custom +The selected action is assigned to the keys you choose. +To choose a custom key for the action you have selected, click on +Custom and then None. +Now type the desired key combination on your keyboard, as in any &kde; application. + + + + +If you define a shortcut for Open Klipper at Mouse Position +pressing this shortcut will open the &klipper; popup menu at the position of +the mouse cursor, instead of their default position (in the &plasma; Panel). +Useful if you use the mouse more than the keyboard. + + + + + +Credits and License + + +&klipper; + + +Program copyright 1998 &Andrew.Stanley-Jones; asj@cban.com + + +Program copyright 1998-2000 &Carsten.Pfeiffer; &Carsten.Pfeiffer.mail; + +Currently maintained by Esben Mose Hansen. See http://mosehansen.dk/about +for contact details. + + + +Documentation copyright 2000-2003, 2005 &Philip.Rodrigues; +&Philip.Rodrigues.mail; + + +&underFDL; +&underGPL; + + + + +&documentation.index; + + + + + + + + + + + + + diff --git a/plasma/workspace/doc/klipper/klipper-widget.png b/plasma/workspace/doc/klipper/klipper-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..bddbad2d9e3cdd31adf270127eb8a4db4f993167 GIT binary patch literal 10082 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 Q^GhDkP|;DYP_zyGFSQW`=l}o! literal 0 HcmV?d00001 diff --git a/plasma/workspace/freespacenotifier/CMakeLists.txt b/plasma/workspace/freespacenotifier/CMakeLists.txt new file mode 100644 index 0000000000..39b5e36b88 --- /dev/null +++ b/plasma/workspace/freespacenotifier/CMakeLists.txt @@ -0,0 +1,26 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"freespacenotifier\") + +set(kded_freespacenotifier_SRCS freespacenotifier.cpp module.cpp) + +ki18n_wrap_ui(kded_freespacenotifier_SRCS freespacenotifier_prefs_base.ui) + +qt_add_dbus_interface(kded_freespacenotifier_SRCS ${KDED_DBUS_INTERFACE} kded_interface) + +kconfig_add_kcfg_files(kded_freespacenotifier_SRCS settings.kcfgc) + +kcoreaddons_add_plugin(freespacenotifier SOURCES ${kded_freespacenotifier_SRCS} INSTALL_NAMESPACE "kf5/kded") + +target_link_libraries(freespacenotifier + KF5::ConfigWidgets + KF5::DBusAddons + KF5::I18n + KF5::KIOCore + KF5::KIOGui + KF5::Notifications + KF5::Service +) + +########### install files ############### + +install( FILES freespacenotifier.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR} ) +install( FILES freespacenotifier.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR} ) diff --git a/plasma/workspace/freespacenotifier/Messages.sh b/plasma/workspace/freespacenotifier/Messages.sh new file mode 100644 index 0000000000..858cfc3b3e --- /dev/null +++ b/plasma/workspace/freespacenotifier/Messages.sh @@ -0,0 +1,3 @@ +#! /bin/sh +$EXTRACTRC *.ui *.kcfg >> rc.cpp +$XGETTEXT *.cpp -o $podir/freespacenotifier.pot diff --git a/plasma/workspace/freespacenotifier/README b/plasma/workspace/freespacenotifier/README new file mode 100644 index 0000000000..208f817d6e --- /dev/null +++ b/plasma/workspace/freespacenotifier/README @@ -0,0 +1,3 @@ +This is a small KDED module that monitors free disk space on the home dir +partition and shows a warning dialog when it runs too low, +with a configurable limit and the possibility to postpone. diff --git a/plasma/workspace/freespacenotifier/freespacenotifier.cpp b/plasma/workspace/freespacenotifier/freespacenotifier.cpp new file mode 100644 index 0000000000..4688f00dc7 --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier.cpp @@ -0,0 +1,154 @@ +/* + SPDX-FileCopyrightText: 2006 Lukas Tinkl + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Ivo Anjo + SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "freespacenotifier.h" + +#include +#include + +#include +#include +#include + +#include + +#include "settings.h" + +FreeSpaceNotifier::FreeSpaceNotifier(const QString &path, const KLocalizedString ¬ificationText, QObject *parent) + : QObject(parent) + , m_path(path) + , m_notificationText(notificationText) +{ + connect(&m_timer, &QTimer::timeout, this, &FreeSpaceNotifier::checkFreeDiskSpace); + m_timer.start(std::chrono::minutes(1)); +} + +FreeSpaceNotifier::~FreeSpaceNotifier() +{ + if (m_notification) { + m_notification->close(); + } +} + +void FreeSpaceNotifier::checkFreeDiskSpace() +{ + if (!FreeSpaceNotifierSettings::enableNotification()) { + // do nothing if notifying is disabled; + // also stop the timer that probably got us here in the first place + m_timer.stop(); + return; + } + + auto *job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(m_path)); + connect(job, &KIO::FileSystemFreeSpaceJob::result, this, [this](KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { + if (job->error()) { + return; + } + + const int limit = FreeSpaceNotifierSettings::minimumSpace(); // MiB + const qint64 avail = available / (1024 * 1024); // to MiB + + if (avail >= limit) { + if (m_notification) { + m_notification->close(); + } + return; + } + + const int availPercent = int(100 * available / size); + const QString text = m_notificationText.subs(avail).subs(availPercent).toString(); + + // Make sure the notification text is always up to date whenever we checked free space + if (m_notification) { + m_notification->setText(text); + } + + // User freed some space, warn if it goes low again + if (m_lastAvail > -1 && avail > m_lastAvail) { + m_lastAvail = avail; + return; + } + + // Always warn the first time or when available space dropped to half of the previous time + const bool warn = (m_lastAvail < 0 || avail < m_lastAvail / 2); + if (!warn) { + return; + } + + m_lastAvail = avail; + + if (!m_notification) { + m_notification = new KNotification(QStringLiteral("freespacenotif")); + m_notification->setComponentName(QStringLiteral("freespacenotifier")); + m_notification->setText(text); + + QStringList actions = {i18n("Configure Warning…")}; + + auto filelight = filelightService(); + if (filelight) { + actions.prepend(i18n("Open in Filelight")); + } else { + // Do we really want the user opening Root in a file manager? + actions.prepend(i18n("Open in File Manager")); + } + + m_notification->setActions(actions); + + connect(m_notification, &KNotification::activated, this, [this](uint actionId) { + if (actionId == 1) { + exploreDrive(); + // TODO once we have "configure" action support in KNotification, wire it up instead of a button + } else if (actionId == 2) { + Q_EMIT configureRequested(); + } + }); + + connect(m_notification, &KNotification::closed, this, &FreeSpaceNotifier::onNotificationClosed); + m_notification->sendEvent(); + } + }); +} + +KService::Ptr FreeSpaceNotifier::filelightService() const +{ + return KService::serviceByDesktopName(QStringLiteral("org.kde.filelight")); +} + +void FreeSpaceNotifier::exploreDrive() +{ + auto service = filelightService(); + if (!service) { + auto *job = new KIO::OpenUrlJob({QUrl::fromLocalFile(m_path)}); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + job->start(); + return; + } + + auto *job = new KIO::ApplicationLauncherJob(service); + job->setUrls({QUrl::fromLocalFile(m_path)}); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + job->start(); +} + +void FreeSpaceNotifier::onNotificationClosed() +{ + // warn again if constantly below limit for too long + if (!m_lastAvailTimer) { + m_lastAvailTimer = new QTimer(this); + connect(m_lastAvailTimer, &QTimer::timeout, this, &FreeSpaceNotifier::resetLastAvailable); + } + m_lastAvailTimer->start(std::chrono::hours(1)); +} + +void FreeSpaceNotifier::resetLastAvailable() +{ + m_lastAvail = -1; + m_lastAvailTimer->deleteLater(); + m_lastAvailTimer = nullptr; +} diff --git a/plasma/workspace/freespacenotifier/freespacenotifier.h b/plasma/workspace/freespacenotifier/freespacenotifier.h new file mode 100644 index 0000000000..7a0ea7e219 --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2006 Lukas Tinkl + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Ivo Anjo + SPDX-FileCopyrightText: 2020 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include + +class KNotification; + +class FreeSpaceNotifier : public QObject +{ + Q_OBJECT + +public: + explicit FreeSpaceNotifier(const QString &path, const KLocalizedString ¬ificationText, QObject *parent = nullptr); + ~FreeSpaceNotifier() override; + +Q_SIGNALS: + void configureRequested(); + +private: + void checkFreeDiskSpace(); + void resetLastAvailable(); + + KService::Ptr filelightService() const; + void exploreDrive(); + void onNotificationClosed(); + + QString m_path; + KLocalizedString m_notificationText; + + QTimer m_timer; + QTimer *m_lastAvailTimer = nullptr; + QPointer m_notification; + qint64 m_lastAvail = -1; // used to suppress repeated warnings when available space hasn't changed +}; diff --git a/plasma/workspace/freespacenotifier/freespacenotifier.json b/plasma/workspace/freespacenotifier/freespacenotifier.json new file mode 100644 index 0000000000..71f556f106 --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier.json @@ -0,0 +1,103 @@ +{ + "KPlugin": { + "Description": "Warns when running out of space on your home folder", + "Description[ar]": "يحذّرك عندما تنفذ مساحة مجلد المنزل", + "Description[az]": "Ev qovluğunda boş disk sahəsi azaldıqda bu haqda xəbərdarlıq edir.", + "Description[ca]": "Avisa quan s'exhaureix l'espai de la carpeta personal", + "Description[cs]": "Varování při blížícím se nedostatku místa v domácí složce", + "Description[de]": "Gibt eine Warnung aus, wenn der freie Speicherplatz in Ihrem Persönlichen Ordner knapp wird", + "Description[en_GB]": "Warns when running out of space on your home folder", + "Description[es]": "Le avisa cuando se está quedando sin espacio en su carpeta personal", + "Description[eu]": "Abisua ematen du, etxeko karpetan lekurik gabe gelditzen ari zarenean", + "Description[fi]": "Varoittaa, kun kotikansiosta on tila loppumassa", + "Description[fr]": "Avertit lorsque l'espace vient à manquer dans votre dossier utilisateur.", + "Description[hu]": "Figyelmeztet, ha kezd elfogyni a szabad lemezterület a saját könyvtárában", + "Description[ia]": "Il avisa quando il termina le spatio sur tu dossier domo (initio)", + "Description[it]": "Avvisa quando lo spazio nella tua cartella Home si sta esaurendo", + "Description[ko]": "홈 폴더에 공간이 없을 때 알려 줍니다", + "Description[lt]": "Įspėja, kai namų aplanke baigiasi laisva vieta", + "Description[nl]": "Geeft een waarschuwing bij een lage stand van de vrije ruimte voor uw persoonlijke map", + "Description[nn]": "Varslar når det er lite plass i heimemappa", + "Description[pa]": "ਚੇਤਾਵਨੀ ਦਿਉ, ਜਦੋਂ ਤੁਹਾਡੇ ਘਰ ਫੋਲਡਰ ਉੱਤੇ ਥਾਂ ਖਤਮ ਹੋ ਰਹੀ ਹੋਵੇ", + "Description[pl]": "Ostrzega, kiedy zaczyna brakować miejsca w katalogu domowym", + "Description[pt_BR]": "Avisa quando ficar sem espaço na sua pasta pessoal", + "Description[ro]": "Atenționează când spațiul de stocare din dosarul acasă este pe cale să se termine", + "Description[ru]": "Предупреждает о нехватке дискового пространства в домашней папке", + "Description[sk]": "Upozorňuje, keď dochádza voľné miesto v domovskom priečinku", + "Description[sl]": "Opozori, ko v domači mapi primanjkuje prostora", + "Description[sv]": "Varnar när utrymmet i hemkatalogen håller på att ta slut", + "Description[ta]": "உங்கள் முகப்பு அடைவில் காலியிடம் குறைவாக இருந்தால் எச்சரிக்கும்", + "Description[tr]": "Ev dizininizde boş alan azaldığı zaman uyarır", + "Description[uk]": "Попереджає, якщо у домашній теці залишається мало місця", + "Description[vi]": "Cảnh báo khi sắp hết không gian cho thư mục nhà của bạn", + "Description[x-test]": "xxWarns when running out of space on your home folderxx", + "Description[zh_CN]": "在主目录磁盘空间不足时发出警告", + "Name": "Free Space Notifier", + "Name[ar]": "مُخطِر المساحة الحرّة", + "Name[ast]": "Avisador d'espaciu llibre", + "Name[az]": "Diskin boş sahəsinin izlənməsi", + "Name[bg]": "Уведомяване за свободно пространство", + "Name[bs]": "Izvještač o slobodnom prostoru", + "Name[ca@valencia]": "Notificador d'espai lliure", + "Name[ca]": "Notificador d'espai lliure", + "Name[cs]": "Informace o volném místě", + "Name[da]": "Bekendtgørelse om ledig plads", + "Name[de]": "Speicherplatzbenachrichtigung", + "Name[el]": "Ειδοποίηση ελεύθερου χώρου", + "Name[en_GB]": "Free Space Notifier", + "Name[es]": "Notificador de espacio libre", + "Name[et]": "Vaba ruumi teavitaja", + "Name[eu]": "Leku askearen jakinarazlea", + "Name[fi]": "Vapaan tilan ilmoitin", + "Name[fr]": "Notification d'espace libre", + "Name[ga]": "Fógróir Spáis Shaoir", + "Name[gl]": "Notificador de espazo libre", + "Name[he]": "Free Space Notifier", + "Name[hi]": "खाली स्थान सूचक", + "Name[hr]": "Glasnik slobodnog prostora", + "Name[hsb]": "Zdźělenka wo swobodnym rumje", + "Name[hu]": "Szabad hely értesítő", + "Name[ia]": "Notificator de spatio libere", + "Name[id]": "Penotifikasi Ruang Bebas", + "Name[is]": "Tilkynningar um laust pláss", + "Name[it]": "Notificatore dello spazio libero", + "Name[ja]": "空き領域の通知", + "Name[kk]": "KDE-нің бос орын туралы құлақтандыруы", + "Name[km]": "កម្មវិធី​ជូនដំណឹង​​ទំហំ​ទំនេរ", + "Name[kn]": "ಖಾಲಿ ಜಾಗ ಸೂಚಕ", + "Name[ko]": "남은 공간 알림이", + "Name[lt]": "Laisvos vietos pranešėjas", + "Name[lv]": "Brīvās vietas ziņotājs", + "Name[ml]": "ലഭ്യമായ സ്ഥലം അറിയിക്കുന്നതിനുള്ള സംവിധാനം", + "Name[mr]": "मोकळी जागा निदर्शक", + "Name[nb]": "Ledig plass-varsler", + "Name[nds]": "Freeruumbescheden", + "Name[nl]": "Vrije ruimte-melder", + "Name[nn]": "Varsel om lite ledig plass", + "Name[pa]": "ਖਾਲੀ ਥਾਂ ਨੋਟੀਫਾਇਰ", + "Name[pl]": "Powiadomienie o wolnym miejscu", + "Name[pt]": "Notificação de Espaço Livre", + "Name[pt_BR]": "Notificação de espaço livre", + "Name[ro]": "Notificare spațiu liber", + "Name[ru]": "Слежение за свободным местом на диске", + "Name[si]": "හිස් ඉඩ දැනුම් දෙන්නා", + "Name[sk]": "Monitor voľného miesta", + "Name[sl]": "Obvestilnik o še prostem prostoru", + "Name[sr@ijekavian]": "Извјештавач о слободном простору", + "Name[sr@ijekavianlatin]": "Izvještavač o slobodnom prostoru", + "Name[sr@latin]": "Izveštavač o slobodnom prostoru", + "Name[sr]": "Извештавач о слободном простору", + "Name[sv]": "Information om ledigt utrymme", + "Name[ta]": "காலியிட அறிவிப்பான்", + "Name[th]": "ตัวแจ้งพื้นที่ว่าง", + "Name[tr]": "Boş Alan Bildirici", + "Name[ug]": "بىكار بوشلۇق خەۋەرچىسى", + "Name[uk]": "Сповіщення про вільне місце", + "Name[vi]": "Trình thông báo không gian trống", + "Name[wa]": "Notifieu del plaece di libe", + "Name[x-test]": "xxFree Space Notifierxx", + "Name[zh_CN]": "空闲空间通知器", + "Name[zh_TW]": "剩餘空間通知" + }, + "X-KDE-Kded-autoload": true +} diff --git a/plasma/workspace/freespacenotifier/freespacenotifier.kcfg b/plasma/workspace/freespacenotifier/freespacenotifier.kcfg new file mode 100644 index 0000000000..51e02b3df2 --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier.kcfg @@ -0,0 +1,19 @@ + + + + + + + 200 + 1 + 100000 + + + + 1 + + + diff --git a/plasma/workspace/freespacenotifier/freespacenotifier.notifyrc b/plasma/workspace/freespacenotifier/freespacenotifier.notifyrc new file mode 100644 index 0000000000..8eda7a898d --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier.notifyrc @@ -0,0 +1,436 @@ +[Global] +IconName=drive-harddisk +Comment=KDE Free Space Notifier Daemon +Comment[ar]=عفريت مُخطِر كدي للمساحة الحرة +Comment[ast]=Degorriu del avisador d'espaciu llibre de KDE +Comment[az]=Diskin boş sahəsi haqqında bildirişlər xidməti +Comment[bg]=Демон на KDE за свободно пространство +Comment[bn]=কে.ডি.ই. ফ্রী স্পেস বিজ্ঞপ্তি ডিমন +Comment[bs]=KDE‑ov demon izveštavača o slobodnom prostoru +Comment[ca]=Dimoni de notificacions d'espai lliure del KDE +Comment[ca@valencia]=Dimoni de notificacions d'espai lliure de KDE +Comment[cs]=Upozorňovací démon volného místa KDE +Comment[da]=KDE dæmon til bekendtgørelse om ledig plads +Comment[de]=KDE-Dienst für Speicherplatzbenachrichtigung +Comment[el]=Δαίμονας ειδοποιήσεων ελευθέρου χώρου του KDE +Comment[en_GB]=KDE Free Space Notifier Dæmon +Comment[es]=Demonio de notificaciones de espacio libre de KDE +Comment[et]=KDE vaba ruumi teavitamise deemon +Comment[eu]=KDEren leku askea jakinarazteko daimona +Comment[fi]=KDE:n vapaan tilan ilmoitustaustaprosessi +Comment[fr]=Démon de notification d'espace libre de KDE +Comment[ga]=Fógróir Spáis Shaoir KDE +Comment[gl]=Servizo de KDE de notificación do espazo libre +Comment[he]=KDE Free Space Notifier Daemon +Comment[hi]=केडीई खाली स्थान सूचना डेमन +Comment[hr]=KDE-ov servis za obavještavanje o slobodnom prostoru +Comment[hsb]=KDE zdźělenski daemon za swobodny rum +Comment[hu]=KDE szabad hely értesítő szolgáltatás +Comment[ia]=Demone de notification de spatio libere de KDE +Comment[id]=Daemon Notifikasi RUang Kosong KDE +Comment[is]=KDE tilkynningaþjónn fyrir laust pláss +Comment[it]=Demone di notifica dello spazio libero di KDE +Comment[ja]=KDE 空き領域デーモン +Comment[kk]=KDE-нің бос орын туралы құлақтандыру қызметі +Comment[km]=ដេមិន​​ជូន​ដំណឹង​​ទំហំ​ទំនេរ​របស់ KDE +Comment[kn]=ಕೆಡಿಇ ಖಾಲಿ ಜಾಗ ಸೂಚನಾ ನೇಪಥಿಕ (ಡೀಮನ್) +Comment[ko]=KDE 남은 공간 알림 데몬 +Comment[lt]=KDE laisvos vietos pranešėjo tarnyba +Comment[lv]=KDE brīvās vietas paziņošanas dēmons +Comment[ml]=കെഡിഇയിലെ ലഭ്യമായ സ്ഥലം അറിയിയ്ക്കുന്നതിനുള്ള നിരന്തര പ്രവൃത്തി +Comment[mr]=केडीई मोकळी जागा निदर्शक डीमन +Comment[nb]=KDEs varslingsdaemon for ledig plass +Comment[nds]=KDE-Dämoon för Freeruumbescheden +Comment[nl]=KDE-daemon voor het melden van vrije ruimte +Comment[nn]=KDE-varsel om lite plass +Comment[pa]=KDE ਖਾਲੀ ਥਾਂ ਨੋਟੀਫਿਕੇਸ਼ਨ ਡੈਮਨ +Comment[pl]=Usługa powiadamiania o wolnym miejscu KDE +Comment[pt]=Servidor de Notificação de Espaço Livre do KDE +Comment[pt_BR]=Serviço de notificação de espaço livre do KDE +Comment[ro]=Demon de notificare KDE a spațiului liber +Comment[ru]=Служба уведомлений о свободном месте на диске +Comment[si]=KDE හිස් ඉඩ දැනුම්දීමේ ඩීමනය +Comment[sk]=Monitor voľného miesta KDE +Comment[sl]=KDE-jev obvestilnik o neporabljenem prostoru +Comment[sr]=КДЕ‑ов демон извештавача о слободном простору +Comment[sr@ijekavian]=КДЕ‑ов демон извјештавача о слободном простору +Comment[sr@ijekavianlatin]=KDE‑ov demon izvještavača o slobodnom prostoru +Comment[sr@latin]=KDE‑ov demon izveštavača o slobodnom prostoru +Comment[sv]=KDE:s demon för information om ledigt utrymme +Comment[ta]=KDE காலியிட அறிவிப்புக்கான பின்னணி சேவை +Comment[th]=ดีมอนการแจ้งพื้นที่ว่างของ KDE +Comment[tr]=KDE Boş Alan Bildirme Servisi +Comment[ug]=KDE بىكار بوشلۇق خەۋەرچى نازارەتچىسى +Comment[uk]=Фонова служба сповіщення про переповнення диска KDE +Comment[vi]=Trình nền thông báo không gian trống KDE +Comment[wa]=Démon KDE notifieu del plaece di libe +Comment[x-test]=xxKDE Free Space Notifier Daemonxx +Comment[zh_CN]=KDE 空闲空间通知守护程序 +Comment[zh_TW]=KDE 剩餘空間通知伺服程式 +Name=Low Disk Space +Name[ar]=مساحة القرص منخفضة +Name[az]=Bitməkdə olan boş disk sahəsi +Name[bg]=Дисковото пространство е твърде малко +Name[bn]=ডিস্ক-এ জায়গা কম +Name[bs]=Malo prostora na disku +Name[ca]=Espai escàs al disc +Name[ca@valencia]=Espai escàs al disc +Name[cs]=Málo místa na disku +Name[da]=Lav diskplads +Name[de]=Wenig Speicherplatz +Name[el]=Χαμηλή χωρητικότητα δίσκου +Name[en_GB]=Low Disk Space +Name[es]=Poco espacio en disco +Name[et]=Kettaruumi napib +Name[eu]=Leku gutxi diskoan +Name[fi]=Vähäinen levytila +Name[fr]=Espace disque faible +Name[ga]=Easpa Spáis ar an Diosca +Name[gl]=Pouco espazo no disco +Name[he]=שטח מועט פנוי בדיסק +Name[hi]=डिस्क जगह कम +Name[hr]=Malo prostora na disku +Name[hsb]=Mało ruma na tačeli +Name[hu]=Kevés a lemezterület +Name[ia]=Spatio de disco basse +Name[id]=Ruang Disk Rendah +Name[is]=Lítið diskpláss +Name[it]=Poco spazio su disco +Name[ja]=ディスクの空き領域が少ないです +Name[kk]=Дискіде орын тапшылығы +Name[km]=ទំហំ​ថាស​ទាប +Name[kn]=ಕಡಿಮೆ ಡಿಸ್ಕ್‌ ಜಾಗ +Name[ko]=디스크 공간 부족 +Name[lt]=Mažai vietos diske +Name[lv]=Maz diska vietas +Name[mai]=कम डिस्क स्थान +Name[ml]=ഡിസ്കില്‍ സഥലം കുറവാണു് +Name[mr]=डिस्कवर कमी जागा शिल्लक आहे +Name[nb]=Lite diskplass +Name[nds]=To minn free Ruum op de Fastplaat +Name[nl]=Schijf bijna vol +Name[nn]=Lite diskplass +Name[pa]=ਘੱਟ ਡਿਸਕ ਥਾਂ +Name[pl]=Mało miejsca na dysku +Name[pt]=Pouco Espaço em Disco +Name[pt_BR]=Pouco espaço em disco +Name[ro]=Spațiu pe disc scăzut +Name[ru]=Недостаточно места на диске +Name[si]=පහත් තැටි ඉඩ +Name[sk]=Nedostatok miesta na disku +Name[sl]=Prostora na disku je malo +Name[sr]=Мало простора на диску +Name[sr@ijekavian]=Мало простора на диску +Name[sr@ijekavianlatin]=Malo prostora na disku +Name[sr@latin]=Malo prostora na disku +Name[sv]=Ont om diskutrymme +Name[ta]=வட்டில் குறைவான காலியிடம் +Name[th]=พื้นที่ดิสก์เหลือน้อย +Name[tr]=Düşük Disk Alanı +Name[ug]=دىسكىدىكى بوشلۇق ئاز +Name[uk]=Замало місця на диску +Name[vi]=Không gian đĩa còn ít +Name[wa]=Pus bråmint d' plaece sol plake +Name[x-test]=xxLow Disk Spacexx +Name[zh_CN]=磁盘空间低 +Name[zh_TW]=磁碟空間過低 + +[Context/warningnot] +Name=Warning +Name[af]=Waarskuwing +Name[ar]=تحذير +Name[as]=সতৰ্কবাণী +Name[az]=Xəbərdarlıq +Name[be]=Папярэджанне +Name[be@latin]=Uvaha +Name[bg]=Предупреждение +Name[bn]=সতর্কবার্তা +Name[bn_IN]=সতর্কবার্তা +Name[bs]=Upozorenje +Name[ca]=Avís +Name[ca@valencia]=Avís +Name[cs]=Varování +Name[csb]=Bôczënk +Name[da]=Advarsel +Name[de]=Warnung +Name[el]=Προειδοποίηση +Name[en_GB]=Warning +Name[eo]=Averto +Name[es]=Aviso +Name[et]=Hoiatus +Name[eu]=Abisua +Name[fi]=Varoitus +Name[fr]=Avertissement +Name[fy]=Warskôging +Name[ga]=Rabhadh +Name[gl]=Aviso +Name[gu]=ચેતવણી +Name[he]=אזהרה +Name[hi]=चेतावनी +Name[hne]=चेतावनी +Name[hr]=Upozorenje +Name[hsb]=Kedźbu +Name[hu]=Figyelmeztetés +Name[ia]=Aviso +Name[id]=Peringatan +Name[is]=Aðvörun +Name[it]=Avviso +Name[ja]=警告 +Name[kk]=Ескерту +Name[km]=ការ​ព្រមាន +Name[kn]=ಎಚ್ಚರಿಕೆ +Name[ko]=경고 +Name[ku]=Hişyarî +Name[lt]=Įspėjimas +Name[lv]=Brīdinājums +Name[mai]=चेतावनी +Name[mk]=Предупредување +Name[ml]=മുന്നറിയിപ്പു് +Name[mr]=इशारा +Name[nb]=Advarsel +Name[nds]=Wohrschoen +Name[ne]=चेतावनी +Name[nl]=Waarschuwing +Name[nn]=Åtvaring +Name[oc]=Alèrta +Name[or]=ଚେତାବନୀ +Name[pa]=ਚੇਤਾਵਨੀ +Name[pl]=Ostrzeżenie +Name[pt]=Aviso +Name[pt_BR]=Aviso +Name[ro]=Atenționare +Name[ru]=Предупреждение +Name[se]=Várrehus +Name[si]=අවවාදය +Name[sk]=Varovanie +Name[sl]=Opozorilo +Name[sr]=Упозорење +Name[sr@ijekavian]=Упозорење +Name[sr@ijekavianlatin]=Upozorenje +Name[sr@latin]=Upozorenje +Name[sv]=Varning +Name[ta]=எச்சரிக்கை +Name[te]=హెచ్చరిక +Name[th]=คำเตือน +Name[tr]=Uyarı +Name[ug]=ئاگاھلاندۇرۇش +Name[uk]=Попередження +Name[uz]=Ogohnoma +Name[uz@cyrillic]=Огоҳнома +Name[vi]=Cảnh báo +Name[wa]=Adviertixhmint +Name[x-test]=xxWarningxx +Name[zh_CN]=警告 +Name[zh_TW]=警告 +Comment=Used for warning notifications +Comment[ar]=يُستخدَم لإخطارات التحذير +Comment[az]=Sistem bildirişləri üçün istifadə edilir +Comment[be@latin]=Dla aścierahalnych paviedamleńniaŭ +Comment[bg]=Използва се за предупреждения +Comment[bn]=সতর্কীকরণ বিজ্ঞপ্তির জন্য ব্যবহৃত হয় +Comment[bs]=Koristi se za obavještenja o upozorenjima +Comment[ca]=S'usa per a les notificacions d'avís +Comment[ca@valencia]=S'usa per a les notificacions d'avís +Comment[cs]=Použito pro varování a upozornění +Comment[da]=Bruges til advarselsbekendtgørelser +Comment[de]=Verwendet für Warnmeldungen +Comment[el]=Χρησιμοποιείται για τις προειδοποιήσεις +Comment[en_GB]=Used for warning notifications +Comment[eo]=Uzita por avertmesaĝoj +Comment[es]=Usado para notificaciones de advertencia +Comment[et]=Hoiatuste jaoks +Comment[eu]=Abisu-jakinarazpenetarako erabiltzen da +Comment[fi]=Käytetään varoitusilmoituksiin +Comment[fr]=Utilisé pour les notifications d'avertissements +Comment[fy]=Brûkt faor warskôging notifikaasjes +Comment[ga]=Úsáidte do rabhaidh +Comment[gl]=Emprégase para as notificacións de aviso +Comment[gu]=ચેતવણી નોંધણીઓ આપવા માટે વપરાય છે +Comment[he]=משמש להתרעות אזהרה +Comment[hi]=चेतावनियाँ देने के लिए उपयोग में लिया जाता है +Comment[hne]=चेतावनी सूचना बर उपयोग होथे +Comment[hr]=Korišten za upozoravajuće obavijesti +Comment[hsb]=Wužiwa so za warnowace skedźbnjenje +Comment[hu]=Kezelőprogram figyelmeztető üzenetekhez +Comment[ia]=Usate pro notificationes de avisos +Comment[id]=Digunakan untuk notifikasi peringatan +Comment[is]=Notað fyrir aðvaranir +Comment[it]=Usato per notifiche di avviso +Comment[ja]=警告の通知に使用 +Comment[kk]=Ескерту хабарларға қолданылады +Comment[km]=បានប្រើ​សម្រាប់​ការ​ជូន​ព្រមាន +Comment[kn]=ಎಚ್ಚರಿಕೆ ಸೂಚನೆಗಳಿಗೆ ಬಳಸಲಾಗುತ್ತದೆ +Comment[ko]=경고 알림에 사용됨 +Comment[lt]=Naudojama įspėjimo pranešimams +Comment[lv]=Izmanto brīdinājumu paziņojumiem +Comment[ml]=താക്കീത് അറിയിപ്പുകള്‍ക്കായി ഉപയോഗിക്കുന്നു +Comment[mr]=इशारा सूचना करिता वापरले जाते +Comment[nb]=Brukt til varselsmeldinger +Comment[nds]=Bi Wohrschoen bruukt +Comment[nl]=Wordt gebruikt voor waarschuwingsmeldingen +Comment[nn]=Er brukt til åtvaringsvarsel +Comment[or]=ଚେତାବନୀ ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ ପାଇଁ ବ୍ୟବହୃତ ହୋଇଥାଏ +Comment[pa]=ਚੇਤਾਵਨੀ ਨੋਟੀਫਿਕੇਸ਼ਨ ਲਈ ਵਰਤਿਆ ਜਾਂਦਾ ਹੈ +Comment[pl]=Używane do ostrzeżeń +Comment[pt]=Usado para as notificações de aviso +Comment[pt_BR]=Usado para as notificações de aviso +Comment[ro]=Utilizat pentru notificări de avertizare +Comment[ru]=Используется для системных уведомлений +Comment[si]=අවවාද පණිවුඩ සඳහා යොදාගනී +Comment[sk]=Používa sa pre varovné upozornenia +Comment[sl]=Uporabljeno za obvestila z opozorili +Comment[sr]=Користи се за позорна обавештења +Comment[sr@ijekavian]=Користи се за позорна обавјештења +Comment[sr@ijekavianlatin]=Koristi se za pozorna obavještenja +Comment[sr@latin]=Koristi se za pozorna obaveštenja +Comment[sv]=Används för varningsmeddelanden +Comment[ta]=எச்சரிக்கும் அறிவிப்புகளுக்காக பயன்படுத்தப்படும் +Comment[te]=హెచ్చరిక నోటీసుల కొరకు వుపయోగించబడింది +Comment[th]=ใช้สำหรับการแจ้งเตือนให้ทราบ +Comment[tr]=Uyarı bildirimleri için kullanılır +Comment[ug]=ئاگاھلاندۇرۇش ئۇقتۇرۇشلىرى ئۈچۈن ئىشلىتىلىدۇ. +Comment[uk]=Використовується для попереджень +Comment[vi]=Dùng cho thông báo cảnh báo +Comment[wa]=Eployî po des notifiaedjes d' adviertixhmint +Comment[x-test]=xxUsed for warning notificationsxx +Comment[zh_CN]=用于发出警告通知 +Comment[zh_TW]=用於警告通知 + +[Event/freespacenotif] +Name=Running low on disk space +Name[ar]=انخفاض مساحة القرص +Name[ast]=Pocu espaciu nel discu +Name[az]=Boş disk sahəsi bitmək üzrədir +Name[bg]=Дисковото пространство е твърде малко +Name[bn]=ডিস্ক-এ বেশী জায়গা বাকি নেই +Name[bs]=Ponestaje prostora na disku +Name[ca]=S'està exhaurint l'espai del disc +Name[ca@valencia]=S'està exhaurint l'espai del disc +Name[cs]=Dochází místo na disku +Name[da]=Ved at løbe tør for diskplads +Name[de]=Speicherplatz wird knapp +Name[el]=Τρέχον χωρητικότητα δίσκου χαμηλή +Name[en_GB]=Running low on disk space +Name[es]=Queda poco espacio en disco +Name[et]=Kettaruumi napib +Name[eu]=Leku gutxi gelditzen da diskoan +Name[fi]=Levytila alkaa olla lopussa +Name[fr]=L'espace disque commence à manquer +Name[ga]=Tá an diosca ag éirí lán +Name[gl]=Esgotando o espazo no disco +Name[he]=השטח הפנוי בדיסק מועט +Name[hi]=डिस्क में जगह कम हो गया है +Name[hr]=Ponestajanje diskovnog prostora +Name[hsb]=Rum na tačeli na fal dźe +Name[hu]=Kezd fogyni a lemezterület +Name[ia]=Exhauriente le spatio de disco +Name[id]=Berjalan pada ruang kosong yang tinggal sedikit +Name[is]=Lítið pláss eftir á diski +Name[it]=Lo spazio su disco si sta esaurendo +Name[ja]=ディスクの空き領域が少ないです +Name[kk]=Диск орын тапшылығы +Name[km]=ដំណើរការ​ទាប​លើ​ទំហំ​ថាស +Name[kn]=ಮುದ್ರಿಕೆಯಲ್ಲಿ (ಡಿಸ್ಕ್) ಜಾಗ ಕಮ್ಮಿಯಾಗುತ್ತಿದೆ. +Name[ko]=디스크 공간 거의 없음 +Name[lt]=Baigiasi vieta diske +Name[lv]=Ir palicis maz brīvas vietas +Name[ml]=ഡിസ്ക് കുറഞ്ഞ ഇടത്തിൽ പ്രവർത്തിക്കുന്നു +Name[mr]=डिस्कवर कमी जागा शिल्लक आहे +Name[nb]=Det er nå lite diskplass +Name[nds]=De free Ruum op de Fastplaat geiht op de Neeg. +Name[nl]=Bijna geen vrije schijfruimte meer +Name[nn]=Det er lite diskplass att +Name[pa]=ਡਿਸਕ ਥਾਂ ਖਤਮ ਹੋ ਰਹੀ ਹੈ +Name[pl]=Zaczyna brakować miejsca na dysku +Name[pt]=Pouco espaço livre em disco +Name[pt_BR]=Pouco espaço livre em disco +Name[ro]=Spațiu pe disc este scăzut +Name[ru]=Недостаточно места на диске +Name[si]=පහත් තැටි ඉඩෙන් ධාවනය කරමි +Name[sk]=Dochádza voľné miesto na disku +Name[sl]=Razpoložljivega prostora na disku je malo +Name[sr]=Понестаје простора на диску +Name[sr@ijekavian]=Понестаје простора на диску +Name[sr@ijekavianlatin]=Ponestaje prostora na disku +Name[sr@latin]=Ponestaje prostora na disku +Name[sv]=Diskutrymmet håller på att ta slut +Name[ta]=வட்டில் குறைவான காலியிடம் +Name[th]=พื้นที่ดิสก์กำลังเหลือน้อย +Name[tr]=Disk alanı çok düşük +Name[ug]=دىسكىدىكى بوشلۇق ئاز ھالەتتە ئىجرا قىلىۋاتىدۇ +Name[uk]=Замало місця на диску +Name[vi]=Sắp hết không gian đĩa +Name[wa]=Gn a pus bråmint d' plaece sol plake +Name[x-test]=xxRunning low on disk spacexx +Name[zh_CN]=磁盘空间过低 +Name[zh_TW]=磁碟空間快用完了 +Comment=You are running low on disk space +Comment[ar]=أنت تعمل على مساحة قرص منخفضة +Comment[ast]=Quédate pocu espaciu nel discu +Comment[az]=Diskinizin boş sahəsi bitmək üzrədir +Comment[bg]=Дисковото Ви пространство е твърде малко +Comment[bn]=আপনার ডিস্ক-এ বেশী জায়গা বাকি নেই +Comment[bs]=Ponestaje vam prostora na disku +Comment[ca]=L'espai del disc s'està exhaurint +Comment[ca@valencia]=L'espai del disc s'està exhaurint +Comment[cs]=Dochází vám místo na disku +Comment[da]=Du er ved at løbe tør for diskplads +Comment[de]=Ihr freier Speicherplatz wird knapp +Comment[el]=Η τρέχουσα χωρητικότητα του δίσκου σας είναι χαμηλή +Comment[en_GB]=You are running low on disk space +Comment[es]=Se está quedando sin espacio en disco +Comment[et]=Kettaruumi on vähe järele jäänud +Comment[eu]=Diskoan lekurik gabe gelditzen ari zara +Comment[fi]=Levytilasi alkaa loppua +Comment[fr]=Vous manquez d'espace disque +Comment[ga]=Tá an diosca ag éirí lán +Comment[gl]=Está a quedar sen espazo no disco +Comment[he]=השטח הפנוי בדיסק מתמעט +Comment[hi]=आप कम डिस्क जगह पर चल रहे हैं +Comment[hr]=Ponestaje vam diskovnog prostora +Comment[hsb]=Tačel je skoro połna +Comment[hu]=A lemezterület kezd elfogyni +Comment[ia]=Tu es exhauriente le spatio de disco +Comment[id]=Anda berjalan pada ruang kosong yang tinggal sedikit +Comment[is]=Þú ert að verða uppiskroppa með diskpláss +Comment[it]=Stai esaurendo lo spazio su disco +Comment[ja]=ディスクの空き領域が少なくなりました +Comment[kk]=Диск орын тапшылықтасыз +Comment[km]=អ្នក​កំពុង​ដំណើរការ​ទាប​​លើ​ទំហំ​ថាស +Comment[kn]=ನಿಮ್ಮಲ್ಲಿ ಕಡಿಮೆ ಡಿಸ್ಕ್‍ ಸ್ಥಳವಿದೆ +Comment[ko]=디스크 공간이 거의 없습니다 +Comment[lt]=Jūsų diske liko mažai vietos +Comment[lv]=Uz diska ir maz brīvas vietas +Comment[ml]=നിങ്ങൾ ഡിസ്ക് ഇടം കുറഞ്ഞ സ്ഥലത്താണ് പ്രവർത്തിക്കുന്നത് +Comment[mr]=तुमच्या डिस्कवर कमी जागा शिल्लक आहे +Comment[nb]=Du har lite diskplass igjen +Comment[nds]=De free Ruum op Dien Fastplaat geiht op de Neeg. +Comment[nl]=U hebt bijna geen vrije schijfruimte meer +Comment[nn]=Du har lite diskplass att +Comment[pa]=ਤੁਹਾਡੇ ਕੋਲ ਡਿਸਕ ਖਾਲੀ ਥਾਂ ਘੱਟ ਰਹੀ ਹੈ +Comment[pl]=Zaczyna brakować miejsca na dysku +Comment[pt]=Está a ficar com pouco espaço livre em disco +Comment[pt_BR]=Você tem pouco espaço livre em disco +Comment[ro]=Spațiul dumneavoastră de stocare este pe cale să se termine +Comment[ru]=На диске осталось мало свободного места +Comment[si]=ඔබගේ තැටි ඉඩ අඩු වෙමින් පවතී +Comment[sk]=Dochádza vám voľné miesto na disku +Comment[sl]=Na disku imate le še malo razpoložljivega prostora +Comment[sr]=Понестаје вам простора на диску +Comment[sr@ijekavian]=Понестаје вам простора на диску +Comment[sr@ijekavianlatin]=Ponestaje vam prostora na disku +Comment[sr@latin]=Ponestaje vam prostora na disku +Comment[sv]=Diskutrymmet håller på att ta slut +Comment[ta]=உங்கள் வட்டில் குறைவான காலியிடம் உள்ளது +Comment[th]=คุณกำลังทำงานด้วยพื้นที่ดิสก์ที่เหลือน้อย +Comment[tr]=Disk alanınız çok düşük +Comment[ug]=دىسكىدىكى بوشلۇق ئاز ھالەتتە ئىجرا قىلىۋاتىسىز +Comment[uk]=На вашому диску залишилося замало місця +Comment[vi]=Bạn đang dùng sắp hết không gian đĩa +Comment[wa]=Vos estoz k' n' a pus bråmint d' plaece sol plake +Comment[x-test]=xxYou are running low on disk spacexx +Comment[zh_CN]=您现在的磁盘空间过低 +Comment[zh_TW]=您的磁碟空間快用完了 +Contexts=warningnot +Action=Popup +Urgency=Critical diff --git a/plasma/workspace/freespacenotifier/freespacenotifier_prefs_base.ui b/plasma/workspace/freespacenotifier/freespacenotifier_prefs_base.ui new file mode 100644 index 0000000000..7f93d49c00 --- /dev/null +++ b/plasma/workspace/freespacenotifier/freespacenotifier_prefs_base.ui @@ -0,0 +1,91 @@ + + + freespacenotifier_prefs_base + + + + 0 + 0 + 320 + 217 + + + + + + + Enable low disk space warning + + + true + + + + + + + Warn when free space is below: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + MiB + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + kcfg_enableNotification + toggled(bool) + kcfg_minimumSpace + setEnabled(bool) + + + 114 + 15 + + + 272 + 44 + + + + + kcfg_enableNotification + toggled(bool) + label_minimumSpace + setEnabled(bool) + + + 114 + 15 + + + 114 + 44 + + + + + diff --git a/plasma/workspace/freespacenotifier/module.cpp b/plasma/workspace/freespacenotifier/module.cpp new file mode 100644 index 0000000000..c3340cc6fc --- /dev/null +++ b/plasma/workspace/freespacenotifier/module.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2006 Lukas Tinkl + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Ivo Anjo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "module.h" + +#include +#include +#include + +#include + +#include "kded_interface.h" + +#include "ui_freespacenotifier_prefs_base.h" + +#include "settings.h" + +K_PLUGIN_CLASS_WITH_JSON(FreeSpaceNotifierModule, "freespacenotifier.json") + +FreeSpaceNotifierModule::FreeSpaceNotifierModule(QObject *parent, const QList &) + : KDEDModule(parent) +{ + // If the module is loaded, notifications are enabled + FreeSpaceNotifierSettings::setEnableNotification(true); + + const QString rootPath = QStringLiteral("/"); + const QString homePath = QDir::homePath(); + + const auto homeMountPoint = KMountPoint::currentMountPoints().findByPath(homePath); + + if ( !homeMountPoint || !homeMountPoint->mountOptions().contains(QLatin1String("ro")) ) { + auto *homeNotifier = new FreeSpaceNotifier(homePath, ki18n("Your Home folder is running out of disk space, you have %1 MiB remaining (%2%)."), this); + connect(homeNotifier, &FreeSpaceNotifier::configureRequested, this, &FreeSpaceNotifierModule::showConfiguration); + } + + // If Home is on a separate partition from Root, warn for it, too. + if (KMountPoint::Ptr rootMountPoint; !homeMountPoint || + ( homeMountPoint->mountPoint() != rootPath && + ( !(rootMountPoint = KMountPoint::currentMountPoints().findByPath(rootPath)) || + !rootMountPoint->mountOptions().contains(QLatin1String("ro")) ) ) ) { + auto *rootNotifier = new FreeSpaceNotifier(rootPath, ki18n("Your Root partition is running out of disk space, you have %1 MiB remaining (%2%)."), this); + connect(rootNotifier, &FreeSpaceNotifier::configureRequested, this, &FreeSpaceNotifierModule::showConfiguration); + } +} + +void FreeSpaceNotifierModule::showConfiguration() +{ + if (KConfigDialog::showDialog(QStringLiteral("settings"))) { + return; + } + + KConfigDialog *dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), FreeSpaceNotifierSettings::self()); + QWidget *generalSettingsDlg = new QWidget(); + + Ui::freespacenotifier_prefs_base preferences; + preferences.setupUi(generalSettingsDlg); + + dialog->addPage(generalSettingsDlg, i18nc("The settings dialog main page name, as in 'general settings'", "General"), QStringLiteral("system-run")); + + connect(dialog, &KConfigDialog::finished, this, [] { + if (!FreeSpaceNotifierSettings::enableNotification()) { + // The idea here is to disable ourselves by telling kded to stop autostarting us, and + // to kill the current running instance. + org::kde::kded5 kded(QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QDBusConnection::sessionBus()); + kded.setModuleAutoloading(QStringLiteral("freespacenotifier"), false); + kded.unloadModule(QStringLiteral("freespacenotifier")); + } + }); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + +#include "module.moc" diff --git a/plasma/workspace/freespacenotifier/module.h b/plasma/workspace/freespacenotifier/module.h new file mode 100644 index 0000000000..61ea4fdc2d --- /dev/null +++ b/plasma/workspace/freespacenotifier/module.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2006 Lukas Tinkl + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Ivo Anjo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "freespacenotifier.h" + +class FreeSpaceNotifierModule : public KDEDModule +{ + Q_OBJECT +public: + FreeSpaceNotifierModule(QObject *parent, const QList &); + +private: + void showConfiguration(); +}; diff --git a/plasma/workspace/freespacenotifier/settings.kcfgc b/plasma/workspace/freespacenotifier/settings.kcfgc new file mode 100644 index 0000000000..3997ce9861 --- /dev/null +++ b/plasma/workspace/freespacenotifier/settings.kcfgc @@ -0,0 +1,6 @@ +# Code generation options for kconfig_compiler +File=freespacenotifier.kcfg +ClassName=FreeSpaceNotifierSettings +Singleton=true +Mutators=minimumSpace,enableNotification +# will create the necessary code for setting those variables diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/CMakeLists.txt b/plasma/workspace/gmenu-dbusmenu-proxy/CMakeLists.txt new file mode 100644 index 0000000000..02996b143a --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/CMakeLists.txt @@ -0,0 +1,49 @@ +find_package(AppMenuGtkModule) +set_package_properties(AppMenuGtkModule PROPERTIES TYPE RUNTIME) + +add_definitions(-DQT_NO_CAST_TO_ASCII +-DQT_NO_CAST_FROM_ASCII +-DQT_NO_CAST_FROM_BYTEARRAY) + +find_package(XCB + REQUIRED COMPONENTS + XCB +) + +set(GMENU_DBUSMENU_PROXY_SRCS + main.cpp + menuproxy.cpp + window.cpp + menu.cpp + actions.cpp + gdbusmenutypes_p.cpp + icons.cpp + utils.cpp + ../libdbusmenuqt/dbusmenutypes_p.cpp + ) + +qt_add_dbus_adaptor(GMENU_DBUSMENU_PROXY_SRCS ../libdbusmenuqt/com.canonical.dbusmenu.xml window.h Window) + +ecm_qt_declare_logging_category(GMENU_DBUSMENU_PROXY_SRCS HEADER debug.h + IDENTIFIER DBUSMENUPROXY + CATEGORY_NAME kde.dbusmenuproxy + DEFAULT_SEVERITY Info) + +add_executable(gmenudbusmenuproxy ${GMENU_DBUSMENU_PROXY_SRCS}) + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + +target_link_libraries(gmenudbusmenuproxy + Qt::Core + Qt::X11Extras + Qt::DBus + KF5::CoreAddons + KF5::ConfigCore + KF5::WindowSystem + XCB::XCB +) + +install(TARGETS gmenudbusmenuproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES gmenudbusmenuproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) + +ecm_install_configured_files(INPUT plasma-gmenudbusmenuproxy.service.in @ONLY DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/actions.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/actions.cpp new file mode 100644 index 0000000000..5d9370c802 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/actions.cpp @@ -0,0 +1,198 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "actions.h" + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include + +static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); + +Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent) + : QObject(parent) + , m_serviceName(serviceName) + , m_objectPath(objectPath) +{ + Q_ASSERT(!serviceName.isEmpty()); + Q_ASSERT(!m_objectPath.isEmpty()); + + if (!QDBusConnection::sessionBus().connect(serviceName, + objectPath, + s_orgGtkActions, + QStringLiteral("Changed"), + this, + SLOT(onActionsChanged(QStringList, StringBoolMap, QVariantMap, GMenuActionMap)))) { + qCWarning(DBUSMENUPROXY) << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath; + } +} + +Actions::~Actions() = default; + +void Actions::load() +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("DescribeAll")); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error(); + Q_EMIT failedToLoad(); + } else { + m_actions = reply.value(); + Q_EMIT loaded(); + } + watcher->deleteLater(); + }); +} + +bool Actions::get(const QString &name, GMenuAction &action) const +{ + auto it = m_actions.find(name); + if (it == m_actions.constEnd()) { + return false; + } + + action = *it; + return true; +} + +GMenuActionMap Actions::getAll() const +{ + return m_actions; +} + +void Actions::trigger(const QString &name, const QVariant &target, uint timestamp) +{ + if (!m_actions.contains(name)) { + qCWarning(DBUSMENUPROXY) << "Cannot invoke action" << name << "which doesn't exist"; + return; + } + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("Activate")); + msg << name; + + QVariantList args; + if (target.isValid()) { + args << target; + } + msg << QVariant::fromValue(args); + + QVariantMap platformData; + + if (timestamp) { + // From documentation: + // If the startup notification id is not available, this can be just "_TIMEtime", where + // time is the time stamp from the event triggering the call. + // see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake + platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp)); + } + + msg << platformData; + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + } + watcher->deleteLater(); + }); +} + +bool Actions::isValid() const +{ + return !m_actions.isEmpty(); +} + +void Actions::onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added) +{ + // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions + QStringList dirtyActions; + + // TODO I bet for most of the loops below we could use a nice short std algorithm + + for (const QString &removedAction : removed) { + if (m_actions.remove(removedAction)) { + dirtyActions.append(removedAction); + } + } + + for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + const bool enabled = it.value(); + + auto actionIt = m_actions.find(actionName); + if (actionIt == m_actions.end()) { + qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + if (action.enabled != enabled) { + action.enabled = enabled; + dirtyActions.append(actionName); + } else { + qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it"; + } + } + + for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + const QVariant &state = it.value(); + + auto actionIt = m_actions.find(actionName); + if (actionIt == m_actions.end()) { + qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know"; + continue; + } + + GMenuAction &action = *actionIt; + + if (action.state.isEmpty()) { + qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before"; + action.state.append(state); + dirtyActions.append(actionName); + } else { + // Action state is a list but the state change only sends us a single variant, so just overwrite the first one + QVariant &firstState = action.state.first(); + if (firstState != state) { + firstState = state; + dirtyActions.append(actionName); + } else { + qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it"; + } + } + } + + // unite() will result in keys being present multiple times, do it manually and overwrite existing ones + for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) { + const QString &actionName = it.key(); + + if (DBUSMENUPROXY().isInfoEnabled()) { + if (m_actions.contains(actionName)) { + qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one"; + } + } + + m_actions.insert(actionName, it.value()); + + dirtyActions.append(actionName); + } + + if (!dirtyActions.isEmpty()) { + Q_EMIT actionsChanged(dirtyActions); + } +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/actions.h b/plasma/workspace/gmenu-dbusmenu-proxy/actions.h new file mode 100644 index 0000000000..dea854ab68 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/actions.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +#include "gdbusmenutypes_p.h" + +class QStringList; + +class Actions : public QObject +{ + Q_OBJECT + +public: + Actions(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr); + ~Actions() override; + + void load(); + + bool get(const QString &name, GMenuAction &action) const; + GMenuActionMap getAll() const; + void trigger(const QString &name, const QVariant &target, uint timestamp = 0); + + bool isValid() const; // basically "has actions" + +Q_SIGNALS: + void loaded(); + void failedToLoad(); // expose error? + void actionsChanged(const QStringList &dirtyActions); + +private Q_SLOTS: + void onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added); + +private: + GMenuActionMap m_actions; + + QString m_serviceName; + QString m_objectPath; +}; diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp new file mode 100644 index 0000000000..10f248cab7 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.cpp @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "gdbusmenutypes_p.h" + +#include +#include + +// GMenuItem +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item) +{ + argument.beginStructure(); + argument << item.id << item.section << item.items; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item) +{ + argument.beginStructure(); + argument >> item.id >> item.section >> item.items; + argument.endStructure(); + return argument; +} + +// GMenuSection +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item) +{ + argument.beginStructure(); + argument << item.subscription << item.menu; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item) +{ + argument.beginStructure(); + argument >> item.subscription >> item.menu; + argument.endStructure(); + return argument; +} + +// GMenuChange +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item) +{ + argument.beginStructure(); + argument << item.subscription << item.menu << item.changePosition << item.itemsToRemoveCount << item.itemsToInsert; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item) +{ + argument.beginStructure(); + argument >> item.subscription >> item.menu >> item.changePosition >> item.itemsToRemoveCount >> item.itemsToInsert; + argument.endStructure(); + return argument; +} + +// GMenuActionProperty +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item) +{ + argument.beginStructure(); + argument << item.enabled << item.signature << item.state; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item) +{ + argument.beginStructure(); + argument >> item.enabled >> item.signature >> item.state; + argument.endStructure(); + return argument; +} + +// GMenuActionsChange +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item) +{ + argument.beginStructure(); + argument << item.removed << item.enabledChanged << item.stateChanged << item.added; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item) +{ + argument.beginStructure(); + argument >> item.removed >> item.enabledChanged >> item.stateChanged >> item.added; + argument.endStructure(); + return argument; +} + +void GDBusMenuTypes_register() +{ + static bool registered = false; + if (registered) { + return; + } + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + registered = true; +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h b/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h new file mode 100644 index 0000000000..e8c13331af --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/gdbusmenutypes_p.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class QDBusArgument; + +// Various +using VariantMapList = QList; +Q_DECLARE_METATYPE(VariantMapList); + +using StringBoolMap = QMap; +Q_DECLARE_METATYPE(StringBoolMap); + +// Menu item itself (Start method) +struct GMenuItem { + uint id; + uint section; + VariantMapList items; +}; +Q_DECLARE_METATYPE(GMenuItem); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuItem &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuItem &item); + +using GMenuItemList = QList; +Q_DECLARE_METATYPE(GMenuItemList); + +// Information about what section or submenu to use for a particular entry +struct GMenuSection { + uint subscription; + uint menu; +}; +Q_DECLARE_METATYPE(GMenuSection); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuSection &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuSection &item); + +// Changes of a menu item (Changed signal) +struct GMenuChange { + uint subscription; + uint menu; + + uint changePosition; + uint itemsToRemoveCount; + VariantMapList itemsToInsert; +}; +Q_DECLARE_METATYPE(GMenuChange); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuChange &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuChange &item); + +using GMenuChangeList = QList; +Q_DECLARE_METATYPE(GMenuChangeList); + +// An application action +struct GMenuAction { + bool enabled; + QDBusSignature signature; + QVariantList state; +}; +Q_DECLARE_METATYPE(GMenuAction); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuAction &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuAction &item); + +using GMenuActionMap = QMap; +Q_DECLARE_METATYPE(GMenuActionMap); + +struct GMenuActionsChange { + QStringList removed; + QMap enabledChanged; + QVariantMap stateChanged; + GMenuActionMap added; +}; +Q_DECLARE_METATYPE(GMenuActionsChange); + +QDBusArgument &operator<<(QDBusArgument &argument, const GMenuActionsChange &item); +const QDBusArgument &operator>>(const QDBusArgument &argument, GMenuActionsChange &item); + +void GDBusMenuTypes_register(); diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop b/plasma/workspace/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop new file mode 100644 index 0000000000..ab65a322af --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/gmenudbusmenuproxy.desktop @@ -0,0 +1,50 @@ +[Desktop Entry] +Exec=gmenudbusmenuproxy +Name=GMenuDBusMenuProxy +Name[ar]=وكيل قائمة D-Bus لـ GMenu. +Name[ast]=GMenuDBusMenuProxy +Name[az]=GMenuDBusMenuProxy +Name[ca]=GMenuDBusMenuProxy +Name[ca@valencia]=GMenuDBusMenuProxy +Name[da]=GMenuDBusMenuProxy +Name[de]=GMenuDBusMenuProxy +Name[el]=GMenuDBusMenuProxy +Name[en_GB]=GMenuDBusMenuProxy +Name[es]=GMenuDBusMenuProxy +Name[et]=GMenuDBusMenuProxy +Name[eu]=GMenuDBusMenuProxy +Name[fi]=GMenuDBusMenuProxy +Name[fr]=GMenuDBusMenuProxy +Name[gl]=Proxy de menú por D-Bus para GMenu. +Name[hi]=जीमेन्यूडीबसमेन्यूप्रॉक्सी +Name[hu]=GMenuDBusMenuProxy +Name[ia]=GMenuDBusMenuProxy +Name[id]=GMenuDBusMenuProxy +Name[it]=GMenuDBusMenuProxy +Name[ko]=GMenuDBusMenuProxy +Name[lt]=GMenuDBusMenuProxy +Name[ml]=ജിമെനുഡിബസ്‍മെനുപ്രോക്സി +Name[nl]=GMenuDBusMenuProxy +Name[nn]=GMenuDBusMenuProxy +Name[pa]=GMenuDBusMenuProxy +Name[pl]=GMenuDBusMenuProxy +Name[pt]=GMenuDBusMenuProxy +Name[pt_BR]=GMenuDBusMenuProxy +Name[ro]=GMenuDBusMenuProxy +Name[ru]=GMenuDBusMenuProxy +Name[sk]=GMenuDBusMenuProxy +Name[sl]=GMenuDBusMenuProxy +Name[sv]=GMenuDBusMenuProxy +Name[ta]=GMenuDBusMenuProxy +Name[tr]=GMenuDBusMenuProxy +Name[uk]=Проксі-меню GMenu D-Bus +Name[vi]=GMenuDBusMenuProxy +Name[x-test]=xxGMenuDBusMenuProxyxx +Name[zh_CN]=GMenuDBusMenuProxy +Name[zh_TW]=GMenuDBusMenuProxy +Type=Application +X-KDE-StartupNotify=false +NoDisplay=true +OnlyShowIn=KDE; +X-KDE-autostart-phase=1 +X-systemd-skip=true diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/icons.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/icons.cpp new file mode 100644 index 0000000000..3cca6c36ff --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/icons.cpp @@ -0,0 +1,308 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "icons.h" + +#include +#include + +QString Icons::actionIcon(const QString &actionName) +{ + QString icon; + + QString action = actionName; + + if (action.isEmpty()) { + return icon; + } + + static const QHash s_icons{ + {QStringLiteral("new"), QStringLiteral("document-new")}, // appmenu-gtk-module "New" + {QStringLiteral("image-new"), QStringLiteral("document-new")}, // Gimp "New" item + {QStringLiteral("adddirect"), QStringLiteral("document-new")}, // LibreOffice "New" item + {QStringLiteral("filenew"), QStringLiteral("document-new")}, // Pluma "New" item + {QStringLiteral("new-window"), QStringLiteral("window-new")}, + {QStringLiteral("newwindow"), QStringLiteral("window-new")}, + {QStringLiteral("yelp-window-new"), QStringLiteral("window-new")}, // Gnome help + {QStringLiteral("new-tab"), QStringLiteral("tab-new")}, + {QStringLiteral("open"), QStringLiteral("document-open")}, + {QStringLiteral("open-location"), QStringLiteral("document-open-remote")}, + {QStringLiteral("openremote"), QStringLiteral("document-open-remote")}, + {QStringLiteral("save"), QStringLiteral("document-save")}, + {QStringLiteral("save-as"), QStringLiteral("document-save-as")}, + {QStringLiteral("saveas"), QStringLiteral("document-save-as")}, + {QStringLiteral("save-all"), QStringLiteral("document-save-all")}, + {QStringLiteral("saveall"), QStringLiteral("document-save-all")}, + {QStringLiteral("import"), QStringLiteral("document-import")}, + {QStringLiteral("export"), QStringLiteral("document-export")}, + {QStringLiteral("exportto"), QStringLiteral("document-export")}, // LibreOffice + {QStringLiteral("exporttopdf"), QStringLiteral("viewpdf")}, // LibreOffice, the icon it uses but the name is quite random + {QStringLiteral("webhtml"), QStringLiteral("text-html")}, // LibreOffice + {QStringLiteral("printpreview"), QStringLiteral("document-print-preview")}, + {QStringLiteral("print-preview"), QStringLiteral("document-print-preview")}, + {QStringLiteral("print"), QStringLiteral("document-print")}, + {QStringLiteral("print-gtk"), QStringLiteral("document-print")}, // Gimp + {QStringLiteral("mail-image"), QStringLiteral("mail-message-new")}, // Gimp + {QStringLiteral("sendmail"), QStringLiteral("mail-message-new")}, // LibreOffice + {QStringLiteral("sendviabluetooth"), QStringLiteral("preferences-system-bluetooth")}, // LibreOffice + {QStringLiteral("sendviabluetooth"), QStringLiteral("preferences-system-bluetooth")}, // LibreOffice + {QStringLiteral("document-properties"), QStringLiteral("document-properties")}, + {QStringLiteral("close"), QStringLiteral("document-close")}, // appmenu-gtk-module "Close" + {QStringLiteral("closedoc"), QStringLiteral("document-close")}, + {QStringLiteral("close-all"), QStringLiteral("document-close")}, + {QStringLiteral("closeall"), QStringLiteral("document-close")}, + {QStringLiteral("closewin"), QStringLiteral("window-close")}, // LibreOffice + {QStringLiteral("quit"), QStringLiteral("application-exit")}, + + {QStringLiteral("undo"), QStringLiteral("edit-undo")}, + {QStringLiteral("redo"), QStringLiteral("edit-redo")}, + {QStringLiteral("revert"), QStringLiteral("document-revert")}, + {QStringLiteral("cut"), QStringLiteral("edit-cut")}, + {QStringLiteral("copy"), QStringLiteral("edit-copy")}, + {QStringLiteral("paste"), QStringLiteral("edit-paste")}, + {QStringLiteral("duplicate"), QStringLiteral("edit-duplicate")}, + + {QStringLiteral("preferences"), QStringLiteral("settings-configure")}, + {QStringLiteral("optionstreedialog"), QStringLiteral("settings-configure")}, // LibreOffice + {QStringLiteral("keyboard-shortcuts"), QStringLiteral("configure-shortcuts")}, + + {QStringLiteral("fullscreen"), QStringLiteral("view-fullscreen")}, + + {QStringLiteral("find"), QStringLiteral("edit-find")}, + {QStringLiteral("searchfind"), QStringLiteral("edit-find")}, + {QStringLiteral("replace"), QStringLiteral("edit-find-replace")}, + {QStringLiteral("searchreplace"), QStringLiteral("edit-find-replace")}, // LibreOffice + {QStringLiteral("searchdialog"), QStringLiteral("edit-find-replace")}, // LibreOffice + {QStringLiteral("find-replace"), QStringLiteral("edit-find-replace")}, // Inkscape + {QStringLiteral("select-all"), QStringLiteral("edit-select-all")}, + {QStringLiteral("selectall"), QStringLiteral("edit-select-all")}, + {QStringLiteral("select-none"), QStringLiteral("edit-select-invert")}, + {QStringLiteral("select-invert"), QStringLiteral("edit-select-invert")}, + {QStringLiteral("invert-selection"), QStringLiteral("edit-select-invert")}, // Inkscape + {QStringLiteral("check-spelling"), QStringLiteral("tools-check-spelling")}, + {QStringLiteral("set-language"), QStringLiteral("set-language")}, + + {QStringLiteral("increasesize"), QStringLiteral("zoom-in")}, + {QStringLiteral("decreasesize"), QStringLiteral("zoom-out")}, + {QStringLiteral("zoom-in"), QStringLiteral("zoom-in")}, + {QStringLiteral("zoom-out"), QStringLiteral("zoom-out")}, + {QStringLiteral("zoomfit"), QStringLiteral("zoom-fit-best")}, + {QStringLiteral("zoom-fit-in"), QStringLiteral("zoom-fit-best")}, + {QStringLiteral("show-guides"), QStringLiteral("show-guides")}, + {QStringLiteral("show-grid"), QStringLiteral("show-grid")}, + + {QStringLiteral("rotateclockwise"), QStringLiteral("object-rotate-right")}, + {QStringLiteral("rotatecounterclockwise"), QStringLiteral("object-rotate-left")}, + {QStringLiteral("fliphorizontally"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("image-flip-horizontal"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("flipvertically"), QStringLiteral("object-flip-vertical")}, + {QStringLiteral("image-flip-vertical"), QStringLiteral("object-flip-vertical")}, + {QStringLiteral("image-scale"), QStringLiteral("transform-scale")}, + + {QStringLiteral("bold"), QStringLiteral("format-text-bold")}, + {QStringLiteral("italic"), QStringLiteral("format-text-italic")}, + {QStringLiteral("underline"), QStringLiteral("format-text-underline")}, + {QStringLiteral("strikeout"), QStringLiteral("format-text-strikethrough")}, + {QStringLiteral("superscript"), QStringLiteral("format-text-superscript")}, + {QStringLiteral("subscript"), QStringLiteral("format-text-subscript")}, + // "grow" is a bit unspecific to always set it to "grow font", so use the exact ID here + {QStringLiteral(".uno:Grow"), QStringLiteral("format-font-size-more")}, // LibreOffice + {QStringLiteral(".uno:Shrink"), QStringLiteral("format-font-size-less")}, // LibreOffice + // also a bit unspecific? + {QStringLiteral("alignleft"), QStringLiteral("format-justify-left")}, + {QStringLiteral("alignhorizontalcenter"), QStringLiteral("format-justify-center")}, + {QStringLiteral("alignright"), QStringLiteral("format-justify-right")}, + {QStringLiteral("alignjustified"), QStringLiteral("format-justify-fill")}, + {QStringLiteral("incrementindent"), QStringLiteral("format-indent-more")}, + {QStringLiteral("decrementindent"), QStringLiteral("format-indent-less")}, + {QStringLiteral("defaultbullet"), QStringLiteral("format-list-unordered")}, // LibreOffice + {QStringLiteral("defaultnumbering"), QStringLiteral("format-list-ordered")}, // LibreOffice + + {QStringLiteral("sortascending"), QStringLiteral("view-sort-ascending")}, + {QStringLiteral("sortdescending"), QStringLiteral("view-sort-descending")}, + + {QStringLiteral("autopilotmenu"), QStringLiteral("tools-wizard")}, // LibreOffice + + {QStringLiteral("layers-new"), QStringLiteral("layer-new")}, + {QStringLiteral("layers-duplicate"), QStringLiteral("layer-duplicate")}, + {QStringLiteral("layers-delete"), QStringLiteral("layer-delete")}, + {QStringLiteral("layers-anchor"), QStringLiteral("anchor")}, + + {QStringLiteral("slideshow"), QStringLiteral("media-playback-start")}, // Gwenview uses this icon for that + {QStringLiteral("playvideo"), QStringLiteral("media-playback-start")}, + + {QStringLiteral("addtags"), QStringLiteral("tag-new")}, + {QStringLiteral("newevent"), QStringLiteral("appointment-new")}, + + {QStringLiteral("previous-document"), QStringLiteral("go-previous")}, + {QStringLiteral("prevphoto"), QStringLiteral("go-previous")}, + {QStringLiteral("next-document"), QStringLiteral("go-next")}, + {QStringLiteral("nextphoto"), QStringLiteral("go-next")}, + + {QStringLiteral("redeye"), QStringLiteral("redeyes")}, + {QStringLiteral("crop"), QStringLiteral("transform-crop")}, + {QStringLiteral("move"), QStringLiteral("transform-move")}, + {QStringLiteral("rotate"), QStringLiteral("transform-rotate")}, + {QStringLiteral("scale"), QStringLiteral("transform-scale")}, + {QStringLiteral("shear"), QStringLiteral("transform-shear")}, + {QStringLiteral("flip"), QStringLiteral("object-flip-horizontal")}, + {QStringLiteral("flag"), QStringLiteral("flag-red")}, // is there a "mark" or "important" icon that isn't email? + + {QStringLiteral("tools-measure"), QStringLiteral("measure")}, + {QStringLiteral("tools-text"), QStringLiteral("draw-text")}, + {QStringLiteral("tools-color-picker"), QStringLiteral("color-picker")}, + {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")}, + {QStringLiteral("tools-eraser"), QStringLiteral("draw-eraser")}, + {QStringLiteral("tools-paintbrush"), QStringLiteral("draw-brush")}, + + {QStringLiteral("help"), QStringLiteral("help-contents")}, + {QStringLiteral("helpindex"), QStringLiteral("help-contents")}, + {QStringLiteral("contents"), QStringLiteral("help-contents")}, + {QStringLiteral("helpcontents"), QStringLiteral("help-contents")}, + {QStringLiteral("context-help"), QStringLiteral("help-whatsthis")}, + {QStringLiteral("extendedhelp"), QStringLiteral("help-whatsthis")}, // LibreOffice + {QStringLiteral("helpreportproblem"), QStringLiteral("tools-report-bug")}, + {QStringLiteral("sendfeedback"), QStringLiteral("tools-report-bug")}, // LibreOffice + {QStringLiteral("about"), QStringLiteral("help-about")}, + + {QStringLiteral("emptytrash"), QStringLiteral("trash-empty")}, + {QStringLiteral("movetotrash"), QStringLiteral("user-trash-symbolic")}, + + // Gnome help + {QStringLiteral("yelp-application-larger-text"), QStringLiteral("format-font-size-more")}, + {QStringLiteral("yelp-application-smaller-text"), QStringLiteral("format-font-size-less")}, // LibreOffice + + // LibreOffice documents in its New menu + {QStringLiteral("private:factory/swriter"), QStringLiteral("application-vnd.oasis.opendocument.text")}, + {QStringLiteral("private:factory/scalc"), QStringLiteral("application-vnd.oasis.opendocument.spreadsheet")}, + {QStringLiteral("private:factory/simpress"), QStringLiteral("application-vnd.oasis.opendocument.presentation")}, + {QStringLiteral("private:factory/sdraw"), QStringLiteral("application-vnd.oasis.opendocument.graphics")}, + {QStringLiteral("private:factory/swriter/web"), QStringLiteral("text-html")}, + {QStringLiteral("private:factory/smath"), QStringLiteral("application-vnd.oasis.opendocument.formula")}, + }; + + // Sometimes we get additional arguments (?slot=123) we don't care about + const int questionMarkIndex = action.indexOf(QLatin1Char('?')); + if (questionMarkIndex > -1) { + action.truncate(questionMarkIndex); + } + + icon = s_icons.value(action); + + if (icon.isEmpty()) { + const int dotIndex = action.indexOf(QLatin1Char('.')); // app., win., or unity. prefix + + QString prefix; + if (dotIndex > -1) { + prefix = action.left(dotIndex); + + action = action.mid(dotIndex + 1); + } + + // appmenu-gtk-module + if (prefix == QLatin1String("unity")) { + // Remove superfluous hyphens added by appmenu-gtk-module + // First remove multiple subsequent ones + static QRegularExpression subsequentHyphenRegExp(QStringLiteral("-{2,}")); + action.replace(subsequentHyphenRegExp, QStringLiteral("-")); + + // now we can be sure we only have a single hyphen at the start or end, remove it if needed + if (action.startsWith(QLatin1Char('-'))) { + action.remove(0, 1); + } + if (action.endsWith(QLatin1Char('-'))) { + action.chop(1); + } + + // It also turns accelerators (&) into hyphens, so remove any hyphen that comes before + // a lower-case letter ("mid sentence"), e.g. "P-references" + static QRegularExpression strayHyphenRegExp(QStringLiteral("-(?=[a-z]+)")); + action.remove(strayHyphenRegExp); + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_dup1Prefix = QStringLiteral("dup:1:"); // can it be dup2 also? + if (action.startsWith(s_dup1Prefix)) { + action = action.mid(s_dup1Prefix.length()); + } + + static const auto s_unoPrefix = QStringLiteral(".uno:"); // LibreOffice with appmenu-gtk + if (action.startsWith(s_unoPrefix)) { + action = action.mid(s_unoPrefix.length()); + } + + // LibreOffice's "Open" entry is always "OpenFromAppname" so we just chop that off + if (action.startsWith(QLatin1String("OpenFrom"))) { + action.truncate(4); // basically "Open" + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_commonPrefix = QStringLiteral("Common"); + if (action.startsWith(s_commonPrefix)) { + action = action.mid(s_commonPrefix.length()); + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_prefixes = QStringList{ + // Gimp with appmenu-gtk + QStringLiteral("file-"), + QStringLiteral("edit-"), + QStringLiteral("view-"), + QStringLiteral("image-"), + QStringLiteral("layers-"), + QStringLiteral("colors-"), + QStringLiteral("tools-"), + QStringLiteral("plug-in-"), + QStringLiteral("windows-"), + QStringLiteral("dialogs-"), + QStringLiteral("help-"), + }; + + for (const QString &prefix : s_prefixes) { + if (action.startsWith(prefix)) { + action = action.mid(prefix.length()); + break; + } + } + + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + action = action.toLower(); + icon = s_icons.value(action); + } + + if (icon.isEmpty()) { + static const auto s_prefixes = QStringList{ + // Pluma with appmenu-gtk + QStringLiteral("file"), + QStringLiteral("edit"), + QStringLiteral("view"), + QStringLiteral("help"), + }; + + for (const QString &prefix : s_prefixes) { + if (action.startsWith(prefix)) { + action = action.mid(prefix.length()); + break; + } + } + + icon = s_icons.value(action); + } + + return icon; +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/icons.h b/plasma/workspace/gmenu-dbusmenu-proxy/icons.h new file mode 100644 index 0000000000..46dd1dbde9 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/icons.h @@ -0,0 +1,15 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include + +namespace Icons +{ +QString actionIcon(const QString &actionName); + +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/main.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/main.cpp new file mode 100644 index 0000000000..72e510c5b1 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/main.cpp @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include +#include + +#include + +#include "menuproxy.h" + +int main(int argc, char **argv) +{ + qputenv("QT_QPA_PLATFORM", "xcb"); + + QGuiApplication::setDesktopSettingsAware(false); + + QGuiApplication app(argc, argv); + + if (!KWindowSystem::isPlatformX11()) { + qFatal("qdbusmenuproxy is only useful XCB. Aborting"); + } + + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement); + + app.setQuitOnLastWindowClosed(false); + + MenuProxy proxy; + + return app.exec(); +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/menu.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/menu.cpp new file mode 100644 index 0000000000..9bac4b9f1c --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/menu.cpp @@ -0,0 +1,328 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "menu.h" + +#include "debug.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "utils.h" + +static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); + +Menu::Menu(const QString &serviceName, const QString &objectPath, QObject *parent) + : QObject(parent) + , m_serviceName(serviceName) + , m_objectPath(objectPath) +{ + Q_ASSERT(!serviceName.isEmpty()); + Q_ASSERT(!m_objectPath.isEmpty()); + + if (!QDBusConnection::sessionBus() + .connect(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("Changed"), this, SLOT(onMenuChanged(GMenuChangeList)))) { + qCWarning(DBUSMENUPROXY) << "Failed to subscribe to menu changes for" << parent << "on" << serviceName << "at" << objectPath; + } +} + +Menu::~Menu() = default; + +void Menu::cleanup() +{ + stop(m_subscriptions); +} + +void Menu::start(uint id) +{ + if (m_subscriptions.contains(id)) { + return; + } + + // TODO watch service disappearing? + + // dbus-send --print-reply --session --dest=:1.103 /org/libreoffice/window/104857641/menus/menubar org.gtk.Menus.Start array:uint32:0 + + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("Start")); + msg.setArguments({QVariant::fromValue(QList{id})}); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id](QDBusPendingCallWatcher *watcher) { + QScopedPointer watcherPtr(watcher); + + QDBusPendingReply reply = *watcherPtr; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to start subscription to" << id << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + Q_EMIT failedToSubscribe(id); + } else { + const bool hadMenu = !m_menus.isEmpty(); + + const auto menus = reply.value(); + for (const auto &menu : menus) { + m_menus[menu.id].append(menus); + } + + // LibreOffice on startup fails to give us some menus right away, we'll also subscribe in onMenuChanged() if necessary + if (menus.isEmpty()) { + qCWarning(DBUSMENUPROXY) << "Got an empty menu for" << id << "on" << m_serviceName << "at" << m_objectPath; + return; + } + + // TODO are we subscribed to all it returns or just to the ones we requested? + m_subscriptions.append(id); + + // do we have a menu now? let's tell everyone + if (!hadMenu && !m_menus.isEmpty()) { + Q_EMIT menuAppeared(); + } + + Q_EMIT subscribed(id); + } + }); +} + +void Menu::stop(const QList &ids) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkMenus, QStringLiteral("End")); + msg.setArguments({ + QVariant::fromValue(ids) // don't let it unwrap it, hence in a variant + }); + + QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, ids](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + qCWarning(DBUSMENUPROXY) << "Failed to stop subscription to" << ids << "on" << m_serviceName << "at" << m_objectPath << reply.error(); + } else { + // remove all subscriptions that we unsubscribed from + // TODO is there a nicer algorithm for that? + // TODO remove all m_menus also? + m_subscriptions.erase( + std::remove_if(m_subscriptions.begin(), m_subscriptions.end(), std::bind(&QList::contains, m_subscriptions, std::placeholders::_1)), + m_subscriptions.end()); + + if (m_subscriptions.isEmpty()) { + Q_EMIT menuDisappeared(); + } + } + watcher->deleteLater(); + }); +} + +bool Menu::hasMenu() const +{ + return !m_menus.isEmpty(); +} + +bool Menu::hasSubscription(uint subscription) const +{ + return m_subscriptions.contains(subscription); +} + +GMenuItem Menu::getSection(int id, bool *ok) const +{ + int subscription; + int section; + int index; + Utils::intToTreeStructure(id, subscription, section, index); + return getSection(subscription, section, ok); +} + +GMenuItem Menu::getSection(int subscription, int section, bool *ok) const +{ + const auto menu = m_menus.value(subscription); + + auto it = std::find_if(menu.begin(), menu.end(), [section](const GMenuItem &item) { + return item.section == section; + }); + + if (it == menu.end()) { + if (ok) { + *ok = false; + } + return GMenuItem(); + } + + if (ok) { + *ok = true; + } + return *it; +} + +QVariantMap Menu::getItem(int id) const +{ + int subscription; + int section; + int index; + Utils::intToTreeStructure(id, subscription, section, index); + return getItem(subscription, section, index); +} + +QVariantMap Menu::getItem(int subscription, int sectionId, int index) const +{ + bool ok; + const GMenuItem section = getSection(subscription, sectionId, &ok); + + if (!ok) { + return QVariantMap(); + } + + const auto items = section.items; + + if (items.count() < index) { + qCWarning(DBUSMENUPROXY) << "Cannot get action" << subscription << sectionId << index << "which is out of bounds"; + return QVariantMap(); + } + + // 0 is the menu itself, items start at 1 + return items.at(index - 1); +} + +void Menu::onMenuChanged(const GMenuChangeList &changes) +{ + const bool hadMenu = !m_menus.isEmpty(); + + QVector dirtyMenus; + QVector dirtyItems; + + for (const auto &change : changes) { + auto updateSection = [&](GMenuItem §ion) { + // Check if the amount of inserted items is identical to the items to be removed, + // just update the existing items and signal a change for that. + // LibreOffice tends to do that e.g. to update its Undo menu entry + if (change.itemsToRemoveCount == change.itemsToInsert.count()) { + for (int i = 0; i < change.itemsToInsert.count(); ++i) { + const auto &newItem = change.itemsToInsert.at(i); + + section.items[change.changePosition + i] = newItem; + + // 0 is the menu itself, items start at 1 + dirtyItems.append(Utils::treeStructureToInt(change.subscription, change.menu, change.changePosition + i + 1)); + } + } else { + for (int i = 0; i < change.itemsToRemoveCount; ++i) { + section.items.removeAt(change.changePosition); // TODO bounds check + } + + for (int i = 0; i < change.itemsToInsert.count(); ++i) { + section.items.insert(change.changePosition + i, change.itemsToInsert.at(i)); + } + + dirtyMenus.append(Utils::treeStructureToInt(change.subscription, change.menu, 0)); + } + }; + + // shouldn't happen, it says only Start() subscribes to changes + if (!m_subscriptions.contains(change.subscription)) { + qCDebug(DBUSMENUPROXY) << "Got menu change for menu" << change.subscription << "that we are not subscribed to, subscribing now"; + // LibreOffice doesn't give us a menu right away but takes a while and then signals us a change + start(change.subscription); + continue; + } + + auto &menu = m_menus[change.subscription]; + + bool sectionFound = false; + // TODO findSectionRef + for (GMenuItem §ion : menu) { + if (section.section != change.menu) { + continue; + } + + qCInfo(DBUSMENUPROXY) << "Updating existing section" << change.menu << "in subscription" << change.subscription; + + sectionFound = true; + updateSection(section); + break; + } + + // Insert new section + if (!sectionFound) { + qCInfo(DBUSMENUPROXY) << "Creating new section" << change.menu << "in subscription" << change.subscription; + + if (change.itemsToRemoveCount > 0) { + qCWarning(DBUSMENUPROXY) << "Menu change requested to remove items from a new (and as such empty) section"; + } + + GMenuItem newSection; + newSection.id = change.subscription; + newSection.section = change.menu; + updateSection(newSection); + menu.append(newSection); + } + } + + // do we have a menu now? let's tell everyone + if (!hadMenu && !m_menus.isEmpty()) { + Q_EMIT menuAppeared(); + } else if (hadMenu && m_menus.isEmpty()) { + Q_EMIT menuDisappeared(); + } + + if (!dirtyItems.isEmpty()) { + Q_EMIT itemsChanged(dirtyItems); + } + + Q_EMIT menusChanged(dirtyMenus); +} + +void Menu::actionsChanged(const QStringList &dirtyActions, const QString &prefix) +{ + auto forEachMenuItem = [this](const std::function &cb) { + for (auto it = m_menus.constBegin(), end = m_menus.constEnd(); it != end; ++it) { + const int subscription = it.key(); + + for (const auto &menu : it.value()) { + const int section = menu.section; + + int count = 0; + + const auto items = menu.items; + for (const auto &item : items) { + ++count; // 0 is a menu, entries start at 1 + + if (!cb(subscription, section, count, item)) { + goto loopExit; // hell yeah + break; + } + } + } + } + + loopExit: // loop exit + return; + }; + + // now find in which menus these actions are and Q_EMIT a change accordingly + QVector dirtyItems; + + for (const QString &action : dirtyActions) { + const QString prefixedAction = prefix + action; + + forEachMenuItem([&prefixedAction, &dirtyItems](int subscription, int section, int index, const QVariantMap &item) { + const QString actionName = Utils::itemActionName(item); + + if (actionName == prefixedAction) { + dirtyItems.append(Utils::treeStructureToInt(subscription, section, index)); + return false; // break + } + + return true; // continue + }); + } + + if (!dirtyItems.isEmpty()) { + Q_EMIT itemsChanged(dirtyItems); + } +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/menu.h b/plasma/workspace/gmenu-dbusmenu-proxy/menu.h new file mode 100644 index 0000000000..9547aec0a6 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/menu.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "../libdbusmenuqt/dbusmenutypes_p.h" +#include "gdbusmenutypes_p.h" + +class Menu : public QObject +{ + Q_OBJECT + +public: + Menu(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr); + ~Menu() override; + + void init(); + void cleanup(); + + void start(uint id); + void stop(const QList &ids); + + bool hasMenu() const; + bool hasSubscription(uint subscription) const; + + GMenuItem getSection(int id, bool *ok = nullptr) const; + GMenuItem getSection(int subscription, int sectionId, bool *ok = nullptr) const; + + QVariantMap getItem(int id) const; // bool ok argument? + QVariantMap getItem(int subscription, int sectionId, int id) const; + +public Q_SLOTS: + void actionsChanged(const QStringList &dirtyActions, const QString &prefix); + +Q_SIGNALS: + void menuAppeared(); // emitted the first time a menu was successfully loaded + void menuDisappeared(); + + void subscribed(uint id); + void failedToSubscribe(uint id); + + void itemsChanged(const QVector &itemIds); + void menusChanged(const QVector &menuIds); + +private Q_SLOTS: + void onMenuChanged(const GMenuChangeList &changes); + +private: + void initMenu(); + + void menuChanged(const GMenuChangeList &changes); + + // QSet? + QList m_subscriptions; // keeps track of which menu trees we're subscribed to + + QHash m_menus; + + QString m_serviceName; + QString m_objectPath; +}; diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.cpp new file mode 100644 index 0000000000..28bc8cb976 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.cpp @@ -0,0 +1,384 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "menuproxy.h" + +#include + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "window.h" + +static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); + +static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar"); + +static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME"); + +static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH"); +static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH"); +static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH"); +static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH"); +// that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar +static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH"); + +static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); +static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); + +static const QString s_gtkModules = QStringLiteral("gtk-modules"); +static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module"); + +MenuProxy::MenuProxy() + : QObject() + , m_xConnection(QX11Info::connection()) + , m_serviceWatcher(new QDBusServiceWatcher(this)) + , m_gtk2RcWatch(new KDirWatch(this)) + , m_writeGtk2SettingsTimer(new QTimer(this)) +{ + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); + m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) { + Q_UNUSED(service); + qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting"; + init(); + }); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + Q_UNUSED(service); + qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up"; + teardown(); + }); + + // It's fine to do a blocking call here as we're a separate binary with no UI + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) { + qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away"; + init(); + } else { + qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything"; + + // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed + enableGtkSettings(false); + } + + // kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again + m_writeGtk2SettingsTimer->setSingleShot(true); + m_writeGtk2SettingsTimer->setInterval(1000); + connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings); + + auto startGtk2SettingsTimer = [this] { + if (!m_writeGtk2SettingsTimer->isActive()) { + m_writeGtk2SettingsTimer->start(); + } + }; + + connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer); + connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer); + m_gtk2RcWatch->addFile(gtkRc2Path()); +} + +MenuProxy::~MenuProxy() +{ + teardown(); +} + +bool MenuProxy::init() +{ + if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { + qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName; + return false; + } + + enableGtkSettings(true); + + connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + const auto windows = KWindowSystem::windows(); + for (WId id : windows) { + onWindowAdded(id); + } + + if (m_windows.isEmpty()) { + qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight"; + } + + return true; +} + +void MenuProxy::teardown() +{ + enableGtkSettings(false); + + QDBusConnection::sessionBus().unregisterService(s_ourServiceName); + + disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); + disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); + + qDeleteAll(m_windows); + m_windows.clear(); +} + +void MenuProxy::enableGtkSettings(bool enable) +{ + m_enabled = enable; + + writeGtk2Settings(); + writeGtk3Settings(); + + // TODO use gconf/dconf directly or at least signal a change somehow? +} + +QString MenuProxy::gtkRc2Path() +{ + return QDir::homePath() + QLatin1String("/.gtkrc-2.0"); +} + +QString MenuProxy::gtk3SettingsIniPath() +{ + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini"); +} + +void MenuProxy::writeGtk2Settings() +{ + QFile rcFile(gtkRc2Path()); + if (!rcFile.exists()) { + // Don't create it here, that would break writing default GTK-2.0 settings on first login, + // as the gtkbreeze kconf_update script only does so if it does not exist + return; + } + + qCDebug(DBUSMENUPROXY) << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support"; + + if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) { + return; + } + + QByteArray content; + + QStringList gtkModules; + + while (!rcFile.atEnd()) { + const QByteArray rawLine = rcFile.readLine(); + + const QString line = QString::fromUtf8(rawLine.trimmed()); + + if (!line.startsWith(s_gtkModules)) { + // keep line as-is + content += rawLine; + continue; + } + + const int equalSignIdx = line.indexOf(QLatin1Char('=')); + if (equalSignIdx < 1) { + continue; + } + + gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), Qt::SkipEmptyParts); + + break; + } + + addOrRemoveAppMenuGtkModule(gtkModules); + + if (!gtkModules.isEmpty()) { + content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8(); + } + + qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; + + m_gtk2RcWatch->stopScan(); + + // now write the new contents of the file + rcFile.resize(0); + rcFile.write(content); + rcFile.close(); + + m_gtk2RcWatch->startScan(); +} + +void MenuProxy::writeGtk3Settings() +{ + qCDebug(DBUSMENUPROXY) << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support"; + + // mostly taken from kde-gtk-config + auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals); + KConfigGroup group(cfg, "Settings"); + + QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), Qt::SkipEmptyParts); + addOrRemoveAppMenuGtkModule(gtkModules); + + if (!gtkModules.isEmpty()) { + group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':'))); + } else { + group.deleteEntry("gtk-modules"); + } + + qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; + + if (m_enabled) { + group.writeEntry("gtk-shell-shows-menubar", 1); + } else { + group.deleteEntry("gtk-shell-shows-menubar"); + } + + qCDebug(DBUSMENUPROXY) << " gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0); + + group.sync(); +} + +void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list) +{ + if (m_enabled && !list.contains(s_appMenuGtkModule)) { + list.append(s_appMenuGtkModule); + } else if (!m_enabled) { + list.removeAll(s_appMenuGtkModule); + } +} + +void MenuProxy::onWindowAdded(WId id) +{ + if (m_windows.contains(id)) { + return; + } + + KWindowInfo info(id, NET::WMWindowType); + + NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask + | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); + + // Only top level windows typically have a menu bar, dialogs, such as settings don't + if (wType != NET::Normal) { + qCDebug(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType; + return; + } + + const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName)); + + if (serviceName.isEmpty()) { + return; + } + + const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath)); + const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath)); + const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath)); + + const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath)); + const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath)); + + if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) { + return; + } + + Window *window = new Window(serviceName); + window->setWinId(id); + window->setApplicationObjectPath(applicationObjectPath); + window->setUnityObjectPath(unityObjectPath); + window->setWindowObjectPath(windowObjectPath); + window->setApplicationMenuObjectPath(applicationMenuObjectPath); + window->setMenuBarObjectPath(menuBarObjectPath); + m_windows.insert(id, window); + + connect(window, &Window::requestWriteWindowProperties, this, [this, window] { + Q_ASSERT(!window->proxyObjectPath().isEmpty()); + + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8()); + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8()); + }); + connect(window, &Window::requestRemoveWindowProperties, this, [this, window] { + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray()); + writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray()); + }); + + window->init(); +} + +void MenuProxy::onWindowRemoved(WId id) +{ + // no need to cleanup() (which removes window properties) when the window is gone, delete right away + delete m_windows.take(id); +} + +QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name) +{ + QByteArray value; + + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return value; + } + + // GTK properties aren't XCB_ATOM_STRING but a custom one + auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING")); + + static const long MAX_PROP_SIZE = 10000; + auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE); + QScopedPointer propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, nullptr)); + if (propertyReply.isNull()) { + qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null"; + return value; + } + + if (propertyReply->type == utf8StringAtom && propertyReply->format == 8 && propertyReply->value_len > 0) { + const char *data = (const char *)xcb_get_property_value(propertyReply.data()); + int len = propertyReply->value_len; + if (data) { + value = QByteArray(data, data[len - 1] ? len : len - 1); + } + } + + return value; +} + +void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value) +{ + auto atom = getAtom(name); + if (atom == XCB_ATOM_NONE) { + return; + } + + if (value.isEmpty()) { + xcb_delete_property(m_xConnection, id, atom); + } else { + xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData()); + } +} + +xcb_atom_t MenuProxy::getAtom(const QByteArray &name) +{ + static QHash s_atoms; + + auto atom = s_atoms.value(name, XCB_ATOM_NONE); + if (atom == XCB_ATOM_NONE) { + const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData()); + QScopedPointer atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, nullptr)); + if (!atomReply.isNull()) { + atom = atomReply->atom; + if (atom != XCB_ATOM_NONE) { + s_atoms.insert(name, atom); + } + } + } + + return atom; +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.h b/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.h new file mode 100644 index 0000000000..7ca4cb124e --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/menuproxy.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include // for WId + +#include + +class QDBusServiceWatcher; +class QTimer; + +class KDirWatch; + +class Window; + +class MenuProxy : public QObject +{ + Q_OBJECT + +public: + MenuProxy(); + ~MenuProxy() override; + +private Q_SLOTS: + void onWindowAdded(WId id); + void onWindowRemoved(WId id); + +private: + bool init(); + void teardown(); + + static QString gtkRc2Path(); + static QString gtk3SettingsIniPath(); + + void enableGtkSettings(bool enabled); + + void writeGtk2Settings(); + void writeGtk3Settings(); + + void addOrRemoveAppMenuGtkModule(QStringList &list); + + xcb_connection_t *m_xConnection; + + QByteArray getWindowPropertyString(WId id, const QByteArray &name); + void writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value); + xcb_atom_t getAtom(const QByteArray &name); + + QHash m_windows; + + QDBusServiceWatcher *m_serviceWatcher; + + KDirWatch *m_gtk2RcWatch; + QTimer *m_writeGtk2SettingsTimer; + + bool m_enabled = false; +}; diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/plasma-gmenudbusmenuproxy.service.in b/plasma/workspace/gmenu-dbusmenu-proxy/plasma-gmenudbusmenuproxy.service.in new file mode 100644 index 0000000000..8b4a424b8b --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/plasma-gmenudbusmenuproxy.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=Proxies GTK DBus menus to a Plasma readable format +PartOf=graphical-session.target +After=plasma-core.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/gmenudbusmenuproxy +Restart=on-failure +Type=simple +Slice=background.slice +TimeoutSec=5sec diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/utils.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/utils.cpp new file mode 100644 index 0000000000..9517e6fce4 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/utils.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "utils.h" + +int Utils::treeStructureToInt(int subscription, int section, int index) +{ + return subscription * 1000000 + section * 1000 + index; +} + +void Utils::intToTreeStructure(int source, int &subscription, int §ion, int &index) +{ + // TODO some better math :) or bit shifting or something + index = source % 1000; + section = (source / 1000) % 1000; + subscription = source / 1000000; +} + +QString Utils::itemActionName(const QVariantMap &item) +{ + QString actionName = item.value(QStringLiteral("action")).toString(); + if (actionName.isEmpty()) { + actionName = item.value(QStringLiteral("submenu-action")).toString(); + } + return actionName; +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/utils.h b/plasma/workspace/gmenu-dbusmenu-proxy/utils.h new file mode 100644 index 0000000000..513783d512 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/utils.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +namespace Utils +{ +int treeStructureToInt(int subscription, int section, int index); +void intToTreeStructure(int source, int &subscription, int §ion, int &index); + +QString itemActionName(const QVariantMap &item); + +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/window.cpp b/plasma/workspace/gmenu-dbusmenu-proxy/window.cpp new file mode 100644 index 0000000000..97d542c5dc --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/window.cpp @@ -0,0 +1,655 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "window.h" + +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "actions.h" +#include "dbusmenuadaptor.h" +#include "icons.h" +#include "menu.h" +#include "utils.h" + +#include "../libdbusmenuqt/dbusmenushortcut_p.h" + +static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); +static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); + +static const QString s_applicationActionsPrefix = QStringLiteral("app."); +static const QString s_unityActionsPrefix = QStringLiteral("unity."); +static const QString s_windowActionsPrefix = QStringLiteral("win."); + +Window::Window(const QString &serviceName) + : QObject() + , m_serviceName(serviceName) +{ + qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName; + + Q_ASSERT(!serviceName.isEmpty()); + + GDBusMenuTypes_register(); + DBusMenuTypes_register(); +} + +Window::~Window() = default; + +void Window::init() +{ + qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" + << m_windowObjectPath << "unity" << m_unityObjectPath; + + if (!m_applicationMenuObjectPath.isEmpty()) { + m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this); + connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties); + connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties); + connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed); + // basically so it replies on DBus no matter what + connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); + connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged); + connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged); + } + + if (!m_menuBarObjectPath.isEmpty()) { + m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this); + connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties); + connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties); + connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed); + connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); + connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged); + connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged); + } + + if (!m_applicationObjectPath.isEmpty()) { + m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this); + connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_applicationActionsPrefix); + }); + connect(m_applicationActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix); + } else { + initMenu(); + } + }); + m_applicationActions->load(); + } + + if (!m_unityObjectPath.isEmpty()) { + m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this); + connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_unityActionsPrefix); + }); + connect(m_unityActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix); + } else { + initMenu(); + } + }); + m_unityActions->load(); + } + + if (!m_windowObjectPath.isEmpty()) { + m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this); + connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { + onActionsChanged(dirtyActions, s_windowActionsPrefix); + }); + connect(m_windowActions, &Actions::loaded, this, [this] { + if (m_menuInited) { + onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix); + } else { + initMenu(); + } + }); + m_windowActions->load(); + } +} + +WId Window::winId() const +{ + return m_winId; +} + +void Window::setWinId(WId winId) +{ + m_winId = winId; +} + +QString Window::serviceName() const +{ + return m_serviceName; +} + +QString Window::applicationObjectPath() const +{ + return m_applicationObjectPath; +} + +void Window::setApplicationObjectPath(const QString &applicationObjectPath) +{ + m_applicationObjectPath = applicationObjectPath; +} + +QString Window::unityObjectPath() const +{ + return m_unityObjectPath; +} + +void Window::setUnityObjectPath(const QString &unityObjectPath) +{ + m_unityObjectPath = unityObjectPath; +} + +QString Window::applicationMenuObjectPath() const +{ + return m_applicationMenuObjectPath; +} + +void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath) +{ + m_applicationMenuObjectPath = applicationMenuObjectPath; +} + +QString Window::menuBarObjectPath() const +{ + return m_menuBarObjectPath; +} + +void Window::setMenuBarObjectPath(const QString &menuBarObjectPath) +{ + m_menuBarObjectPath = menuBarObjectPath; +} + +QString Window::windowObjectPath() const +{ + return m_windowObjectPath; +} + +void Window::setWindowObjectPath(const QString &windowObjectPath) +{ + m_windowObjectPath = windowObjectPath; +} + +QString Window::currentMenuObjectPath() const +{ + return m_currentMenuObjectPath; +} + +QString Window::proxyObjectPath() const +{ + return m_proxyObjectPath; +} + +void Window::initMenu() +{ + if (m_menuInited) { + return; + } + + if (!registerDBusObject()) { + return; + } + + // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none + // so we subscribe to the menu bar as soon as it shows up so we can figure out + // if we have a menu bar, an app menu, or just nothing + if (m_applicationMenu) { + m_applicationMenu->start(0); + } + + if (m_menuBar) { + m_menuBar->start(0); + } + + m_menuInited = true; +} + +void Window::menuItemsChanged(const QVector &itemIds) +{ + if (qobject_cast
(sender()) != m_currentMenu) { + return; + } + + DBusMenuItemList items; + + for (uint id : itemIds) { + const auto newItem = m_currentMenu->getItem(id); + + DBusMenuItem dBusItem{// 0 is menu, items start at 1 + static_cast(id), + gMenuToDBusMenuProperties(newItem)}; + items.append(dBusItem); + } + + Q_EMIT ItemsPropertiesUpdated(items, {}); +} + +void Window::menuChanged(const QVector &menuIds) +{ + if (qobject_cast(sender()) != m_currentMenu) { + return; + } + + for (uint menu : menuIds) { + Q_EMIT LayoutUpdated(3 /*revision*/, menu); + } +} + +void Window::onMenuSubscribed(uint id) +{ + // When it was a delayed GetLayout request, send the reply now + const auto pendingReplies = m_pendingGetLayouts.values(id); + if (!pendingReplies.isEmpty()) { + for (const auto &pendingReply : pendingReplies) { + if (pendingReply.type() != QDBusMessage::InvalidMessage) { + auto reply = pendingReply.createReply(); + + DBusMenuLayoutItem item; + uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item); + + reply << revision << QVariant::fromValue(item); + + QDBusConnection::sessionBus().send(reply); + } + } + m_pendingGetLayouts.remove(id); + } else { + Q_EMIT LayoutUpdated(2 /*revision*/, id); + } +} + +bool Window::getAction(const QString &name, GMenuAction &action) const +{ + QString lookupName; + Actions *actions = getActionsForAction(name, lookupName); + + if (!actions) { + return false; + } + + return actions->get(lookupName, action); +} + +void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp) +{ + QString lookupName; + Actions *actions = getActionsForAction(name, lookupName); + if (!actions) { + return; + } + + actions->trigger(lookupName, target, timestamp); +} + +Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const +{ + if (name.startsWith(QLatin1String("app."))) { + lookupName = name.mid(4); + return m_applicationActions; + } else if (name.startsWith(QLatin1String("unity."))) { + lookupName = name.mid(6); + return m_unityActions; + } else if (name.startsWith(QLatin1String("win."))) { + lookupName = name.mid(4); + return m_windowActions; + } + + return nullptr; +} + +void Window::onActionsChanged(const QStringList &dirty, const QString &prefix) +{ + if (m_applicationMenu) { + m_applicationMenu->actionsChanged(dirty, prefix); + } + if (m_menuBar) { + m_menuBar->actionsChanged(dirty, prefix); + } +} + +bool Window::registerDBusObject() +{ + Q_ASSERT(m_proxyObjectPath.isEmpty()); + + static int menus = 0; + ++menus; + + new DbusmenuAdaptor(this); + + const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus)); + qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath; + + if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { + qCWarning(DBUSMENUPROXY) << "Failed to register object"; + return false; + } + + m_proxyObjectPath = objectPath; + + return true; +} + +void Window::updateWindowProperties() +{ + const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu())); + + if (!hasMenu) { + Q_EMIT requestRemoveWindowProperties(); + return; + } + + Menu *oldMenu = m_currentMenu; + Menu *newMenu = qobject_cast(sender()); + // set current menu as needed + if (!m_currentMenu) { + m_currentMenu = newMenu; + // Menu Bar takes precedence over application menu + } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) { + qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar"; + m_currentMenu = newMenu; + // TODO update layout + } + + if (m_currentMenu != oldMenu) { + // update entire menu now + Q_EMIT LayoutUpdated(4 /*revision*/, 0); + } + + Q_EMIT requestWriteWindowProperties(); +} + +// DBus +bool Window::AboutToShow(int id) +{ + // We always request the first time GetLayout is called and keep up-to-date internally + // No need to have us prepare anything here + Q_UNUSED(id); + return false; +} + +void Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) +{ + Q_UNUSED(data); + + if (!m_currentMenu) { + return; + } + + // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked" + + if (eventId == QLatin1String("clicked")) { + const QVariantMap item = m_currentMenu->getItem(id); + const QString action = item.value(QStringLiteral("action")).toString(); + const QVariant target = item.value(QStringLiteral("target")); + if (!action.isEmpty()) { + triggerAction(action, target, timestamp); + } + } +} + +DBusMenuItemList Window::GetGroupProperties(const QList &ids, const QStringList &propertyNames) +{ + Q_UNUSED(ids); + Q_UNUSED(propertyNames); + return DBusMenuItemList(); +} + +uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem) +{ + Q_UNUSED(recursionDepth); // TODO + Q_UNUSED(propertyNames); + + int subscription; + int sectionId; + int index; + + Utils::intToTreeStructure(parentId, subscription, sectionId, index); + + if (!m_currentMenu) { + return 1; + } + + if (!m_currentMenu->hasSubscription(subscription)) { + // let's serve multiple similar requests in one go once we've processed them + m_pendingGetLayouts.insert(subscription, message()); + setDelayedReply(true); + + m_currentMenu->start(subscription); + return 1; + } + + bool ok; + const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok); + + if (!ok) { + qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId; + return 1; + } + + // If a particular entry is requested, see what it is and resolve as necessary + // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0 + // so resolve that and return the correct menu + if (index > 0) { + // non-zero index indicates item within a menu but the index in the list still starts at zero + if (section.items.count() < index) { + qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds"; + return 0; + } + + const auto &requestedItem = section.items.at(index - 1); + + auto it = requestedItem.constFind(QStringLiteral(":submenu")); + if (it != requestedItem.constEnd()) { + const GMenuSection gmenuSection = qdbus_cast(it->value()); + return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem); + } else { + // TODO + return 0; + } + } + + dbusItem.id = parentId; // TODO + dbusItem.properties = {{QStringLiteral("children-display"), QStringLiteral("submenu")}}; + + int count = 0; + + const auto itemsToBeAdded = section.items; + for (const auto &item : itemsToBeAdded) { + DBusMenuLayoutItem child{ + Utils::treeStructureToInt(section.id, sectionId, ++count), + gMenuToDBusMenuProperties(item), + {} // children + }; + dbusItem.children.append(child); + + // Now resolve section aliases + auto it = item.constFind(QStringLiteral(":section")); + if (it != item.constEnd()) { + // references another place, add it instead + GMenuSection gmenuSection = qdbus_cast(it->value()); + + // remember where the item came from and give it an appropriate ID + // so updates signalled by the app will map to the right place + int originalSubscription = gmenuSection.subscription; + int originalMenu = gmenuSection.menu; + + // TODO start subscription if we don't have it + auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items; + + // Check whether it's an alias to an alias + // FIXME make generic/recursive + if (items.count() == 1) { + const auto &aliasedItem = items.constFirst(); + auto findIt = aliasedItem.constFind(QStringLiteral(":section")); + if (findIt != aliasedItem.constEnd()) { + GMenuSection gmenuSection2 = qdbus_cast(findIt->value()); + items = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items; + + originalSubscription = gmenuSection2.subscription; + originalMenu = gmenuSection2.menu; + } + } + + int aliasedCount = 0; + for (const auto &aliasedItem : qAsConst(items)) { + DBusMenuLayoutItem aliasedChild{ + Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount), + gMenuToDBusMenuProperties(aliasedItem), + {} // children + }; + dbusItem.children.append(aliasedChild); + } + } + } + + // revision, unused in libdbusmenuqt + return 1; +} + +QDBusVariant Window::GetProperty(int id, const QString &property) +{ + Q_UNUSED(id); + Q_UNUSED(property); + QDBusVariant value; + return value; +} + +QString Window::status() const +{ + return QStringLiteral("normal"); +} + +uint Window::version() const +{ + return 4; +} + +QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const +{ + QVariantMap result; + + result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString()); + + if (source.contains(QLatin1String(":section"))) { + result.insert(QStringLiteral("type"), QStringLiteral("separator")); + } + + const bool isMenu = source.contains(QLatin1String(":submenu")); + if (isMenu) { + result.insert(QStringLiteral("children-display"), QStringLiteral("submenu")); + } + + QString accel = source.value(QStringLiteral("accel")).toString(); + if (!accel.isEmpty()) { + QStringList shortcut; + + // TODO use regexp or something + if (accel.contains(QLatin1String("")) || accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Control")); + accel.remove(QLatin1String("")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Shift")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Alt")); + accel.remove(QLatin1String("")); + } + + if (accel.contains(QLatin1String(""))) { + shortcut.append(QStringLiteral("Super")); + accel.remove(QLatin1String("")); + } + + if (!accel.isEmpty()) { + // TODO replace "+" by "plus" and "-" by "minus" + shortcut.append(accel); + + // TODO does gmenu support multiple? + DBusMenuShortcut dbusShortcut; + dbusShortcut.append(shortcut); // don't let it unwrap the list we append + + result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut)); + } + } + + bool enabled = true; + + const QString actionName = Utils::itemActionName(source); + + GMenuAction action; + // if no action is specified this is fine but if there is an action we don't have + // disable the menu entry + bool actionOk = true; + if (!actionName.isEmpty()) { + actionOk = getAction(actionName, action); + enabled = actionOk && action.enabled; + } + + // we used to only send this if not enabled but then dbusmenuimporter does not + // update the enabled state when it changes from disabled to enabled + result.insert(QStringLiteral("enabled"), enabled); + + bool visible = true; + const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString(); + if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) { + visible = false; + } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) { + visible = false; + // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate) + } else if (hiddenWhen == QLatin1String("macos-menubar")) { + visible = true; + } + + result.insert(QStringLiteral("visible"), visible); + + QString icon = source.value(QStringLiteral("icon")).toString(); + if (icon.isEmpty()) { + icon = source.value(QStringLiteral("verb-icon")).toString(); + } + + icon = Icons::actionIcon(actionName); + if (!icon.isEmpty()) { + result.insert(QStringLiteral("icon-name"), icon); + } + + const QVariant target = source.value(QStringLiteral("target")); + + if (actionOk) { + const auto actionStates = action.state; + if (actionStates.count() == 1) { + const auto &actionState = actionStates.first(); + // assume this is a checkbox + if (!isMenu) { + if (actionState.type() == QVariant::Bool) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox")); + result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0); + } else if (actionState.type() == QVariant::String) { + result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio")); + result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0); + } + } + } + } + + return result; +} diff --git a/plasma/workspace/gmenu-dbusmenu-proxy/window.h b/plasma/workspace/gmenu-dbusmenu-proxy/window.h new file mode 100644 index 0000000000..2aad53a393 --- /dev/null +++ b/plasma/workspace/gmenu-dbusmenu-proxy/window.h @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include // for WId + +#include + +#include "../libdbusmenuqt/dbusmenutypes_p.h" +#include "gdbusmenutypes_p.h" + +class QDBusVariant; + +class Actions; +class Menu; + +class Window : public QObject, protected QDBusContext +{ + Q_OBJECT + + // DBus + Q_PROPERTY(QString Status READ status) + Q_PROPERTY(uint Version READ version) + +public: + Window(const QString &serviceName); + ~Window() override; + + void init(); + + WId winId() const; + void setWinId(WId winId); + + QString serviceName() const; + + QString applicationObjectPath() const; + void setApplicationObjectPath(const QString &applicationObjectPath); + + QString unityObjectPath() const; + void setUnityObjectPath(const QString &unityObjectPath); + + QString windowObjectPath() const; + void setWindowObjectPath(const QString &windowObjectPath); + + QString applicationMenuObjectPath() const; + void setApplicationMenuObjectPath(const QString &applicationMenuObjectPath); + + QString menuBarObjectPath() const; + void setMenuBarObjectPath(const QString &menuBarObjectPath); + + QString currentMenuObjectPath() const; + + QString proxyObjectPath() const; + + // DBus + bool AboutToShow(int id); + void Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp); + DBusMenuItemList GetGroupProperties(const QList &ids, const QStringList &propertyNames); + uint GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem); + QDBusVariant GetProperty(int id, const QString &property); + + QString status() const; + uint version() const; + +Q_SIGNALS: + // don't want to pollute X stuff into Menu, let all of that be in MenuProxy + void requestWriteWindowProperties(); + void requestRemoveWindowProperties(); + + // DBus + void ItemActivationRequested(int id, uint timestamp); + void ItemsPropertiesUpdated(const DBusMenuItemList &updatedProps, const DBusMenuItemKeysList &removedProps); + void LayoutUpdated(uint revision, int parent); + +private: + void initMenu(); + + bool registerDBusObject(); + void updateWindowProperties(); + + bool getAction(const QString &name, GMenuAction &action) const; + void triggerAction(const QString &name, const QVariant &target, uint timestamp = 0); + Actions *getActionsForAction(const QString &name, QString &lookupName) const; + + void menuChanged(const QVector &menuIds); + void menuItemsChanged(const QVector &itemIds); + + void onActionsChanged(const QStringList &dirty, const QString &prefix); + void onMenuSubscribed(uint id); + + QVariantMap gMenuToDBusMenuProperties(const QVariantMap &source) const; + + WId m_winId = 0; + QString m_serviceName; // original GMenu service (the gtk app) + + QString m_applicationObjectPath; + QString m_unityObjectPath; + QString m_windowObjectPath; + QString m_applicationMenuObjectPath; + QString m_menuBarObjectPath; + + QString m_currentMenuObjectPath; + + QString m_proxyObjectPath; // our object path on this proxy app + + QMultiHash m_pendingGetLayouts; + + Menu *m_applicationMenu = nullptr; + Menu *m_menuBar = nullptr; + + Menu *m_currentMenu = nullptr; + + Actions *m_applicationActions = nullptr; + Actions *m_unityActions = nullptr; + Actions *m_windowActions = nullptr; + + bool m_menuInited = false; +}; diff --git a/plasma/workspace/interactiveconsole/CMakeLists.txt b/plasma/workspace/interactiveconsole/CMakeLists.txt new file mode 100644 index 0000000000..5999fad81b --- /dev/null +++ b/plasma/workspace/interactiveconsole/CMakeLists.txt @@ -0,0 +1,8 @@ +set(interactiveconsole_SRCS + interactiveconsole.cpp + main.cpp +) + +add_executable(plasma-interactiveconsole ${interactiveconsole_SRCS}) +target_link_libraries(plasma-interactiveconsole Qt::Widgets Qt::DBus KF5::KIOCore KF5::WidgetsAddons KF5::ConfigWidgets KF5::TextEditor KF5::Package) +install(TARGETS plasma-interactiveconsole ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/plasma/workspace/interactiveconsole/interactiveconsole.cpp b/plasma/workspace/interactiveconsole/interactiveconsole.cpp new file mode 100644 index 0000000000..14a6698e5e --- /dev/null +++ b/plasma/workspace/interactiveconsole/interactiveconsole.cpp @@ -0,0 +1,591 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "interactiveconsole.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// TODO: +// interactive help? +static const QString s_autosaveFileName(QStringLiteral("interactiveconsoleautosave.js")); +static const QString s_kwinService = QStringLiteral("org.kde.KWin"); +static const QString s_plasmaShellService = QStringLiteral("org.kde.plasmashell"); + +InteractiveConsole::InteractiveConsole(ConsoleMode mode, QWidget *parent) + : QDialog(parent) + , m_splitter(new QSplitter(Qt::Vertical, this)) + , m_editorPart(nullptr) + , m_editor(nullptr) + , m_output(nullptr) + , m_loadAction(KStandardAction::open(this, SLOT(openScriptFile()), this)) + , m_saveAction(KStandardAction::saveAs(this, SLOT(saveScript()), this)) + , m_clearAction(KStandardAction::clear(this, SLOT(clearEditor()), this)) + , m_executeAction(new QAction(QIcon::fromTheme(QStringLiteral("system-run")), i18n("&Execute"), this)) + , m_plasmaAction(new QAction(QIcon::fromTheme(QStringLiteral("plasma")), i18nc("Toolbar Button to switch to Plasma Scripting Mode", "Plasma"), this)) + , m_kwinAction(new QAction(QIcon::fromTheme(QStringLiteral("kwin")), i18nc("Toolbar Button to switch to KWin Scripting Mode", "KWin"), this)) + , m_snippetsMenu(new QMenu(i18n("Templates"), this)) + , m_fileDialog(nullptr) + , m_closeWhenCompleted(false) + , m_mode(mode) +{ + addAction(KStandardAction::close(this, SLOT(close()), this)); + addAction(m_saveAction); + addAction(m_clearAction); + + setWindowTitle(i18n("Desktop Shell Scripting Console")); + setAttribute(Qt::WA_DeleteOnClose); + // setButtons(QDialog::None); + + QWidget *widget = new QWidget(m_splitter); + QVBoxLayout *editorLayout = new QVBoxLayout(widget); + + QLabel *label = new QLabel(i18n("Editor"), widget); + QFont f = label->font(); + f.setBold(true); + label->setFont(f); + editorLayout->addWidget(label); + + connect(m_snippetsMenu, &QMenu::aboutToShow, this, &InteractiveConsole::populateTemplatesMenu); + + QToolButton *loadTemplateButton = new QToolButton(this); + loadTemplateButton->setPopupMode(QToolButton::InstantPopup); + loadTemplateButton->setMenu(m_snippetsMenu); + loadTemplateButton->setText(i18n("Load")); + connect(loadTemplateButton, &QToolButton::triggered, this, &InteractiveConsole::loadTemplate); + + QToolButton *useTemplateButton = new QToolButton(this); + useTemplateButton->setPopupMode(QToolButton::InstantPopup); + useTemplateButton->setMenu(m_snippetsMenu); + useTemplateButton->setText(i18n("Use")); + connect(useTemplateButton, &QToolButton::triggered, this, &InteractiveConsole::useTemplate); + + QActionGroup *modeGroup = new QActionGroup(this); + modeGroup->addAction(m_plasmaAction); + modeGroup->addAction(m_kwinAction); + m_plasmaAction->setCheckable(true); + m_kwinAction->setCheckable(true); + m_kwinAction->setChecked(mode == KWinConsole); + m_plasmaAction->setChecked(mode == PlasmaConsole); + connect(modeGroup, &QActionGroup::triggered, this, &InteractiveConsole::modeSelectionChanged); + + KToolBar *toolBar = new KToolBar(this, true, false); + toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toolBar->addAction(m_loadAction); + toolBar->addAction(m_saveAction); + toolBar->addAction(m_clearAction); + toolBar->addAction(m_executeAction); + toolBar->addAction(m_plasmaAction); + toolBar->addAction(m_kwinAction); + toolBar->addWidget(loadTemplateButton); + toolBar->addWidget(useTemplateButton); + + editorLayout->addWidget(toolBar); + + auto tryLoadingKatePart = [=]() -> KTextEditor::Document * { + const auto loadResult = KPluginFactory::instantiatePlugin(KPluginMetaData(QStringLiteral("kf5/parts/katepart")), this); + + if (!loadResult) { + qWarning() << "Error loading katepart plugin:" << loadResult.errorString; + return nullptr; + } + + KTextEditor::Document *result = loadResult.plugin; + + result->setHighlightingMode(QStringLiteral("JavaScript/PlasmaDesktop")); + + KTextEditor::View *view = result->createView(widget); + view->setContextMenu(view->defaultContextMenu()); + + KTextEditor::ConfigInterface *config = qobject_cast(view); + if (config) { + config->setConfigValue(QStringLiteral("line-numbers"), true); + config->setConfigValue(QStringLiteral("dynamic-word-wrap"), true); + } + + editorLayout->addWidget(view); + connect(result, &KTextEditor::Document::textChanged, this, &InteractiveConsole::scriptTextChanged); + + return result; + }; + + m_editorPart = tryLoadingKatePart(); + + if (!m_editorPart) { + m_editor = new KTextEdit(widget); + editorLayout->addWidget(m_editor); + connect(m_editor, &QTextEdit::textChanged, this, &InteractiveConsole::scriptTextChanged); + } + + m_splitter->addWidget(widget); + + widget = new QWidget(m_splitter); + QVBoxLayout *outputLayout = new QVBoxLayout(widget); + + label = new QLabel(i18n("Output"), widget); + f = label->font(); + f.setBold(true); + label->setFont(f); + outputLayout->addWidget(label); + + KToolBar *outputToolBar = new KToolBar(widget, true, false); + outputToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + QAction *clearOutputAction = KStandardAction::clear(this, SLOT(clearOutput()), this); + outputToolBar->addAction(clearOutputAction); + outputLayout->addWidget(outputToolBar); + + m_output = new QTextBrowser(widget); + outputLayout->addWidget(m_output); + m_splitter->addWidget(widget); + + QVBoxLayout *l = new QVBoxLayout(this); + l->addWidget(m_splitter); + + KConfigGroup cg(KSharedConfig::openConfig(), "InteractiveConsole"); + restoreGeometry(cg.readEntry("Geometry", QByteArray())); + + m_splitter->setStretchFactor(0, 10); + m_splitter->restoreState(cg.readEntry("SplitterState", QByteArray())); + + scriptTextChanged(); + + connect(m_executeAction, &QAction::triggered, this, &InteractiveConsole::evaluateScript); + m_executeAction->setShortcut(Qt::CTRL | Qt::Key_E); + + const QString autosave = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/" + s_autosaveFileName; + if (QFile::exists(autosave)) { + loadScript(autosave); + } +} + +InteractiveConsole::~InteractiveConsole() +{ + KConfigGroup cg(KSharedConfig::openConfig(), "InteractiveConsole"); + cg.writeEntry("Geometry", saveGeometry()); + cg.writeEntry("SplitterState", m_splitter->saveState()); +} + +void InteractiveConsole::setMode(const QString &mode) +{ + if (mode.toLower() == QLatin1String("desktop")) { + m_plasmaAction->trigger(); + } else if (mode.toLower() == QLatin1String("windowmanager")) { + m_kwinAction->trigger(); + } +} + +void InteractiveConsole::modeSelectionChanged() +{ + if (m_plasmaAction->isChecked()) { + m_mode = PlasmaConsole; + } else if (m_kwinAction->isChecked()) { + m_mode = KWinConsole; + } + + Q_EMIT modeChanged(); +} + +QString InteractiveConsole::mode() const +{ + if (m_mode == KWinConsole) { + return QStringLiteral("windowmanager"); + } + + return QStringLiteral("desktop"); +} + +void InteractiveConsole::setScriptInterface(QObject *obj) +{ + if (m_scriptEngine != obj) { + if (m_scriptEngine) { + disconnect(m_scriptEngine, nullptr, this, nullptr); + } + + m_scriptEngine = obj; + connect(m_scriptEngine, SIGNAL(print(QString)), this, SLOT(print(QString))); + connect(m_scriptEngine, SIGNAL(printError(QString)), this, SLOT(print(QString))); + Q_EMIT scriptEngineChanged(); + } +} + +QObject *InteractiveConsole::scriptEngine() const +{ + return m_scriptEngine; +} + +void InteractiveConsole::loadScript(const QString &script) +{ + if (m_editorPart) { + m_editorPart->closeUrl(false); + if (m_editorPart->openUrl(QUrl::fromLocalFile(script))) { + m_editorPart->setHighlightingMode(QStringLiteral("JavaScript/PlasmaDesktop")); + return; + } + } else { + QFile file(KShell::tildeExpand(script)); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + m_editor->setText(file.readAll()); + return; + } + } + + m_output->append(i18n("Unable to load script file %1", script)); +} + +void InteractiveConsole::showEvent(QShowEvent *) +{ + if (m_editorPart) { + m_editorPart->views().constFirst()->setFocus(); + } else { + m_editor->setFocus(); + } + + KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop()); + Q_EMIT visibleChanged(true); +} + +void InteractiveConsole::closeEvent(QCloseEvent *event) +{ + // need to save first! + const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/" + s_autosaveFileName; + m_closeWhenCompleted = true; + saveScript(QUrl::fromLocalFile(path)); + QDialog::closeEvent(event); + Q_EMIT visibleChanged(false); +} + +void InteractiveConsole::reject() +{ + QDialog::reject(); + close(); +} + +void InteractiveConsole::print(const QString &string) +{ + m_output->append(string); +} + +void InteractiveConsole::scriptTextChanged() +{ + const bool enable = m_editorPart ? !m_editorPart->isEmpty() : !m_editor->document()->isEmpty(); + m_saveAction->setEnabled(enable); + m_clearAction->setEnabled(enable); + m_executeAction->setEnabled(enable); +} + +void InteractiveConsole::openScriptFile() +{ + delete m_fileDialog; + + m_fileDialog = new QFileDialog(); + m_fileDialog->setAcceptMode(QFileDialog::AcceptOpen); + m_fileDialog->setWindowTitle(i18n("Open Script File")); + + QStringList mimetypes; + mimetypes << QStringLiteral("application/javascript"); + m_fileDialog->setMimeTypeFilters(mimetypes); + + connect(m_fileDialog, &QDialog::finished, this, &InteractiveConsole::openScriptUrlSelected); + m_fileDialog->show(); +} + +void InteractiveConsole::openScriptUrlSelected(int result) +{ + if (!m_fileDialog) { + return; + } + + if (result == QDialog::Accepted) { + const QUrl url = m_fileDialog->selectedUrls().constFirst(); + if (!url.isEmpty()) { + loadScriptFromUrl(url); + } + } + + m_fileDialog->deleteLater(); + m_fileDialog = nullptr; +} + +void InteractiveConsole::loadScriptFromUrl(const QUrl &url) +{ + if (m_editorPart) { + m_editorPart->closeUrl(false); + m_editorPart->openUrl(url); + m_editorPart->setHighlightingMode(QStringLiteral("JavaScript/PlasmaDesktop")); + } else { + m_editor->clear(); + m_editor->setEnabled(false); + + if (m_job) { + m_job.data()->kill(); + } + + auto job = KIO::get(url, KIO::Reload, KIO::HideProgressInfo); + connect(job, &KIO::TransferJob::data, this, &InteractiveConsole::scriptFileDataRecvd); + connect(job, &KJob::result, this, &InteractiveConsole::reenableEditor); + m_job = job; + } +} + +void InteractiveConsole::populateTemplatesMenu() +{ + m_snippetsMenu->clear(); + auto templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), [](const KPluginMetaData &metaData) { + return metaData.value(QStringLiteral("X-Plasma-Shell")) == qApp->applicationName(); + }); + std::sort(templates.begin(), templates.end(), [](const KPluginMetaData &left, const KPluginMetaData &right) { + return left.name() < right.name(); + }); + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate")); + for (const auto &templateMetaData : qAsConst(templates)) { + package.setPath(templateMetaData.pluginId()); + const QString scriptFile = package.filePath("mainscript"); + if (!scriptFile.isEmpty()) { + QAction *action = m_snippetsMenu->addAction(templateMetaData.name()); + action->setData(templateMetaData.pluginId()); + } + } +} + +void InteractiveConsole::loadTemplate(QAction *action) +{ + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate"), action->data().toString()); + const QString scriptFile = package.filePath("mainscript"); + if (!scriptFile.isEmpty()) { + loadScriptFromUrl(QUrl::fromLocalFile(scriptFile)); + } +} + +void InteractiveConsole::useTemplate(QAction *action) +{ + QString code("var template = loadTemplate('" + action->data().toString() + "')"); + if (m_editorPart) { + const QList views = m_editorPart->views(); + if (views.isEmpty()) { + m_editorPart->insertLines(m_editorPart->lines(), QStringList() << code); + } else { + KTextEditor::Cursor cursor = views.at(0)->cursorPosition(); + m_editorPart->insertLines(cursor.line(), QStringList() << code); + cursor.setLine(cursor.line() + 1); + views.at(0)->setCursorPosition(cursor); + } + } else { + m_editor->insertPlainText(code); + } +} + +void InteractiveConsole::scriptFileDataRecvd(KIO::Job *job, const QByteArray &data) +{ + Q_ASSERT(m_editor); + + if (job == m_job.data()) { + m_editor->insertPlainText(data); + } +} + +void InteractiveConsole::saveScript() +{ + if (m_editorPart) { + m_editorPart->documentSaveAs(); + return; + } + + delete m_fileDialog; + + m_fileDialog = new QFileDialog(); + m_fileDialog->setAcceptMode(QFileDialog::AcceptSave); + m_fileDialog->setWindowTitle(i18n("Save Script File")); + + QStringList mimetypes; + mimetypes << QStringLiteral("application/javascript"); + m_fileDialog->setMimeTypeFilters(mimetypes); + + connect(m_fileDialog, &QDialog::finished, this, &InteractiveConsole::saveScriptUrlSelected); + m_fileDialog->show(); +} + +void InteractiveConsole::saveScriptUrlSelected(int result) +{ + if (!m_fileDialog) { + return; + } + + if (result == QDialog::Accepted) { + const QUrl url = m_fileDialog->selectedUrls().constFirst(); + if (!url.isEmpty()) { + saveScript(url); + } + } + + m_fileDialog->deleteLater(); + m_fileDialog = nullptr; +} + +void InteractiveConsole::saveScript(const QUrl &url) +{ + // create the folder to save if doesn't exists + QFileInfo info(url.path()); + QDir dir; + dir.mkpath(info.absoluteDir().absolutePath()); + + if (m_editorPart) { + m_editorPart->saveAs(url); + } else { + m_editor->setEnabled(false); + + if (m_job) { + m_job.data()->kill(); + } + + auto job = KIO::put(url, -1, KIO::HideProgressInfo); + connect(job, &KIO::TransferJob::dataReq, this, &InteractiveConsole::scriptFileDataReq); + connect(job, &KJob::result, this, &InteractiveConsole::reenableEditor); + m_job = job; + } +} + +void InteractiveConsole::scriptFileDataReq(KIO::Job *job, QByteArray &data) +{ + Q_ASSERT(m_editor); + + if (!m_job || m_job.data() != job) { + return; + } + + data.append(m_editor->toPlainText().toLocal8Bit()); + m_job.clear(); +} + +void InteractiveConsole::reenableEditor(KJob *job) +{ + Q_ASSERT(m_editor); + if (m_closeWhenCompleted && job->error() != 0) { + close(); + } + + m_closeWhenCompleted = false; + m_editor->setEnabled(true); +} + +void InteractiveConsole::evaluateScript() +{ + // qDebug() << "evaluating" << m_editor->toPlainText(); + m_output->moveCursor(QTextCursor::End); + QTextCursor cursor = m_output->textCursor(); + m_output->setTextCursor(cursor); + + QTextCharFormat format; + format.setFontWeight(QFont::Bold); + format.setFontUnderline(true); + + if (cursor.position() > 0) { + cursor.insertText(QStringLiteral("\n\n")); + } + + QDateTime dt = QDateTime::currentDateTime(); + cursor.insertText(i18n("Executing script at %1", QLocale().toString(dt))); + + format.setFontWeight(QFont::Normal); + format.setFontUnderline(false); + QTextBlockFormat block = cursor.blockFormat(); + block.setLeftMargin(10); + cursor.insertBlock(block, format); + QElapsedTimer t; + t.start(); + + if (m_mode == PlasmaConsole) { + QDBusMessage message = QDBusMessage::createMethodCall(s_plasmaShellService, + QStringLiteral("/PlasmaShell"), + QStringLiteral("org.kde.PlasmaShell"), + QStringLiteral("evaluateScript")); + QList arguments; + arguments << QVariant(m_editorPart->text()); + message.setArguments(arguments); + QDBusMessage reply = QDBusConnection::sessionBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + print(reply.errorMessage()); + } else { + print(reply.arguments().first().toString()); + } + } else if (m_mode == KWinConsole) { + const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/" + s_autosaveFileName; + saveScript(QUrl::fromLocalFile(path)); + + QDBusMessage message = QDBusMessage::createMethodCall(s_kwinService, QStringLiteral("/Scripting"), QString(), QStringLiteral("loadScript")); + QList arguments; + arguments << QVariant(path); + message.setArguments(arguments); + QDBusMessage reply = QDBusConnection::sessionBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + print(reply.errorMessage()); + } else { + const int id = reply.arguments().constFirst().toInt(); + QDBusConnection::sessionBus().connect(s_kwinService, "/" + QString::number(id), QString(), QStringLiteral("print"), this, SLOT(print(QString))); + QDBusConnection::sessionBus() + .connect(s_kwinService, "/" + QString::number(id), QString(), QStringLiteral("printError"), this, SLOT(print(QString))); + message = QDBusMessage::createMethodCall(s_kwinService, "/" + QString::number(id), QString(), QStringLiteral("run")); + reply = QDBusConnection::sessionBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + print(reply.errorMessage()); + } + } + } + + cursor.insertText(QStringLiteral("\n\n")); + format.setFontWeight(QFont::Bold); + // xgettext:no-c-format + cursor.insertText(i18n("Runtime: %1ms", QString::number(t.elapsed())), format); + block.setLeftMargin(0); + cursor.insertBlock(block); + m_output->ensureCursorVisible(); +} + +void InteractiveConsole::clearEditor() +{ + if (m_editorPart) { + m_editorPart->clear(); + } else { + m_editor->clear(); + } +} + +void InteractiveConsole::clearOutput() +{ + m_output->clear(); +} diff --git a/plasma/workspace/interactiveconsole/interactiveconsole.h b/plasma/workspace/interactiveconsole/interactiveconsole.h new file mode 100644 index 0000000000..d2fc3d1470 --- /dev/null +++ b/plasma/workspace/interactiveconsole/interactiveconsole.h @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +class QSplitter; + +class QAction; +class QFileDialog; +class QMenu; +class KTextEdit; +class QTextBrowser; + +class ShellCorona; + +namespace KTextEditor +{ +class Document; +} // namespace KParts + +namespace Plasma +{ +class Corona; +} // namespace Plasma + +class InteractiveConsole : public QDialog +{ + Q_OBJECT + +public: + enum ConsoleMode { + PlasmaConsole, + KWinConsole, + }; + + explicit InteractiveConsole(ConsoleMode mode, QWidget *parent = nullptr); + ~InteractiveConsole() override; + + void setMode(const QString &mode); + QString mode() const; + + void setScriptInterface(QObject *obj); + QObject *scriptEngine() const; + + void loadScript(const QString &path); + +Q_SIGNALS: + void scriptEngineChanged(); + void modeChanged(); + void visibleChanged(bool); + +protected: + void showEvent(QShowEvent *) override; + void closeEvent(QCloseEvent *event) override; + +protected Q_SLOTS: + void print(const QString &string); + void reject() override; + +private Q_SLOTS: + void openScriptFile(); + void saveScript(); + void scriptTextChanged(); + void evaluateScript(); + void clearEditor(); + void clearOutput(); + void scriptFileDataRecvd(KIO::Job *job, const QByteArray &data); + void scriptFileDataReq(KIO::Job *job, QByteArray &data); + void reenableEditor(KJob *job); + void saveScriptUrlSelected(int result); + void openScriptUrlSelected(int result); + void loadScriptFromUrl(const QUrl &url); + void populateTemplatesMenu(); + void loadTemplate(QAction *); + void useTemplate(QAction *); + void modeSelectionChanged(); + +private: + void saveScript(const QUrl &url); + + ShellCorona *m_corona; + QSplitter *m_splitter; + KTextEditor::Document *m_editorPart; + KTextEdit *m_editor; + QTextBrowser *m_output; + QAction *m_loadAction; + QAction *m_saveAction; + QAction *m_clearAction; + QAction *m_executeAction; + QAction *m_plasmaAction; + QAction *m_kwinAction; + QMenu *m_snippetsMenu; + + QFileDialog *m_fileDialog; + QPointer m_job; + bool m_closeWhenCompleted; + ConsoleMode m_mode; + QPointer m_scriptEngine; +}; diff --git a/plasma/workspace/interactiveconsole/main.cpp b/plasma/workspace/interactiveconsole/main.cpp new file mode 100644 index 0000000000..00b11e94b3 --- /dev/null +++ b/plasma/workspace/interactiveconsole/main.cpp @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2021 David Edmundson + SPDX-FileCopyrightText: 2021 Alexander Lohnau + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "interactiveconsole.h" +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + InteractiveConsole::ConsoleMode mode = InteractiveConsole::PlasmaConsole; + + QCommandLineParser parser; + QCommandLineOption plasmaOpt(QStringLiteral("plasma")); + QCommandLineOption kwinOpt(QStringLiteral("kwin")); + parser.addOption(plasmaOpt); + parser.addOption(kwinOpt); + parser.addHelpOption(); + parser.process(app); + if (parser.isSet(plasmaOpt) && parser.isSet(kwinOpt)) { + qWarning() << "Only one mode can be specified when launching the interactive console"; + exit(1); + } else if (parser.isSet(kwinOpt)) { + mode = InteractiveConsole::KWinConsole; + } else if (parser.isSet(plasmaOpt)) { + mode = InteractiveConsole::PlasmaConsole; + } + // set to delete on close + auto console = new InteractiveConsole(mode); + console->show(); + app.exec(); +} diff --git a/plasma/workspace/kcms/CMakeLists.txt b/plasma/workspace/kcms/CMakeLists.txt new file mode 100644 index 0000000000..53eadf8453 --- /dev/null +++ b/plasma/workspace/kcms/CMakeLists.txt @@ -0,0 +1,27 @@ +add_subdirectory(krdb) + +add_subdirectory(desktoptheme) +add_subdirectory(icons) +add_subdirectory(translations) + +if(KUserFeedback_FOUND) + add_subdirectory(feedback) +endif() + +add_subdirectory(style) +add_subdirectory(lookandfeel) +add_subdirectory(colors) +if(X11_Xcursor_FOUND) + add_subdirectory(cursortheme) +endif() + +if(FONTCONFIG_FOUND) + add_subdirectory( kfontinst ) + add_subdirectory( fonts ) +endif() + +add_subdirectory(autostart) +add_subdirectory(formats) +add_subdirectory(notifications) +add_subdirectory(nightcolor) +add_subdirectory(users) diff --git a/plasma/workspace/kcms/autostart/CMakeLists.txt b/plasma/workspace/kcms/autostart/CMakeLists.txt new file mode 100644 index 0000000000..e4ac7f8bc8 --- /dev/null +++ b/plasma/workspace/kcms/autostart/CMakeLists.txt @@ -0,0 +1,19 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_autostart\") + +set(kcm_autostart_PART_SRCS + autostartmodel.cpp + autostart.cpp ) + +kcoreaddons_add_plugin(kcm_autostart SOURCES ${kcm_autostart_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + +target_link_libraries(kcm_autostart KF5::I18n KF5::KIOCore KF5::KIOWidgets KF5::QuickAddons PW::KWorkspace) + +ecm_qt_declare_logging_category(kcm_autostart + HEADER kcm_autostart_debug.h + IDENTIFIER KCM_AUTOSTART_DEBUG + CATEGORY_NAME org.kde.plasma.kcm_autostart +) + +install(FILES kcm_autostart.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +kpackage_install_package(package kcm_autostart kcms) diff --git a/plasma/workspace/kcms/autostart/Messages.sh b/plasma/workspace/kcms/autostart/Messages.sh new file mode 100644 index 0000000000..6a41f58b10 --- /dev/null +++ b/plasma/workspace/kcms/autostart/Messages.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +$XGETTEXT `find . -name '*.cpp' -o -name '*.qml'` -o $podir/kcm_autostart.pot diff --git a/plasma/workspace/kcms/autostart/autostart.cpp b/plasma/workspace/kcms/autostart/autostart.cpp new file mode 100644 index 0000000000..dc8df46708 --- /dev/null +++ b/plasma/workspace/kcms/autostart/autostart.cpp @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Stephen Leaf + SPDX-FileCopyrightText: 2008 Montel Laurent + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "autostart.h" + +K_PLUGIN_CLASS_WITH_JSON(Autostart, "kcm_autostart.json") + +Autostart::Autostart(QObject *parent, const KPluginMetaData &data, const QVariantList &) + : KQuickAddons::ConfigModule(parent, data) + , m_model(new AutostartModel(this)) +{ + setButtons(Help); + + qmlRegisterUncreatableType("org.kde.plasma.kcm.autostart", 1, 0, "AutostartModel", QStringLiteral("Only for enums")); +} + +Autostart::~Autostart() +{ +} + +AutostartModel *Autostart::model() const +{ + return m_model; +} + +void Autostart::load() +{ + m_model->load(); +} + +void Autostart::defaults() +{ +} + +void Autostart::save() +{ +} + +#include "autostart.moc" diff --git a/plasma/workspace/kcms/autostart/autostart.h b/plasma/workspace/kcms/autostart/autostart.h new file mode 100644 index 0000000000..315b1d88f5 --- /dev/null +++ b/plasma/workspace/kcms/autostart/autostart.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Stephen Leaf + SPDX-FileCopyrightText: 2008 Montel Laurent + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "autostartmodel.h" + +class Autostart : public KQuickAddons::ConfigModule +{ + Q_OBJECT + Q_PROPERTY(AutostartModel *model READ model CONSTANT) + +public: + explicit Autostart(QObject *parent, const KPluginMetaData &data, const QVariantList &); + ~Autostart() override; + + void load() override; + void save() override; + void defaults() override; + + AutostartModel *model() const; + +private: + AutostartModel *m_model; +}; diff --git a/plasma/workspace/kcms/autostart/autostartmodel.cpp b/plasma/workspace/kcms/autostart/autostartmodel.cpp new file mode 100644 index 0000000000..cc962cb6e8 --- /dev/null +++ b/plasma/workspace/kcms/autostart/autostartmodel.cpp @@ -0,0 +1,413 @@ +/* + SPDX-FileCopyrightText: 2020 Méven Car + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "autostartmodel.h" +#include "kcm_autostart_debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// FDO user autostart directories are +// .config/autostart which has .desktop files executed by klaunch or systemd, some of which might be scripts + +// Then we have Plasma-specific locations which run scripts +// .config/autostart-scripts which has scripts executed by plasma_session (now migrated to .desktop files) +// .config/plasma-workspace/shutdown which has scripts executed by plasma-shutdown +// .config/plasma-workspace/env which has scripts executed by startplasma + +// in the case of pre-startup they have to end in .sh +// everywhere else it doesn't matter + +// the comment above describes how autostart *currently* works, it is not definitive documentation on how autostart *should* work + +// share/autostart shouldn't be an option as this should be reserved for global autostart entries + +std::optional AutostartModel::loadDesktopEntry(const QString &fileName) +{ + KDesktopFile config(fileName); + const KConfigGroup grp = config.desktopGroup(); + const auto name = config.readName(); + + const bool hidden = grp.readEntry("Hidden", false); + + if (hidden) { + return {}; + } + + const QStringList notShowList = grp.readXdgListEntry("NotShowIn"); + const QStringList onlyShowList = grp.readXdgListEntry("OnlyShowIn"); + const bool enabled = !(notShowList.contains(QLatin1String("KDE")) || (!onlyShowList.isEmpty() && !onlyShowList.contains(QLatin1String("KDE")))); + + if (!enabled) { + return {}; + } + + const auto lstEntry = grp.readXdgListEntry("OnlyShowIn"); + const bool onlyInPlasma = lstEntry.contains(QLatin1String("KDE")); + const QString iconName = !config.readIcon().isEmpty() ? config.readIcon() : QStringLiteral("dialog-scripts"); + const auto kind = AutostartScriptDesktopFile::isAutostartScript(config) ? XdgScripts : XdgAutoStart; // .config/autostart load desktop at startup + const QString tryCommand = grp.readEntry("TryExec"); + + // Try to filter out entries that point to nonexistant programs + // If TryExec is either found in $PATH or is an absolute file path that exists + // This doesn't detect uninstalled Flatpaks for example though + if (!tryCommand.isEmpty() && QStandardPaths::findExecutable(tryCommand).isEmpty() && !QFile::exists(tryCommand)) { + return {}; + } + + return AutostartEntry{name, kind, enabled, fileName, onlyInPlasma, iconName}; +} + +AutostartModel::AutostartModel(QObject *parent) + : QAbstractListModel(parent) + , m_xdgConfigPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)) + , m_xdgAutoStartPath(m_xdgConfigPath.filePath(QStringLiteral("autostart"))) +{ +} + +void AutostartModel::load() +{ + beginResetModel(); + + m_entries.clear(); + + // Creates if doesn't already exist + m_xdgAutoStartPath.mkpath(QStringLiteral(".")); + + // Needed to add all script entries after application entries + QVector scriptEntries; + const auto filesInfo = m_xdgAutoStartPath.entryInfoList(QDir::Files); + for (const QFileInfo &fi : filesInfo) { + if (!KDesktopFile::isDesktopFile(fi.fileName())) { + continue; + } + + const std::optional entry = loadDesktopEntry(fi.absoluteFilePath()); + + if (!entry) { + continue; + } + + if (entry->source == XdgScripts) { + scriptEntries.push_back(entry.value()); + } else { + m_entries.push_back(entry.value()); + } + } + + m_entries.append(scriptEntries); + + loadScriptsFromDir(QStringLiteral("plasma-workspace/env/"), AutostartModel::AutostartEntrySource::PlasmaEnvScripts); + + loadScriptsFromDir(QStringLiteral("plasma-workspace/shutdown/"), AutostartModel::AutostartEntrySource::PlasmaShutdown); + + endResetModel(); +} + +void AutostartModel::loadScriptsFromDir(const QString &subDir, AutostartModel::AutostartEntrySource kind) +{ + QDir dir(m_xdgConfigPath.filePath(subDir)); + // Creates if doesn't already exist + dir.mkpath(QStringLiteral(".")); + + const auto autostartDirFilesInfo = dir.entryInfoList(QDir::Files); + for (const QFileInfo &fi : autostartDirFilesInfo) { + QString fileName = fi.absoluteFilePath(); + const bool isSymlink = fi.isSymLink(); + if (isSymlink) { + fileName = fi.symLinkTarget(); + } + + m_entries.push_back({fileName, kind, true, fi.absoluteFilePath(), false, QStringLiteral("dialog-scripts")}); + } +} + +int AutostartModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_entries.count(); +} + +bool AutostartModel::reloadEntry(const QModelIndex &index, const QString &fileName) +{ + if (!checkIndex(index)) { + return false; + } + + const std::optional newEntry = loadDesktopEntry(fileName); + + if (!newEntry) { + return false; + } + + m_entries.replace(index.row(), newEntry.value()); + Q_EMIT dataChanged(index, index); + return true; +} + +QVariant AutostartModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index)) { + return QVariant(); + } + + const auto &entry = m_entries.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return entry.name; + case Enabled: + return entry.enabled; + case Source: + return entry.source; + case FileName: + return entry.fileName; + case OnlyInPlasma: + return entry.onlyInPlasma; + case IconName: + return entry.iconName; + } + + return QVariant(); +} + +void AutostartModel::addApplication(const KService::Ptr &service) +{ + QString desktopPath; + // It is important to ensure that we make an exact copy of an existing + // desktop file (if selected) to enable users to override global autostarts. + // Also see + // https://bugs.launchpad.net/ubuntu/+source/kde-workspace/+bug/923360 + if (service->desktopEntryName().isEmpty() || service->entryPath().isEmpty()) { + // create a new desktop file in s_desktopPath + desktopPath = m_xdgAutoStartPath.filePath(service->name() + QStringLiteral(".desktop")); + + KDesktopFile desktopFile(desktopPath); + KConfigGroup kcg = desktopFile.desktopGroup(); + kcg.writeEntry("Name", service->name()); + kcg.writeEntry("Exec", service->exec()); + kcg.writeEntry("Icon", service->icon()); + kcg.writeEntry("Path", ""); + kcg.writeEntry("Terminal", service->terminal() ? "True" : "False"); + kcg.writeEntry("Type", "Application"); + desktopFile.sync(); + + } else { + desktopPath = m_xdgAutoStartPath.filePath(service->storageId()); + + QFile::remove(desktopPath); + + // copy original desktop file to new path + KDesktopFile desktopFile(service->entryPath()); + auto newDeskTopFile = desktopFile.copyTo(desktopPath); + newDeskTopFile->sync(); + } + + const QString iconName = !service->icon().isEmpty() ? service->icon() : QStringLiteral("dialog-scripts"); + + const auto entry = AutostartEntry{service->name(), + AutostartModel::AutostartEntrySource::XdgAutoStart, // .config/autostart load desktop at startup + true, + desktopPath, + false, + iconName}; + + int lastApplication = -1; + for (const AutostartEntry &e : qAsConst(m_entries)) { + if (e.source == AutostartModel::AutostartEntrySource::XdgScripts) { + break; + } + ++lastApplication; + } + + // push before the script items + const int index = lastApplication + 1; + + beginInsertRows(QModelIndex(), index, index); + + m_entries.insert(index, entry); + + endInsertRows(); +} + +void AutostartModel::showApplicationDialog(QQuickItem *context) +{ + KOpenWithDialog *owdlg = new KOpenWithDialog(); + owdlg->setAttribute(Qt::WA_DeleteOnClose); + + if (context && context->window()) { + if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { + owdlg->winId(); // so it creates windowHandle + owdlg->windowHandle()->setTransientParent(actualWindow); + owdlg->setModal(true); + } + } + + connect(owdlg, &QDialog::finished, this, [this, owdlg](int result) { + if (result != QDialog::Accepted) { + return; + } + + const KService::Ptr service = owdlg->service(); + + Q_ASSERT(service); + if (!service) { + return; // Don't crash if KOpenWith wasn't able to create service. + } + + addApplication(service); + }); + owdlg->open(); +} + +void AutostartModel::addScript(const QUrl &url, AutostartModel::AutostartEntrySource kind) +{ + const QFileInfo file(url.toLocalFile()); + + if (!file.isAbsolute()) { + Q_EMIT error(i18n("\"%1\" is not an absolute url.", url.toLocalFile())); + return; + } else if (!file.exists()) { + Q_EMIT error(i18n("\"%1\" does not exist.", url.toLocalFile())); + return; + } else if (!file.isFile()) { + Q_EMIT error(i18n("\"%1\" is not a file.", url.toLocalFile())); + return; + } else if (!file.isReadable()) { + Q_EMIT error(i18n("\"%1\" is not readable.", url.toLocalFile())); + return; + } + + const QString fileName = url.fileName(); + + if (kind == AutostartModel::AutostartEntrySource::XdgScripts) { + int lastLoginScript = -1; + for (const AutostartEntry &e : qAsConst(m_entries)) { + if (e.source == AutostartModel::AutostartEntrySource::PlasmaShutdown) { + break; + } + ++lastLoginScript; + } + + AutostartScriptDesktopFile desktopFile(fileName, file.filePath()); + insertScriptEntry(lastLoginScript + 1, fileName, desktopFile.fileName(), kind); + } else if (kind == AutostartModel::AutostartEntrySource::PlasmaShutdown) { + const QUrl destinationScript = QUrl::fromLocalFile(QDir(m_xdgConfigPath.filePath(QStringLiteral("plasma-workspace/shutdown/"))).filePath(fileName)); + KIO::CopyJob *job = KIO::link(url, destinationScript, KIO::HideProgressInfo); + job->setAutoRename(true); + job->setProperty("finalUrl", destinationScript); + + connect(job, &KIO::CopyJob::renamed, this, [](KIO::Job *job, const QUrl &from, const QUrl &to) { + Q_UNUSED(from) + // in case the destination filename had to be renamed + job->setProperty("finalUrl", to); + }); + + connect(job, &KJob::finished, this, [this, url, kind](KJob *theJob) { + if (theJob->error()) { + qCWarning(KCM_AUTOSTART_DEBUG) << "Could not add script entry" << theJob->errorString(); + return; + } + const QUrl dest = theJob->property("finalUrl").toUrl(); + insertScriptEntry(m_entries.size(), dest.fileName(), dest.path(), kind); + }); + + job->start(); + } else { + Q_ASSERT(0); + } +} + +void AutostartModel::insertScriptEntry(int index, const QString &name, const QString &path, AutostartEntrySource kind) +{ + beginInsertRows(QModelIndex(), index, index); + + AutostartEntry entry = AutostartEntry{name, kind, true, path, false, QStringLiteral("dialog-scripts")}; + + m_entries.insert(index, entry); + + endInsertRows(); +} + +void AutostartModel::removeEntry(int row) +{ + const auto entry = m_entries.at(row); + + KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(entry.fileName), KIO::HideProgressInfo); + + connect(job, &KJob::finished, this, [this, row, entry](KJob *theJob) { + if (theJob->error()) { + qCWarning(KCM_AUTOSTART_DEBUG) << "Could not remove entry" << theJob->errorString(); + return; + } + + beginRemoveRows(QModelIndex(), row, row); + m_entries.remove(row); + + endRemoveRows(); + }); + + job->start(); +} + +QHash AutostartModel::roleNames() const +{ + QHash roleNames = QAbstractListModel::roleNames(); + + roleNames.insert(Name, QByteArrayLiteral("name")); + roleNames.insert(Enabled, QByteArrayLiteral("enabled")); + roleNames.insert(Source, QByteArrayLiteral("source")); + roleNames.insert(FileName, QByteArrayLiteral("fileName")); + roleNames.insert(OnlyInPlasma, QByteArrayLiteral("onlyInPlasma")); + roleNames.insert(IconName, QByteArrayLiteral("iconName")); + + return roleNames; +} + +void AutostartModel::editApplication(int row, QQuickItem *context) +{ + const QModelIndex idx = index(row, 0); + + const QString fileName = data(idx, AutostartModel::Roles::FileName).toString(); + KFileItem kfi(QUrl::fromLocalFile(fileName)); + kfi.setDelayedMimeTypes(true); + + KPropertiesDialog *dlg = new KPropertiesDialog(kfi, nullptr); + dlg->setAttribute(Qt::WA_DeleteOnClose); + + if (context && context->window()) { + if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(context->window())) { + dlg->winId(); // so it creates windowHandle + dlg->windowHandle()->setTransientParent(actualWindow); + dlg->setModal(true); + } + } + + connect(dlg, &QDialog::finished, this, [this, idx, dlg](int result) { + if (result == QDialog::Accepted) { + reloadEntry(idx, dlg->item().localPath()); + } + }); + dlg->open(); +} diff --git a/plasma/workspace/kcms/autostart/autostartmodel.h b/plasma/workspace/kcms/autostart/autostartmodel.h new file mode 100644 index 0000000000..6897c9c934 --- /dev/null +++ b/plasma/workspace/kcms/autostart/autostartmodel.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2020 Méven Car + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include + +struct AutostartEntry; +class QQuickItem; + +class AutostartModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit AutostartModel(QObject *parent = nullptr); + + enum Roles { + Name = Qt::DisplayRole, + IconName = Qt::DecorationRole, + Enabled = Qt::UserRole + 1, + Source, + FileName, + OnlyInPlasma, + }; + + enum AutostartEntrySource { + XdgAutoStart = 0, + XdgScripts = 1, + PlasmaShutdown = 2, + PlasmaEnvScripts = 3, + }; + Q_ENUM(AutostartEntrySource) + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + bool reloadEntry(const QModelIndex &index, const QString &fileName); + + Q_INVOKABLE void removeEntry(int row); + Q_INVOKABLE void editApplication(int row, QQuickItem *context); + Q_INVOKABLE void addScript(const QUrl &url, AutostartEntrySource kind); + Q_INVOKABLE void showApplicationDialog(QQuickItem *context); + + void load(); + +Q_SIGNALS: + void error(const QString &message); + +private: + void addApplication(const KService::Ptr &service); + void loadScriptsFromDir(const QString &subDir, AutostartEntrySource kind); + void insertScriptEntry(int index, const QString &name, const QString &path, AutostartModel::AutostartEntrySource kind); + static std::optional loadDesktopEntry(const QString &fileName); + + QDir m_xdgConfigPath; + QDir m_xdgAutoStartPath; + QVector m_entries; +}; + +struct AutostartEntry { + QString name; // Human readable name or script file path. In case of symlinks the target file path + AutostartModel::AutostartEntrySource source; + bool enabled; + QString fileName; // the file backing the entry + bool onlyInPlasma; + QString iconName; +}; +Q_DECLARE_TYPEINFO(AutostartEntry, Q_MOVABLE_TYPE); diff --git a/plasma/workspace/kcms/autostart/kcm_autostart.desktop b/plasma/workspace/kcms/autostart/kcm_autostart.desktop new file mode 100644 index 0000000000..e3494acef6 --- /dev/null +++ b/plasma/workspace/kcms/autostart/kcm_autostart.desktop @@ -0,0 +1,43 @@ +[Desktop Entry] +Name=Autostart +Name[ar]=بدء تلقائي +Name[az]=Avtomatik başlama +Name[ca]=Inici automàtic +Name[cs]=Automatické spuštění +Name[da]=Autostart +Name[de]=Autostart +Name[en_GB]=Autostart +Name[es]=Inicio automático +Name[eu]=Abio automatikoa +Name[fi]=Automaattikäynnistys +Name[fr]=Démarrage automatique +Name[hi]=स्वतः चालू +Name[hsb]=Awtostart +Name[hu]=Automatikus indítás +Name[ia]=Auto starta +Name[it]=Avvio automatico +Name[ko]=자동 시작 +Name[lt]=Automatinis paleidimas +Name[ml]=സ്വയം തുടങ്ങുന്നവ +Name[nl]=Autostart +Name[nn]=Autostart +Name[pa]=ਆਟੋ-ਸਟਾਰਟ +Name[pl]=Samo-uruchamiane +Name[pt]=Arranque +Name[pt_BR]=Iniciar automaticamente +Name[ro]=Pornire automată +Name[ru]=Автозапуск +Name[sk]=Automatické spustenie +Name[sl]=Samodejni zagon +Name[sv]=Automatisk start +Name[ta]=சுயதுவக்கம் +Name[tr]=Otomatik başlat +Name[uk]=Автозапуск +Name[vi]=Khởi động tự động +Name[x-test]=xxAutostartxx +Name[zh_CN]=自动启动 +Encoding=UTF-8 +Type=Application +Exec=systemsettings kcm_autostart +NoDisplay=true +Icon=system-run diff --git a/plasma/workspace/kcms/autostart/kcm_autostart.json b/plasma/workspace/kcms/autostart/kcm_autostart.json new file mode 100644 index 0000000000..fedcb65e9b --- /dev/null +++ b/plasma/workspace/kcms/autostart/kcm_autostart.json @@ -0,0 +1,97 @@ +{ + "KPlugin": { + "Description": "Automatically Started Applications", + "Description[ar]": "التطبيقات التي تبدأ آليا", + "Description[az]": "Avtomatik işə düşən tətbiqlər", + "Description[ca]": "Aplicacions iniciades automàticament", + "Description[cs]": "Automaticky spouštěné aplikace", + "Description[de]": "Automatisch zu startende Anwendungen", + "Description[en_GB]": "Automatically Started Applications", + "Description[es]": "Aplicaciones que se inician automáticamente", + "Description[eu]": "Automatikoki abiatutako aplikazioak", + "Description[fi]": "Automaattisesti käynnistettävät ohjelmat", + "Description[fr]": "Applications automatiquement démarrées", + "Description[hu]": "Automatikusan indított alkalmazások", + "Description[ia]": "Applicationes initiate automaticamente", + "Description[it]": "Applicazioni avviate automaticamente", + "Description[ko]": "자동으로 시작되는 프로그램", + "Description[lt]": "Automatiškai paleidžiamos programos", + "Description[nl]": "Automatisch gestarte toepassingen", + "Description[nn]": "Automatisk starta program", + "Description[pa]": "ਆਪਣੇ-ਆਪ ਸ਼ੁਰੂ ਹੋਈਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ", + "Description[pl]": "Samoczynnie uruchamiane programy", + "Description[pt_BR]": "Aplicativos iniciados automaticamente", + "Description[ro]": "Aplicații pornite automat", + "Description[ru]": "Автоматический запуск приложений", + "Description[sk]": "Automaticky spúšťané aplikácie", + "Description[sl]": "Samodejno zagnani programi", + "Description[sv]": "Program som startas automatiskt", + "Description[ta]": "தானாக துவக்கப்படும் செயலிகள்", + "Description[tr]": "Otomatik Başlatılan Uygulamalar", + "Description[uk]": "Програми, що запускаються автоматично", + "Description[vi]": "Các ứng dụng được khởi động tự động", + "Description[x-test]": "xxAutomatically Started Applicationsxx", + "Description[zh_CN]": "自动启动的应用程序", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "system-run", + "Name": "Autostart", + "Name[ar]": "بدء تلقائي", + "Name[az]": "Avtomatik başlama", + "Name[ca]": "Inici automàtic", + "Name[cs]": "Automatické spuštění", + "Name[da]": "Autostart", + "Name[de]": "Autostart", + "Name[en_GB]": "Autostart", + "Name[es]": "Inicio automático", + "Name[eu]": "Abio automatikoa", + "Name[fi]": "Automaattikäynnistys", + "Name[fr]": "Démarrage automatique", + "Name[hi]": "स्वतः चालू", + "Name[hsb]": "Awtostart", + "Name[hu]": "Automatikus indítás", + "Name[ia]": "Auto starta", + "Name[it]": "Avvio automatico", + "Name[ko]": "자동 시작", + "Name[lt]": "Automatinis paleidimas", + "Name[ml]": "സ്വയം തുടങ്ങുന്നവ", + "Name[nl]": "Autostart", + "Name[nn]": "Autostart", + "Name[pa]": "ਆਟੋ-ਸਟਾਰਟ", + "Name[pl]": "Samo-uruchamiane", + "Name[pt]": "Arranque", + "Name[pt_BR]": "Iniciar automaticamente", + "Name[ro]": "Pornire automată", + "Name[ru]": "Автозапуск", + "Name[sk]": "Automatické spustenie", + "Name[sl]": "Samodejni zagon", + "Name[sv]": "Automatisk start", + "Name[ta]": "சுயதுவக்கம்", + "Name[tr]": "Otomatik başlatma", + "Name[uk]": "Автозапуск", + "Name[vi]": "Khởi động tự động", + "Name[x-test]": "xxAutostartxx", + "Name[zh_CN]": "自动启动" + }, + "X-DocPath": "kcontrol/autostart/index.html", + "X-KDE-Keywords": "Autostart Manager,system startup,plasma start,cron,auto,start,start up,boot,load,session,startup,launch,login", + "X-KDE-Keywords[az]": "Avtobaşlama meneceri,sistemin açılması,plasma'nın açılması,cron,avtomatik,başlama,başladılma,önyükləmə,yüklənmə,sessiya,işə düşmə,giriş", + "X-KDE-Keywords[ca]": "Gestor de l'inici automàtic,inici del sistema,inici del Plasma,cron,automàtic,inici,engegar,engegada,càrrega,sessió,llançament,inici de sessió", + "X-KDE-Keywords[es]": "Gestor de inicio automático,inicio del sistema,inicio de plasma,cron,automático,inicio,arranque,cargar,sesión,lanzamiento,lanzar,inicio de sesión", + "X-KDE-Keywords[fr]": "Gestionnaire de démarrage automatique, démarrage du système, démarrage de Plasma, cron, auto, démarrage, boot, chargement, session, démarrage, lancement, connexion", + "X-KDE-Keywords[hu]": "Automatikus indítás kezelő,rendszerindítás,plasma indítása,cron,auto,start,indítás,boot,betöltés,munkamenet,indulás,indítás,bejelentkezés", + "X-KDE-Keywords[it]": "Gestore avvio automatico,avvio del sistema,avvio di plasma,cron,automatico,avvio,sessione,caricamento,accesso", + "X-KDE-Keywords[ko]": "자동 시작,자동시작,시작프로그램,시스템 시작,plasma 시작,자동,시작,부트,부팅,로드,로딩,세션,로그인", + "X-KDE-Keywords[nl]": "Autostartbeheerder,systeem,opstarten systeem,opstarten plasma,automatisch,starten,opstarten,systeemopstart,boot,laden,sessie,aanmelden", + "X-KDE-Keywords[pt_BR]": "Gerenciado de inicialização,inicialização do sistema,início do plasma,cron, automático,início,inicialização,boot,carregar,sessão,inicialização,lançar,lançamento", + "X-KDE-Keywords[sl]": "Upravljalnik samodejnega zagona,zagon sistema,zagon Plasme,zagon cron;samodejno;zagon;hladni zagon;inicialno nalaganje;cron,nalaganje,seja;prijava", + "X-KDE-Keywords[sv]": "Hantering av autostart,systemstart,plasma start,cron,auto,start,ladda,session,inloggning", + "X-KDE-Keywords[uk]": "AAutostart Manager,system startup,plasma start,cron,auto,start,start up,boot,load,session,startup,launch,login,керування автозапуском,автозапуск,запуск,запуск системи,запуск плазми,завантаження,сеанс,запуск,вхід,логін,лоґін", + "X-KDE-Keywords[vi]": "trình quản lí khởi động tự động,khởi động hệ thống,khởi động plasma,cron,khởi động,tự động,tải,phiên,đăng nhập", + "X-KDE-Keywords[x-test]": "xxAutostart Managerxx,xxsystem startupxx,xxplasma startxx,xxcronxx,xxautoxx,xxstartxx,xxstart upxx,xxbootxx,xxloadxx,xxsessionxx,xxstartupxx,xxlaunchxx,xxloginxx", + "X-KDE-System-Settings-Parent-Category": "session", + "X-KDE-Weight": 30 +} diff --git a/plasma/workspace/kcms/autostart/package/contents/ui/main.qml b/plasma/workspace/kcms/autostart/package/contents/ui/main.qml new file mode 100644 index 0000000000..2ff6cc880f --- /dev/null +++ b/plasma/workspace/kcms/autostart/package/contents/ui/main.qml @@ -0,0 +1,185 @@ +/* + SPDX-FileCopyrightText: 2020 Nicolas Fella Add… button below to add some") + } + } + + footer: Row { + spacing: Kirigami.Units.largeSpacing + + Loader { + id: loginFileDialogLoader + + active: false + + sourceComponent: FileDialog { + id: loginFileDialog + title: i18n("Choose Login Script") + folder: shortcuts.home + selectMultiple: false + onAccepted: { + kcm.model.addScript(loginFileDialog.fileUrl, AutostartModel.XdgScripts) + loginFileDialogLoader.active = false + } + + onRejected: loginFileDialogLoader.active = false + + Component.onCompleted: open() + } + } + + Loader { + id: logoutFileDialogLoader + + active: false + + sourceComponent: FileDialog { + id: logoutFileDialog + title: i18n("Choose Logout Script") + folder: shortcuts.home + selectMultiple: false + onAccepted: { + kcm.model.addScript(logoutFileDialog.fileUrl, AutostartModel.PlasmaShutdown) + logoutFileDialogLoader.active = false + } + + onRejected: logoutFileDialogLoader.active = false + + Component.onCompleted: open() + } + } + + Button { + id: menuButton + + icon.name: "list-add" + text: i18n("Add…") + + checkable: true + checked: menu.opened + onClicked: menu.opened? menu.close() : menu.open() + } + + Menu { + id: menu + y: -height + + modal: true + dim: false + + MenuItem { + text: i18n("Add Application…") + icon.name: "list-add" + + onClicked: kcm.model.showApplicationDialog(root) + } + MenuItem { + text: i18n("Add Login Script…") + icon.name: "list-add" + + onClicked: loginFileDialogLoader.active = true + } + MenuItem { + text: i18n("Add Logout Script…") + icon.name: "list-add" + + onClicked: logoutFileDialogLoader.active = true + } + } + } +} diff --git a/plasma/workspace/kcms/colors/CMakeLists.txt b/plasma/workspace/kcms/colors/CMakeLists.txt new file mode 100644 index 0000000000..045c6f2413 --- /dev/null +++ b/plasma/workspace/kcms/colors/CMakeLists.txt @@ -0,0 +1,75 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_colors\") + +set(kcm_colors_SRCS + ../kcms-common.cpp + colors.cpp + colorsapplicator.cpp + colorsmodel.cpp + filterproxymodel.cpp +) + +kcmutils_generate_module_data( + kcm_colors_SRCS + MODULE_DATA_HEADER colorsdata.h + MODULE_DATA_CLASS_NAME ColorsData + SETTINGS_HEADERS colorssettings.h + SETTINGS_CLASSES ColorsSettings +) + +# needed for krdb +kconfig_add_kcfg_files(kcm_colors_SRCS colorssettings.kcfgc GENERATE_MOC) + +kcoreaddons_add_plugin(kcm_colors SOURCES ${kcm_colors_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") +target_link_libraries(kcm_colors + Qt::DBus + KF5::KCMUtils + KF5::CoreAddons + KF5::Declarative + KF5::GuiAddons + KF5::I18n + KF5::KIOCore + KF5::KIOWidgets + KF5::NewStuffCore + KF5::QuickAddons + KF5::WindowSystem + krdb +) + +if(X11_FOUND) + target_link_libraries(kcm_colors X11::X11 Qt::X11Extras) +endif() + +set(plasma-apply-colorscheme_SRCS + plasma-apply-colorscheme.cpp + colorsapplicator.cpp + colorsmodel.cpp + ../kcms-common.cpp + ../krdb/krdb.cpp +) + +kconfig_add_kcfg_files(plasma-apply-colorscheme_SRCS colorssettings.kcfgc GENERATE_MOC) + +add_executable(plasma-apply-colorscheme ${plasma-apply-colorscheme_SRCS}) + +target_link_libraries(plasma-apply-colorscheme + Qt::Core + Qt::DBus + Qt::Gui + Qt::X11Extras + KF5::GuiAddons + KF5::KCMUtils + KF5::I18n + KF5::WindowSystem + PW::KWorkspace + X11::X11 +) + +install(FILES colorssettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES kcm_colors.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(TARGETS plasma-apply-colorscheme DESTINATION ${KDE_INSTALL_BINDIR}) +install(FILES colorschemes.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) + +kpackage_install_package(package kcm_colors kcms) + +add_subdirectory(editor) diff --git a/plasma/workspace/kcms/colors/Messages.sh b/plasma/workspace/kcms/colors/Messages.sh new file mode 100644 index 0000000000..91361002bf --- /dev/null +++ b/plasma/workspace/kcms/colors/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name "*.ui" -o -name "*.kcfg"` >> rc.cpp +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_colors.pot +rm -f rc.cpp diff --git a/plasma/workspace/kcms/colors/README.i18n b/plasma/workspace/kcms/colors/README.i18n new file mode 100644 index 0000000000..1033fcb9b4 --- /dev/null +++ b/plasma/workspace/kcms/colors/README.i18n @@ -0,0 +1,54 @@ +There are two sets of preview strings in the color kcm, with special translation needs. In general, length of the translations, and "gist"-level clarity is more important than precise translation. It should be easy to identify what color role a string represents, but there are size considerations that should be taken into account. The tooltips should be translated normally. + + + +When translating the color kcm, it is helpful to understand the intended meaning of the various color roles in order to chose the best possible translation if more than one would be possible. Hopefully the sets (view, window, button, selection, tooltip) are obvious, as well as normal background/foreground. The others are as follows: + +Alternate [Background]: used in lists when every other row uses a different color; "different". +Inactive: comments, something which is old, unimportant/uninteresting/secondary text +Active: e.g. something new, something active (currently engaged in activity), something requesting attention, a hovered hyperlink +Link: hyperlink, somewhere the user can go, new history +Visited: visited hyperlinks, somewhere the user has been, old history +Negative: "bad", untrusted, unreliable, a mistake, an error the user made, an error that occurred +Neutral: warning, encrypted, anything "between" negative and positive +Positive: success, completion messages, trusted content + +For Link, although other uses are possible, translate as referring to hyperlinks to minimize user confusion. + + + +The "generic" preview uses these strings (all with disambiguation "color-kcm-preview"): +- Window Text +- Push Button +- Normal Text +- Selected Text +- link +- visited +- a +- i +- ! +- = +- + + +The two-word strings may be translated normally, though it is suggested to omit the translation of "Text" if the strings would be excessively (around 20+ characters) long and doing so does not cause confusion (i.e. substantially change the conveyed meaning). The "link" and "visited" strings should be translated according to the notes above, ideally keeping the length to four to eight characters; abbreviation (to not less than four characters if possible) is acceptable as the tooltips will provide more complete descriptions. + +The remaining one-character strings represent (respectively) the text roles active (a), inactive (i), negative (!), neutral (=) and positive (+). These should be translated as one- or two-character strings which uniquely correspond to the aforementioned text roles, ideally taking the first letter of the corresponding translation, or maintaining the symbols (!, =, +) as-is if culturally appropriate. + +Ideographic languages (e.g. Japanese, Mandarin, etc.) where the single-character strings can be replaced with one or two characters representing the full word should do so. These should also, if needed, consider a phonetic representation or relevant phrase for the longer elements, to keep the translations close to the same number of characters as the English. + + + +The "set" preview uses these strings (all with disambiguation "color-kcm-set-preview"): +- normal +- link +- visited +- active +- inactive +- alternate +- negative +- neutral +- positive +- hover +- focus + +These live in a preview widget that is two rows tall by nine items wide, and is often the determining factor in the minimum width of the dialog. For this reason, it is important to keep the strings SHORT. To ensure each text block is of sufficient size to provide an adequate preview, each should be at least a few characters long, but please try to limit them to no more than eight or ten characters on average. Abbreviation is fine and encouraged if necessary to achieve this average. In particular, PLEASE avoid adding the translated equivalent of " text" to these strings if at all possible; the tooltips will disambiguate if required. If these objectives cannot be achieved via direct translation, semantic translation (e.g. "negative" -> i18n("oops!"), "neutral" -> i18n("meh")) would be better. (Ideographic languages, which may need to ADD text to achieve a minimum length, should ignore the previous two sentences.) Note also that "hover" and "focus" are actually "decoration" roles, so e.g. "hover text" is incorrect (should be "hover decoration"). diff --git a/plasma/workspace/kcms/colors/colors.cpp b/plasma/workspace/kcms/colors/colors.cpp new file mode 100644 index 0000000000..96fc39ef11 --- /dev/null +++ b/plasma/workspace/kcms/colors/colors.cpp @@ -0,0 +1,411 @@ +/* + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + SPDX-FileCopyrightText: 2016 Olivier Churlaud + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "colors.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include "krdb.h" + +#include "colorsapplicator.h" +#include "colorsdata.h" +#include "colorsmodel.h" +#include "colorssettings.h" +#include "filterproxymodel.h" + +#include "../kcms-common_p.h" + +K_PLUGIN_FACTORY_WITH_JSON(KCMColorsFactory, "kcm_colors.json", registerPlugin(); registerPlugin();) + +KCMColors::KCMColors(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + , m_model(new ColorsModel(this)) + , m_filteredModel(new FilterProxyModel(this)) + , m_data(new ColorsData(this)) + , m_config(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))) +{ + auto uri = "org.kde.private.kcms.colors"; + qmlRegisterUncreatableType(uri, 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + + connect(m_model, &ColorsModel::pendingDeletionsChanged, this, &KCMColors::settingsChanged); + + connect(m_model, &ColorsModel::selectedSchemeChanged, this, [this](const QString &scheme) { + m_selectedSchemeDirty = true; + colorsSettings()->setColorScheme(scheme); + }); + + connect(colorsSettings(), &ColorsSettings::colorSchemeChanged, this, [this] { + m_model->setSelectedScheme(colorsSettings()->colorScheme()); + }); + + connect(colorsSettings(), &ColorsSettings::accentColorChanged, this, &KCMColors::accentColorChanged); + + connect(m_model, &ColorsModel::selectedSchemeChanged, m_filteredModel, &FilterProxyModel::setSelectedScheme); + m_filteredModel->setSourceModel(m_model); +} + +KCMColors::~KCMColors() +{ + m_config->markAsClean(); +} + +ColorsModel *KCMColors::model() const +{ + return m_model; +} + +FilterProxyModel *KCMColors::filteredModel() const +{ + return m_filteredModel; +} + +ColorsSettings *KCMColors::colorsSettings() const +{ + return m_data->settings(); +} + +QColor KCMColors::accentColor() const +{ + const QColor color = colorsSettings()->accentColor(); + if (!color.isValid()) { + return QColor(Qt::transparent); + } + return color; +} + +void KCMColors::setAccentColor(const QColor &accentColor) +{ + colorsSettings()->setAccentColor(accentColor); + Q_EMIT settingsChanged(); +} + +bool KCMColors::downloadingFile() const +{ + return m_tempCopyJob; +} + +void KCMColors::knsEntryChanged(KNSCore::EntryWrapper *entry) +{ + if (!entry) { + return; + } + m_model->load(); + + // If a new theme was installed, select the first color file in it + QStringList installedThemes; + const QString suffix = QStringLiteral(".colors"); + if (entry->entry().status() == KNS3::Entry::Installed) { + for (const QString &path : entry->entry().installedFiles()) { + const QString fileName = path.section(QLatin1Char('/'), -1, -1); + + const int suffixPos = fileName.indexOf(suffix); + if (suffixPos != fileName.length() - suffix.length()) { + continue; + } + + installedThemes.append(fileName.left(suffixPos)); + } + + if (!installedThemes.isEmpty()) { + // The list is sorted by (potentially translated) name + // but that would require us parse every file, so this should be close enough + std::sort(installedThemes.begin(), installedThemes.end()); + + m_model->setSelectedScheme(installedThemes.constFirst()); + } + } +} + +void KCMColors::loadSelectedColorScheme() +{ + colorsSettings()->config()->reparseConfiguration(); + colorsSettings()->read(); + const QString schemeName = colorsSettings()->colorScheme(); + + // If the scheme named in kdeglobals doesn't exist, show a warning and use default scheme + if (m_model->indexOfScheme(schemeName) == -1) { + m_model->setSelectedScheme(colorsSettings()->defaultColorSchemeValue()); + // These are normally synced but initially the model doesn't Q_EMIT a change to avoid the + // Apply button from being enabled without any user interaction. Sync manually here. + m_filteredModel->setSelectedScheme(colorsSettings()->defaultColorSchemeValue()); + Q_EMIT showSchemeNotInstalledWarning(schemeName); + } else { + m_model->setSelectedScheme(schemeName); + m_filteredModel->setSelectedScheme(schemeName); + } + setNeedsSave(false); +} + +void KCMColors::installSchemeFromFile(const QUrl &url) +{ + if (url.isLocalFile()) { + installSchemeFile(url.toLocalFile()); + return; + } + + if (m_tempCopyJob) { + return; + } + + m_tempInstallFile.reset(new QTemporaryFile()); + if (!m_tempInstallFile->open()) { + Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); + m_tempInstallFile.reset(); + return; + } + + // Ideally we copied the file into the proper location right away but + // (for some reason) we determine the file name from the "Name" inside the file + m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + Q_EMIT downloadingFileChanged(); + + connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + if (job->error() != KJob::NoError) { + Q_EMIT showErrorMessage(i18n("Unable to download the color scheme: %1", job->errorText())); + return; + } + + installSchemeFile(m_tempInstallFile->fileName()); + m_tempInstallFile.reset(); + }); + connect(m_tempCopyJob, &QObject::destroyed, this, &KCMColors::downloadingFileChanged); +} + +void KCMColors::installSchemeFile(const QString &path) +{ + KSharedConfigPtr config = KSharedConfig::openConfig(path, KConfig::SimpleConfig); + + KConfigGroup group(config, "General"); + const QString name = group.readEntry("Name"); + + if (name.isEmpty()) { + Q_EMIT showErrorMessage(i18n("This file is not a color scheme file.")); + return; + } + + // Do not overwrite another scheme + int increment = 0; + QString newName = name; + QString testpath; + do { + if (increment) { + newName = name + QString::number(increment); + } + testpath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(newName)); + increment++; + } while (!testpath.isEmpty()); + + QString newPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/color-schemes/"); + + if (!QDir().mkpath(newPath)) { + Q_EMIT showErrorMessage(i18n("Failed to create 'color-scheme' data folder.")); + return; + } + + newPath += newName + QLatin1String(".colors"); + + if (!QFile::copy(path, newPath)) { + Q_EMIT showErrorMessage(i18n("Failed to copy color scheme into 'color-scheme' data folder.")); + return; + } + + // Update name + KSharedConfigPtr config2 = KSharedConfig::openConfig(newPath, KConfig::SimpleConfig); + KConfigGroup group2(config2, "General"); + group2.writeEntry("Name", newName); + config2->sync(); + + m_model->load(); + + const auto results = m_model->match(m_model->index(0, 0), ColorsModel::SchemeNameRole, newName, 1, Qt::MatchExactly); + if (!results.isEmpty()) { + m_model->setSelectedScheme(newName); + } + + Q_EMIT showSuccessMessage(i18n("Color scheme installed successfully.")); +} + +void KCMColors::editScheme(const QString &schemeName, QQuickItem *ctx) +{ + if (m_editDialogProcess) { + return; + } + + QModelIndex idx = m_model->index(m_model->indexOfScheme(schemeName), 0); + + m_editDialogProcess = new QProcess(this); + connect(m_editDialogProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitCode); + Q_UNUSED(exitStatus); + + const auto savedThemes = QString::fromUtf8(m_editDialogProcess->readAllStandardOutput()).split(QLatin1Char('\n'), Qt::SkipEmptyParts); + + if (!savedThemes.isEmpty()) { + m_model->load(); // would be cool to just reload/add the changed/new ones + + // If the currently active scheme was edited, consider settings dirty even if the scheme itself didn't change + if (savedThemes.contains(colorsSettings()->colorScheme())) { + m_activeSchemeEdited = true; + settingsChanged(); + } + + m_model->setSelectedScheme(savedThemes.last()); + } + + m_editDialogProcess->deleteLater(); + m_editDialogProcess = nullptr; + }); + + QStringList args; + args << idx.data(ColorsModel::SchemeNameRole).toString(); + if (idx.data(ColorsModel::RemovableRole).toBool()) { + args << QStringLiteral("--overwrite"); + } + + if (ctx && ctx->window()) { + // QQuickWidget, used for embedding QML KCMs, renders everything into an offscreen window + // Qt is able to resolve this on its own when setting transient parents in-process. + // However, since we pass the ID to an external process which has no idea of this + // we need to resolve the actual window we end up showing in. + if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(ctx->window())) { + if (KWindowSystem::isPlatformX11()) { + // TODO wayland: once we have foreign surface support + args << QStringLiteral("--attach") << (QStringLiteral("x11:") + QString::number(actualWindow->winId())); + } + } + } + + m_editDialogProcess->start(QStringLiteral("kcolorschemeeditor"), args); +} + +bool KCMColors::isSaveNeeded() const +{ + return m_activeSchemeEdited || !m_model->match(m_model->index(0, 0), ColorsModel::PendingDeletionRole, true).isEmpty() || colorsSettings()->isSaveNeeded(); +} + +void KCMColors::load() +{ + ManagedConfigModule::load(); + m_model->load(); + + m_config->markAsClean(); + m_config->reparseConfiguration(); + + loadSelectedColorScheme(); + + { + KConfig cfg(QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals); + KConfigGroup group(m_config, "General"); + group = KConfigGroup(&cfg, "X11"); + m_applyToAlien = group.readEntry("exportKDEColors", true); + } + + // If need save is true at the end of load() function, it will stay disabled forever. + // setSelectedScheme() call due to unexisting scheme name in kdeglobals will trigger a need to save. + // this following call ensure the apply button will work properly. + setNeedsSave(false); +} + +void KCMColors::save() +{ + // We need to save the colors change first, to avoid a situation, + // when we announced that the color scheme has changed, but + // the colors themselves in the color scheme have not yet + if (m_selectedSchemeDirty || m_activeSchemeEdited || colorsSettings()->isSaveNeeded()) { + saveColors(); + } + ManagedConfigModule::save(); + notifyKcmChange(GlobalChangeType::PaletteChanged); + m_activeSchemeEdited = false; + + processPendingDeletions(); +} + +void KCMColors::saveColors() +{ + const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(m_model->selectedScheme())); + // hard to figure out why mutating from the colours settings + // doesn't affect the applicator's view on config, but operating on the + // globalConfig directly works. + // code already a mess, so might as well just do what works. + KSharedConfigPtr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); + + auto setGlobals = [=]() { + globalConfig->group("General").writeEntry("AccentColor", QColor()); + if (accentColor() != QColor(Qt::transparent)) { + globalConfig->group("General").writeEntry("AccentColor", accentColor(), KConfig::Notify); + } else { + globalConfig->group("General").deleteEntry("AccentColor", KConfig::Notify); + } + }; + + setGlobals(); + applyScheme(path, colorsSettings()->config()); + m_selectedSchemeDirty = false; + setGlobals(); +} + +QColor KCMColors::accentBackground(const QColor& accent, const QColor& background) +{ + return ::accentBackground(accent, background); +} + +QColor KCMColors::accentForeground(const QColor& accent, const bool& isActive) +{ + return ::accentForeground(accent, isActive); +} + +void KCMColors::processPendingDeletions() +{ + const QStringList pendingDeletions = m_model->pendingDeletions(); + + for (const QString &schemeName : pendingDeletions) { + Q_ASSERT(schemeName != m_model->selectedScheme()); + + const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(schemeName)); + + auto *job = KIO::del(QUrl::fromLocalFile(path), KIO::HideProgressInfo); + // needs to block for it to work on "OK" where the dialog (kcmshell) closes + job->exec(); + } + + m_model->removeItemsPendingDeletion(); +} + +#include "colors.moc" diff --git a/plasma/workspace/kcms/colors/colors.h b/plasma/workspace/kcms/colors/colors.h new file mode 100644 index 0000000000..743e1d44fe --- /dev/null +++ b/plasma/workspace/kcms/colors/colors.h @@ -0,0 +1,112 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +#include + +#include + +#include "colorsmodel.h" +#include "colorssettings.h" + +class QProcess; +class QTemporaryFile; + +namespace KIO +{ +class FileCopyJob; +} + +class FilterProxyModel; +class ColorsData; + +class KCMColors : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + + Q_PROPERTY(ColorsModel *model READ model CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) + Q_PROPERTY(ColorsSettings *colorsSettings READ colorsSettings CONSTANT) + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + + Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) + +public: + KCMColors(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~KCMColors() override; + + enum SchemeFilter { + AllSchemes, + LightSchemes, + DarkSchemes, + }; + Q_ENUM(SchemeFilter) + + ColorsModel *model() const; + FilterProxyModel *filteredModel() const; + ColorsSettings *colorsSettings() const; + bool downloadingFile() const; + + Q_INVOKABLE void loadSelectedColorScheme(); + Q_INVOKABLE void knsEntryChanged(KNSCore::EntryWrapper *entry); + + QColor accentColor() const; + void setAccentColor(const QColor& accentColor); + void resetAccentColor(); + Q_SIGNAL void accentColorChanged(); + + Q_INVOKABLE void installSchemeFromFile(const QUrl &url); + + Q_INVOKABLE void editScheme(const QString &schemeName, QQuickItem *ctx); + + Q_INVOKABLE QColor accentBackground(const QColor& accent, const QColor& background); + Q_INVOKABLE QColor accentForeground(const QColor& accent, const bool& isActive); + +public Q_SLOTS: + void load() override; + void save() override; + +Q_SIGNALS: + void downloadingFileChanged(); + + void showSuccessMessage(const QString &message); + void showErrorMessage(const QString &message); + + void showSchemeNotInstalledWarning(const QString &schemeName); + +private: + bool isSaveNeeded() const override; + + void saveColors(); + void processPendingDeletions(); + + void installSchemeFile(const QString &path); + + ColorsModel *m_model; + FilterProxyModel *m_filteredModel; + ColorsData *m_data; + + bool m_selectedSchemeDirty = false; + bool m_activeSchemeEdited = false; + + bool m_applyToAlien = true; + + QProcess *m_editDialogProcess = nullptr; + + KSharedConfigPtr m_config; + + QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; +}; diff --git a/plasma/workspace/kcms/colors/colorsapplicator.cpp b/plasma/workspace/kcms/colors/colorsapplicator.cpp new file mode 100644 index 0000000000..db47306947 --- /dev/null +++ b/plasma/workspace/kcms/colors/colorsapplicator.cpp @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + SPDX-FileCopyrightText: 2021 Benjamin Port + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "../kcms-common_p.h" +#include "../krdb/krdb.h" + +#include +#include + +#include +#include + +#include "colorsapplicator.h" + +static void copyEntry(KConfigGroup &from, KConfigGroup &to, const QString &entry, KConfig::WriteConfigFlags writeConfigFlag = KConfig::Normal) +{ + if (from.hasKey(entry)) { + to.writeEntry(entry, from.readEntry(entry), writeConfigFlag); + } +} + +void applyScheme(const QString &colorSchemePath, KConfig *configOutput, KConfig::WriteConfigFlags writeConfigFlag) +{ + KSharedConfigPtr globalConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); + globalConfig->sync(); + + const auto hasAccent = [globalConfig]() { + return globalConfig->group("General").hasKey("AccentColor"); + }; + const auto getAccent = [globalConfig]() { + return globalConfig->group("General").readEntry("AccentColor", QColor()); + }; + + // Using KConfig::SimpleConfig because otherwise Header colors won't be + // rewritten when a new color scheme is loaded. + KSharedConfigPtr config = KSharedConfig::openConfig(colorSchemePath, KConfig::SimpleConfig); + + const auto accentActiveTitlebar = config->group("General").readEntry("accentActiveTitlebar", false); + const auto accentInactiveTitlebar = config->group("General").readEntry("accentInactiveTitlebar", false); + + const QStringList colorSetGroupList{QStringLiteral("Colors:View"), + QStringLiteral("Colors:Window"), + QStringLiteral("Colors:Button"), + QStringLiteral("Colors:Selection"), + QStringLiteral("Colors:Tooltip"), + QStringLiteral("Colors:Complementary"), + QStringLiteral("Colors:Header")}; + + const QStringList colorSetKeyList{QStringLiteral("BackgroundNormal"), + QStringLiteral("BackgroundAlternate"), + QStringLiteral("ForegroundNormal"), + QStringLiteral("ForegroundInactive"), + QStringLiteral("ForegroundActive"), + QStringLiteral("ForegroundLink"), + QStringLiteral("ForegroundVisited"), + QStringLiteral("ForegroundNegative"), + QStringLiteral("ForegroundNeutral"), + QStringLiteral("ForegroundPositive"), + QStringLiteral("DecorationFocus"), + QStringLiteral("DecorationHover")}; + + const QStringList accentList{QStringLiteral("ForegroundActive"), + QStringLiteral("ForegroundLink"), + QStringLiteral("DecorationFocus"), + QStringLiteral("DecorationHover")}; + + for (auto item : colorSetGroupList) { + configOutput->deleteGroup(item); + + // Not all color schemes have header colors; in this case we don't want + // to write out any header color data because then various things will think + // the color scheme *does* have header colors, which it mostly doesn't, and + // things will visually break in creative ways + if (item == QStringLiteral("Colors:Header") && !config->hasGroup(QStringLiteral("Colors:Header"))) { + continue; + } + + KConfigGroup sourceGroup(config, item); + KConfigGroup targetGroup(configOutput, item); + + for (const auto &entry : colorSetKeyList) { + if (hasAccent() && accentList.contains(entry)) { + targetGroup.writeEntry(entry, getAccent()); + } else { + copyEntry(sourceGroup, targetGroup, entry); + } + } + + if (item == QStringLiteral("Colors:Selection") && hasAccent()) { + QColor accentbg = accentBackground(getAccent(), config->group("Colors:View").readEntry("BackgroundNormal", QColor())); + for (const auto& entry : {QStringLiteral("BackgroundNormal"), QStringLiteral("BackgroundAlternate")}) { + targetGroup.writeEntry(entry, accentbg); + } + for (const auto& entry : {QStringLiteral("ForegroundNormal"), QStringLiteral("ForegroundInactive")}) { + targetGroup.writeEntry(entry, accentForeground(accentbg, true)); + } + } + + if (sourceGroup.hasGroup("Inactive")) { + sourceGroup = sourceGroup.group("Inactive"); + targetGroup = targetGroup.group("Inactive"); + + for (const auto &entry : colorSetKeyList) { + copyEntry(sourceGroup, targetGroup, entry, writeConfigFlag); + } + } + + //Header accent colouring + if (item == QStringLiteral("Colors:Header") && config->hasGroup(QStringLiteral("Colors:Header")) && hasAccent()) { + QColor accentbg = accentBackground(getAccent(), config->group("Colors:Window").readEntry("BackgroundNormal", QColor())); + if (accentActiveTitlebar) { + targetGroup = KConfigGroup(configOutput, item); + targetGroup.writeEntry("BackgroundNormal", accentbg); + targetGroup.writeEntry("ForegroundNormal", accentForeground(accentbg, true)); + } + if (accentInactiveTitlebar) { + targetGroup = targetGroup.group("Inactive"); + targetGroup.writeEntry("BackgroundNormal", accentbg); + targetGroup.writeEntry("ForegroundNormal", accentForeground(accentbg, false)); //Dimmed foreground + } + } + } + + KConfigGroup groupWMTheme(config, "WM"); + KConfigGroup groupWMOut(configOutput, "WM"); + KColorScheme inactiveHeaderColorScheme(QPalette::Inactive, KColorScheme::Header, config); + + const QStringList colorItemListWM{QStringLiteral("activeBackground"), + QStringLiteral("activeForeground"), + QStringLiteral("inactiveBackground"), + QStringLiteral("inactiveForeground"), + QStringLiteral("activeBlend"), + QStringLiteral("inactiveBlend")}; + + const QVector defaultWMColors{KColorScheme(QPalette::Normal, KColorScheme::Header, config).background().color(), + KColorScheme(QPalette::Normal, KColorScheme::Header, config).foreground().color(), + inactiveHeaderColorScheme.background().color(), + inactiveHeaderColorScheme.foreground().color(), + KColorScheme(QPalette::Normal, KColorScheme::Header, config).background().color(), + inactiveHeaderColorScheme.background().color()}; + + int i = 0; + for (const QString &coloritem : colorItemListWM) { + groupWMOut.writeEntry(coloritem, groupWMTheme.readEntry(coloritem, defaultWMColors.value(i)), writeConfigFlag); + ++i; + } + + if (hasAccent()) { //Titlebar accent colouring + QColor accentbg = accentBackground(getAccent(), config->group("Colors:Window").readEntry("BackgroundNormal", QColor())); + if (accentActiveTitlebar) { + groupWMOut.writeEntry("activeBackground", accentbg); + groupWMOut.writeEntry("activeForeground", accentForeground(accentbg, true)); + } + if (accentInactiveTitlebar) { + groupWMOut.writeEntry("inactiveBackground", accentbg); + groupWMOut.writeEntry("inactiveForeground", accentForeground(accentbg, false)); //Dimmed foreground + } + } + + + const QStringList groupNameList{QStringLiteral("ColorEffects:Inactive"), QStringLiteral("ColorEffects:Disabled")}; + + const QStringList effectList{QStringLiteral("Enable"), + QStringLiteral("ChangeSelectionColor"), + QStringLiteral("IntensityEffect"), + QStringLiteral("IntensityAmount"), + QStringLiteral("ColorEffect"), + QStringLiteral("ColorAmount"), + QStringLiteral("Color"), + QStringLiteral("ContrastEffect"), + QStringLiteral("ContrastAmount")}; + + for (const QString &groupName : groupNameList) { + KConfigGroup groupEffectOut(configOutput, groupName); + KConfigGroup groupEffectTheme(config, groupName); + + for (const QString &effect : effectList) { + groupEffectOut.writeEntry(effect, groupEffectTheme.readEntry(effect), writeConfigFlag); + } + } + + configOutput->sync(); + + bool applyToAlien{true}; + { + KConfig cfg(QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals); + KConfigGroup group(configOutput, "General"); + group = KConfigGroup(&cfg, "X11"); + applyToAlien = group.readEntry("exportKDEColors", applyToAlien); + } + runRdb(KRdbExportQtColors | KRdbExportGtkTheme | (applyToAlien ? KRdbExportColors : 0)); +} diff --git a/plasma/workspace/kcms/colors/colorsapplicator.h b/plasma/workspace/kcms/colors/colorsapplicator.h new file mode 100644 index 0000000000..78808ce28a --- /dev/null +++ b/plasma/workspace/kcms/colors/colorsapplicator.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + SPDX-FileCopyrightText: 2021 Benjamin Port + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include +#include + +inline QColor alphaBlend(const QColor &foreground, const QColor &background) +{ + const auto foregroundAlpha = foreground.alphaF(); + const auto inverseForegroundAlpha = 1.0 - foregroundAlpha; + const auto backgroundAlpha = background.alphaF(); + + if (foregroundAlpha == 0.0) { + return background; + } + + if (backgroundAlpha == 1.0) { + return QColor::fromRgb( + (foregroundAlpha*foreground.red()) + (inverseForegroundAlpha*background.red()), + (foregroundAlpha*foreground.green()) + (inverseForegroundAlpha*background.green()), + (foregroundAlpha*foreground.blue()) + (inverseForegroundAlpha*background.blue()), + 0xff + ); + } else { + const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha); + const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha; + Q_ASSERT(finalAlpha != 0.0); + + return QColor::fromRgb( + (foregroundAlpha*foreground.red()) + (inverseBackgroundAlpha*background.red()), + (foregroundAlpha*foreground.green()) + (inverseBackgroundAlpha*background.green()), + (foregroundAlpha*foreground.blue()) + (inverseBackgroundAlpha*background.blue()), + finalAlpha + ); + } +} + +inline QColor accentBackground(const QColor& accent, const QColor& background) +{ + auto c = accent; + // light bg + if (KColorUtils::luma(background) > 0.5) { + c.setAlphaF(0.7); + } else { + // dark bg + c.setAlphaF(0.4); + } + return alphaBlend(c, background); +} + +inline QColor accentForeground(const QColor& accent, const bool& isActive) +{ + auto c = QColor(Qt::white); + // light bg + if (KColorUtils::luma(accent) > 0.5) { + c = QColor(Qt::black); + } else { + // dark bg + c = QColor(Qt::white); + } + + if (isActive) { + c.setAlphaF(1.0); + } else { + c.setAlphaF(0.6); + } + return alphaBlend(c, accent); +} + +/** + * Performs the task of actually applying a color scheme to the current session, based on + * color scheme file path and configuration file. + * When using this function, you select the scheme to use by setting the model's selected scheme + * @param colorFilePath The scheme color file path + * @param configOut The config which holds the information on which scheme is currently selected, and what colors it contains + */ +void applyScheme(const QString &colorSchemePath, KConfig *configOut, KConfig::WriteConfigFlags writeFlags = KConfig::Normal); diff --git a/plasma/workspace/kcms/colors/colorschemes.knsrc b/plasma/workspace/kcms/colors/colorschemes.knsrc new file mode 100644 index 0000000000..2732e5e3d5 --- /dev/null +++ b/plasma/workspace/kcms/colors/colorschemes.knsrc @@ -0,0 +1,48 @@ +[KNewStuff3] +Name=Color Schemes +Name[ar]=تشكيلات الألوان +Name[az]=Rəng Sxemləri +Name[ca]=Esquemes de color +Name[cs]=Barevná schémata +Name[da]=Farvetemaer +Name[de]=Farbschemata +Name[en_GB]=Colour Schemes +Name[es]=Esquema de colores +Name[et]=Värviskeemid +Name[eu]=Kolore-antolaera +Name[fi]=Väriteemat +Name[fr]=Schémas de couleurs +Name[hi]=रंग योजनाएँ +Name[hsb]=Barbowe kombinacije +Name[hu]=Színsémák +Name[ia]=Schemas de Color +Name[id]=Skema Warna +Name[it]=Schemi di colore +Name[ko]=색 배열 +Name[lt]=Spalvų rinkiniai +Name[ml]=നിറക്കൂട്ടുകൾ +Name[nl]=Kleurenschema's +Name[nn]=Fargeoppsett +Name[pa]=ਰੰਗ ਸਕੀਮ +Name[pl]=Zestawy kolorów +Name[pt]=Esquemas de Cores +Name[pt_BR]=Esquemas de cores +Name[ro]=Scheme de culori +Name[ru]=Цветовые схемы +Name[sk]=Farebné schémy +Name[sl]=Barvne sheme +Name[sv]=Färgscheman +Name[ta]=வண்ண திட்டங்கள் +Name[tr]=Renk Şemaları +Name[uk]=Схеми кольорів +Name[vi]=Quy hoạch màu +Name[x-test]=xxColor Schemesxx +Name[zh_CN]=配色方案 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +TargetDir=color-schemes +Uncompress=archive +Categories=KDE Color Scheme KDE4 +UploadCategories=KDE Color Scheme KDE4 +RemoveDeadEntries=true +AdoptionCommand=plasma-apply-colorscheme %f diff --git a/plasma/workspace/kcms/colors/colorsmodel.cpp b/plasma/workspace/kcms/colors/colorsmodel.cpp new file mode 100644 index 0000000000..ef8b390dec --- /dev/null +++ b/plasma/workspace/kcms/colors/colorsmodel.cpp @@ -0,0 +1,249 @@ +/* + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + SPDX-FileCopyrightText: 2016 Olivier Churlaud + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "colorsmodel.h" + +#include +#include +#include + +#include +#include +#include + +#include + +ColorsModel::ColorsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +ColorsModel::~ColorsModel() = default; + +int ColorsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant ColorsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return item.display; + case SchemeNameRole: + return item.schemeName; + case PaletteRole: + return item.palette; + case ActiveTitleBarBackgroundRole: + return item.activeTitleBarBackground; + case ActiveTitleBarForegroundRole: + return item.activeTitleBarForeground; + case PendingDeletionRole: + return item.pendingDeletion; + case RemovableRole: + return item.removable; + case AccentActiveTitlebarRole: + return item.accentActiveTitlebar; + } + + return QVariant(); +} + +bool ColorsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return false; + } + + if (role == PendingDeletionRole) { + auto &item = m_data[index.row()]; + + const bool pendingDeletion = value.toBool(); + + if (item.pendingDeletion != pendingDeletion) { + item.pendingDeletion = pendingDeletion; + Q_EMIT dataChanged(index, index, {PendingDeletionRole}); + + if (index.row() == selectedSchemeIndex() && pendingDeletion) { + // move to the next non-pending theme + const auto nonPending = match(index, PendingDeletionRole, false); + if (!nonPending.isEmpty()) { + setSelectedScheme(nonPending.first().data(SchemeNameRole).toString()); + } + } + + Q_EMIT pendingDeletionsChanged(); + return true; + } + } + + return false; +} + +QHash ColorsModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {SchemeNameRole, QByteArrayLiteral("schemeName")}, + {PaletteRole, QByteArrayLiteral("palette")}, + {ActiveTitleBarBackgroundRole, QByteArrayLiteral("activeTitleBarBackground")}, + {ActiveTitleBarForegroundRole, QByteArrayLiteral("activeTitleBarForeground")}, + {RemovableRole, QByteArrayLiteral("removable")}, + {AccentActiveTitlebarRole, QByteArrayLiteral("accentActiveTitlebar")}, + {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")}, + }; +} + +QString ColorsModel::selectedScheme() const +{ + return m_selectedScheme; +} + +void ColorsModel::setSelectedScheme(const QString &scheme) +{ + if (m_selectedScheme == scheme) { + return; + } + + m_selectedScheme = scheme; + + Q_EMIT selectedSchemeChanged(scheme); + Q_EMIT selectedSchemeIndexChanged(); +} + +int ColorsModel::indexOfScheme(const QString &scheme) const +{ + auto it = std::find_if(m_data.begin(), m_data.end(), [&scheme](const ColorsModelData &item) { + return item.schemeName == scheme; + }); + + if (it != m_data.end()) { + return std::distance(m_data.begin(), it); + } + + return -1; +} + +int ColorsModel::selectedSchemeIndex() const +{ + return indexOfScheme(m_selectedScheme); +} + +void ColorsModel::load() +{ + beginResetModel(); + + const int oldCount = m_data.count(); + + m_data.clear(); + + QStringList schemeFiles; + + const QStringList schemeDirs = + QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes"), QStandardPaths::LocateDirectory); + for (const QString &dir : schemeDirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList{QStringLiteral("*.colors")}); + for (const QString &file : fileNames) { + const QString suffixedFileName = QLatin1String("color-schemes/") + file; + // can't use QSet because of the transform below (passing const QString as this argument discards qualifiers) + if (!schemeFiles.contains(suffixedFileName)) { + schemeFiles.append(suffixedFileName); + } + } + } + + std::transform(schemeFiles.begin(), schemeFiles.end(), schemeFiles.begin(), [](const QString &item) { + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, item); + }); + + for (const QString &schemeFile : schemeFiles) { + const QFileInfo fi(schemeFile); + const QString baseName = fi.baseName(); + + KSharedConfigPtr config = KSharedConfig::openConfig(schemeFile, KConfig::SimpleConfig); + KConfigGroup group(config, "General"); + const QString name = group.readEntry("Name", baseName); + + const QPalette palette = KColorScheme::createApplicationPalette(config); + + QColor activeTitleBarBackground, activeTitleBarForeground; + if (KColorScheme::isColorSetSupported(config, KColorScheme::Header)) { + KColorScheme headerColorScheme(QPalette::Active, KColorScheme::Header, config); + activeTitleBarBackground = headerColorScheme.background().color(); + activeTitleBarForeground = headerColorScheme.foreground().color(); + } else { + KConfigGroup wmConfig(config, QStringLiteral("WM")); + activeTitleBarBackground = wmConfig.readEntry("activeBackground", palette.color(QPalette::Active, QPalette::Highlight)); + activeTitleBarForeground = wmConfig.readEntry("activeForeground", palette.color(QPalette::Active, QPalette::HighlightedText)); + } + + const bool colorActiveTitleBar = group.readEntry("accentActiveTitlebar", false); + + ColorsModelData item{ + name, + baseName, + palette, + activeTitleBarBackground, + activeTitleBarForeground, + fi.isWritable(), + colorActiveTitleBar, + false, // pending deletion + }; + + m_data.append(item); + } + + // Sort case-insensitively + QCollator collator; + collator.setCaseSensitivity(Qt::CaseInsensitive); + std::sort(m_data.begin(), m_data.end(), [&collator](const ColorsModelData &a, const ColorsModelData &b) { + return collator.compare(a.display, b.display) < 0; + }); + + endResetModel(); + + // an item might have been added before the currently selected one + if (oldCount != m_data.count()) { + Q_EMIT selectedSchemeIndexChanged(); + } +} + +QStringList ColorsModel::pendingDeletions() const +{ + QStringList pendingDeletions; + + for (const auto &item : m_data) { + if (item.pendingDeletion) { + pendingDeletions.append(item.schemeName); + } + } + + return pendingDeletions; +} + +void ColorsModel::removeItemsPendingDeletion() +{ + for (int i = m_data.count() - 1; i >= 0; --i) { + if (m_data.at(i).pendingDeletion) { + beginRemoveRows(QModelIndex(), i, i); + m_data.remove(i); + endRemoveRows(); + } + } +} diff --git a/plasma/workspace/kcms/colors/colorsmodel.h b/plasma/workspace/kcms/colors/colorsmodel.h new file mode 100644 index 0000000000..f7bee89d45 --- /dev/null +++ b/plasma/workspace/kcms/colors/colorsmodel.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + SPDX-FileCopyrightText: 2016 Olivier Churlaud + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include + +struct ColorsModelData { + QString display; + QString schemeName; + QPalette palette; + QColor activeTitleBarBackground; + QColor activeTitleBarForeground; + bool removable; + bool accentActiveTitlebar; + bool pendingDeletion; +}; +Q_DECLARE_TYPEINFO(ColorsModelData, Q_MOVABLE_TYPE); + +class ColorsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedScheme READ selectedScheme WRITE setSelectedScheme NOTIFY selectedSchemeChanged) + Q_PROPERTY(int selectedSchemeIndex READ selectedSchemeIndex NOTIFY selectedSchemeIndexChanged) + +public: + ColorsModel(QObject *parent); + ~ColorsModel() override; + + enum Roles { + SchemeNameRole = Qt::UserRole + 1, + PaletteRole, + // Colors which aren't in QPalette + ActiveTitleBarBackgroundRole, + ActiveTitleBarForegroundRole, + RemovableRole, + AccentActiveTitlebarRole, + PendingDeletionRole, + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QHash roleNames() const override; + + QString selectedScheme() const; + void setSelectedScheme(const QString &scheme); + + int indexOfScheme(const QString &scheme) const; + int selectedSchemeIndex() const; + + QStringList pendingDeletions() const; + void removeItemsPendingDeletion(); + + void load(); + +Q_SIGNALS: + void selectedSchemeChanged(const QString &scheme); + void selectedSchemeIndexChanged(); + + void pendingDeletionsChanged(); + +private: + QString m_selectedScheme; + + QVector m_data; +}; diff --git a/plasma/workspace/kcms/colors/colorssettings.kcfg b/plasma/workspace/kcms/colors/colorssettings.kcfg new file mode 100644 index 0000000000..800f6728a1 --- /dev/null +++ b/plasma/workspace/kcms/colors/colorssettings.kcfg @@ -0,0 +1,17 @@ + + + + + + + BreezeLight + + + + transparent + + + diff --git a/plasma/workspace/kcms/colors/colorssettings.kcfgc b/plasma/workspace/kcms/colors/colorssettings.kcfgc new file mode 100644 index 0000000000..803bdfbd72 --- /dev/null +++ b/plasma/workspace/kcms/colors/colorssettings.kcfgc @@ -0,0 +1,7 @@ +File=colorssettings.kcfg +ClassName=ColorsSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=colorScheme diff --git a/plasma/workspace/kcms/colors/editor/CMakeLists.txt b/plasma/workspace/kcms/colors/editor/CMakeLists.txt new file mode 100644 index 0000000000..d299d6f297 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/CMakeLists.txt @@ -0,0 +1,32 @@ +set(scheme_editor_SRCS + kcolorschemeeditor.cpp + scmeditordialog.cpp + scmeditoroptions.cpp + scmeditorcolors.cpp + scmeditoreffects.cpp + previewwidget.cpp + setpreviewwidget.cpp +) + +ki18n_wrap_ui(scheme_editor_SRCS + scmeditordialog.ui + scmeditoroptions.ui + scmeditorcolors.ui + scmeditoreffects.ui + preview.ui + setpreview.ui +) + +add_executable(kcolorschemeeditor ${scheme_editor_SRCS}) + +target_link_libraries(kcolorschemeeditor + KF5::ConfigWidgets + KF5::GuiAddons + KF5::I18n + KF5::CoreAddons + KF5::NewStuff + KF5::WindowSystem +) + +install(TARGETS kcolorschemeeditor DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES org.kde.kcolorschemeeditor.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/plasma/workspace/kcms/colors/editor/kcolorschemeeditor.cpp b/plasma/workspace/kcms/colors/editor/kcolorschemeeditor.cpp new file mode 100644 index 0000000000..6af007ac01 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/kcolorschemeeditor.cpp @@ -0,0 +1,81 @@ +/* KDE Display scheme editor + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scmeditordialog.h" + +#include +#include +#include + +#include +#include + +int main(int argc, char *argv[]) +{ + // Fixes blurry icons with fractional scaling + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QApplication app(argc, argv); + + KAboutData aboutData(QStringLiteral("kcolorschemeeditor"), + i18n("KColorSchemeEditor"), + QStringLiteral("0.1"), + i18n("Utility to edit and create color schemes"), + KAboutLicense::GPL_V3); + aboutData.addAuthor(i18n("Olivier Churlaud"), i18n("Utility creation"), QStringLiteral("olivier@churlaud.com")); + aboutData.addAuthor(i18n("Jeremy Whiting"), i18n("KCM code (reused in here)"), QStringLiteral("jpwhiting@kde.org")); + aboutData.addAuthor(i18n("Matthew Woehlke"), i18n("KCM code (reused in here)"), QStringLiteral("mw_triad@users.sourceforge.net")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + parser.addPositionalArgument("theme", i18n("Scheme to edit or to use as a base."), QStringLiteral("kcolorschemeeditor ThemeName")); + + QCommandLineOption overwriteOption(QStringLiteral("overwrite"), i18n("Show 'Apply' button that saves changes without asking (unlike 'Save As' button)")); + parser.addOption(overwriteOption); + + QCommandLineOption attachOption(QStringLiteral("attach"), + i18n("Makes the dialog transient for another application window specified by handle"), + QStringLiteral("handle")); + parser.addOption(attachOption); + + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + const QStringList args = parser.positionalArguments(); + QString path = ""; + if (args.count() == 1) { + const QString fileBaseName(args.at(0)); + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "color-schemes/" + fileBaseName + ".colors"); + } + if (path.isEmpty()) { + QTextStream out(stderr); + out << i18n("Scheme not found, falling back to current one.\n"); + } + + SchemeEditorDialog dialog(path); + dialog.setShowApplyOverwriteButton(parser.isSet(overwriteOption)); + + // FIXME doesn't work :( + const QString attachHandle = parser.value(attachOption); + if (!attachHandle.isEmpty()) { + // TODO wayland: once we have foreign surface support + const QString x11Prefix = QStringLiteral("x11:"); + + if (attachHandle.startsWith(x11Prefix)) { + bool ok = false; + WId winId = attachHandle.mid(x11Prefix.length()).toLong(&ok, 0); + if (ok) { + dialog.setModal(true); + dialog.setAttribute(Qt::WA_NativeWindow, true); + KWindowSystem::setMainWindow(dialog.windowHandle(), winId); + } + } + } + + dialog.show(); + + return app.exec(); +} diff --git a/plasma/workspace/kcms/colors/editor/org.kde.kcolorschemeeditor.desktop b/plasma/workspace/kcms/colors/editor/org.kde.kcolorschemeeditor.desktop new file mode 100644 index 0000000000..f0dd8893b5 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/org.kde.kcolorschemeeditor.desktop @@ -0,0 +1,125 @@ +[Desktop Entry] +GenericName=Color scheme editor +GenericName[ar]=محرر تشكيلة الألوان +GenericName[az]=Rəng sxemləri redaktoru +GenericName[ca]=Editor de l'esquema de color +GenericName[cs]=Editor barevných schémat +GenericName[da]=Editor til farvetema +GenericName[de]=Farbschema-Editor +GenericName[en_GB]=Colour scheme editor +GenericName[es]=Editor de esquema de color +GenericName[et]=Värviskeemi redaktor +GenericName[eu]=Kolore-antolaeren editorea +GenericName[fi]=Väriteemamuokkain +GenericName[fr]=Éditeur de schémas de couleurs +GenericName[hi]=रंग योजना संपादक +GenericName[hsb]=Wobdźěłar za kombinacije barbow +GenericName[hu]=Színséma-szerkesztő +GenericName[ia]=Redactor (Editor) de schema de color +GenericName[id]=Pengedit skema warna +GenericName[it]=Editor dello schema di colore +GenericName[ko]=색 배열 편집기 +GenericName[lt]=Spalvų rinkinių redaktorius +GenericName[ml]=നിറക്കൂട്ട് എഡിറ്റർ +GenericName[nl]=Bewerker van kleurenschema +GenericName[nn]=Rediger fargeoppsett +GenericName[pa]=ਰੰਗ ਸਕੀਮ ਐਡੀਟਰ +GenericName[pl]=Edytor zestawu kolorów +GenericName[pt]=Editor de esquemas de cores +GenericName[pt_BR]=Editor de esquema de cores +GenericName[ro]=Redactor pentru scheme de culori +GenericName[ru]=Редактор цветовых схем +GenericName[sk]=Editor farebnej schémy +GenericName[sl]=Urejevalnik barvnih shem +GenericName[sv]=Färgschemaeditor +GenericName[ta]=வண்ண திட்டத்தைத் திருத்தும் நிரல் +GenericName[tr]=Renk şeması düzenleyicisi +GenericName[uk]=Редактор схем кольорів +GenericName[vi]=Trình biên tập quy hoạch màu +GenericName[x-test]=xxColor scheme editorxx +GenericName[zh_CN]=配色编辑器 +Name=KColorSchemeEditor +Name[ar]=محرر تشكيلة الألوان ك +Name[ast]=KColorSchemeEditor +Name[az]=KColorSchemeEditor +Name[ca]=KColorSchemeEditor +Name[cs]=KColorSchemeEditor +Name[da]=KColorSchemeEditor +Name[de]=KColorSchemeEditor +Name[en_GB]=KColorSchemeEditor +Name[es]=KColorSchemeEditor +Name[et]=KColorSchemeEditor +Name[eu]=KColorSchemeEditor +Name[fi]=KColorSchemeEditor +Name[fr]=KColorSchemeEditor +Name[hi]=के-कलरस्कीमएडिटर +Name[hu]=KColorSchemeEditor +Name[ia]=KColorSchemeEditor +Name[id]=KColorSchemeEditor +Name[it]=KColorSchemeEditor +Name[ko]=KColorSchemeEditor +Name[lt]=KColorSchemeEditor +Name[ml]=കെകളർസ്‌കീംഎഡിറ്റർ +Name[nl]=KColorSchemeEditor +Name[nn]=KColorSchemeEditor +Name[pa]=ਕੇ-ਰੰਗ-ਸਕੀਮ-ਐਡੀਟਰ +Name[pl]=KColorSchemeEditor +Name[pt]=KColorSchemeEditor +Name[pt_BR]=KColorSchemeEditor +Name[ro]=KColorSchemeEditor +Name[ru]=KColorSchemeEditor +Name[sk]=KColorSchemeEditor +Name[sl]=KColorSchemeEditor +Name[sv]=Färgschemaeditor +Name[ta]=KColorSchemeEditor +Name[tr]=KColorSchemeEditor +Name[uk]=KColorSchemeEditor +Name[vi]=KColorSchemeEditor +Name[x-test]=xxKColorSchemeEditorxx +Name[zh_CN]=KColorSchemeEditor +Comment=Plasma color scheme editor +Comment[ar]=محرر تشكيلة ألوان بلازما +Comment[az]=Plasma rəng sxemləri redaktoru +Comment[ca]=Editor de l'esquema de color del Plasma +Comment[cs]=Editor barevných schémat Plasma +Comment[da]=Editor til Plasmas farvetema +Comment[de]=Plasma-Farbschema-Editor +Comment[en_GB]=Plasma colour scheme editor +Comment[es]=Editor de esquema de color de Plasma +Comment[et]=Plasma värviskeemi redaktor +Comment[eu]=Plasmaren kolore-antolaera editorea +Comment[fi]=Plasma-väriteemamuokkain +Comment[fr]=Éditeur de schémas de couleurs de Plasma +Comment[hi]=प्लाज़्मा रंग योजना संपादक +Comment[hu]=Plasma színséma-szerkesztő +Comment[ia]=Redactor (editor) de schema de color de Plasma +Comment[id]=Pengedit skema warna plasma +Comment[it]=Editor dello schema di colore di Plasma +Comment[ko]=Plasma 색 배열 편집기 +Comment[lt]=Plasma spalvų rinkinių redaktorius +Comment[ml]=പ്ലാസ്മ നിറക്കൂട്ടു് എഡിറ്റർ +Comment[nl]=Bewerker van kleurenschema van Plasma +Comment[nn]=Redigering av fargeoppsett for Plasma +Comment[pa]=ਪਲਾਜ਼ਮਾ ਰੰਗ ਸਕੀਮ ਐਡੀਟਰ +Comment[pl]=Edytor zestawu kolorów +Comment[pt]=Editor de esquemas de cores do Plasma +Comment[pt_BR]=Editor de esquema de cores do Plasma +Comment[ro]=Redactor scheme de culori Plasma +Comment[ru]=Редактор цветовых схем для Plasma +Comment[sk]=Editor farebnej schémy Plasma +Comment[sl]=Plasma urejevalnik barvnih shem +Comment[sv]=Plasma färgschemaeditor +Comment[ta]=பிளாஸ்மா வண்ண திட்டத்தைத் திருத்தும் நிரல் +Comment[tr]=Plasma renk şeması düzenleyicisi +Comment[uk]=Редактор схем кольорів Плазми +Comment[vi]=Trình biên tập quy hoạch màu Plasma +Comment[x-test]=xxPlasma color scheme editorxx +Comment[zh_CN]=Plasma 配色编辑器 +Exec=kcolorschemeeditor +StartupNotify=true +Icon=preferences-desktop-color +Type=Application +Terminal=false +NoDisplay=true +OnlyShowIn=KDE; +Categories=Qt;KDE;Settings;X-KDE-settings-looknfeel; diff --git a/plasma/workspace/kcms/colors/editor/preview.ui b/plasma/workspace/kcms/colors/editor/preview.ui new file mode 100644 index 0000000000..72910e2470 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/preview.ui @@ -0,0 +1,360 @@ + + + preview + + + + 0 + 0 + 467 + 116 + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + Window text on Window Background + + + Window text + + + + + + + Button text on Button Background + + + Push Button + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 4 + 51 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Selection Normal Text against Selection Normal Background + + + Selected text + + + + + + + + true + + + + Selection Link Text against Selection Normal Background + + + link + + + + + + + + true + + + + Selection Visited Text against Selection Normal Background + + + visited + + + + + + + Selection Active Text against Selection Normal Background + + + a + + + + + + + Selection Inactive Text against Selection Normal Background + + + i + + + + + + + Selection Negative Text against Selection Normal Background + + + ! + + + + + + + Selection Neutral Text against Selection Normal Background + + + = + + + + + + + Selection Positive Text against Selection Normal Background + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 141 + 0 + + + + + + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + View Normal Text against View Normal Background + + + Normal text + + + + + + + + true + + + + View Link Text against View Normal Background + + + link + + + + + + + + true + + + + View Visited Text against View Normal Background + + + visited + + + + + + + View Active Text against View Normal Background + + + a + + + + + + + View Inactive Text against View Normal Background + + + i + + + + + + + View Negative Text against View Normal Background + + + ! + + + + + + + View Neutral Text against View Normal Background + + + = + + + + + + + View Positive Text against View Normal Background + + + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 0 + + + + + + + + + diff --git a/plasma/workspace/kcms/colors/editor/previewwidget.cpp b/plasma/workspace/kcms/colors/editor/previewwidget.cpp new file mode 100644 index 0000000000..28b5ba3ee7 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/previewwidget.cpp @@ -0,0 +1,145 @@ +/* Preview widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Urs Wolfer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "previewwidget.h" + +#include + +PreviewWidget::PreviewWidget(QWidget *parent) + : QFrame(parent) +{ + setupUi(this); + + // set correct colors on... lots of things + setAutoFillBackground(true); + frame->setBackgroundRole(QPalette::Base); + viewWidget->setBackgroundRole(QPalette::Base); + labelView0->setBackgroundRole(QPalette::Base); + labelView3->setBackgroundRole(QPalette::Base); + labelView4->setBackgroundRole(QPalette::Base); + labelView2->setBackgroundRole(QPalette::Base); + labelView1->setBackgroundRole(QPalette::Base); + labelView5->setBackgroundRole(QPalette::Base); + labelView6->setBackgroundRole(QPalette::Base); + labelView7->setBackgroundRole(QPalette::Base); + selectionWidget->setBackgroundRole(QPalette::Highlight); + labelSelection0->setBackgroundRole(QPalette::Highlight); + labelSelection3->setBackgroundRole(QPalette::Highlight); + labelSelection4->setBackgroundRole(QPalette::Highlight); + labelSelection2->setBackgroundRole(QPalette::Highlight); + labelSelection1->setBackgroundRole(QPalette::Highlight); + labelSelection5->setBackgroundRole(QPalette::Highlight); + labelSelection6->setBackgroundRole(QPalette::Highlight); + labelSelection7->setBackgroundRole(QPalette::Highlight); + + QList widgets = findChildren(); + foreach (QWidget *widget, widgets) { + widget->installEventFilter(this); + widget->setFocusPolicy(Qt::NoFocus); + } +} + +PreviewWidget::~PreviewWidget() +{ +} + +bool PreviewWidget::eventFilter(QObject *, QEvent *ev) +{ + switch (ev->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + case QEvent::MouseMove: + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::Enter: + case QEvent::Leave: + case QEvent::Wheel: + case QEvent::ContextMenu: + return true; // ignore + default: + break; + } + return false; +} + +inline void copyPaletteBrush(QPalette &palette, QPalette::ColorGroup state, QPalette::ColorRole role) +{ + palette.setBrush(QPalette::Active, role, palette.brush(state, role)); + if (state == QPalette::Disabled) + // ### hack, while Qt has no inactive+disabled state + // TODO copy from Inactive+Disabled to Inactive instead + palette.setBrush(QPalette::Inactive, role, palette.brush(QPalette::Disabled, role)); +} + +void PreviewWidget::setPaletteRecursive(QWidget *widget, const QPalette &palette) +{ + widget->setPalette(palette); + + const QObjectList children = widget->children(); + foreach (QObject *child, children) { + if (child->isWidgetType()) + setPaletteRecursive((QWidget *)child, palette); + } +} + +inline void adjustWidgetForeground(QWidget *widget, + QPalette::ColorGroup state, + const KSharedConfigPtr &config, + QPalette::ColorRole color, + KColorScheme::ColorSet set, + KColorScheme::ForegroundRole role) +{ + QPalette palette = widget->palette(); + KColorScheme::adjustForeground(palette, role, color, set, config); + copyPaletteBrush(palette, state, color); + widget->setPalette(palette); +} + +void PreviewWidget::setPalette(const KSharedConfigPtr &config, QPalette::ColorGroup state) +{ + QPalette palette = KColorScheme::createApplicationPalette(config); + + if (state != QPalette::Active) { + copyPaletteBrush(palette, state, QPalette::Base); + copyPaletteBrush(palette, state, QPalette::Text); + copyPaletteBrush(palette, state, QPalette::Window); + copyPaletteBrush(palette, state, QPalette::WindowText); + copyPaletteBrush(palette, state, QPalette::Button); + copyPaletteBrush(palette, state, QPalette::ButtonText); + copyPaletteBrush(palette, state, QPalette::Highlight); + copyPaletteBrush(palette, state, QPalette::HighlightedText); + copyPaletteBrush(palette, state, QPalette::AlternateBase); + copyPaletteBrush(palette, state, QPalette::Link); + copyPaletteBrush(palette, state, QPalette::LinkVisited); + copyPaletteBrush(palette, state, QPalette::Light); + copyPaletteBrush(palette, state, QPalette::Midlight); + copyPaletteBrush(palette, state, QPalette::Mid); + copyPaletteBrush(palette, state, QPalette::Dark); + copyPaletteBrush(palette, state, QPalette::Shadow); + } + + setPaletteRecursive(this, palette); + +#define ADJUST_WIDGET_FOREGROUND(w, c, s, r) adjustWidgetForeground(w, state, config, QPalette::c, KColorScheme::s, KColorScheme::r); + + ADJUST_WIDGET_FOREGROUND(labelView1, Text, View, InactiveText); + ADJUST_WIDGET_FOREGROUND(labelView2, Text, View, ActiveText); + ADJUST_WIDGET_FOREGROUND(labelView3, Text, View, LinkText); + ADJUST_WIDGET_FOREGROUND(labelView4, Text, View, VisitedText); + ADJUST_WIDGET_FOREGROUND(labelView5, Text, View, NegativeText); + ADJUST_WIDGET_FOREGROUND(labelView6, Text, View, NeutralText); + ADJUST_WIDGET_FOREGROUND(labelView7, Text, View, PositiveText); + + ADJUST_WIDGET_FOREGROUND(labelSelection1, HighlightedText, Selection, InactiveText); + ADJUST_WIDGET_FOREGROUND(labelSelection2, HighlightedText, Selection, ActiveText); + ADJUST_WIDGET_FOREGROUND(labelSelection3, HighlightedText, Selection, LinkText); + ADJUST_WIDGET_FOREGROUND(labelSelection4, HighlightedText, Selection, VisitedText); + ADJUST_WIDGET_FOREGROUND(labelSelection5, HighlightedText, Selection, NegativeText); + ADJUST_WIDGET_FOREGROUND(labelSelection6, HighlightedText, Selection, NeutralText); + ADJUST_WIDGET_FOREGROUND(labelSelection7, HighlightedText, Selection, PositiveText); +} diff --git a/plasma/workspace/kcms/colors/editor/previewwidget.h b/plasma/workspace/kcms/colors/editor/previewwidget.h new file mode 100644 index 0000000000..4fe5190c9b --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/previewwidget.h @@ -0,0 +1,32 @@ +/* Preview widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +#include "ui_preview.h" + +/** + * The Desktop/Colors tab in kcontrol. + */ +class PreviewWidget : public QFrame, Ui::preview +{ + Q_OBJECT + +public: + PreviewWidget(QWidget *parent); + ~PreviewWidget() override; + + void setPalette(const KSharedConfigPtr &config, QPalette::ColorGroup state = QPalette::Active); + +protected: + void setPaletteRecursive(QWidget *, const QPalette &); + bool eventFilter(QObject *, QEvent *) override; +}; diff --git a/plasma/workspace/kcms/colors/editor/scmeditorcolors.cpp b/plasma/workspace/kcms/colors/editor/scmeditorcolors.cpp new file mode 100644 index 0000000000..b6b7c77cbc --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditorcolors.cpp @@ -0,0 +1,492 @@ +/* KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scmeditorcolors.h" + +#include + +#include +#include + +// BEGIN WindecoColors +SchemeEditorColors::WindecoColors::WindecoColors(const KSharedConfigPtr &config) +{ + load(config); +} + +void SchemeEditorColors::WindecoColors::load(const KSharedConfigPtr &config) +{ + // NOTE: keep this in sync with kdelibs/kdeui/kernel/kglobalsettings.cpp + KConfigGroup group(config, "WM"); + m_colors[ActiveBackground] = group.readEntry("activeBackground", QColor(48, 174, 232)); + m_colors[ActiveForeground] = group.readEntry("activeForeground", QColor(255, 255, 255)); + m_colors[InactiveBackground] = group.readEntry("inactiveBackground", QColor(224, 223, 222)); + m_colors[InactiveForeground] = group.readEntry("inactiveForeground", QColor(75, 71, 67)); + m_colors[ActiveBlend] = group.readEntry("activeBlend", m_colors[ActiveForeground]); + m_colors[InactiveBlend] = group.readEntry("inactiveBlend", m_colors[InactiveForeground]); +} + +QColor SchemeEditorColors::WindecoColors::color(WindecoColors::Role role) const +{ + return m_colors[role]; +} +// END WindecoColors + +SchemeEditorColors::SchemeEditorColors(KSharedConfigPtr config, QWidget *parent) + : QWidget(parent) + , m_config(config) +{ + setupUi(this); + setupColorTable(); + connect(colorSet, static_cast(&QComboBox::currentIndexChanged), this, &SchemeEditorColors::updateColorTable); +} + +void SchemeEditorColors::updateValues() +{ + const int currentSet = colorSet->currentIndex() - 1; + setPreview->setPalette(m_config, (KColorScheme::ColorSet)currentSet); + colorPreview->setPalette(m_config); +} + +void SchemeEditorColors::setupColorTable() +{ + // first setup the common colors table + commonColorTable->verticalHeader()->hide(); + commonColorTable->horizontalHeader()->hide(); + commonColorTable->setShowGrid(false); + commonColorTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + int minWidth = QPushButton(i18n("Varies")).minimumSizeHint().width(); + commonColorTable->horizontalHeader()->setMinimumSectionSize(minWidth); + commonColorTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + for (int i = 0; i < 26; ++i) { + KColorButton *button = new KColorButton(this); + commonColorTable->setRowHeight(i, button->sizeHint().height()); + button->setObjectName(QString::number(i)); + connect(button, &KColorButton::changed, this, &SchemeEditorColors::colorChanged); + m_commonColorButtons << button; + + if (i > 8 && i < 18) { + // Inactive Text row through Positive Text role all need a varies button + QPushButton *variesButton = new QPushButton(nullptr); + variesButton->setText(i18n("Varies")); + variesButton->setObjectName(QString::number(i)); + connect(variesButton, &QPushButton::clicked, this, &SchemeEditorColors::variesClicked); + + QStackedWidget *widget = new QStackedWidget(this); + widget->addWidget(button); + widget->addWidget(variesButton); + m_stackedWidgets.append(widget); + + commonColorTable->setCellWidget(i, 1, widget); + } else { + commonColorTable->setCellWidget(i, 1, button); + } + } + + // then the colorTable that the colorSets will use + colorTable->verticalHeader()->hide(); + colorTable->horizontalHeader()->hide(); + colorTable->setShowGrid(false); + colorTable->setRowCount(12); + colorTable->horizontalHeader()->setMinimumSectionSize(minWidth); + colorTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + createColorEntry(i18n("Normal Background"), QStringLiteral("BackgroundNormal"), m_backgroundButtons, 0); + createColorEntry(i18n("Alternate Background"), QStringLiteral("BackgroundAlternate"), m_backgroundButtons, 1); + createColorEntry(i18n("Normal Text"), QStringLiteral("ForegroundNormal"), m_foregroundButtons, 2); + createColorEntry(i18n("Inactive Text"), QStringLiteral("ForegroundInactive"), m_foregroundButtons, 3); + createColorEntry(i18n("Active Text"), QStringLiteral("ForegroundActive"), m_foregroundButtons, 4); + createColorEntry(i18n("Link Text"), QStringLiteral("ForegroundLink"), m_foregroundButtons, 5); + createColorEntry(i18n("Visited Text"), QStringLiteral("ForegroundVisited"), m_foregroundButtons, 6); + createColorEntry(i18n("Negative Text"), QStringLiteral("ForegroundNegative"), m_foregroundButtons, 7); + createColorEntry(i18n("Neutral Text"), QStringLiteral("ForegroundNeutral"), m_foregroundButtons, 8); + createColorEntry(i18n("Positive Text"), QStringLiteral("ForegroundPositive"), m_foregroundButtons, 9); + createColorEntry(i18n("Focus Decoration"), QStringLiteral("DecorationFocus"), m_decorationButtons, 10); + createColorEntry(i18n("Hover Decoration"), QStringLiteral("DecorationHover"), m_decorationButtons, 11); + + colorTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + colorTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + updateColorSchemes(); + updateColorTable(); +} + +void SchemeEditorColors::createColorEntry(const QString &text, const QString &key, QList &list, int index) +{ + KColorButton *button = new KColorButton(this); + button->setObjectName(QString::number(index)); + connect(button, &KColorButton::changed, this, &SchemeEditorColors::colorChanged); + list.append(button); + + m_colorKeys.insert(index, key); + + QTableWidgetItem *label = new QTableWidgetItem(text); + colorTable->setItem(index, 0, label); + colorTable->setCellWidget(index, 1, button); + colorTable->setRowHeight(index, button->sizeHint().height()); +} + +void SchemeEditorColors::variesClicked() +{ + // find which button was changed + const int row = sender()->objectName().toInt(); + + QColor color = QColorDialog::getColor(QColor(), this); + if (color.isValid()) { + changeColor(row, color); + m_stackedWidgets[row - 9]->setCurrentIndex(0); + } +} + +void SchemeEditorColors::colorChanged(const QColor &newColor) +{ + // find which button was changed + const int row = sender()->objectName().toInt(); + changeColor(row, newColor); +} + +void SchemeEditorColors::changeColor(int row, const QColor &newColor) +{ + // update the m_colorSchemes for the selected colorSet + const int currentSet = colorSet->currentIndex() - 1; + + if (currentSet == -1) { + // common colors is selected + switch (row) { + case 0: + // View Background button + KConfigGroup(m_config, "Colors:View").writeEntry("BackgroundNormal", newColor); + break; + case 1: + // View Text button + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundNormal", newColor); + break; + case 2: + // Window Background Button + KConfigGroup(m_config, "Colors:Window").writeEntry("BackgroundNormal", newColor); + break; + case 3: + // Window Text Button + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundNormal", newColor); + break; + case 4: + // Button Background button + KConfigGroup(m_config, "Colors:Button").writeEntry("BackgroundNormal", newColor); + break; + case 5: + // Button Text button + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundNormal", newColor); + break; + case 6: + // Selection Background Button + KConfigGroup(m_config, "Colors:Selection").writeEntry("BackgroundNormal", newColor); + break; + case 7: + // Selection Text Button + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundNormal", newColor); + break; + case 8: + // Selection Inactive Text Button + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundInactive", newColor); + break; + + // buttons that could have varies in their place + case 9: + // Inactive Text Button (set all but Selection Inactive Text color) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundInactive", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundInactive", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundInactive", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundInactive", newColor); + break; + case 10: + // Active Text Button (set all active text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundActive", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundActive", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundActive", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundActive", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundActive", newColor); + break; + case 11: + // Link Text Button (set all link text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundLink", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundLink", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundLink", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundLink", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundLink", newColor); + break; + case 12: + // Visited Text Button (set all visited text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundVisited", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundVisited", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundVisited", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundVisited", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundVisited", newColor); + break; + case 13: + // Negative Text Button (set all negative text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundNegative", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundNegative", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundNegative", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundNegative", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundNegative", newColor); + break; + case 14: + // Neutral Text Button (set all neutral text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundNeutral", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundNeutral", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundNeutral", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundNeutral", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundNeutral", newColor); + break; + case 15: + // Positive Text Button (set all positive text colors) + KConfigGroup(m_config, "Colors:View").writeEntry("ForegroundPositive", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("ForegroundPositive", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("ForegroundPositive", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("ForegroundPositive", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundPositive", newColor); + break; + + case 16: + // Focus Decoration Button (set all focus decoration colors) + KConfigGroup(m_config, "Colors:View").writeEntry("DecorationFocus", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("DecorationFocus", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("DecorationFocus", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("DecorationFocus", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("DecorationFocus", newColor); + break; + case 17: + // Hover Decoration Button (set all hover decoration colors) + KConfigGroup(m_config, "Colors:View").writeEntry("DecorationHover", newColor); + KConfigGroup(m_config, "Colors:Window").writeEntry("DecorationHover", newColor); + KConfigGroup(m_config, "Colors:Selection").writeEntry("DecorationHover", newColor); + KConfigGroup(m_config, "Colors:Button").writeEntry("DecorationHover", newColor); + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("DecorationHover", newColor); + break; + + case 18: + // Tooltip Background button + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("BackgroundNormal", newColor); + break; + case 19: + // Tooltip Text button + KConfigGroup(m_config, "Colors:Tooltip").writeEntry("ForegroundNormal", newColor); + break; + case 20: + // Active Title Background + KConfigGroup(m_config, "WM").writeEntry("activeBackground", newColor); + break; + case 21: + // Active Title Text + KConfigGroup(m_config, "WM").writeEntry("activeForeground", newColor); + break; + case 22: + // Active Title Secondary + KConfigGroup(m_config, "WM").writeEntry("activeBlend", newColor); + break; + case 23: + // Inactive Title Background + KConfigGroup(m_config, "WM").writeEntry("inactiveBackground", newColor); + break; + case 24: + // Inactive Title Text + KConfigGroup(m_config, "WM").writeEntry("inactiveForeground", newColor); + break; + case 25: + // Inactive Title Secondary + KConfigGroup(m_config, "WM").writeEntry("inactiveBlend", newColor); + break; + } + m_commonColorButtons[row]->blockSignals(true); + m_commonColorButtons[row]->setColor(newColor); + m_commonColorButtons[row]->blockSignals(false); + } else { + QString group = colorSetGroupKey(currentSet); + KConfigGroup(m_config, group).writeEntry(m_colorKeys[row], newColor); + } + + updateColorSchemes(); + + Q_EMIT changed(true); +} + +QString SchemeEditorColors::colorSetGroupKey(int colorSet) +{ + QString group; + switch (colorSet) { + case KColorScheme::Window: + group = QStringLiteral("Colors:Window"); + break; + case KColorScheme::Button: + group = QStringLiteral("Colors:Button"); + break; + case KColorScheme::Selection: + group = QStringLiteral("Colors:Selection"); + break; + case KColorScheme::Tooltip: + group = QStringLiteral("Colors:Tooltip"); + break; + case KColorScheme::Complementary: + group = QStringLiteral("Colors:Complementary"); + break; + case KColorScheme::Header: + group = QStringLiteral("Colors:Header"); + break; + default: + group = QStringLiteral("Colors:View"); + } + return group; +} + +void SchemeEditorColors::updateColorSchemes() +{ + m_colorSchemes.clear(); + + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::View, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Window, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Button, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Selection, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Tooltip, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Complementary, m_config)); + m_colorSchemes.append(KColorScheme(QPalette::Active, KColorScheme::Header, m_config)); + + m_wmColors.load(m_config); +} + +void SchemeEditorColors::updateColorTable() +{ + // subtract one here since the 0 item is "Common Colors" + const int currentSet = colorSet->currentIndex() - 1; + + if (currentSet == -1) { + // common colors is selected + stackColors->setCurrentIndex(0); + stackPreview->setCurrentIndex(0); + + KColorButton *button; + foreach (button, m_commonColorButtons) { + button->blockSignals(true); + } + + m_commonColorButtons[0]->setColor(m_colorSchemes[KColorScheme::View].background(KColorScheme::NormalBackground).color()); + m_commonColorButtons[1]->setColor(m_colorSchemes[KColorScheme::View].foreground(KColorScheme::NormalText).color()); + m_commonColorButtons[2]->setColor(m_colorSchemes[KColorScheme::Window].background(KColorScheme::NormalBackground).color()); + m_commonColorButtons[3]->setColor(m_colorSchemes[KColorScheme::Window].foreground(KColorScheme::NormalText).color()); + m_commonColorButtons[4]->setColor(m_colorSchemes[KColorScheme::Button].background(KColorScheme::NormalBackground).color()); + m_commonColorButtons[5]->setColor(m_colorSchemes[KColorScheme::Button].foreground(KColorScheme::NormalText).color()); + m_commonColorButtons[6]->setColor(m_colorSchemes[KColorScheme::Selection].background(KColorScheme::NormalBackground).color()); + m_commonColorButtons[7]->setColor(m_colorSchemes[KColorScheme::Selection].foreground(KColorScheme::NormalText).color()); + m_commonColorButtons[8]->setColor(m_colorSchemes[KColorScheme::Selection].foreground(KColorScheme::InactiveText).color()); + + setCommonForeground(KColorScheme::InactiveText, 0, 9); + setCommonForeground(KColorScheme::ActiveText, 1, 10); + setCommonForeground(KColorScheme::LinkText, 2, 11); + setCommonForeground(KColorScheme::VisitedText, 3, 12); + setCommonForeground(KColorScheme::NegativeText, 4, 13); + setCommonForeground(KColorScheme::NeutralText, 5, 14); + setCommonForeground(KColorScheme::PositiveText, 6, 15); + + setCommonDecoration(KColorScheme::FocusColor, 7, 16); + setCommonDecoration(KColorScheme::HoverColor, 8, 17); + + m_commonColorButtons[18]->setColor(m_colorSchemes[KColorScheme::Tooltip].background(KColorScheme::NormalBackground).color()); + m_commonColorButtons[19]->setColor(m_colorSchemes[KColorScheme::Tooltip].foreground(KColorScheme::NormalText).color()); + + m_commonColorButtons[20]->setColor(m_wmColors.color(WindecoColors::ActiveBackground)); + m_commonColorButtons[21]->setColor(m_wmColors.color(WindecoColors::ActiveForeground)); + m_commonColorButtons[22]->setColor(m_wmColors.color(WindecoColors::ActiveBlend)); + m_commonColorButtons[23]->setColor(m_wmColors.color(WindecoColors::InactiveBackground)); + m_commonColorButtons[24]->setColor(m_wmColors.color(WindecoColors::InactiveForeground)); + m_commonColorButtons[25]->setColor(m_wmColors.color(WindecoColors::InactiveBlend)); + + foreach (button, m_commonColorButtons) { + button->blockSignals(false); + } + } else { + // a real color set is selected + setPreview->setPalette(m_config, (KColorScheme::ColorSet)currentSet); + stackColors->setCurrentIndex(1); + stackPreview->setCurrentIndex(1); + + for (int i = KColorScheme::NormalBackground; i <= KColorScheme::AlternateBackground; ++i) { + m_backgroundButtons[i]->blockSignals(true); + m_backgroundButtons[i]->setColor(m_colorSchemes[currentSet].background(KColorScheme::BackgroundRole(i)).color()); + m_backgroundButtons[i]->blockSignals(false); + } + + for (int i = KColorScheme::NormalText; i <= KColorScheme::PositiveText; ++i) { + m_foregroundButtons[i]->blockSignals(true); + m_foregroundButtons[i]->setColor(m_colorSchemes[currentSet].foreground(KColorScheme::ForegroundRole(i)).color()); + m_foregroundButtons[i]->blockSignals(false); + } + + for (int i = KColorScheme::FocusColor; i <= KColorScheme::HoverColor; ++i) { + m_decorationButtons[i]->blockSignals(true); + m_decorationButtons[i]->setColor(m_colorSchemes[currentSet].decoration(KColorScheme::DecorationRole(i)).color()); + m_decorationButtons[i]->blockSignals(false); + } + } +} + +void SchemeEditorColors::setCommonForeground(KColorScheme::ForegroundRole role, int stackIndex, int buttonIndex) +{ + QColor color = m_colorSchemes[KColorScheme::View].foreground(role).color(); + for (int i = KColorScheme::Window; i < KColorScheme::Tooltip; ++i) { + if (i == KColorScheme::Selection && role == KColorScheme::InactiveText) + break; + + if (m_colorSchemes[i].foreground(role).color() != color) { + m_stackedWidgets[stackIndex]->setCurrentIndex(1); + return; + } + } + + m_stackedWidgets[stackIndex]->setCurrentIndex(0); + m_commonColorButtons[buttonIndex]->setColor(color); +} + +void SchemeEditorColors::setCommonDecoration(KColorScheme::DecorationRole role, int stackIndex, int buttonIndex) +{ + QColor color = m_colorSchemes[KColorScheme::View].decoration(role).color(); + for (int i = KColorScheme::Window; i < KColorScheme::Tooltip; ++i) { + if (m_colorSchemes[i].decoration(role).color() != color) { + m_stackedWidgets[stackIndex]->setCurrentIndex(1); + return; + } + } + + m_stackedWidgets[stackIndex]->setCurrentIndex(0); + m_commonColorButtons[buttonIndex]->setColor(color); +} + +void SchemeEditorColors::updateFromColorSchemes() +{ + for (int i = KColorScheme::View; i <= KColorScheme::Tooltip; ++i) { + KConfigGroup group(m_config, colorSetGroupKey(i)); + group.writeEntry("BackgroundNormal", m_colorSchemes[i].background(KColorScheme::NormalBackground).color()); + group.writeEntry("BackgroundAlternate", m_colorSchemes[i].background(KColorScheme::AlternateBackground).color()); + group.writeEntry("ForegroundNormal", m_colorSchemes[i].foreground(KColorScheme::NormalText).color()); + group.writeEntry("ForegroundInactive", m_colorSchemes[i].foreground(KColorScheme::InactiveText).color()); + group.writeEntry("ForegroundActive", m_colorSchemes[i].foreground(KColorScheme::ActiveText).color()); + group.writeEntry("ForegroundLink", m_colorSchemes[i].foreground(KColorScheme::LinkText).color()); + group.writeEntry("ForegroundVisited", m_colorSchemes[i].foreground(KColorScheme::VisitedText).color()); + group.writeEntry("ForegroundNegative", m_colorSchemes[i].foreground(KColorScheme::NegativeText).color()); + group.writeEntry("ForegroundNeutral", m_colorSchemes[i].foreground(KColorScheme::NeutralText).color()); + group.writeEntry("ForegroundPositive", m_colorSchemes[i].foreground(KColorScheme::PositiveText).color()); + group.writeEntry("DecorationFocus", m_colorSchemes[i].decoration(KColorScheme::FocusColor).color()); + group.writeEntry("DecorationHover", m_colorSchemes[i].decoration(KColorScheme::HoverColor).color()); + } + + KConfigGroup WMGroup(m_config, "WM"); + WMGroup.writeEntry("activeBackground", m_wmColors.color(WindecoColors::ActiveBackground)); + WMGroup.writeEntry("activeForeground", m_wmColors.color(WindecoColors::ActiveForeground)); + WMGroup.writeEntry("inactiveBackground", m_wmColors.color(WindecoColors::InactiveBackground)); + WMGroup.writeEntry("inactiveForeground", m_wmColors.color(WindecoColors::InactiveForeground)); + WMGroup.writeEntry("activeBlend", m_wmColors.color(WindecoColors::ActiveBlend)); + WMGroup.writeEntry("inactiveBlend", m_wmColors.color(WindecoColors::InactiveBlend)); +} diff --git a/plasma/workspace/kcms/colors/editor/scmeditorcolors.h b/plasma/workspace/kcms/colors/editor/scmeditorcolors.h new file mode 100644 index 0000000000..1710a6239b --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditorcolors.h @@ -0,0 +1,93 @@ +/* ColorEdit widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "ui_scmeditorcolors.h" + +class KColorButton; + +class SchemeEditorColors : public QWidget, public Ui::ScmEditorColors +{ + Q_OBJECT + +public: + SchemeEditorColors(KSharedConfigPtr config, QWidget *parent = nullptr); + void updateValues(); + void updateFromColorSchemes(); + +Q_SIGNALS: + void changed(bool); + +private Q_SLOTS: + + /** slot called when any varies button is clicked */ + void variesClicked(); + + /** slot called when color on a KColorButton changes */ + void colorChanged(const QColor &newColor); + + /** set the colortable color buttons up according to the current colorset */ + void updateColorTable(); + + /** update m_colorSchemes contents from the values in m_config */ + void updateColorSchemes(); + + /** setup the colortable with its buttons and labels */ + void setupColorTable(); + +private: + class WindecoColors + { + public: + enum Role { ActiveForeground = 0, ActiveBackground = 1, InactiveForeground = 2, InactiveBackground = 3, ActiveBlend = 4, InactiveBlend = 5 }; + + WindecoColors() + { + } + WindecoColors(const KSharedConfigPtr &); + virtual ~WindecoColors() + { + } + + void load(const KSharedConfigPtr &); + QColor color(Role) const; + + private: + QColor m_colors[6]; + }; + + void changeColor(int row, const QColor &newColor); + + /** helper to create color entries */ + void createColorEntry(const QString &text, const QString &key, QList &list, int index); + + void setCommonForeground(KColorScheme::ForegroundRole role, int stackIndex, int buttonIndex); + void setCommonDecoration(KColorScheme::DecorationRole role, int stackIndex, int buttonIndex); + + /** get the groupKey for the given colorSet */ + static QString colorSetGroupKey(int colorSet); + + QList m_backgroundButtons; + QList m_foregroundButtons; + QList m_decorationButtons; + QList m_commonColorButtons; + QList m_colorSchemes; + QList m_stackedWidgets; + + QStringList m_colorKeys; + + WindecoColors m_wmColors; + + KSharedConfigPtr m_config; +}; diff --git a/plasma/workspace/kcms/colors/editor/scmeditorcolors.ui b/plasma/workspace/kcms/colors/editor/scmeditorcolors.ui new file mode 100644 index 0000000000..5b3d1514f1 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditorcolors.ui @@ -0,0 +1,523 @@ + + + ScmEditorColors + + + + 0 + 0 + 400 + 300 + + + + + + + + 0 + 0 + + + + Color set: + + + + + + + + 0 + 0 + + + + Colorset to view/modify + + + + Common Colors + + + + + View + + + + + Window + + + + + Button + + + + + Selection + + + + + Tooltip + + + + + Complementary + + + + + Header + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + 2 + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + New Row + + + + + 0 + + + + + 1 + + + + + View Background + + + + + View Text + + + + + Window Background + + + + + Window Text + + + + + Button Background + + + + + Button Text + + + + + Selection Background + + + + + Selection Text + + + + + Selection Inactive Text + + + + + Inactive Text + + + + + Active Text + + + + + Link Text + + + + + Visited Text + + + + + Negative Text + + + + + Neutral Text + + + + + Positive Text + + + + + Focus Decoration + + + + + Hover Decoration + + + + + Tooltip Background + + + + + Tooltip Text + + + + + Active Titlebar + + + + + Active Titlebar Text + + + + + Active Titlebar Secondary + + + + + Inactive Titlebar + + + + + Inactive Titlebar Text + + + + + Inactive Titlebar Secondary + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + 2 + + + + + + + + + + + + + + 0 + 0 + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 10 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 10 + + + + + + + + + + + + + PreviewWidget + QWidget +
previewwidget.h
+ 1 +
+ + SetPreviewWidget + QWidget +
setpreviewwidget.h
+
+
+ + +
diff --git a/plasma/workspace/kcms/colors/editor/scmeditordialog.cpp b/plasma/workspace/kcms/colors/editor/scmeditordialog.cpp new file mode 100644 index 0000000000..07df373c29 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditordialog.cpp @@ -0,0 +1,210 @@ +/* ColorEdit widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scmeditordialog.h" +#include "scmeditorcolors.h" +#include "scmeditoreffects.h" +#include "scmeditoroptions.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +SchemeEditorDialog::SchemeEditorDialog(KSharedConfigPtr config, QWidget *parent) + : QDialog(parent) +{ + m_config = config; + init(); +} + +SchemeEditorDialog::SchemeEditorDialog(const QString &path, QWidget *parent) + : QDialog(parent) + , m_filePath(path) +{ + m_config = KSharedConfig::openConfig(path); + + m_schemeName = KConfigGroup(m_config, "General").readEntry("Name"); + this->setWindowTitle(m_schemeName); + init(); +} + +bool SchemeEditorDialog::showApplyOverwriteButton() const +{ + return m_showApplyOverwriteButton; +} + +void SchemeEditorDialog::setShowApplyOverwriteButton(bool show) +{ + m_showApplyOverwriteButton = show; + + buttonBox->button(QDialogButtonBox::Apply)->setVisible(show); +} + +void SchemeEditorDialog::init() +{ + setupUi(this); + + m_optionTab = new SchemeEditorOptions(m_config); + m_colorTab = new SchemeEditorColors(m_config); + m_disabledTab = new SchemeEditorEffects(m_config, QPalette::Disabled); + m_inactiveTab = new SchemeEditorEffects(m_config, QPalette::Inactive); + tabWidget->addTab(m_optionTab, i18n("Options")); + tabWidget->addTab(m_colorTab, i18n("Colors")); + tabWidget->addTab(m_disabledTab, i18n("Disabled")); + tabWidget->setCurrentWidget(m_colorTab); + + connect(m_optionTab, &SchemeEditorOptions::changed, this, &SchemeEditorDialog::updateTabs); + connect(m_colorTab, &SchemeEditorColors::changed, this, &SchemeEditorDialog::updateTabs); + connect(m_disabledTab, &SchemeEditorEffects::changed, this, &SchemeEditorDialog::updateTabs); + connect(m_inactiveTab, &SchemeEditorEffects::changed, this, &SchemeEditorDialog::updateTabs); + + // "Save" is only shown in overwrite mode + KGuiItem::assign(buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::save()); + buttonBox->button(QDialogButtonBox::Apply)->setVisible(false); + buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + + KGuiItem::assign(buttonBox->button(QDialogButtonBox::Save), KStandardGuiItem::saveAs()); + buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + updateTabs(); +} + +void SchemeEditorDialog::on_buttonBox_clicked(QAbstractButton *button) +{ + if (buttonBox->standardButton(button) == QDialogButtonBox::Reset) { + m_config->markAsClean(); + m_config->reparseConfiguration(); + updateTabs(); + setUnsavedChanges(false); + } else if (buttonBox->standardButton(button) == QDialogButtonBox::Save) { + saveScheme(false /*overwrite*/); + } else if (buttonBox->standardButton(button) == QDialogButtonBox::Apply) { + saveScheme(true /*overwrite*/); + } else if (buttonBox->standardButton(button) == QDialogButtonBox::Close) { + if (m_unsavedChanges) { + KMessageBox::ButtonCode ans = + KMessageBox::questionYesNo(this, i18n("You have unsaved changes. Do you really want to quit?"), i18n("Unsaved changes")); + if (ans == KMessageBox::No) { + return; + } + } + m_config->markAsClean(); + m_config->reparseConfiguration(); + this->accept(); + } +} + +void SchemeEditorDialog::saveScheme(bool overwrite) +{ + QString name = m_schemeName; + + // prompt for the name to save as + if (!overwrite) { + bool ok; + name = QInputDialog::getText(this, i18n("Save Color Scheme"), i18n("&Enter a name for the color scheme:"), QLineEdit::Normal, m_schemeName, &ok); + if (!ok) { + return; + } + } + + QString filename = name; + filename.remove(QLatin1Char('\'')); // So Foo's does not become FooS + QRegExp fixer(QStringLiteral("[\\W,.-]+(.?)")); + int offset; + while ((offset = fixer.indexIn(filename)) >= 0) + filename.replace(offset, fixer.matchedLength(), fixer.cap(1).toUpper()); + filename.replace(0, 1, filename.at(0).toUpper()); + + // check if that name is already in the list + const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/") + filename + QStringLiteral(".colors")); + + QFile file(path); + const int permissions = file.permissions(); + const bool canWrite = (permissions & QFile::WriteUser); + // or if we can overwrite it if it exists + if (path.isEmpty() || !file.exists() || canWrite) { + if (canWrite && !overwrite) { + int ret = KMessageBox::questionYesNo(this, + i18n("A color scheme with that name already exists.\nDo you want to overwrite it?"), + i18n("Save Color Scheme"), + KStandardGuiItem::overwrite(), + KStandardGuiItem::cancel()); + + // on don't overwrite, call again the function + if (ret == KMessageBox::No) { + this->saveScheme(overwrite); + return; + } + } + + // go ahead and save it + QString newpath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/color-schemes/"; + QDir dir; + dir.mkpath(newpath); + newpath += filename + ".colors"; + + KConfig *config = m_config->copyTo(newpath); + m_config->markAsClean(); + m_config->reparseConfiguration(); + KConfigGroup group(config, "General"); + group.writeEntry("Name", name); + + // sync it and delete pointer + config->sync(); + delete config; + // reopen and update window + m_config = KSharedConfig::openConfig(newpath); + m_schemeName = name; + setWindowTitle(name); + + setUnsavedChanges(false); + + QTextStream out(stdout); + out << filename << Qt::endl; + } else if (!canWrite && file.exists()) { + KMessageBox::error(this, i18n("You do not have permission to overwrite that scheme"), i18n("Error")); + } +} + +void SchemeEditorDialog::updateTabs(bool madeByUser) +{ + if (madeByUser) { + setUnsavedChanges(true); + } + KConfigGroup group(m_config, "ColorEffects:Inactive"); + bool showInactiveTab = group.readEntry("Enable", QVariant(true)).toBool(); + + const int idx = tabWidget->indexOf(m_inactiveTab); + + if (showInactiveTab && idx == -1) { + tabWidget->addTab(m_inactiveTab, i18n("Inactive")); + } else if (!showInactiveTab && idx > -1) { + tabWidget->removeTab(idx); + } + + m_optionTab->updateValues(); + m_colorTab->updateValues(); + m_inactiveTab->updateValues(); + m_disabledTab->updateValues(); +} + +void SchemeEditorDialog::setUnsavedChanges(bool changes) +{ + m_unsavedChanges = changes; + if (changes) { + buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + buttonBox->button(QDialogButtonBox::Reset)->setEnabled(true); + } else { + buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); + } +} diff --git a/plasma/workspace/kcms/colors/editor/scmeditordialog.h b/plasma/workspace/kcms/colors/editor/scmeditordialog.h new file mode 100644 index 0000000000..7da0ee7a05 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditordialog.h @@ -0,0 +1,61 @@ +/* ColorEdit widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "ui_scmeditordialog.h" + +class SchemeEditorOptions; +class SchemeEditorColors; +class SchemeEditorEffects; + +class SchemeEditorDialog : public QDialog, public Ui::ScmEditorDialog +{ + Q_OBJECT + +public: + SchemeEditorDialog(const QString &path, QWidget *parent = nullptr); + SchemeEditorDialog(KSharedConfigPtr config, QWidget *parent = nullptr); + + bool showApplyOverwriteButton() const; + void setShowApplyOverwriteButton(bool show); + +Q_SIGNALS: + void changed(bool); + +private Q_SLOTS: + + + void on_buttonBox_clicked(QAbstractButton *button); + + void updateTabs(bool byUser = false); + +private: + void init(); + /** save the current scheme */ + void saveScheme(bool overwrite); + void setUnsavedChanges(bool changes); + + const QString m_filePath; + QString m_schemeName; + KSharedConfigPtr m_config; + bool m_disableUpdates = false; + bool m_unsavedChanges = false; + + SchemeEditorOptions *m_optionTab; + SchemeEditorColors *m_colorTab; + SchemeEditorEffects *m_disabledTab; + SchemeEditorEffects *m_inactiveTab; + + bool m_showApplyOverwriteButton = false; +}; diff --git a/plasma/workspace/kcms/colors/editor/scmeditordialog.ui b/plasma/workspace/kcms/colors/editor/scmeditordialog.ui new file mode 100644 index 0000000000..91ebffcf80 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditordialog.ui @@ -0,0 +1,49 @@ + + + ScmEditorDialog + + + + 0 + 0 + 477 + 422 + + + + + + + -1 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Close|QDialogButtonBox::Reset|QDialogButtonBox::Apply|QDialogButtonBox::Save + + + + + + + + + + diff --git a/plasma/workspace/kcms/colors/editor/scmeditoreffects.cpp b/plasma/workspace/kcms/colors/editor/scmeditoreffects.cpp new file mode 100644 index 0000000000..6f097a43ef --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoreffects.cpp @@ -0,0 +1,153 @@ +/* KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scmeditoreffects.h" + +#include +#include + +SchemeEditorEffects::SchemeEditorEffects(const KSharedConfigPtr &config, QPalette::ColorGroup palette, QWidget *parent) + : QWidget(parent) + , m_palette(palette) + , m_config(config) +{ + setupUi(this); +} + +void SchemeEditorEffects::on_intensityBox_currentIndexChanged(int index) +{ + Q_UNUSED(index); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_intensitySlider_valueChanged(int value) +{ + Q_UNUSED(value); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_colorBox_currentIndexChanged(int index) +{ + Q_UNUSED(index); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_colorSlider_valueChanged(int value) +{ + Q_UNUSED(value); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_colorButton_changed(const QColor &color) +{ + Q_UNUSED(color); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_contrastBox_currentIndexChanged(int index) +{ + Q_UNUSED(index); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::on_contrastSlider_valueChanged(int value) +{ + Q_UNUSED(value); + updateFromEffectsPage(); +} + +void SchemeEditorEffects::updateValues() +{ + m_disableUpdates = true; + + // NOTE: keep this in sync with kdelibs/kdeui/colors/kcolorscheme.cpp + if (m_palette == QPalette::Inactive) { + KConfigGroup group(m_config, "ColorEffects:Inactive"); + intensityBox->setCurrentIndex(abs(group.readEntry("IntensityEffect", 0))); + intensitySlider->setValue(int(group.readEntry("IntensityAmount", 0.0) * 20.0) + 20); + colorBox->setCurrentIndex(abs(group.readEntry("ColorEffect", 2))); + if (colorBox->currentIndex() > 1) { + colorSlider->setValue(int(group.readEntry("ColorAmount", 0.025) * 40.0)); + } else { + colorSlider->setValue(int(group.readEntry("ColorAmount", 0.05) * 20.0) + 20); + } + colorButton->setColor(group.readEntry("Color", QColor(112, 111, 110))); + contrastBox->setCurrentIndex(abs(group.readEntry("ContrastEffect", 2))); + contrastSlider->setValue(int(group.readEntry("ContrastAmount", 0.1) * 20.0)); + + } else if (m_palette == QPalette::Disabled) { + KConfigGroup group(m_config, "ColorEffects:Disabled"); + intensityBox->setCurrentIndex(group.readEntry("IntensityEffect", 2)); + intensitySlider->setValue(int(group.readEntry("IntensityAmount", 0.1) * 20.0) + 20); + colorBox->setCurrentIndex(group.readEntry("ColorEffect", 0)); + if (colorBox->currentIndex() > 1) { + colorSlider->setValue(int(group.readEntry("ColorAmount", 0.0) * 40.0)); + } else { + colorSlider->setValue(int(group.readEntry("ColorAmount", 0.0) * 20.0) + 20); + } + colorButton->setColor(group.readEntry("Color", QColor(56, 56, 56))); + contrastBox->setCurrentIndex(group.readEntry("ContrastEffect", 1)); + contrastSlider->setValue(int(group.readEntry("ContrastAmount", 0.65) * 20.0)); + } else { + return; + } + + m_disableUpdates = false; + + // enable/disable controls + intensitySlider->setDisabled(intensityBox->currentIndex() == 0); + colorSlider->setDisabled(colorBox->currentIndex() == 0); + colorButton->setDisabled(colorBox->currentIndex() < 2); + contrastSlider->setDisabled(contrastBox->currentIndex() == 0); + preview->setPalette(m_config, m_palette); +} + +void SchemeEditorEffects::updateFromEffectsPage() +{ + if (m_disableUpdates) { + // don't write the config as we are reading it! + return; + } + + QString groupName; + if (m_palette == QPalette::Inactive) { + groupName = QStringLiteral("ColorEffects:Inactive"); + } else if (m_palette == QPalette::Disabled) { + groupName = QStringLiteral("ColorEffects:Disabled"); + } else { + return; + } + KConfigGroup group(m_config, groupName); + + // intensity + group.writeEntry("IntensityEffect", intensityBox->currentIndex()); + group.writeEntry("IntensityAmount", qreal(intensitySlider->value() - 20) * 0.05); + + // color + group.writeEntry("ColorEffect", colorBox->currentIndex()); + if (colorBox->currentIndex() > 1) { + group.writeEntry("ColorAmount", qreal(colorSlider->value()) * 0.025); + } else { + group.writeEntry("ColorAmount", qreal(colorSlider->value() - 20) * 0.05); + } + + group.writeEntry("Color", colorButton->color()); + + // contrast + group.writeEntry("ContrastEffect", contrastBox->currentIndex()); + group.writeEntry("ContrastAmount", qreal(contrastSlider->value()) * 0.05); + + // enable/disable controls + intensitySlider->setDisabled(intensityBox->currentIndex() == 0); + colorSlider->setDisabled(colorBox->currentIndex() == 0); + colorButton->setDisabled(colorBox->currentIndex() < 2); + contrastSlider->setDisabled(contrastBox->currentIndex() == 0); + + preview->setPalette(m_config, m_palette); + + Q_EMIT changed(true); +} diff --git a/plasma/workspace/kcms/colors/editor/scmeditoreffects.h b/plasma/workspace/kcms/colors/editor/scmeditoreffects.h new file mode 100644 index 0000000000..c9dba36099 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoreffects.h @@ -0,0 +1,50 @@ +/* ColorEdit widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "ui_scmeditoreffects.h" + +class SchemeEditorEffects : public QWidget, public Ui::ScmEditorEffects +{ + Q_OBJECT + +public: + SchemeEditorEffects(const KSharedConfigPtr &config, QPalette::ColorGroup palette, QWidget *parent = nullptr); + void updateValues(); + void updateFromEffectsPage(); + +Q_SIGNALS: + void changed(bool); + +private Q_SLOTS: + + void on_intensityBox_currentIndexChanged(int index); + + void on_intensitySlider_valueChanged(int value); + + void on_colorBox_currentIndexChanged(int index); + + void on_colorSlider_valueChanged(int value); + + void on_colorButton_changed(const QColor &color); + + void on_contrastBox_currentIndexChanged(int index); + + void on_contrastSlider_valueChanged(int value); + +private: + QPalette::ColorGroup m_palette; + KSharedConfigPtr m_config; + bool m_disableUpdates; +}; diff --git a/plasma/workspace/kcms/colors/editor/scmeditoreffects.ui b/plasma/workspace/kcms/colors/editor/scmeditoreffects.ui new file mode 100644 index 0000000000..91fa6b5c18 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoreffects.ui @@ -0,0 +1,265 @@ + + + ScmEditorEffects + + + + 0 + 0 + 400 + 300 + + + + + + + Color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Intensity: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Disabled color effect type + + + + None + + + + + Desaturate + + + + + Fade + + + + + Tint + + + + + + + + Disabled intensity effect type + + + + None + + + + + Shade + + + + + Darken + + + + + Lighten + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 69 + 22 + + + + + + + + Contrast: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + Disabled color effect amount + + + 40 + + + 10 + + + Qt::Horizontal + + + + + + + false + + + Disabled intensity effect amount + + + 40 + + + 10 + + + Qt::Horizontal + + + + + + + false + + + Disabled color + + + + + + + + 0 + 0 + + + + + 0 + 10 + + + + + + + + Disabled contrast type + + + + None + + + + + Fade + + + + + Tint + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 69 + 20 + + + + + + + + false + + + Disabled contrast amount + + + 20 + + + 10 + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+ + PreviewWidget + QWidget +
previewwidget.h
+ 1 +
+
+ + +
diff --git a/plasma/workspace/kcms/colors/editor/scmeditoroptions.cpp b/plasma/workspace/kcms/colors/editor/scmeditoroptions.cpp new file mode 100644 index 0000000000..0835bf0bd4 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoroptions.cpp @@ -0,0 +1,110 @@ +/* KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scmeditoroptions.h" + +#include +#include + +SchemeEditorOptions::SchemeEditorOptions(KSharedConfigPtr config, QWidget *parent) + : QWidget(parent) + , m_config(config) +{ + setupUi(this); + m_disableUpdates = false; + loadOptions(); +} + +void SchemeEditorOptions::updateValues() +{ + loadOptions(); +} + +void SchemeEditorOptions::loadOptions() +{ + KConfigGroup generalGroup(m_config, "General"); + shadeSortedColumn->setChecked(generalGroup.readEntry("shadeSortColumn", true)); + + accentActiveTitlebar->setChecked(generalGroup.readEntry("accentActiveTitlebar", false)); + accentInactiveTitlebar->setChecked(generalGroup.readEntry("accentInactiveTitlebar", false)); + + KConfigGroup KDEgroup(m_config, "KDE"); + contrastSlider->setValue(KDEgroup.readEntry("contrast", KColorScheme::contrast())); + + KConfigGroup group(m_config, "ColorEffects:Inactive"); + useInactiveEffects->setChecked(group.readEntry("Enable", false)); + + // NOTE: keep this in sync with kdelibs/kdeui/colors/kcolorscheme.cpp + // NOTE: remove extra logic from updateFromOptions and on_useInactiveEffects_stateChanged when this changes! + inactiveSelectionEffect->setChecked(group.readEntry("ChangeSelectionColor", group.readEntry("Enable", true))); +} + +// Option slot +void SchemeEditorOptions::on_contrastSlider_valueChanged(int value) +{ + KConfigGroup group(m_config, "KDE"); + group.writeEntry("contrast", value); + + Q_EMIT changed(true); +} + +void SchemeEditorOptions::on_shadeSortedColumn_stateChanged(int state) +{ + if (m_disableUpdates) + return; + KConfigGroup group(m_config, "General"); + group.writeEntry("shadeSortColumn", bool(state != Qt::Unchecked)); + + Q_EMIT changed(true); +} + +void SchemeEditorOptions::on_useInactiveEffects_stateChanged(int state) +{ + KConfigGroup group(m_config, "ColorEffects:Inactive"); + group.writeEntry("Enable", bool(state != Qt::Unchecked)); + + m_disableUpdates = true; + inactiveSelectionEffect->setChecked(group.readEntry("ChangeSelectionColor", bool(state != Qt::Unchecked))); + m_disableUpdates = false; + + Q_EMIT changed(true); +} + +void SchemeEditorOptions::on_inactiveSelectionEffect_stateChanged(int state) +{ + if (m_disableUpdates) { + // don't write the config as we are reading it! + return; + } + + KConfigGroup group(m_config, "ColorEffects:Inactive"); + group.writeEntry("ChangeSelectionColor", bool(state != Qt::Unchecked)); + + Q_EMIT changed(true); +} + +void SchemeEditorOptions::on_accentActiveTitlebar_stateChanged(int state) +{ + if (m_disableUpdates) + return; + + KConfigGroup group(m_config, "General"); + group.writeEntry("accentActiveTitlebar", bool(state == Qt::Checked)); + + Q_EMIT changed(true); +} + +void SchemeEditorOptions::on_accentInactiveTitlebar_stateChanged(int state) +{ + if (m_disableUpdates) + return; + + KConfigGroup group(m_config, "General"); + group.writeEntry("accentInactiveTitlebar", bool(state == Qt::Checked)); + + Q_EMIT changed(true); +} diff --git a/plasma/workspace/kcms/colors/editor/scmeditoroptions.h b/plasma/workspace/kcms/colors/editor/scmeditoroptions.h new file mode 100644 index 0000000000..c028b5daea --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoroptions.h @@ -0,0 +1,47 @@ +/* ColorEdit widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2016 Olivier Churlaud + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "ui_scmeditoroptions.h" + +class SchemeEditorOptions : public QWidget, public Ui::ScmEditorOptions +{ + Q_OBJECT + +public: + SchemeEditorOptions(KSharedConfigPtr config, QWidget *parent = nullptr); + void updateValues(); + +Q_SIGNALS: + void changed(bool); + +private Q_SLOTS: + + // options slots + void on_contrastSlider_valueChanged(int value); + void on_shadeSortedColumn_stateChanged(int state); + void on_inactiveSelectionEffect_stateChanged(int state); + void on_useInactiveEffects_stateChanged(int state); + void on_accentActiveTitlebar_stateChanged(int state); + void on_accentInactiveTitlebar_stateChanged(int state); + +private: + /** load options from global */ + void loadOptions(); + void setCommonForeground(KColorScheme::ForegroundRole role, int stackIndex, int buttonIndex); + void setCommonDecoration(KColorScheme::DecorationRole role, int stackIndex, int buttonIndex); + + KSharedConfigPtr m_config; + bool m_disableUpdates; +}; diff --git a/plasma/workspace/kcms/colors/editor/scmeditoroptions.ui b/plasma/workspace/kcms/colors/editor/scmeditoroptions.ui new file mode 100644 index 0000000000..1b02a9d925 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/scmeditoroptions.ui @@ -0,0 +1,131 @@ + + + ScmEditorOptions + + + + 0 + 0 + 400 + 300 + + + + + + + Apply &effects to inactive windows + + + + + + + Use different colors for in&active selections + + + + + + + Apply a&ccent color to active window titlebars + + + + + + + Apply acce&nt color to inactive window titlebars + + + + + + + Shade sorted column &in lists + + + + + + + Shading of frames and lighting ("3D") effects + + + Shading + + + + 12 + + + 0 + + + + + Minimum + + + + + + + Qt::Horizontal + + + + 198 + 20 + + + + + + + + Maximum + + + + + + + Contrast + + + + + + + 10 + + + 5 + + + Qt::Horizontal + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/plasma/workspace/kcms/colors/editor/setpreview.ui b/plasma/workspace/kcms/colors/editor/setpreview.ui new file mode 100644 index 0000000000..09ae55dee8 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/setpreview.ui @@ -0,0 +1,347 @@ + + + setpreview + + + + 0 + 0 + 550 + 96 + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 0 + + + + + + + + Normal Text on Normal Background + + + normal + + + + + + + + + + + + + + true + + + + Link Text on Normal Background + + + link + + + + + + + + + + + + + + true + + + + Visited Text on Normal Background + + + visited + + + + + + + + + + Active Text on Normal Background + + + + + + active + + + + + + + + + + + + + Inactive Text on Normal Background + + + inactive + + + + + + + + + + + + + Negative Text on Normal Background + + + negative + + + + + + + + + + + + + Neutral Text on Normal Background + + + neutral + + + + + + + + + + + + + Positive Text on Normal Background + + + positive + + + + + + + + + + Hover on Normal Background + + + + + + hover + + + + + + + + + + + + + Normal Text on Normal Background + + + normal + + + + + + + + + + + + + + true + + + + Link Text on Link Background +(Note: Link Background is derived from Link Text and cannot be separately configured at this time) + + + link + + + + + + + + + + + + + + true + + + + Visited Text on Visited Background +(Note: Visited Background is derived from Visited Text and cannot be separately configured at this time) + + + visited + + + + + + + + + + + + + Active Text on Active Background +(Note: Active Background is derived from Active Text and cannot be separately configured at this time) + + + active + + + + + + + + + + + + + Inactive Text on Alternate Background + + + alternate + + + + + + + + + + + + + Negative Text on Negative Background +(Note: Negative Background is derived from Negative Text and cannot be separately configured at this time) + + + negative + + + + + + + + + + + + + Neutral Text on Neutral Background +(Note: Neutral Background is derived from Neutral Text and cannot be separately configured at this time) + + + neutral + + + + + + + + + + + + + Positive Text on Positive Background +(Note: Positive Background is derived from Positive Text and cannot be separately configured at this time) + + + positive + + + + + + + + + + + + + Focus on Normal Background + + + focus + + + + + + + + + + + diff --git a/plasma/workspace/kcms/colors/editor/setpreviewwidget.cpp b/plasma/workspace/kcms/colors/editor/setpreviewwidget.cpp new file mode 100644 index 0000000000..c6a4f329a0 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/setpreviewwidget.cpp @@ -0,0 +1,118 @@ +/* Preview widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Urs Wolfer + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "setpreviewwidget.h" + +void setAutoFill(QWidget *widget) +{ + widget->setAutoFillBackground(true); + widget->setBackgroundRole(QPalette::Base); +} + +SetPreviewWidget::SetPreviewWidget(QWidget *parent) + : QFrame(parent) +{ + setupUi(this); + + // set correct colors on... lots of things + setAutoFillBackground(true); + setBackgroundRole(QPalette::Base); + setAutoFill(widgetBack0); + setAutoFill(widgetBack1); + setAutoFill(widgetBack2); + setAutoFill(widgetBack3); + setAutoFill(widgetBack4); + setAutoFill(widgetBack5); + setAutoFill(widgetBack6); + setAutoFill(widgetBack7); + setAutoFillBackground(true); + /* + frame->setBackgroundRole(QPalette::Base); + viewWidget->setBackgroundRole(QPalette::Base); + labelView0->setBackgroundRole(QPalette::Base); + labelView3->setBackgroundRole(QPalette::Base); + labelView4->setBackgroundRole(QPalette::Base); + labelView2->setBackgroundRole(QPalette::Base); + labelView1->setBackgroundRole(QPalette::Base); + labelView5->setBackgroundRole(QPalette::Base); + labelView6->setBackgroundRole(QPalette::Base); + labelView7->setBackgroundRole(QPalette::Base); + selectionWidget->setBackgroundRole(QPalette::Highlight); + labelSelection0->setBackgroundRole(QPalette::Highlight); + labelSelection3->setBackgroundRole(QPalette::Highlight); + labelSelection4->setBackgroundRole(QPalette::Highlight); + labelSelection2->setBackgroundRole(QPalette::Highlight); + labelSelection1->setBackgroundRole(QPalette::Highlight); + labelSelection5->setBackgroundRole(QPalette::Highlight); + labelSelection6->setBackgroundRole(QPalette::Highlight); + labelSelection7->setBackgroundRole(QPalette::Highlight); + */ + + const QList widgets = findChildren(); + for (QWidget *widget : widgets) { + widget->installEventFilter(this); + widget->setFocusPolicy(Qt::NoFocus); + } +} + +SetPreviewWidget::~SetPreviewWidget() +{ +} + +bool SetPreviewWidget::eventFilter(QObject *, QEvent *ev) +{ + switch (ev->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseButtonDblClick: + case QEvent::MouseMove: + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::Enter: + case QEvent::Leave: + case QEvent::Wheel: + case QEvent::ContextMenu: + return true; // ignore + default: + break; + } + return false; +} + +void SetPreviewWidget::setPalette(const KSharedConfigPtr &config, KColorScheme::ColorSet set) +{ + QPalette palette = KColorScheme::createApplicationPalette(config); + KColorScheme::adjustBackground(palette, KColorScheme::NormalBackground, QPalette::Base, set, config); + QFrame::setPalette(palette); + +#define SET_ROLE_PALETTE(n, f, b) \ + KColorScheme::adjustForeground(palette, KColorScheme::f, QPalette::Text, set, config); \ + labelFore##n->setPalette(palette); \ + KColorScheme::adjustBackground(palette, KColorScheme::b, QPalette::Base, set, config); \ + labelBack##n->setPalette(palette); \ + widgetBack##n->setPalette(palette); + + SET_ROLE_PALETTE(0, NormalText, NormalBackground); + SET_ROLE_PALETTE(1, InactiveText, AlternateBackground); + SET_ROLE_PALETTE(2, ActiveText, ActiveBackground); + SET_ROLE_PALETTE(3, LinkText, LinkBackground); + SET_ROLE_PALETTE(4, VisitedText, VisitedBackground); + SET_ROLE_PALETTE(5, NegativeText, NegativeBackground); + SET_ROLE_PALETTE(6, NeutralText, NeutralBackground); + SET_ROLE_PALETTE(7, PositiveText, PositiveBackground); + + KColorScheme kcs(QPalette::Active, set, config); + QBrush deco; + +#define SET_DECO_PALETTE(n, d) \ + deco = kcs.decoration(KColorScheme::d); \ + palette.setBrush(QPalette::Text, deco); \ + labelFore##n->setPalette(palette); + + SET_DECO_PALETTE(8, HoverColor); + SET_DECO_PALETTE(9, FocusColor); +} diff --git a/plasma/workspace/kcms/colors/editor/setpreviewwidget.h b/plasma/workspace/kcms/colors/editor/setpreviewwidget.h new file mode 100644 index 0000000000..f481b932e1 --- /dev/null +++ b/plasma/workspace/kcms/colors/editor/setpreviewwidget.h @@ -0,0 +1,33 @@ +/* Colorset Preview widget for KDE Display color scheme setup module + SPDX-FileCopyrightText: 2008 Matthew Woehlke + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include + +#include "ui_setpreview.h" + +/** + * The Desktop/Colors tab in kcontrol. + */ +class SetPreviewWidget : public QFrame, Ui::setpreview +{ + Q_OBJECT + +public: + explicit SetPreviewWidget(QWidget *parent); + ~SetPreviewWidget() override; + + void setPalette(const KSharedConfigPtr &config, KColorScheme::ColorSet); + +protected: + void setPaletteRecursive(QWidget *, const QPalette &); + bool eventFilter(QObject *, QEvent *) override; +}; diff --git a/plasma/workspace/kcms/colors/filterproxymodel.cpp b/plasma/workspace/kcms/colors/filterproxymodel.cpp new file mode 100644 index 0000000000..9211e08449 --- /dev/null +++ b/plasma/workspace/kcms/colors/filterproxymodel.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "filterproxymodel.h" + +#include "colorsmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::selectedScheme() const +{ + return m_selectedScheme; +} + +void FilterProxyModel::setSelectedScheme(const QString &scheme) +{ + if (m_selectedScheme == scheme) { + return; + } + + m_selectedScheme = scheme; + + Q_EMIT selectedSchemeChanged(); + Q_EMIT selectedSchemeIndexChanged(); +} + +int FilterProxyModel::selectedSchemeIndex() const +{ + // We must search in the source model and then map the index to our proxy model. + const auto results = sourceModel()->match(sourceModel()->index(0, 0), ColorsModel::SchemeNameRole, m_selectedScheme, 1, Qt::MatchExactly); + + if (results.count() == 1) { + const QModelIndex result = mapFromSource(results.first()); + if (result.isValid()) { + return result.row(); + } + } + + return -1; +} + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + const int oldIndex = selectedSchemeIndex(); + + m_query = query; + invalidateFilter(); + + Q_EMIT queryChanged(); + + if (selectedSchemeIndex() != oldIndex) { + Q_EMIT selectedSchemeIndexChanged(); + } + } +} + +KCMColors::SchemeFilter FilterProxyModel::filter() const +{ + return m_filter; +} + +void FilterProxyModel::setFilter(KCMColors::SchemeFilter filter) +{ + if (m_filter != filter) { + const int oldIndex = selectedSchemeIndex(); + + m_filter = filter; + invalidateFilter(); + + Q_EMIT filterChanged(); + + if (selectedSchemeIndex() != oldIndex) { + Q_EMIT selectedSchemeIndexChanged(); + } + } +} + +bool FilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (!m_query.isEmpty()) { + if (!idx.data(Qt::DisplayRole).toString().contains(m_query, Qt::CaseInsensitive) + && !idx.data(ColorsModel::SchemeNameRole).toString().contains(m_query, Qt::CaseInsensitive)) { + return false; + } + } + + if (m_filter != KCMColors::AllSchemes) { + const QPalette palette = idx.data(ColorsModel::PaletteRole).value(); + + const int windowBackgroundGray = qGray(palette.window().color().rgb()); + + if (m_filter == KCMColors::DarkSchemes) { + return windowBackgroundGray < 192; + } else if (m_filter == KCMColors::LightSchemes) { + return windowBackgroundGray >= 192; + } + } + + return true; +} diff --git a/plasma/workspace/kcms/colors/filterproxymodel.h b/plasma/workspace/kcms/colors/filterproxymodel.h new file mode 100644 index 0000000000..f37eb4fd7b --- /dev/null +++ b/plasma/workspace/kcms/colors/filterproxymodel.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include "colors.h" + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedScheme READ selectedScheme WRITE setSelectedScheme NOTIFY selectedSchemeChanged) + Q_PROPERTY(int selectedSchemeIndex READ selectedSchemeIndex NOTIFY selectedSchemeIndexChanged) + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(KCMColors::SchemeFilter filter READ filter WRITE setFilter NOTIFY filterChanged) + +public: + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString selectedScheme() const; + void setSelectedScheme(const QString &scheme); + + int selectedSchemeIndex() const; + + QString query() const; + void setQuery(const QString &query); + + KCMColors::SchemeFilter filter() const; + void setFilter(KCMColors::SchemeFilter filter); + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +Q_SIGNALS: + void queryChanged(); + void filterChanged(); + + void selectedSchemeChanged(); + void selectedSchemeIndexChanged(); + +private: + void emitSelectedSchemeIndexChange(); + + QString m_selectedScheme; + + QString m_query; + KCMColors::SchemeFilter m_filter = KCMColors::AllSchemes; +}; diff --git a/plasma/workspace/kcms/colors/kcm_colors.desktop b/plasma/workspace/kcms/colors/kcm_colors.desktop new file mode 100644 index 0000000000..eae5cd471b --- /dev/null +++ b/plasma/workspace/kcms/colors/kcm_colors.desktop @@ -0,0 +1,48 @@ +[Desktop Entry] +Name=Colors +Name[ar]=الألوان +Name[ast]=Colores +Name[az]=Rənglər +Name[ca]=Colors +Name[cs]=Barvy +Name[da]=Farver +Name[de]=Farben +Name[en_GB]=Colours +Name[es]=Colores +Name[et]=Värvid +Name[eu]=Koloreak +Name[fi]=Värit +Name[fr]=Couleurs +Name[hi]=रंग +Name[hsb]=Barby +Name[hu]=Színek +Name[ia]=Colores +Name[id]=Warna +Name[it]=Colori +Name[ja]=色 +Name[ko]=색상 +Name[lt]=Spalvos +Name[ml]=നിറങ്ങള്‍ +Name[nl]=Kleuren +Name[nn]=Fargar +Name[pa]=ਰੰਗ +Name[pl]=Kolory +Name[pt]=Cores +Name[pt_BR]=Cores +Name[ro]=Culori +Name[ru]=Цвета +Name[sk]=Farby +Name[sl]=Barve +Name[sv]=Färger +Name[ta]=நிறங்கள் +Name[tg]=Рангҳо +Name[tr]=Renkler +Name[uk]=Кольори +Name[vi]=Màu +Name[x-test]=xxColorsxx +Name[zh_CN]=颜色 + +Icon=preferences-desktop-color +Type=Application +Exec=systemsettings kcm_colors +NoDisplay=true diff --git a/plasma/workspace/kcms/colors/kcm_colors.json b/plasma/workspace/kcms/colors/kcm_colors.json new file mode 100644 index 0000000000..4d689ae7e0 --- /dev/null +++ b/plasma/workspace/kcms/colors/kcm_colors.json @@ -0,0 +1,156 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "Kai Uwe Broulik", + "Name[az]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[x-test]": "xxKai Uwe Broulikxx", + "Name[zh_CN]": "Kai Uwe Broulik" + } + ], + "Description": "Choose color scheme", + "Description[ar]": "اختر مخطط الألوان", + "Description[az]": "Rəng sxemini seçmək", + "Description[ca]": "Trieu l'esquema de color", + "Description[cs]": "Vyberte barevné schéma", + "Description[de]": "Farbschema auswählen", + "Description[en_GB]": "Choose colour scheme", + "Description[es]": "Escoger un esquema de color", + "Description[eu]": "Hautatu kolore-antolaera", + "Description[fi]": "Valitse väriteema", + "Description[fr]": "Sélectionnez un thème de couleurs", + "Description[hu]": "Színséma kiválasztása", + "Description[ia]": "Selige le schema de color ", + "Description[it]": "Scegli lo schema di colori", + "Description[ko]": "색 배열 선택", + "Description[lt]": "Pasirinkti spalvų rinkinį", + "Description[nl]": "Kleurenschema kiezen", + "Description[nn]": "Vel fargeoppsett", + "Description[pa]": "ਰੰਗ ਸਕੀਮ ਚੁਣੋ", + "Description[pl]": "Wybierz zestaw kolorów", + "Description[pt_BR]": "Escolha o esquema de cores", + "Description[ro]": "Alege schema de culori", + "Description[ru]": "Выбор цветовой схемы", + "Description[sk]": "Vybrať farebnú schému", + "Description[sl]": "Izberite barvno shemo", + "Description[sv]": "Välj färgschema", + "Description[ta]": "நிறத்திட்டத்தை தேர்ந்தெடுங்கள்", + "Description[tr]": "Renk şeması seçin", + "Description[uk]": "Вибір схеми кольорів", + "Description[vi]": "Chọn quy hoạch màu", + "Description[x-test]": "xxChoose color schemexx", + "Description[zh_CN]": "选择配色方案", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-color", + "Id": "kcm_colors", + "License": "GPL", + "Name": "Colors", + "Name[ar]": "الألوان", + "Name[ast]": "Colores", + "Name[az]": "Rənglər", + "Name[ca]": "Colors", + "Name[cs]": "Barvy", + "Name[da]": "Farver", + "Name[de]": "Farben", + "Name[en_GB]": "Colours", + "Name[es]": "Colores", + "Name[et]": "Värvid", + "Name[eu]": "Koloreak", + "Name[fi]": "Värit", + "Name[fr]": "Couleurs", + "Name[hi]": "रंग", + "Name[hsb]": "Barby", + "Name[hu]": "Színek", + "Name[ia]": "Colores", + "Name[id]": "Warna", + "Name[it]": "Colori", + "Name[ja]": "色", + "Name[ko]": "색상", + "Name[lt]": "Spalvos", + "Name[ml]": "നിറങ്ങള്‍", + "Name[nl]": "Kleuren", + "Name[nn]": "Fargar", + "Name[pa]": "ਰੰਗ", + "Name[pl]": "Kolory", + "Name[pt]": "Cores", + "Name[pt_BR]": "Cores", + "Name[ro]": "Culori", + "Name[ru]": "Цвета", + "Name[sk]": "Farby", + "Name[sl]": "Barve", + "Name[sv]": "Färger", + "Name[ta]": "நிறங்கள்", + "Name[tg]": "Рангҳо", + "Name[tr]": "Renkler", + "Name[uk]": "Кольори", + "Name[vi]": "Màu", + "Name[x-test]": "xxColorsxx", + "Name[zh_CN]": "颜色", + "Website": "https://www.kde.org/plasma-desktop" + }, + "X-DocPath": "kcontrol/colors/index.html", + "X-KDE-Keywords": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour", + "X-KDE-Keywords[ar]": "ألوان,تشكيلة,تباين,ألوان الأدوات,تشكيلة ألوان,لون نمط,سمة ألوان,تمييز,نمط داكن,نمط فاتح", + "X-KDE-Keywords[az]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour,rəng,rəngləmək,sxem,təzad.vidjet rəngləri,Rəng Sxemi, rəng üslubu,rəng mövzusu,ton,vurğulama,rəng,rəng sxemi,qaranlıq rejim,işılı rejim,vurğulamanın rənglənməsi", + "X-KDE-Keywords[ca]": "color,esquema,contrast,colors de giny,esquema de color,estil de color, tema de color,tint,color d'accent,esquema de color,mode fosc,mode clar,accent,ressaltat", + "X-KDE-Keywords[cs]": "barva,schéma,motiv,kontrast,barvy widgetu,barevné schéma,styl barev,barevný motiv,odstín,barevný nádech,barevné schéma,tmavý režim,světlý režim,nádech,zvýraznění", + "X-KDE-Keywords[da]": "farve,skema,tema,kontrast,Widgetfarver,farveskema,farvestil,toning,accent, farvetema,mørk tilstand,lys tilstand,fremhæv", + "X-KDE-Keywords[en_GB]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour", + "X-KDE-Keywords[es]": "color,colores,esquema,contraste,colores de los widgets,colores de los elementos gráficos,esquema de color,estilo de color,tema de color,tinte,color de acento,modo oscuro,modo claro,resaltar", + "X-KDE-Keywords[eu]": "kolorea,antolaera,kontrastea,trepeten koloreak,kolore-antolaera,kolore-estiloa,tonua,azentu-kolorea,kolore-antolaera,modu iluna,modu argia, azentu-kolorea,azentua,nabarmentzea,kolorea", + "X-KDE-Keywords[fi]": "väri,värit,teema,kontrasti,käyttöliittymän värit,väriteema,värityyli,värimalli,sävy,korostus,korostusväri,tumma tila,vaalea tila", + "X-KDE-Keywords[fr]": "couleurs, couleurs, thème, contraste, couleurs des composants graphiques, thème de couleurs, style de couleurs, thème de couleurs, teinte, couleur accentuée, mode sombre, mode clair, accentuation de couleur, accentuation, surbrillance", + "X-KDE-Keywords[hi]": "रंग,वर्ण,योजना,व्यतिरेक,विजेट रंग,रंग योजना,रंग शैली,रंग प्रसंग,टिंट, एक्सेंट रंग, रंग योजना, डार्क मोड, लाइट मोड, एक्सेंट रंग, एक्सेंट, चिन्हांकित, रंग", + "X-KDE-Keywords[hu]": "szín,szín,séma,kontraszt,Elemszín,Színséma,színstílus,színtéma,árnyalat,jelölőszín,színséma,sötét mód,világos mód,jelölőszín,jelölő,kiemelés,szín", + "X-KDE-Keywords[ia]": "color,colores,schema,contrasto,colores de Widget,Color de schema,stilo de color, thema de color,tinta,color de accento,modo obscur,modo clar,color de accento, accento,evidentia,color", + "X-KDE-Keywords[it]": "colore,schema,contrasto,colore degli oggetti,schema di colore,stile colore,tema colore,tinta,colore secondario,modalità scura,modalità chiara,secondario,evidenziazione", + "X-KDE-Keywords[ko]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour,색,색 배열,위젯 색상,색 스타일,색상 스타일,색 테마,색상 테마,강조,강조색,어두운 모드,밝은 모드", + "X-KDE-Keywords[nl]": "color,colour,kleur,kleuren,scheme,schema,contrast,Widget colors,Widgetkleuren,Color Scheme,kleurenschema,kleurstijl,kleurthema,tint,accentkleur,modus donker,modus licht,accent,accentuering", + "X-KDE-Keywords[nn]": "fargar,oppsett,kontrast,elementfargar,fargeoppsett,fargestil,fargetema,nyanse,aksentfarge,kontrastfarge,fargetema,fargedrakt,fargeoppsett,mørk modus,lys modus,aksent,konstrast,markering,farge", + "X-KDE-Keywords[pl]": "kolory,schemat,kontrast,kolory elementów interfejsu,zestaw kolorów,styl kolorów,motyw kolorów,tusz,kolor akcentujący,zestaw kolorów,tryb ciemny,tryb jasny,kolor akcentujący,akcent,podświetlenie,barwa", + "X-KDE-Keywords[pt]": "cores,esquema,contraste,cores do elemento gráfico,esquema de cores,estilo de cores,tema de cores,pintura,cor acentuada,esquema de cores,modo escuro,modo claro, cor acentuada,acento,tom claro,cor", + "X-KDE-Keywords[pt_BR]": "cor,cores,esquema,contraste,cores do widget,esquema de cores,estilo de cores,tema de cores,tinta,cor de destaque,esquema de cor,modo escuro,modo claro, destaque,realce", + "X-KDE-Keywords[ru]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour,цвет,цвета,схема,контраст,цвета виджета,цветовая схема,цветовой стиль,цветовая тема,оттенок,цветовой акцент,набор цветов,тёмный режим,светлый режим,выделение", + "X-KDE-Keywords[sk]": "farba,farba,schéma,kontrast,Farby widgetov,Farebná schéma,farebný štýl,farba téma,odtieň,farba akcentu,farebná schéma,tmavý režim,svetlý režim,farba akcentu,akcent,zvýraznenie,farba", + "X-KDE-Keywords[sl]": "barva,barve,sheme,kontrast,gradnik,barve gradnikov,barvne sheme,barvni slog,barvna tema,odtenek,poudarjena barva,barvna shema,temni način,svetli način,barva poudarka,osvetljevanje", + "X-KDE-Keywords[sv]": "färg,schema,kontrast,Komponentfärger,Färgschema,färgstil,färgtema,färgton,accentfärg,färgschema,mörkt läge,ljust läge,accent,färgläggning", + "X-KDE-Keywords[ta]": "color,colour, scheme, contrast, Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour, accent,highlight,colour, நிறம், வண்ணம், நிரங்கள், வண்ணங்கள், நிறத் திட்டம், நிறத்திட்டம், தோற்றத்திட்டம், வண்ணத் திட்டம், வெள்ளை, கருப்பு, இருட்டு, வெளிச்சம், வெளிர்ந்த, கருமையான, சிறப்பு நிறம்", + "X-KDE-Keywords[uk]": "кольори,кольори,схема,контраст,кольори віджетів,схема кольорів,стиль кольорів,тема кольорів,відтінок,акцент,схема,темний режим,світлий режим,підсвічування,colors,colours,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour", + "X-KDE-Keywords[vi]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour,màu,sắc,quy hoạch,tương phản,màu phụ kiện,quy hoạch màu,kiểu cách màu,chủ đề màu,phủ,màu chủ đạo,chế độ tối,chế độ sáng,chủ đạo,tô sáng", + "X-KDE-Keywords[x-test]": "xxcolorxx,xxcolourxx,xxschemexx,xxcontrastxx,xxWidget colorsxx,xxColor Schemexx,xxcolor stylexx,xxcolor themexx,xxtintxx,xxaccent colorxx,xxcolor schemexx,xxdark modexx,xxlight modexx,xxaccent colourxx,xxaccentxx,xxhighlightxx,xxcolourxx", + "X-KDE-Keywords[zh_CN]": "color,colour,scheme,contrast,Widget colors,Color Scheme,color style,color theme,tint,accent color,color scheme,dark mode,light mode,accent colour,accent,highlight,colour,颜色,配色方案,对比度,部件颜色,颜色风格,颜色样式,配色样式,配色风格,颜色主题,着色,主题色,重点色,暗色模式,深色,深色模式,暗黑模式,黑暗模式,浅色,浅色模式,亮色模式,明亮模式,高光,高亮", + "X-KDE-System-Settings-Parent-Category": "appearance", + "X-KDE-Weight": 40 +} diff --git a/plasma/workspace/kcms/colors/package/contents/ui/main.qml b/plasma/workspace/kcms/colors/package/contents/ui/main.qml new file mode 100644 index 0000000000..dbfe5c91f8 --- /dev/null +++ b/plasma/workspace/kcms/colors/package/contents/ui/main.qml @@ -0,0 +1,523 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.6 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.0 as QtDialogs +import QtQuick.Controls 2.3 as QtControls +import QtQuick.Templates 2.3 as T +import QtQml 2.15 + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.newstuff 1.81 as NewStuff +import org.kde.kcm 1.5 as KCM +import org.kde.kquickcontrols 2.0 as KQuickControls +import org.kde.private.kcms.colors 1.0 as Private + +KCM.GridViewKCM { + id: root + KCM.ConfigModule.quickHelp: i18n("This module lets you choose the color scheme.") + + view.model: kcm.filteredModel + view.currentIndex: kcm.filteredModel.selectedSchemeIndex + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + restoreMode: Binding.RestoreBinding + } + + Binding { + target: kcm.filteredModel + property: "filter" + value: filterCombo.model[filterCombo.currentIndex].filter + restoreMode: Binding.RestoreBinding + } + + KCM.SettingStateBinding { + configObject: kcm.colorsSettings + settingName: "colorScheme" + extraEnabledConditions: !kcm.downloadingFile + } + + KCM.SettingHighlighter { + target: accentBox + highlight: accentBox.checked + } + + Component.onCompleted: { + // The thumbnails are a bit more elaborate and need more room, especially when translated + view.implicitCellWidth = Kirigami.Units.gridUnit * 13; + view.implicitCellHeight = Kirigami.Units.gridUnit * 12; + } + + // we have a duplicate property here as "var" instead of "color", so that we + // can set it to "undefined", which lets us use the "a || b" shorthand for + // "a if a is defined, otherwise b" + readonly property var accentColor: Qt.colorEqual(kcm.accentColor, "transparent") ? undefined : kcm.accentColor + + DropArea { + anchors.fill: parent + onEntered: { + if (!drag.hasUrls) { + drag.accepted = false; + } + } + onDropped: { + infoLabel.visible = false; + kcm.installSchemeFromFile(drop.urls[0]); + } + } + + // putting the InlineMessage as header item causes it to show up initially despite visible false + header: ColumnLayout { + Kirigami.InlineMessage { + id: notInstalledWarning + Layout.fillWidth: true + + type: Kirigami.MessageType.Warning + showCloseButton: true + visible: false + + Connections { + target: kcm + function onShowSchemeNotInstalledWarning(schemeName) { + notInstalledWarning.text = i18n("The color scheme '%1' is not installed. Selecting the default theme instead.", schemeName) + notInstalledWarning.visible = true; + } + } + } + + RowLayout { + Layout.fillWidth: true + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + } + + QtControls.ComboBox { + id: filterCombo + textRole: "text" + model: [ + {text: i18n("All Schemes"), filter: Private.KCM.AllSchemes}, + {text: i18n("Light Schemes"), filter: Private.KCM.LightSchemes}, + {text: i18n("Dark Schemes"), filter: Private.KCM.DarkSchemes} + ] + + // HACK QQC2 doesn't support icons, so we just tamper with the desktop style ComboBox's background + // and inject a nice little filter icon. + Component.onCompleted: { + if (!background || !background.hasOwnProperty("properties")) { + // not a KQuickStyleItem + return; + } + + var props = background.properties || {}; + + background.properties = Qt.binding(function() { + var newProps = props; + newProps.currentIcon = "view-filter"; + newProps.iconColor = Kirigami.Theme.textColor; + return newProps; + }); + } + } + } + + Kirigami.FormLayout { + Layout.fillWidth: true + + QtControls.ButtonGroup { + buttons: [notAccentBox, accentBox] + } + + QtControls.RadioButton { + id: notAccentBox + + Kirigami.FormData.label: i18n("Use accent color:") + text: i18n("From current color scheme") + leftPadding: Kirigami.Units.largeSpacing + checked: Qt.colorEqual(kcm.accentColor, "transparent") + + onToggled: { + if (enabled) { + kcm.accentColor = "transparent" + } + } + } + RowLayout { + QtControls.RadioButton { + id: accentBox + checked: !Qt.colorEqual(kcm.accentColor, "transparent") + + onToggled: { + if (enabled) { + kcm.accentColor = colorRepeater.model[0] + } + } + } + component ColorRadioButton : T.RadioButton { + id: control + opacity: accentBox.checked ? 1.0 : 0.5 + autoExclusive: false + + property color color: "transparent" + + implicitWidth: Math.round(Kirigami.Units.gridUnit * 1.25) + implicitHeight: Math.round(Kirigami.Units.gridUnit * 1.25) + + background: Rectangle { + color: control.color + radius: height / 2 + border { + color: Qt.rgba(0, 0, 0, 0.15) + width: control.visualFocus ? 2 : 0 + } + } + indicator: Rectangle { + radius: height / 2 + visible: control.checked + anchors { + fill: parent + margins: Math.round(Kirigami.Units.smallSpacing * 1.25) + } + border { + color: Qt.rgba(0, 0, 0, 0.15) + width: 1 + } + } + + MouseArea { + enabled: false + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } + + Repeater { + id: colorRepeater + + model: [ + "#e93a9a", + "#e93d58", + "#e9643a", + "#e8cb2d", + "#3dd425", + "#00d3b8", + "#3daee9", + "#b875dc", + "#926ee4", + "#686b6f", + ] + + delegate: ColorRadioButton { + color: modelData + checked: Qt.colorEqual(kcm.accentColor, modelData) + + onToggled: { + kcm.accentColor = modelData + checked = Qt.binding(() => Qt.colorEqual(kcm.accentColor, modelData)); + } + } + } + + QtControls.Label { + id: customColorPickerLabel + text: i18n("Custom:") + opacity: customColorIndicator.opacity + Layout.leftMargin: Kirigami.Units.smallSpacing + } + + QtDialogs.ColorDialog { + id: colorDialog + title: i18n("Choose custom accent color") + // User must either choose a colour or cancel the operation before doing something else + modality: Qt.ApplicationModal + color: kcm.accentColor + onAccepted: { + kcm.accentColor = colorDialog.color + } + } + + ColorRadioButton { + id: customColorIndicator + + property bool isCustomColor: root.accentColor ? + !colorRepeater.model.some(color => Qt.colorEqual(color, root.accentColor)) + : false + + /* The qt binding will keep the binding alive as well as uncheck the button + * we can't just disable the button because then the icon will become grey + * and also we have to provide a MouseArea for interaction. Both of these + * can be done with the button being disabled but it will become very + * complex and will result in lot of extra code */ + + function openColorDialog(){ + checked = Qt.binding(() => customColorIndicator.isCustomColor) + colorDialog.open() + } + + color: isCustomColor ? kcm.accentColor : "transparent" + checked: isCustomColor + + onClicked: openColorDialog() + + QtControls.RoundButton { + id: customColorButtonPickerIconContainer + + anchors.fill: parent + padding: 0 // Round button adds some padding by default which we don't need. Setting this to 0 centers the icon + + visible: !customColorIndicator.isCustomColor + + onClicked: customColorIndicator.openColorDialog() + + icon.name: "color-picker" + icon.width : Kirigami.Units.iconSizes.small // This provides a nice padding + } + } + } + } + } + + view.delegate: KCM.GridDelegate { + id: delegate + + text: model.display + + thumbnailAvailable: true + thumbnail: Rectangle { + anchors.fill: parent + + opacity: model.pendingDeletion ? 0.3 : 1 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + + color: model.palette.window + + Kirigami.Theme.inherit: false + Kirigami.Theme.highlightColor: root.accentColor || model.palette.highlight + Kirigami.Theme.textColor: model.palette.text + + Rectangle { + id: windowTitleBar + width: parent.width + height: Math.round(Kirigami.Units.gridUnit * 1.5) + color: (model.accentActiveTitlebar && root.accentColor) ? kcm.accentBackground(root.accentColor, model.palette.window) : model.activeTitleBarBackground + + QtControls.Label { + anchors { + fill: parent + leftMargin: Kirigami.Units.smallSpacing + rightMargin: Kirigami.Units.smallSpacing + } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (model.accentActiveTitlebar && root.accentColor) ? kcm.accentForeground(kcm.accentBackground(root.accentColor, model.palette.window), true) : model.activeTitleBarForeground + text: i18n("Window Title") + elide: Text.ElideRight + } + } + + ColumnLayout { + anchors { + left: parent.left + right: parent.right + top: windowTitleBar.bottom + bottom: parent.bottom + margins: Kirigami.Units.smallSpacing + } + spacing: Kirigami.Units.smallSpacing + + RowLayout { + Layout.fillWidth: true + + QtControls.Label { + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter + text: i18n("Window text") + elide: Text.ElideRight + color: model.palette.windowText + } + + QtControls.Button { + Layout.alignment: Qt.AlignBottom + text: i18n("Button") + Kirigami.Theme.backgroundColor: model.palette.button + Kirigami.Theme.textColor: model.palette.buttonText + activeFocusOnTab: false + } + } + + QtControls.Frame { + Layout.fillWidth: true + Layout.fillHeight: true + padding: 0 + + activeFocusOnTab: false + + // Frame by default has a transparent background, override it so we can use the view color + // instead. + background: Rectangle { + color: Kirigami.Theme.backgroundColor + border.width: 1 + border.color: Qt.rgba(model.palette.text.r, model.palette.text.g, model.palette.text.b, 0.3) + } + + // We need to set inherit to false here otherwise the child ItemDelegates will not use the + // alternative base color we set here. + Kirigami.Theme.inherit: false + Kirigami.Theme.backgroundColor: model.palette.base + Kirigami.Theme.highlightColor: root.accentColor ? kcm.accentBackground(root.accentColor, model.palette.base) : model.palette.highlight + Kirigami.Theme.highlightedTextColor: root.accentColor ? kcm.accentForeground(kcm.accentBackground(root.accentColor, model.palette.base), true) : model.palette.highlightedText + Kirigami.Theme.linkColor: root.accentColor || model.palette.link + Kirigami.Theme.textColor: model.palette.text + Column { + id: listPreviewColumn + + readonly property string demoText: "
%2 %4" + .arg(i18nc("Hyperlink", "link")) + .arg(model.palette.linkVisited) + .arg(i18nc("Visited hyperlink", "visited")) + + anchors.fill: parent + anchors.margins: 1 + + QtControls.ItemDelegate { + width: parent.width + text: i18n("Normal text") + listPreviewColumn.demoText + activeFocusOnTab: false + } + + QtControls.ItemDelegate { + width: parent.width + highlighted: true + // TODO use proper highlighted link color + text: i18n("Highlighted text") + listPreviewColumn.demoText + activeFocusOnTab: false + } + + QtControls.ItemDelegate { + width: parent.width + enabled: false + text: i18n("Disabled text") + listPreviewColumn.demoText + activeFocusOnTab: false + } + } + } + } + + // Make the preview non-clickable but still reacting to hover + MouseArea { + anchors.fill: parent + onClicked: delegate.clicked() + onDoubleClicked: delegate.doubleClicked() + } + } + + actions: [ + Kirigami.Action { + iconName: "document-edit" + tooltip: i18n("Edit Color Scheme…") + enabled: !model.pendingDeletion + onTriggered: kcm.editScheme(model.schemeName, root) + }, + Kirigami.Action { + iconName: "edit-delete" + tooltip: i18n("Remove Color Scheme") + enabled: model.removable + visible: !model.pendingDeletion + onTriggered: model.pendingDeletion = true + }, + Kirigami.Action { + iconName: "edit-undo" + tooltip: i18n("Restore Color Scheme") + visible: model.pendingDeletion + onTriggered: model.pendingDeletion = false + } + ] + onClicked: { + kcm.model.selectedScheme = model.schemeName; + view.forceActiveFocus(); + } + onDoubleClicked: { + kcm.save(); + } + } + + footer: ColumnLayout { + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + showCloseButton: true + + Connections { + target: kcm + function onShowSuccessMessage(message) { + infoLabel.type = Kirigami.MessageType.Positive; + infoLabel.text = message; + infoLabel.visible = true; + // Avoid dual message widgets + notInstalledWarning.visible = false; + } + function onShowErrorMessage(message) { + infoLabel.type = Kirigami.MessageType.Error; + infoLabel.text = message; + infoLabel.visible = true; + notInstalledWarning.visible = false; + } + } + } + + Kirigami.ActionToolBar { + flat: false + alignment: Qt.AlignRight + actions: [ + Kirigami.Action { + text: i18n("Install from File…") + icon.name: "document-import" + onTriggered: fileDialogLoader.active = true + }, + NewStuff.Action { + text: i18n("Get New Color Schemes…") + configFile: "colorschemes.knsrc" + onEntryEvent: function (entry, event) { + if (event == 1) { // StatusChangedEvent + kcm.knsEntryChanged(entry) + } else if (event == 2) { // AdoptedEvent + kcm.loadSelectedColorScheme() + } + } + } + ] + } + } + + Loader { + id: fileDialogLoader + active: false + sourceComponent: QtDialogs.FileDialog { + title: i18n("Open Color Scheme") + folder: shortcuts.home + nameFilters: [ i18n("Color Scheme Files (*.colors)") ] + Component.onCompleted: open() + onAccepted: { + infoLabel.visible = false; + kcm.installSchemeFromFile(fileUrls[0]) + fileDialogLoader.active = false + } + onRejected: { + fileDialogLoader.active = false + } + } + } +} diff --git a/plasma/workspace/kcms/colors/plasma-apply-colorscheme.cpp b/plasma/workspace/kcms/colors/plasma-apply-colorscheme.cpp new file mode 100644 index 0000000000..adbe55d604 --- /dev/null +++ b/plasma/workspace/kcms/colors/plasma-apply-colorscheme.cpp @@ -0,0 +1,150 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "colorsapplicator.h" +#include "colorsmodel.h" +#include "colorssettings.h" + +#include "../kcms-common_p.h" +#include "../krdb/krdb.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + // This is a CLI application, but we require at least a QGuiApplication to be able + // to use QColor, so let's just roll with one of these + QGuiApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("plasma-apply-colorscheme")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + KLocalizedString::setApplicationDomain("plasma-apply-colorscheme"); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->setApplicationDescription( + i18n("This tool allows you to set the color scheme for the current Plasma session, without accidentally setting it to one that is either not " + "available, or which is already set.")); + parser->addPositionalArgument( + QStringLiteral("colorscheme"), + i18n("The name of the color scheme you wish to set for your current Plasma session (passing a full path will only use the last part of the path)")); + parser->addOption( + QCommandLineOption(QStringLiteral("list-schemes"), i18n("Show all the color schemes available on the system (and which is the current theme)"))); + parser->addOption( + QCommandLineOption(QStringLiteral("accent-color"), + i18n("The name of the accent color you want to set. SVG color names (https://www.w3.org/TR/SVG11/types.html#ColorKeywords) and hex " + "color codes are supported. Quote the hex code if there is possibility of shell expansion"), + "accentColor", + "0")); + parser->process(app); + + int exitCode{0}; + ColorsSettings *settings = new ColorsSettings(&app); + QTextStream ts(stdout); + ColorsModel *model = new ColorsModel(&app); + model->load(); + model->setSelectedScheme(settings->colorScheme()); + if (!parser->positionalArguments().isEmpty() && !parser->isSet(QStringLiteral("accent-color"))) { + QString requestedScheme{parser->positionalArguments().first()}; + const QString dirSplit{"/"}; + if (requestedScheme.contains(dirSplit)) { + QStringList splitScheme = requestedScheme.split(dirSplit, Qt::SkipEmptyParts); + requestedScheme = splitScheme.last(); + if (requestedScheme.endsWith(QStringLiteral(".colors"))) { + requestedScheme = requestedScheme.left(requestedScheme.lastIndexOf(QStringLiteral("."))); + } else { + exitCode = -1; + } + } + + if (exitCode == 0) { + if (settings->colorScheme() == requestedScheme) { + ts << i18n("The requested theme \"%1\" is already set as the theme for the current Plasma session.", requestedScheme) << Qt::endl; + // Not an error condition, no reason to set the theme, but basically this is fine + } else if (!requestedScheme.isEmpty()) { + int newSchemeIndex{-1}; + QStringList availableThemes; + for (int i = 0; i < model->rowCount(QModelIndex()); ++i) { + QString schemeName = model->data(model->index(i, 0), ColorsModel::SchemeNameRole).toString(); + availableThemes << schemeName; + if (schemeName == requestedScheme) { + newSchemeIndex = i; + // No breaking out, we're using the list of names if things fail, and + // it's not particularly expensive compared to what we've already done + } + } + + if (newSchemeIndex > -1) { + model->setSelectedScheme(requestedScheme); + settings->setColorScheme(requestedScheme); + const QString path = + QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(model->selectedScheme())); + applyScheme(path, settings->config()); + settings->save(); + notifyKcmChange(GlobalChangeType::PaletteChanged); + ts << i18n("Successfully applied the color scheme %1 to your current Plasma session", requestedScheme) << Qt::endl; + } else { + ts << i18n("Could not find theme \"%1\". The theme should be one of the following options: %2", + requestedScheme, + availableThemes.join(QLatin1String{", "})) + << Qt::endl; + } + } else { + // This shouldn't happen, but let's catch it and make angry noises, just in case... + ts << i18n("You have managed to pass an empty color scheme name, which isn't supported behavior.") << Qt::endl; + exitCode = -1; + } + } else { + ts << i18n("The file you attempted to set as your scheme, %1, could not be identified as a color scheme.", parser->positionalArguments().first()) + << Qt::endl; + exitCode = -1; + } + } else if (parser->isSet(QStringLiteral("accent-color"))) { + QString accentColor = parser->value("accent-color"); + + if (QColor::isValidColor(accentColor)) { + const QString path = + QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(model->selectedScheme())); + settings->setAccentColor(accentColor); + settings->save(); + applyScheme(path, settings->config()); + notifyKcmChange(GlobalChangeType::PaletteChanged); + ts << i18n("Successfully applied the accent color %1", accentColor) << Qt::endl; + } else { + ts << i18n("Invalid accent color ") << accentColor << Qt::endl; + exitCode = -1; + } + + } else if (parser->isSet(QStringLiteral("list-schemes"))) { + ts << i18n("You have the following color schemes on your system:") << Qt::endl; + int currentThemeIndex = model->selectedSchemeIndex(); + for (int i = 0; i < model->rowCount(QModelIndex()); ++i) { + const QString schemeName = model->data(model->index(i, 0), ColorsModel::SchemeNameRole).toString(); + if (i == currentThemeIndex) { + ts << i18n(" * %1 (current color scheme)", schemeName) << Qt::endl; + } else { + ts << QString(" * %1").arg(schemeName) << Qt::endl; + } + } + } else { + parser->showHelp(); + } + QTimer::singleShot(0, &app, [&app, &exitCode]() { + app.exit(exitCode); + }); + + return app.exec(); +} diff --git a/plasma/workspace/kcms/cursortheme/CMakeLists.txt b/plasma/workspace/kcms/cursortheme/CMakeLists.txt new file mode 100644 index 0000000000..c4d240620a --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/CMakeLists.txt @@ -0,0 +1,101 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_cursortheme\") + +set( libnoinst_SRCS +xcursor/thememodel.cpp +xcursor/themeapplicator.cpp +xcursor/cursortheme.cpp +xcursor/xcursortheme.cpp +xcursor/previewwidget.cpp +xcursor/sortproxymodel.cpp +../kcms-common.cpp +) + +include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/xcursor/ ) + +########### next target ############### + +set(kcm_cursortheme_PART_SRCS kcmcursortheme.cpp ${libnoinst_SRCS}) + +kcmutils_generate_module_data( + kcm_cursortheme_PART_SRCS + MODULE_DATA_HEADER cursorthemedata.h + MODULE_DATA_CLASS_NAME CursorThemeData + SETTINGS_HEADERS cursorthemesettings.h + SETTINGS_CLASSES CursorThemeSettings +) + + +kconfig_add_kcfg_files(kcm_cursortheme_PART_SRCS cursorthemesettings.kcfgc GENERATE_MOC) +kcoreaddons_add_plugin(kcm_cursortheme SOURCES ${kcm_cursortheme_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + + +target_link_libraries(kcm_cursortheme + Qt::DBus + Qt::X11Extras + Qt::Quick + KF5::Archive + KF5::KCMUtils + KF5::I18n + KF5::GuiAddons + KF5::WindowSystem + KF5::KIOCore + KF5::KIOWidgets + KF5::NewStuffCore + KF5::QuickAddons + PW::KWorkspace + krdb +) + +if (X11_Xcursor_FOUND) + target_link_libraries(kcm_cursortheme X11::Xcursor) +endif () +if (X11_Xfixes_FOUND) + target_link_libraries(kcm_cursortheme X11::Xfixes) +endif () + +########### next target ############### + +set(plasma-apply-cursortheme_SRCS + plasma-apply-cursortheme.cpp + + xcursor/cursortheme.cpp + xcursor/themeapplicator.cpp + xcursor/thememodel.cpp + xcursor/xcursortheme.cpp + ../kcms-common.cpp + ../krdb/krdb.cpp +) + +kconfig_add_kcfg_files(plasma-apply-cursortheme_SRCS cursorthemesettings.kcfgc GENERATE_MOC) +add_executable(plasma-apply-cursortheme ${plasma-apply-cursortheme_SRCS}) + +target_link_libraries(plasma-apply-cursortheme + Qt::DBus + Qt::X11Extras + KF5::GuiAddons + KF5::I18n + KF5::KCMUtils + KF5::WindowSystem + X11::X11 + XCB::XCB + PW::KWorkspace +) +if (X11_Xcursor_FOUND) + target_link_libraries(plasma-apply-cursortheme X11::Xcursor) +endif () +if (X11_Xfixes_FOUND) + target_link_libraries(plasma-apply-cursortheme X11::Xfixes) +endif () + +install(TARGETS plasma-apply-cursortheme DESTINATION ${KDE_INSTALL_BINDIR}) + +########### install files ############### + +install(FILES cursorthemesettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES delete_cursor_old_default_size.upd delete_cursor_old_default_size.pl DESTINATION ${KDE_INSTALL_DATADIR}/kconf_update) +install( FILES kcm_cursortheme.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) +install( FILES xcursor/xcursor.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR} ) + +kpackage_install_package(package kcm_cursortheme kcms) + diff --git a/plasma/workspace/kcms/cursortheme/Messages.sh b/plasma/workspace/kcms/cursortheme/Messages.sh new file mode 100644 index 0000000000..0f9b7449d6 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_cursortheme.pot diff --git a/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfg b/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfg new file mode 100644 index 0000000000..2b0d93e25c --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfg @@ -0,0 +1,17 @@ + + + + + + + breeze_cursors + + + + 24 + + + diff --git a/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfgc b/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfgc new file mode 100644 index 0000000000..c5306aa694 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/cursorthemesettings.kcfgc @@ -0,0 +1,7 @@ +File=cursorthemesettings.kcfg +ClassName=CursorThemeSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=true diff --git a/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.pl b/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.pl new file mode 100644 index 0000000000..7fec429e29 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.pl @@ -0,0 +1,10 @@ +#! /usr/bin/perl + +use strict; + +while (<>) +{ + chomp; + s/cursorSize=0/# DELETE cursorSize/; + print "$_\n"; +} diff --git a/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.upd b/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.upd new file mode 100644 index 0000000000..d615b4c853 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/delete_cursor_old_default_size.upd @@ -0,0 +1,8 @@ +Version=5 + +# Delete cursor size if it's old default +Id=DeleteCursorOldDefaultSize +Options=overwrite +File=kcminputrc +Group=Mouse +Script=delete_cursor_old_default_size.pl,perl diff --git a/plasma/workspace/kcms/cursortheme/kcm_cursortheme.desktop b/plasma/workspace/kcms/cursortheme/kcm_cursortheme.desktop new file mode 100644 index 0000000000..fd3f030c9c --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/kcm_cursortheme.desktop @@ -0,0 +1,48 @@ +[Desktop Entry] +Icon=preferences-desktop-cursors +Type=Application +Exec=systemsettings kcm_cursortheme +NoDisplay=true + +Name=Cursors +Name[ar]=المؤشرات +Name[ast]=Cursores +Name[az]=Kursorlar +Name[ca]=Cursors +Name[cs]=Kurzory +Name[da]=Markører +Name[de]=Zeiger +Name[en_GB]=Cursors +Name[es]=Cursores +Name[et]=Kursorid +Name[eu]=Kurtsoreak +Name[fi]=Osoittimet +Name[fr]=Pointeurs +Name[hi]=कर्सर +Name[hsb]=Cursory +Name[hu]=Kurzorok +Name[ia]=Cursores +Name[id]=Kursor +Name[it]=Puntatori +Name[ja]=カーソル +Name[ko]=커서 +Name[lt]=Žymekliai +Name[ml]=ചൂണ്ടുവിരലുകൾ +Name[nl]=Cursors +Name[nn]=Peikarar +Name[pa]=ਕਰਸਰਾਂ +Name[pl]=Wskaźniki +Name[pt]=Cursores +Name[pt_BR]=Cursores +Name[ro]=Cursori +Name[ru]=Курсоры мыши +Name[sk]=Kurzory +Name[sl]=Kazalke +Name[sv]=Pekare +Name[ta]=சுட்டிக்குறிகள் +Name[tg]=Курсорҳои муш +Name[tr]=İmleçler +Name[uk]=Вказівники +Name[vi]=Con trỏ +Name[x-test]=xxCursorsxx +Name[zh_CN]=光标 diff --git a/plasma/workspace/kcms/cursortheme/kcm_cursortheme.json b/plasma/workspace/kcms/cursortheme/kcm_cursortheme.json new file mode 100644 index 0000000000..aed29556aa --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/kcm_cursortheme.json @@ -0,0 +1,107 @@ +{ + "KPlugin": { + "Description": "Choose mouse cursor theme", + "Description[ar]": "اختر سِمة المؤشر", + "Description[az]": "Siçan kursoru mövzusunu seçmək", + "Description[ca]": "Trieu el tema del cursor del ratolí", + "Description[cs]": "Zvolte motiv ukazatele myši", + "Description[de]": "Design für den Mauszeiger auswählen", + "Description[en_GB]": "Choose mouse cursor theme", + "Description[es]": "Escoger un tema de cursores para el ratón", + "Description[eu]": "Hautatu sagu-kurtsore gaia", + "Description[fi]": "Valitse hiiriosoitinteema", + "Description[fr]": "Sélectionnez un thème de pointeur de souris", + "Description[hu]": "Az egér kurzortémájának kiválasztása", + "Description[ia]": "Selige le thema de cursor de mus", + "Description[it]": "Scegli tema dei puntatori del mouse", + "Description[ko]": "마우스 커서 테마 선택", + "Description[lt]": "Pasirinkti pelės žymeklio apipavidalinimą", + "Description[nl]": "Muiscursorthema kiezen", + "Description[nn]": "Vel peikartema", + "Description[pa]": "ਮਾਊਸ ਕਰਸਰ ਥੀਮ ਚੁਣੋ", + "Description[pl]": "Wybierz zestaw wskaźników myszy", + "Description[pt_BR]": "Escolha o tema do cursor do mouse", + "Description[ro]": "Alege tematica cursorului mausului", + "Description[ru]": "Выбор набора курсоров мыши", + "Description[sk]": "Vybrať tému kurzora myši", + "Description[sl]": "Izberite temo kazalke miši", + "Description[sv]": "Välj muspekartema", + "Description[ta]": "சுட்டிக்குறி திட்டத்தை தேர்ந்தெடுங்கள்", + "Description[tr]": "Fare imleci teması seçin", + "Description[uk]": "Вибір теми вказівника миші", + "Description[vi]": "Chọn chủ đề của con trỏ chuột", + "Description[x-test]": "xxChoose mouse cursor themexx", + "Description[zh_CN]": "选择鼠标光标主题", + "Icon": "preferences-desktop-cursors", + "Name": "Cursors", + "Name[ar]": "المؤشرات", + "Name[ast]": "Cursores", + "Name[az]": "Kursorlar", + "Name[ca]": "Cursors", + "Name[cs]": "Kurzory", + "Name[da]": "Markører", + "Name[de]": "Zeiger", + "Name[en_GB]": "Cursors", + "Name[es]": "Cursores", + "Name[et]": "Kursorid", + "Name[eu]": "Kurtsoreak", + "Name[fi]": "Osoittimet", + "Name[fr]": "Pointeurs", + "Name[hi]": "कर्सर", + "Name[hsb]": "Cursory", + "Name[hu]": "Kurzorok", + "Name[ia]": "Cursores", + "Name[id]": "Kursor", + "Name[it]": "Puntatori", + "Name[ja]": "カーソル", + "Name[ko]": "커서", + "Name[lt]": "Žymekliai", + "Name[ml]": "ചൂണ്ടുവിരലുകൾ", + "Name[nl]": "Cursors", + "Name[nn]": "Peikarar", + "Name[pa]": "ਕਰਸਰਾਂ", + "Name[pl]": "Wskaźniki", + "Name[pt]": "Cursores", + "Name[pt_BR]": "Cursores", + "Name[ro]": "Cursori", + "Name[ru]": "Курсоры мыши", + "Name[sk]": "Kurzory", + "Name[sl]": "Kazalke", + "Name[sv]": "Pekare", + "Name[ta]": "சுட்டிக்குறிகள்", + "Name[tg]": "Курсорҳои муш", + "Name[tr]": "İmleçler", + "Name[uk]": "Вказівники", + "Name[vi]": "Con trỏ", + "Name[x-test]": "xxCursorsxx", + "Name[zh_CN]": "光标" + }, + "X-DocPath": "kcontrol/cursortheme/index.html", + "X-KDE-Keywords": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer,", + "X-KDE-Keywords[ar]": "فأرة,مؤشر,سمة,مظهر المؤشر,لون المؤشر, سمة,سمة المؤشر,مظهر الفأرة,جلد الفأرة,لون المؤشر,مظهر المؤشر", + "X-KDE-Keywords[az]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer,Siçan,kursor,mövzü,kursor görünüşü,kursor rəngi,kursor mövzusu,siçan görünüşü,siçan örtüyü,göstəricinin rəngi,göstəricinin görünüşü,kursor örtüyü,kursor tərzi,kursor ölçüsü,siçan tərzi,göstərici", + "X-KDE-Keywords[ca]": "Ratolí,Cursor,Tema,Aparença de cursor,Color de cursor,Tema de cursor,Tema de ratolí,Aparença de ratolí,Pell de ratolí,Colors d'apuntador,Aparença d'apuntador,pell de cursor,estil de cursor,mida de cursor,estil de ratolí,apuntador", + "X-KDE-Keywords[cs]": "myš,kurzor,téma,vzhled kurzoru,barva kurzoru,motivy kurzoru,motiv myši,vzhled myši,barvy ukazatele,styl ukazatele,motiv kurzoru,styl kurzoru,velikost kurzoru,styl myši,ukazatel,", + "X-KDE-Keywords[es]": "Ratón,mouse,cursor,tema,aspecto del cursor,apariencia del cursor,color del cursor,tema de cursores,tema del ratón,aspecto del ratón,piel del ratón,colores del puntero,aspecto del puntero,piel del cursor,estilo del cursor,tamaño del cursor,estilo del ratón,puntero,", + "X-KDE-Keywords[eu]": "sagua,kurtsorea,gaia,kurtsorearen itxura,kurtsorearen kolorea,kurtsorearen gaia, saguaren gaia,saguaren itxura,saguaren azala,erakuslearen koloreak,erakuslearen itxura,kurtsorearen azala,kurtsorearen estiloa,kurtsorearen neurria,saguaren estiloa,erakuslea,", + "X-KDE-Keywords[fr]": "Souris, curseur, thème, apparence du pointeur, couleur du pointeur, thème de pointeurs, thème de souris, apparence de la souris, habillage de souris, couleurs de pointeurs, apparence de pointeurs, habillage de pointeurs, style de pointeurs, taille de pointeur, style de souris, pointeur", + "X-KDE-Keywords[hu]": "Egér,Kurzor,Téma,Kurzormegjelenés,Kurzorszín,Kurzortéma,Egértéma,Egérmegjelenés,Egérfelszín,Mutatószínek,Mutató-megjelenés,kurzorfelszín,kurzorszílus,kurzorméret,egérstílus,mutató,", + "X-KDE-Keywords[ia]": "Mus,Cursor,Thema,Apparentia de Cursor,Color de Cursor,Thema de Cursor,Thema de Mus, Apparentia de Mus,Apparentia de Mus,Colores de punctator,Apparentia de punctator, apparentia de cursor, grandor de cursor,stilo de cursor,stilo de mus,punctator", + "X-KDE-Keywords[it]": "Mouse,Puntatore,Tema,Aspetto puntatore,Colore puntatore,Tema puntatore,Tema mouse,Aspetto mouse,Skin mouse,Colori puntatore,Aspetto puntatore,skin puntatore,stile puntatore,dimensioni puntatore,stile mouse,puntatore,", + "X-KDE-Keywords[ko]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer,마우스,커서,테마,커서 색상,커서 테마,마우스 테마,마우스 모양,마우스 스킨,포인터 색상,포인터 모양,커서 스타일,커서 크기,마우스 스타일,포인터", + "X-KDE-Keywords[nl]": "Muis,Cursor,Thema,Uiterlijk van cursor,kleur van cursor,Thema van cursor,Thema van muis,uiterlijk van muis,Muisoppervlak,Kleuren van aanwijzer,Uiterlijk van aanwijzer,uiterlijk van cursor,cursorstijl,cursorgrootte, muisstijl,aanwijzer,", + "X-KDE-Keywords[pl]": "Mysz,Kursor,Motyw,Wygląd kursora,Kolor kursora,Motyw kursora,Motyw myszy,Wygląd myszy,Skórka myszy,Kolory wskaźnika,Wygląd wskaźnika,skórka myszy,wygląd wskaźnika,rozmiar wskaźnika,styl myszy,wskaźnik", + "X-KDE-Keywords[pt]": "Rato,Cursor,Tema,Aparência do Cursor,Cor do Cursor,Tema do Cursor,Tema do Rato,Aparência do Rato,Visuais do Rato,Cores do Ponteiro,Aparência do Ponteiro,visual do cursor,estilo do cursor,estilo do rato,ponteiro,", + "X-KDE-Keywords[pt_BR]": "mouse,cursor,tema,aparência do cursor,cor do cursor,tema do cursor,tema do mouse,aparência do mouse,visuais do mouse,cores do ponteiro,aparência do ponteiro,visual do cursor,estilo do cursor,tamanho do cursor,estilo do mouse,ponteiro,", + "X-KDE-Keywords[ru]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer,мышь,курсор,тема,внешний вид курсора мыши,цвет указателя,внешний вид указателя,размер указателя,размер курсора мыши,указатель", + "X-KDE-Keywords[sk]": "Myš,Kurzor,Téma,Vzhľad kurzora,motív,Farba kurzora,Téma kurzora,Téma myši,Vzhľad myši,Skin myši,Farby ukazovateľa,Vzhľad ukazovateľa,Skin kurzora,štýl kurzora,štýl myši,ukazovateľ,", + "X-KDE-Keywords[sl]": "Miška,Kazalka,Tema,Videz kazalke,Barva kazalke,Tema kazalke,Tema miške,Videz,Preobleke,barve miške,barva kazalke,videz kazalke,slog kazalke,slog miške,kazalec,", + "X-KDE-Keywords[sv]": "Mus,Pekare,Tema,Utseende,Pekarfärg,Pekartema,Mustema,Musutseende,Musskal,Pekarfärger,Pekarutseende,färgskal,pekarstil,pekarstorlek,musstil,pekare", + "X-KDE-Keywords[ta]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance, ,cursor skin, cursor style, cursor size, mouse style,pointer, சுட்டி,சுட்டுக்குறி, இடஞ்சுட்டி, சுட்டி நிறம், சுட்டி வண்ணம், சுட்டிக்குறி நிறம், சுட்டிக்குறி வண்ணம்,சுட்டிக்குறி தோற்றம், சுட்டிக்குறி அளவு,", + "X-KDE-Keywords[uk]": "миша,вказівник,тема,вигляд вказівника,колір вказівника,тема вказівника,тема миші,вигляд миші,стиль вказівника,розмір вказівника,стиль миші,Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skins,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer", + "X-KDE-Keywords[vi]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,cursor size,mouse style,pointer,chuột,con trỏ,chủ đề,diện mạo con trỏ,màu con trỏ,chủ đề của con trỏ,chủ đề của chuột,diện mạo chuột,da chuột,da con trỏ,kiểu cách con trỏ,kích cỡ con trỏ,kiểu cách chuột", + "X-KDE-Keywords[x-test]": "xxMousexx,xxCursorxx,xxThemexx,xxCursor Appearancexx,xxCursor Colorxx,xxCursor Themexx,xxMouse Themexx,xxMouse Appearancexx,xxMouse Skinxx,xxPointer Colorsxx,xxPointer Appearancexx,xxcursor skinxx,xxcursor stylexx,xxcursor sizexx,xxmouse stylexx,xxpointerxx,", + "X-KDE-Keywords[zh_CN]": "Mouse,Cursor,Theme,Cursor Appearance,Cursor Color,Cursor Theme,Mouse Theme,Mouse Appearance,Mouse Skin,Pointer Colors,Pointer Appearance,cursor skin,cursor style,mouse style,pointer,鼠标,光标,指针,主题,光标外观,光标颜色,光标主题,光标风格,鼠标主题,鼠标外观,鼠标样式,鼠标风格,鼠标皮肤,指针颜色,指针外观,指针皮肤,指针大小,", + "X-KDE-System-Settings-Parent-Category": "appearance", + "X-KDE-Weight": 70 +} diff --git a/plasma/workspace/kcms/cursortheme/kcmcursortheme.cpp b/plasma/workspace/kcms/cursortheme/kcmcursortheme.cpp new file mode 100644 index 0000000000..0bb63fc837 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/kcmcursortheme.cpp @@ -0,0 +1,497 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "cursorthemedata.h" +#include "kcmcursortheme.h" + +#include "../kcms-common_p.h" +#include "krdb.h" + +#include "xcursor/cursortheme.h" +#include "xcursor/previewwidget.h" +#include "xcursor/sortproxymodel.h" +#include "xcursor/themeapplicator.h" +#include "xcursor/thememodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include "cursorthemesettings.h" + +#ifdef HAVE_XFIXES +#include +#endif + +K_PLUGIN_FACTORY_WITH_JSON(CursorThemeConfigFactory, "kcm_cursortheme.json", registerPlugin(); registerPlugin();) + +CursorThemeConfig::CursorThemeConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + , m_data(new CursorThemeData(this)) + , m_canInstall(true) + , m_canResize(true) + , m_canConfigure(true) +{ + m_preferredSize = cursorThemeSettings()->cursorSize(); + connect(cursorThemeSettings(), &CursorThemeSettings::cursorThemeChanged, this, &CursorThemeConfig::updateSizeComboBox); + qmlRegisterType("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget"); + qmlRegisterAnonymousType("SortProxyModel",1); + qmlRegisterAnonymousType("CursorThemeSettings",1); + + m_themeModel = new CursorThemeModel(this); + + m_themeProxyModel = new SortProxyModel(this); + m_themeProxyModel->setSourceModel(m_themeModel); + // sort ordering is already case-insensitive; match that for filtering too + m_themeProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_themeProxyModel->sort(NameColumn, Qt::AscendingOrder); + + m_sizesModel = new QStandardItemModel(this); + + // Disable the install button if we can't install new themes to ~/.icons, + // or Xcursor isn't set up to look for cursor themes there. + if (!m_themeModel->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) { + setCanInstall(false); + } + + connect(m_themeModel, &QAbstractItemModel::dataChanged, this, &CursorThemeConfig::settingsChanged); + connect(m_themeModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &start, const QModelIndex &end, const QVector &roles) { + const QModelIndex currentThemeIndex = m_themeModel->findIndex(cursorThemeSettings()->cursorTheme()); + if (roles.contains(CursorTheme::PendingDeletionRole) && currentThemeIndex.data(CursorTheme::PendingDeletionRole) == true + && start.row() <= currentThemeIndex.row() && currentThemeIndex.row() <= end.row()) { + cursorThemeSettings()->setCursorTheme(m_themeModel->theme(m_themeModel->defaultIndex())->name()); + } + }); +} + +CursorThemeConfig::~CursorThemeConfig() +{ +} + +CursorThemeSettings *CursorThemeConfig::cursorThemeSettings() const +{ + return m_data->settings(); +} + +void CursorThemeConfig::setCanInstall(bool can) +{ + if (m_canInstall == can) { + return; + } + + m_canInstall = can; + Q_EMIT canInstallChanged(); +} + +bool CursorThemeConfig::canInstall() const +{ + return m_canInstall; +} + +void CursorThemeConfig::setCanResize(bool can) +{ + if (m_canResize == can) { + return; + } + + m_canResize = can; + Q_EMIT canResizeChanged(); +} + +bool CursorThemeConfig::canResize() const +{ + return m_canResize; +} + +void CursorThemeConfig::setCanConfigure(bool can) +{ + if (m_canConfigure == can) { + return; + } + + m_canConfigure = can; + Q_EMIT canConfigureChanged(); +} + +int CursorThemeConfig::preferredSize() const +{ + return m_preferredSize; +} + +void CursorThemeConfig::setPreferredSize(int size) +{ + if (m_preferredSize == size) { + return; + } + m_preferredSize = size; + Q_EMIT preferredSizeChanged(); +} + +bool CursorThemeConfig::canConfigure() const +{ + return m_canConfigure; +} + +bool CursorThemeConfig::downloadingFile() const +{ + return m_tempCopyJob; +} + +QAbstractItemModel *CursorThemeConfig::cursorsModel() +{ + return m_themeProxyModel; +} + +QAbstractItemModel *CursorThemeConfig::sizesModel() +{ + return m_sizesModel; +} + +bool CursorThemeConfig::iconsIsWritable() const +{ + const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons"); + const QFileInfo home = QFileInfo(QDir::homePath()); + + return ((icons.exists() && icons.isDir() && icons.isWritable()) || (!icons.exists() && home.isWritable())); +} + +void CursorThemeConfig::updateSizeComboBox() +{ + // clear the combo box + m_sizesModel->clear(); + + // refill the combo box and adopt its icon size + int row = cursorThemeIndex(cursorThemeSettings()->cursorTheme()); + QModelIndex selected = m_themeProxyModel->index(row, 0); + int maxIconWidth = 0; + int maxIconHeight = 0; + if (selected.isValid()) { + const CursorTheme *theme = m_themeProxyModel->theme(selected); + const QList sizes = theme->availableSizes(); + // only refill the combobox if there is more that 1 size + if (sizes.size() > 1) { + int i; + QList comboBoxList; + QPixmap m_pixmap; + + // insert the items + m_pixmap = theme->createIcon(0); + if (m_pixmap.width() > maxIconWidth) { + maxIconWidth = m_pixmap.width(); + } + if (m_pixmap.height() > maxIconHeight) { + maxIconHeight = m_pixmap.height(); + } + + foreach (i, sizes) { + m_pixmap = theme->createIcon(i); + if (m_pixmap.width() > maxIconWidth) { + maxIconWidth = m_pixmap.width(); + } + if (m_pixmap.height() > maxIconHeight) { + maxIconHeight = m_pixmap.height(); + } + QStandardItem *item = new QStandardItem(QIcon(m_pixmap), QString::number(i)); + item->setData(i); + m_sizesModel->appendRow(item); + comboBoxList << i; + } + + // select an item + int size = m_preferredSize; + int selectItem = comboBoxList.indexOf(size); + + // cursor size not available for this theme + if (selectItem < 0) { + /* Search the value next to cursor size. The first entry (0) + is ignored. (If cursor size would have been 0, then we + would had found it yet. As cursor size is not 0, we won't + default to "automatic size".)*/ + int j; + int distance; + int smallestDistance; + selectItem = 1; + j = comboBoxList.value(selectItem); + size = j; + smallestDistance = qAbs(m_preferredSize - j); + for (int i = 2; i < comboBoxList.size(); ++i) { + j = comboBoxList.value(i); + distance = qAbs(m_preferredSize - j); + if (distance < smallestDistance || (distance == smallestDistance && j > m_preferredSize)) { + smallestDistance = distance; + selectItem = i; + size = j; + } + } + } + cursorThemeSettings()->setCursorSize(size); + } + } + + // enable or disable the combobox + if (cursorThemeSettings()->isImmutable("cursorSize")) { + setCanResize(false); + } else { + setCanResize(m_sizesModel->rowCount() > 0); + } + // We need to Q_EMIT a cursorSizeChanged in all case to refresh UI + Q_EMIT cursorThemeSettings()->cursorSizeChanged(); +} + +int CursorThemeConfig::cursorSizeIndex(int cursorSize) const +{ + if (m_sizesModel->rowCount() > 0) { + const auto items = m_sizesModel->findItems(QString::number(cursorSize)); + if (items.count() == 1) { + return items.first()->row(); + } + } + return -1; +} + +int CursorThemeConfig::cursorSizeFromIndex(int index) +{ + Q_ASSERT(index < m_sizesModel->rowCount() && index >= 0); + + return m_sizesModel->item(index)->data().toInt(); +} + +int CursorThemeConfig::cursorThemeIndex(const QString &cursorTheme) const +{ + auto results = m_themeProxyModel->findIndex(cursorTheme); + return results.row(); +} + +QString CursorThemeConfig::cursorThemeFromIndex(int index) const +{ + QModelIndex idx = m_themeProxyModel->index(index, 0); + return m_themeProxyModel->theme(idx)->name(); +} + +void CursorThemeConfig::save() +{ + ManagedConfigModule::save(); + setPreferredSize(cursorThemeSettings()->cursorSize()); + + int row = cursorThemeIndex(cursorThemeSettings()->cursorTheme()); + QModelIndex selected = m_themeProxyModel->index(row, 0); + const CursorTheme *theme = selected.isValid() ? m_themeProxyModel->theme(selected) : nullptr; + + if (!applyTheme(theme, cursorThemeSettings()->cursorSize())) { + Q_EMIT showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect.")); + } + removeThemes(); + + notifyKcmChange(GlobalChangeType::CursorChanged); +} + +void CursorThemeConfig::load() +{ + ManagedConfigModule::load(); + setPreferredSize(cursorThemeSettings()->cursorSize()); + + // Disable the listview and the buttons if we're in kiosk mode + if (cursorThemeSettings()->isImmutable(QStringLiteral("cursorTheme"))) { + setCanConfigure(false); + setCanInstall(false); + } + + updateSizeComboBox(); // This handles also the kiosk mode + + setNeedsSave(false); +} + +void CursorThemeConfig::defaults() +{ + ManagedConfigModule::defaults(); + m_preferredSize = cursorThemeSettings()->cursorSize(); +} + +bool CursorThemeConfig::isSaveNeeded() const +{ + return !m_themeModel->match(m_themeModel->index(0, 0), CursorTheme::PendingDeletionRole, true).isEmpty(); +} + +void CursorThemeConfig::ghnsEntryChanged(KNSCore::EntryWrapper *entry) +{ + if (entry->entry().status() == KNS3::Entry::Deleted) { + for (const QString &deleted : entry->entry().uninstalledFiles()) { + QVector list = deleted.splitRef(QLatin1Char('/')); + if (list.last() == QLatin1Char('*')) { + list.takeLast(); + } + QModelIndex idx = m_themeModel->findIndex(list.last().toString()); + if (idx.isValid()) { + m_themeModel->removeTheme(idx); + } + } + } else if (entry->entry().status() == KNS3::Entry::Installed) { + for (const QString &created : entry->entry().installedFiles()) { + QStringList list = created.split(QLatin1Char('/')); + if (list.last() == QLatin1Char('*')) { + list.takeLast(); + } + // Because we sometimes get some extra slashes in the installed files list + list.removeAll({}); + // Because we'll also get the containing folder, if it was not already there + // we need to ignore it. + if (list.last() == QLatin1String(".icons")) { + continue; + } + m_themeModel->addTheme(list.join(QLatin1Char('/'))); + } + } +} + +void CursorThemeConfig::installThemeFromFile(const QUrl &url) +{ + if (url.isLocalFile()) { + installThemeFile(url.toLocalFile()); + return; + } + + if (m_tempCopyJob) { + return; + } + + m_tempInstallFile.reset(new QTemporaryFile()); + if (!m_tempInstallFile->open()) { + Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); + m_tempInstallFile.reset(); + return; + } + + m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + Q_EMIT downloadingFileChanged(); + + connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + if (job->error() != KJob::NoError) { + Q_EMIT showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText())); + return; + } + + installThemeFile(m_tempInstallFile->fileName()); + m_tempInstallFile.reset(); + }); + connect(m_tempCopyJob, &QObject::destroyed, this, &CursorThemeConfig::downloadingFileChanged); +} + +void CursorThemeConfig::installThemeFile(const QString &path) +{ + KTar archive(path); + archive.open(QIODevice::ReadOnly); + + const KArchiveDirectory *archiveDir = archive.directory(); + QStringList themeDirs; + + // Extract the dir names of the cursor themes in the archive, and + // append them to themeDirs + foreach (const QString &name, archiveDir->entries()) { + const KArchiveEntry *entry = archiveDir->entry(name); + if (entry->isDirectory() && entry->name().toLower() != "default") { + const KArchiveDirectory *dir = static_cast(entry); + if (dir->entry("index.theme") && dir->entry("cursors")) { + themeDirs << dir->name(); + } + } + } + + if (themeDirs.isEmpty()) { + Q_EMIT showErrorMessage(i18n("The file is not a valid icon theme archive.")); + return; + } + + // The directory we'll install the themes to + QString destDir = QDir::homePath() + "/.icons/"; + if (!QDir().mkpath(destDir)) { + Q_EMIT showErrorMessage(i18n("Failed to create 'icons' folder.")); + return; + } + + // Process each cursor theme in the archive + foreach (const QString &dirName, themeDirs) { + QDir dest(destDir + dirName); + if (dest.exists()) { + QString question = i18n( + "A theme named %1 already exists in your icon " + "theme folder. Do you want replace it with this one?", + dirName); + + int answer = KMessageBox::warningContinueCancel(nullptr, question, i18n("Overwrite Theme?"), KStandardGuiItem::overwrite()); + + if (answer != KMessageBox::Continue) { + continue; + } + + // ### If the theme that's being replaced is the current theme, it + // will cause cursor inconsistencies in newly started apps. + } + + // ### Should we check if a theme with the same name exists in a global theme dir? + // If that's the case it will effectively replace it, even though the global theme + // won't be deleted. Checking for this situation is easy, since the global theme + // will be in the listview. Maybe this should never be allowed since it might + // result in strange side effects (from the average users point of view). OTOH + // a user might want to do this 'upgrade' a global theme. + + const KArchiveDirectory *dir = static_cast(archiveDir->entry(dirName)); + dir->copyTo(dest.path()); + m_themeModel->addTheme(dest); + } + + archive.close(); + + Q_EMIT showSuccessMessage(i18n("Theme installed successfully.")); + + m_themeModel->refreshList(); +} + +void CursorThemeConfig::removeThemes() +{ + const QModelIndexList indices = m_themeModel->match(m_themeModel->index(0, 0), CursorTheme::PendingDeletionRole, true, -1); + QList persistentIndices; + persistentIndices.reserve(indices.count()); + std::transform(indices.constBegin(), indices.constEnd(), std::back_inserter(persistentIndices), [](const QModelIndex index) { + return QPersistentModelIndex(index); + }); + for (const auto &idx : qAsConst(persistentIndices)) { + const CursorTheme *theme = m_themeModel->theme(idx); + + // Delete the theme from the harddrive + KIO::del(QUrl::fromLocalFile(theme->path())); // async + + // Remove the theme from the model + m_themeModel->removeTheme(idx); + } + + // TODO: + // Since it's possible to substitute cursors in a system theme by adding a local + // theme with the same name, we shouldn't remove the theme from the list if it's + // still available elsewhere. We could add a + // bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but + // since KIO::del() is an asynchronos operation, the theme we're deleting will be + // readded to the list again before KIO has removed it. +} + +#include "kcmcursortheme.moc" diff --git a/plasma/workspace/kcms/cursortheme/kcmcursortheme.h b/plasma/workspace/kcms/cursortheme/kcmcursortheme.h new file mode 100644 index 0000000000..1409d90f75 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/kcmcursortheme.h @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "cursorthemesettings.h" + +class QQmlListReference; +class QStandardItemModel; +class QTemporaryFile; + +class CursorThemeModel; +class SortProxyModel; +class CursorTheme; +class CursorThemeData; + +namespace KIO +{ +class FileCopyJob; +} + +class CursorThemeConfig : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(CursorThemeSettings *cursorThemeSettings READ cursorThemeSettings CONSTANT) + Q_PROPERTY(bool canInstall READ canInstall WRITE setCanInstall NOTIFY canInstallChanged) + Q_PROPERTY(bool canResize READ canResize WRITE setCanResize NOTIFY canResizeChanged) + Q_PROPERTY(bool canConfigure READ canConfigure WRITE setCanConfigure NOTIFY canConfigureChanged) + Q_PROPERTY(QAbstractItemModel *cursorsModel READ cursorsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *sizesModel READ sizesModel CONSTANT) + + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + Q_PROPERTY(int preferredSize READ preferredSize WRITE setPreferredSize NOTIFY preferredSizeChanged) + +public: + CursorThemeConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &); + ~CursorThemeConfig() override; + + void load() override; + void save() override; + void defaults() override; + + // for QML properties + CursorThemeSettings *cursorThemeSettings() const; + + bool canInstall() const; + void setCanInstall(bool can); + + bool canResize() const; + void setCanResize(bool can); + + bool canConfigure() const; + void setCanConfigure(bool can); + + int preferredSize() const; + void setPreferredSize(int size); + + bool downloadingFile() const; + + QAbstractItemModel *cursorsModel(); + QAbstractItemModel *sizesModel(); + + Q_INVOKABLE int cursorSizeIndex(int cursorSize) const; + Q_INVOKABLE int cursorSizeFromIndex(int index); + Q_INVOKABLE int cursorThemeIndex(const QString &cursorTheme) const; + Q_INVOKABLE QString cursorThemeFromIndex(int index) const; + +Q_SIGNALS: + void canInstallChanged(); + void canResizeChanged(); + void canConfigureChanged(); + void downloadingFileChanged(); + void preferredSizeChanged(); + void themeApplied(); + + void showSuccessMessage(const QString &message); + void showInfoMessage(const QString &message); + void showErrorMessage(const QString &message); + +public Q_SLOTS: + void ghnsEntryChanged(KNSCore::EntryWrapper *entry); + void installThemeFromFile(const QUrl &url); + +private Q_SLOTS: + /** Updates the size combo box. It loads the size list of the selected cursor + theme with the corresponding icons and chooses an appropriate entry. It + enables the combo box and the label if the theme provides more than one + size, otherwise it disables it. If the size setting is looked in kiosk + mode, it stays always disabled. */ + void updateSizeComboBox(); + +private: + bool isSaveNeeded() const override; + void installThemeFile(const QString &path); + bool iconsIsWritable() const; + void removeThemes(); + + CursorThemeModel *m_themeModel; + SortProxyModel *m_themeProxyModel; + QStandardItemModel *m_sizesModel; + CursorThemeData *m_data; + + /** Holds the last size that was chosen by the user. Example: The user chooses + theme1 which provides the sizes 24 and 36. He chooses 36. preferredSize gets + set to 36. Now, he switches to theme2 which provides the sizes 30 and 40. + preferredSize still is 36, so the UI will default to 40, which is next to 36. + Now, he chooses theme3 which provides the sizes 34 and 44. preferredSize is + still 36, so the UI defaults to 34. Now the user changes manually to 44. This + will also change preferredSize. */ + int m_preferredSize; + + bool m_canInstall; + bool m_canResize; + bool m_canConfigure; + + QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; +}; diff --git a/plasma/workspace/kcms/cursortheme/package/contents/ui/Delegate.qml b/plasma/workspace/kcms/cursortheme/package/contents/ui/Delegate.qml new file mode 100644 index 0000000000..767757a145 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/package/contents/ui/Delegate.qml @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.1 +import QtQuick.Window 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as Controls +import QtQuick.Templates 2.2 as T2 +import QtGraphicalEffects 1.0 + +import org.kde.kirigami 2.2 as Kirigami + +import org.kde.kcm 1.1 as KCM +import org.kde.private.kcm_cursortheme 1.0 + +KCM.GridDelegate { + id: delegate + + text: model.display + toolTip: model.description + + opacity: model.pendingDeletion ? 0.3 : 1 + + thumbnailAvailable: true + thumbnail: PreviewWidget { + id: previewWidget + //for cursor themes we must ignore the native scaling, + //as they will be rendered by X11/KWin, ignoring whatever Qt + //scaling + width: parent.width * Screen.devicePixelRatio + height: parent.height * Screen.devicePixelRatio + x: Screen.devicePixelRatio % 1 + y: Screen.devicePixelRatio % 1 + transformOrigin: Item.TopLeft + scale: 1 / Screen.devicePixelRatio + themeModel: kcm.cursorsModel + currentIndex: index + currentSize: kcm.cursorThemeSettings.cursorSize + } + + Connections { + target: kcm + function onThemeApplied() { + previewWidget.refresh(); + } + } + + actions: [ + Kirigami.Action { + iconName: "edit-delete" + tooltip: i18n("Remove Theme") + enabled: model.isWritable + visible: !model.pendingDeletion + onTriggered: model.pendingDeletion = true + }, + Kirigami.Action { + iconName: "edit-undo" + tooltip: i18n("Restore Cursor Theme") + visible: model.pendingDeletion + onTriggered: model.pendingDeletion = false + } + ] + + onClicked: { + view.forceActiveFocus(); + kcm.cursorThemeSettings.cursorTheme = kcm.cursorThemeFromIndex(index); + } + onDoubleClicked: { + kcm.save(); + } +} diff --git a/plasma/workspace/kcms/cursortheme/package/contents/ui/main.qml b/plasma/workspace/kcms/cursortheme/package/contents/ui/main.qml new file mode 100644 index 0000000000..84dead5ea4 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/package/contents/ui/main.qml @@ -0,0 +1,171 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.7 +import QtQuick.Window 2.2 // for Screen +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as QtControls +import QtQuick.Dialogs 1.1 as QtDialogs +import org.kde.kirigami 2.5 as Kirigami +import org.kde.newstuff 1.81 as NewStuff +import org.kde.kcm 1.3 as KCM + +import org.kde.private.kcm_cursortheme 1.0 + +KCM.GridViewKCM { + id: root + KCM.ConfigModule.quickHelp: i18n("This module lets you choose the mouse cursor theme.") + + view.model: kcm.cursorsModel + view.delegate: Delegate {} + view.currentIndex: kcm.cursorThemeIndex(kcm.cursorThemeSettings.cursorTheme); + + view.onCurrentIndexChanged: { + kcm.cursorThemeSettings.cursorTheme = kcm.cursorThemeFromIndex(view.currentIndex) + view.positionViewAtIndex(view.currentIndex, view.GridView.Beginning); + } + + Component.onCompleted: { + view.positionViewAtIndex(view.currentIndex, GridView.Beginning); + } + + KCM.SettingStateBinding { + configObject: kcm.cursorThemeSettings + settingName: "cursorTheme" + extraEnabledConditions: !kcm.downloadingFile + } + + DropArea { + anchors.fill: parent + onEntered: { + if (!drag.hasUrls) { + drag.accepted = false; + } + } + onDropped: kcm.installThemeFromFile(drop.urls[0]) + } + + footer: ColumnLayout { + id: footerLayout + + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + showCloseButton: true + + Connections { + target: kcm + function onShowSuccessMessage(message) { + infoLabel.type = Kirigami.MessageType.Positive; + infoLabel.text = message; + infoLabel.visible = true; + } + function onShowInfoMessage(message) { + infoLabel.type = Kirigami.MessageType.Information; + infoLabel.text = message; + infoLabel.visible = true; + } + function onShowErrorMessage(message) { + infoLabel.type = Kirigami.MessageType.Error; + infoLabel.text = message; + infoLabel.visible = true; + } + } + } + + RowLayout { + id: row1 + + QtControls.Label { + text: i18n("Size:") + } + QtControls.ComboBox { + id: sizeCombo + model: kcm.sizesModel + textRole: "display" + currentIndex: kcm.cursorSizeIndex(kcm.cursorThemeSettings.cursorSize); + onActivated: { + kcm.cursorThemeSettings.cursorSize = kcm.cursorSizeFromIndex(sizeCombo.currentIndex); + kcm.preferredSize = kcm.cursorSizeFromIndex(sizeCombo.currentIndex); + } + + KCM.SettingStateBinding { + configObject: kcm.cursorThemeSettings + settingName: "cursorSize" + extraEnabledConditions: kcm.canResize + } + + delegate: QtControls.ItemDelegate { + id: sizeComboDelegate + + readonly property int size: parseInt(model.display) + + width: parent.width + highlighted: ListView.isCurrentItem + text: model.display + + contentItem: RowLayout { + Kirigami.Icon { + source: model.decoration + smooth: true + Layout.preferredWidth: sizeComboDelegate.size / Screen.devicePixelRatio + Layout.preferredHeight: sizeComboDelegate.size / Screen.devicePixelRatio + visible: valid && sizeComboDelegate.size > 0 + } + + QtControls.Label { + Layout.fillWidth: true + color: sizeComboDelegate.highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + text: model[sizeCombo.textRole] + elide: Text.ElideRight + } + } + } + } + Kirigami.ActionToolBar { + flat: false + alignment: Qt.AlignRight + actions: [ + Kirigami.Action { + text: i18n("&Install from File…") + icon.name: "document-import" + onTriggered: fileDialogLoader.active = true + enabled: kcm.canInstall + }, + NewStuff.Action { + text: i18n("&Get New Cursors…") + configFile: "xcursor.knsrc" + onEntryEvent: function (entry, event) { + if (event == 1) { // StatusChangedEvent + kcm.ghnsEntryChanged(entry); + } + } + } + ] + } + } + } + + Loader { + id: fileDialogLoader + active: false + sourceComponent: QtDialogs.FileDialog { + title: i18n("Open Theme") + folder: shortcuts.home + nameFilters: [ i18n("Cursor Theme Files (*.tar.gz *.tar.bz2)") ] + Component.onCompleted: open() + onAccepted: { + kcm.installThemeFromFile(fileUrls[0]) + fileDialogLoader.active = false + } + onRejected: { + fileDialogLoader.active = false + } + } + } +} + diff --git a/plasma/workspace/kcms/cursortheme/plasma-apply-cursortheme.cpp b/plasma/workspace/kcms/cursortheme/plasma-apply-cursortheme.cpp new file mode 100644 index 0000000000..aa3c13e6e3 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/plasma-apply-cursortheme.cpp @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "cursorthemesettings.h" + +#include "../kcms-common_p.h" + +#include "xcursor/cursortheme.h" +#include "xcursor/themeapplicator.h" +#include "xcursor/thememodel.h" + +#include + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + // This is a CLI application, but we require at least a QGuiApplication for things + // in Plasma::Theme, so let's just roll with one of these + QApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("plasma-apply-cursortheme")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + KLocalizedString::setApplicationDomain("plasma-apply-cursortheme"); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->setApplicationDescription( + i18n("This tool allows you to set the mouse cursor theme for the current Plasma session, without accidentally setting it to one that is either not " + "available, or which is already set.")); + parser->addPositionalArgument( + QStringLiteral("cursortheme"), + i18n("The name of the cursor theme you wish to set for your current Plasma session (passing a full path will only use the last part of the path)")); + parser->addOption(QCommandLineOption(QStringLiteral("list-themes"), i18n("Show all the themes available on the system (and which is the current theme)"))); + parser->process(app); + + int errorCode{0}; + CursorThemeSettings *settings = new CursorThemeSettings(&app); + QTextStream ts(stdout); + CursorThemeModel *model = new CursorThemeModel(&app); + if (!parser->positionalArguments().isEmpty()) { + QString requestedTheme{parser->positionalArguments().first()}; + const QString dirSplit{"/"}; + if (requestedTheme.contains(dirSplit)) { + QStringList splitTheme = requestedTheme.split(dirSplit, Qt::SkipEmptyParts); + // Cursor themes installed through KNewStuff will commonly be given an installed files entry + // which has the main directory name and an asterix to say the cursors are all in that directory, + // and since one of the main purposes of this tool is to allow adopting things from a kns dialog, + // we handle that little weirdness here. + splitTheme.removeAll(QStringLiteral("*")); + requestedTheme = splitTheme.last(); + } + + if (settings->cursorTheme() == requestedTheme) { + ts << i18n("The requested theme \"%1\" is already set as the theme for the current Plasma session.", requestedTheme) << Qt::endl; + // This is not an error condition, no reason to set an error code + } else { + auto results = model->findIndex(requestedTheme); + QModelIndex selected = model->index(results.row(), 0); + const CursorTheme *theme = selected.isValid() ? model->theme(selected) : nullptr; + + if (theme) { + settings->setCursorTheme(theme->name()); + if (settings->save() && applyTheme(theme, theme->defaultCursorSize())) { + notifyKcmChange(GlobalChangeType::CursorChanged); + ts << i18n("Successfully applied the mouse cursor theme %1 to your current Plasma session", theme->title()) << Qt::endl; + } else { + ts << i18n("You have to restart the Plasma session for your newly applied mouse cursor theme to display correctly.") << Qt::endl; + // A bit of an odd one, more a warning than an error, but this means we can forward it + errorCode = -1; + } + } else { + QStringList availableThemes; + for (int i = 0; i < model->rowCount(); ++i) { + const CursorTheme *theme = model->theme(model->index(i, 0)); + availableThemes << theme->name(); + } + ts << i18n("Could not find theme \"%1\". The theme should be one of the following options: %2", + requestedTheme, + availableThemes.join(QLatin1String{", "})) + << Qt::endl; + errorCode = -1; + } + } + } else if (parser->isSet(QStringLiteral("list-themes"))) { + ts << i18n("You have the following mouse cursor themes on your system:") << Qt::endl; + for (int i = 0; i < model->rowCount(); ++i) { + const CursorTheme *theme = model->theme(model->index(i, 0)); + if (settings->cursorTheme() == theme->name()) { + ts << QString(" * %1 (%2 - current theme for this Plasma session)").arg(theme->title()).arg(theme->name()) << Qt::endl; + } else { + ts << QString(" * %1 (%2)").arg(theme->title()).arg(theme->name()) << Qt::endl; + } + } + } else { + parser->showHelp(); + } + QTimer::singleShot(0, &app, [&app, &errorCode]() { + app.exit(errorCode); + }); + + return app.exec(); +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.cpp b/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.cpp new file mode 100644 index 0000000000..e1d5a3529b --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.cpp @@ -0,0 +1,146 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#include +#include +#include +#include +#include +#include + +#include "cursortheme.h" + +#include + +#ifdef HAVE_XFIXES +#include +#include +#endif + +CursorTheme::CursorTheme(const QString &title, const QString &description) +{ + setTitle(title); + setDescription(description); + setSample(QStringLiteral("left_ptr")); + setIsHidden(false); + setIsWritable(false); +} + +QPixmap CursorTheme::icon() const +{ + if (m_icon.isNull()) + m_icon = createIcon(); + + return m_icon; +} + +QImage CursorTheme::autoCropImage(const QImage &image) const +{ + // Compute an autocrop rectangle for the image + QRect r(image.rect().bottomRight(), image.rect().topLeft()); + const quint32 *pixels = reinterpret_cast(image.bits()); + + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + if (*(pixels++)) { + if (x < r.left()) + r.setLeft(x); + if (x > r.right()) + r.setRight(x); + if (y < r.top()) + r.setTop(y); + if (y > r.bottom()) + r.setBottom(y); + } + } + } + + // Normalize the rectangle + return image.copy(r.normalized()); +} + +QPixmap CursorTheme::loadPixmap(const QString &name, int size) const +{ + QImage image = loadImage(name, size); + if (image.isNull()) + return QPixmap(); + + return QPixmap::fromImage(image); +} + +static int nominalCursorSize(int iconSize) +{ + for (int i = 512; i > 8; i /= 2) { + if (i < iconSize) + return i; + + if ((i * .75) < iconSize) + return int(i * .75); + } + + return 8; +} + +QPixmap CursorTheme::createIcon() const +{ + int iconSize = QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize); + int cursorSize = nominalCursorSize(iconSize); + QSize size = QSize(iconSize, iconSize); + + QPixmap pixmap = createIcon(cursorSize); + + if (!pixmap.isNull()) { + // Scale the pixmap if it's larger than the preferred icon size + if (pixmap.width() > size.width() || pixmap.height() > size.height()) + pixmap = pixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + + return pixmap; +} + +QPixmap CursorTheme::createIcon(int size) const +{ + QPixmap pixmap; + QImage image = loadImage(sample(), size); + + if (image.isNull() && sample() != QLatin1String("left_ptr")) + image = loadImage(QStringLiteral("left_ptr"), size); + + if (!image.isNull()) { + pixmap = QPixmap::fromImage(image); + } + + return pixmap; +} + +void CursorTheme::setCursorName(qulonglong cursor, const QString &name) const +{ +#ifdef HAVE_XFIXES + + if (haveXfixes()) { + XFixesSetCursorName(QX11Info::display(), cursor, QFile::encodeName(name)); + } +#endif +} + +bool CursorTheme::haveXfixes() +{ + bool result = false; + +#ifdef HAVE_XFIXES + if (!QX11Info::isPlatformX11()) { + return result; + } + int event_base, error_base; + if (XFixesQueryExtension(QX11Info::display(), &event_base, &error_base)) { + int major, minor; + XFixesQueryVersion(QX11Info::display(), &major, &minor); + result = (major >= 2); + } +#endif + + return result; +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.h b/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.h new file mode 100644 index 0000000000..1666605d42 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/cursortheme.h @@ -0,0 +1,185 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#pragma once + +#include +#include + +/** + * This is the abstract base class for all cursor themes stored in a + * CursorThemeModel and previewed in a PreviewWidget. + * + * All cursor themes have a title, a description, an icon, and an internal + * name, all of which, except for the internal name, CursorThemeModel + * supplies to item views. + * + * A cursor theme may also have a path to the directory where the theme + * is located in the filesystem. If isWritable() returns true, This directory + * may be deleted in order to remove the theme at the users request. + * + * Subclasses must reimplement loadImage() and loadCursor(), which are + * called by PreviewWidget to load cursors and cursor images. Subclasses may + * optionally reimplement loadPixmap(), which in the default implementation + * calls loadImage(), and converts the returned image to a pixmap. + * Subclasses may also reimplement the protected function createIcon(), + * which creates the icon pixmap that's supplied to item views. The default + * implementation calls loadImage() to load the sample cursor, and creates + * the icon from that. + */ +class CursorTheme +{ +public: + enum ItemDataRole { + // Note: use printf "0x%08X\n" $(($RANDOM*$RANDOM)) + // to define additional roles. + DisplayDetailRole = 0x24A3DAF8, + IsWritableRole, + PendingDeletionRole, + }; + + CursorTheme() + { + } + CursorTheme(const QString &title, const QString &description = QString()); + virtual ~CursorTheme() + { + } + + const QString title() const + { + return m_title; + } + const QString description() const + { + return m_description; + } + const QString sample() const + { + return m_sample; + } + const QString name() const + { + return m_name; + } + const QString path() const + { + return m_path; + } + /** @returns A list of the available sizes in this cursor theme, + @warning This list may be empty if the engine doesn't support + the recognition of the size. */ + const QList availableSizes() const + { + return m_availableSizes; + } + bool isWritable() const + { + return m_writable; + } + bool isHidden() const + { + return m_hidden; + } + QPixmap icon() const; + + /// Hash value for the internal name + uint hash() const + { + return m_hash; + } + + /// Loads the cursor image @p name, with the nominal size @p size. + /// The image should be autocropped to the smallest possible size. + /// If the theme doesn't have the cursor @p name, it should return a null image. + virtual QImage loadImage(const QString &name, int size = 0) const = 0; + + /// Convenience function. Default implementation calls + /// QPixmap::fromImage(loadImage()); + virtual QPixmap loadPixmap(const QString &name, int size = 0) const; + + /// Loads the cursor @p name, with the nominal size @p size. + /// If the theme doesn't have the cursor @p name, it should return + /// the default cursor from the active theme instead. + virtual qulonglong loadCursor(const QString &name, int size = 0) const = 0; + + virtual int defaultCursorSize() const = 0; + + /** Creates the icon returned by @ref icon(). Don't use this function + directly but use @ref icon() instead, because @ref icon() caches + the icon. + @returns A pixmap with a cursor (usually left_ptr) that can + be used as icon for this theme. The size is adopted to + standard icon sizes.*/ + virtual QPixmap createIcon() const; + /** @returns A pixmap with a cursor (usually left_ptr) that can + be used as icon for this theme. */ + virtual QPixmap createIcon(int size) const; + + static bool haveXfixes(); + +protected: + void setTitle(const QString &title) + { + m_title = title; + } + void setDescription(const QString &desc) + { + m_description = desc; + } + void setSample(const QString &sample) + { + m_sample = sample; + } + inline void setName(const QString &name); + void setPath(const QString &path) + { + m_path = path; + } + void setAvailableSizes(const QList &availableSizes) + { + m_availableSizes = availableSizes; + } + void setIcon(const QPixmap &icon) + { + m_icon = icon; + } + void setIsWritable(bool val) + { + m_writable = val; + } + void setIsHidden(bool val) + { + m_hidden = val; + } + + /// Convenience function for cropping an image. + QImage autoCropImage(const QImage &image) const; + + // Convenience function that uses Xfixes to tag a cursor with a name + void setCursorName(qulonglong cursor, const QString &name) const; + + QString m_title; + QString m_description; + QString m_path; + QList m_availableSizes; + QString m_sample; + mutable QPixmap m_icon; + bool m_writable : 1; + bool m_hidden : 1; + +private: + QString m_name; + uint m_hash; + + friend class CursorThemeModel; +}; + +void CursorTheme::setName(const QString &name) +{ + m_name = name; + m_hash = qHash(name); +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.cpp b/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.cpp new file mode 100644 index 0000000000..d4b253449f --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.cpp @@ -0,0 +1,289 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only +*/ + +#include +#include +#include +#include + +#include + +#include "previewwidget.h" + +#include "cursortheme.h" + +namespace +{ +// Preview cursors +const char *const cursor_names[] = { + "left_ptr", + "left_ptr_watch", + "wait", + "pointer", + "help", + "ibeam", + "size_all", + "size_fdiag", + "cross", + "split_h", + "size_ver", + "size_hor", + "size_bdiag", + "split_v", +}; + +const int numCursors = 9; // The number of cursors from the above list to be previewed +const int cursorSpacing = 20; // Spacing between preview cursors +const qreal widgetMinWidth = 10; // The minimum width of the preview widget +const qreal widgetMinHeight = 48; // The minimum height of the preview widget +} + +class PreviewCursor +{ +public: + PreviewCursor(const CursorTheme *theme, const QString &name, int size); + + const QPixmap &pixmap() const + { + return m_pixmap; + } + int width() const + { + return m_pixmap.width(); + } + int height() const + { + return m_pixmap.height(); + } + int boundingSize() const + { + return m_boundingSize; + } + inline QRect rect() const; + void setPosition(const QPoint &p) + { + m_pos = p; + } + void setPosition(int x, int y) + { + m_pos = QPoint(x, y); + } + QPoint position() const + { + return m_pos; + } + operator const QPixmap &() const + { + return pixmap(); + } + +private: + int m_boundingSize; + QPixmap m_pixmap; + QPoint m_pos; +}; + +PreviewCursor::PreviewCursor(const CursorTheme *theme, const QString &name, int size) + : m_boundingSize(size > 0 ? size : theme->defaultCursorSize()) +{ + // Create the preview pixmap + QImage image = theme->loadImage(name, size); + + if (image.isNull()) + return; + + m_pixmap = QPixmap::fromImage(image); +} + +QRect PreviewCursor::rect() const +{ + return QRect(m_pos, m_pixmap.size()).adjusted(-(cursorSpacing / 2), -(cursorSpacing / 2), cursorSpacing / 2, cursorSpacing / 2); +} + +// ------------------------------------------------------------------------------ + +PreviewWidget::PreviewWidget(QQuickItem *parent) + : QQuickPaintedItem(parent) + , m_currentIndex(-1) + , m_currentSize(0) +{ + setAcceptHoverEvents(true); + current = nullptr; +} + +PreviewWidget::~PreviewWidget() +{ + qDeleteAll(list); + list.clear(); +} + +void PreviewWidget::setThemeModel(SortProxyModel *themeModel) +{ + if (m_themeModel == themeModel) { + return; + } + + m_themeModel = themeModel; + Q_EMIT themeModelChanged(); +} + +SortProxyModel *PreviewWidget::themeModel() +{ + return m_themeModel; +} + +void PreviewWidget::setCurrentIndex(int idx) +{ + if (m_currentIndex == idx) { + return; + } + + m_currentIndex = idx; + Q_EMIT currentIndexChanged(); + + if (!m_themeModel) { + return; + } + const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(idx, 0)); + setTheme(theme, m_currentSize); +} + +int PreviewWidget::currentIndex() const +{ + return m_currentIndex; +} + +void PreviewWidget::setCurrentSize(int size) +{ + if (m_currentSize == size) { + return; + } + + m_currentSize = size; + Q_EMIT currentSizeChanged(); + + if (!m_themeModel) { + return; + } + const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); + setTheme(theme, size); +} + +int PreviewWidget::currentSize() const +{ + return m_currentSize; +} + +void PreviewWidget::refresh() +{ + if (!m_themeModel) { + return; + } + + const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); + setTheme(theme, m_currentSize); +} + +void PreviewWidget::updateImplicitSize() +{ + qreal totalWidth = 0; + qreal maxHeight = 0; + + foreach (const PreviewCursor *c, list) { + totalWidth += c->width(); + maxHeight = qMax(c->height(), (int)maxHeight); + } + + totalWidth += (list.count() - 1) * cursorSpacing; + maxHeight = qMax(maxHeight, widgetMinHeight); + + setImplicitWidth(qMax(totalWidth, widgetMinWidth)); + setImplicitHeight(qMax(height(), maxHeight)); +} + +void PreviewWidget::layoutItems() +{ + if (!list.isEmpty()) { + const int spacing = 12; + int nextX = spacing; + int nextY = spacing; + + foreach (PreviewCursor *c, list) { + c->setPosition(nextX, nextY); + nextX += c->boundingSize() + spacing; + if (nextX + c->boundingSize() > width()) { + nextX = spacing; + nextY += c->boundingSize() + spacing; + } + } + } + + needLayout = false; +} + +void PreviewWidget::setTheme(const CursorTheme *theme, const int size) +{ + qDeleteAll(list); + list.clear(); + + if (theme) { + for (int i = 0; i < numCursors; i++) + list << new PreviewCursor(theme, cursor_names[i], size); + + needLayout = true; + updateImplicitSize(); + } + + current = nullptr; + update(); +} + +void PreviewWidget::paint(QPainter *painter) +{ + if (needLayout) + layoutItems(); + + foreach (const PreviewCursor *c, list) { + if (c->pixmap().isNull()) + continue; + + painter->drawPixmap(c->position(), *c); + } +} + +void PreviewWidget::hoverMoveEvent(QHoverEvent *e) +{ + if (needLayout) + layoutItems(); + + for (const PreviewCursor *c : qAsConst(list)) { + if (c->rect().contains(e->pos())) { + if (c != current) { + setCursor(c->pixmap()); + current = c; + } + return; + } + } + + setCursor(Qt::ArrowCursor); + current = nullptr; +} + +void PreviewWidget::hoverLeaveEvent(QHoverEvent *e) +{ + Q_UNUSED(e); + unsetCursor(); +} + +void PreviewWidget::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_UNUSED(newGeometry) + Q_UNUSED(oldGeometry) + if (!list.isEmpty()) { + needLayout = true; + } +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.h b/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.h new file mode 100644 index 0000000000..509f6899ac --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/previewwidget.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only +*/ + +#pragma once + +#include "sortproxymodel.h" +#include +#include + +class CursorTheme; +class PreviewCursor; + +class PreviewWidget : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(SortProxyModel *themeModel READ themeModel WRITE setThemeModel NOTIFY themeModelChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(int currentSize READ currentSize WRITE setCurrentSize NOTIFY currentSizeChanged) + +public: + explicit PreviewWidget(QQuickItem *parent = nullptr); + ~PreviewWidget() override; + + void setTheme(const CursorTheme *theme, const int size); + void setUseLables(bool); + void updateImplicitSize(); + + void setThemeModel(SortProxyModel *themeModel); + SortProxyModel *themeModel(); + + void setCurrentIndex(int idx); + int currentIndex() const; + + void setCurrentSize(int size); + int currentSize() const; + + Q_INVOKABLE void refresh(); + +Q_SIGNALS: + void themeModelChanged(); + void currentIndexChanged(); + void currentSizeChanged(); + +protected: + void paint(QPainter *) override; + void hoverMoveEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *e) override; + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private: + void layoutItems(); + + QList list; + const PreviewCursor *current; + bool needLayout : 1; + QPointer m_themeModel; + int m_currentIndex; + int m_currentSize; +}; diff --git a/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.cpp b/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.cpp new file mode 100644 index 0000000000..ea89d35dc6 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#include "sortproxymodel.h" +#include "cursortheme.h" +#include + +QHash SortProxyModel::roleNames() const +{ + QHash roleNames = QSortFilterProxyModel::roleNames(); + roleNames[CursorTheme::DisplayDetailRole] = "description"; + + return roleNames; +} + +int SortProxyModel::compare(const QModelIndex &left, const QModelIndex &right, int role) const +{ + const QAbstractItemModel *model = sourceModel(); + + QString first = model->data(left, role).toString(); + QString second = model->data(right, role).toString(); + + if (filterCaseSensitivity() == Qt::CaseInsensitive) { + first = first.toLower(); + second = second.toLower(); + } + + return QString::localeAwareCompare(first, second); +} + +bool SortProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + const int result = compare(left, right, Qt::DisplayRole); + + if (result != 0) + return (result < 0); + else + return compare(left, right, CursorTheme::DisplayDetailRole) < 0; +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.h b/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.h new file mode 100644 index 0000000000..ff3c00b7fe --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/sortproxymodel.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#pragma once + +#include +#include + +/** + * SortProxyModel is a sorting proxy model intended to be used in combination + * with the ItemDelegate class. + * + * First it compares the Qt::DisplayRoles, and if they match it compares + * the CursorTheme::DisplayDetailRoles. + * + * The model assumes both display roles are QStrings. + */ +class SortProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit SortProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + { + } + ~SortProxyModel() override + { + } + QHash roleNames() const override; + inline const CursorTheme *theme(const QModelIndex &index) const; + inline QModelIndex findIndex(const QString &name) const; + inline QModelIndex defaultIndex() const; + inline void removeTheme(const QModelIndex &index); + +private: + int compare(const QModelIndex &left, const QModelIndex &right, int role) const; + +protected: + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; +}; + +const CursorTheme *SortProxyModel::theme(const QModelIndex &index) const +{ + CursorThemeModel *model = static_cast(sourceModel()); + return model->theme(mapToSource(index)); +} + +QModelIndex SortProxyModel::findIndex(const QString &name) const +{ + CursorThemeModel *model = static_cast(sourceModel()); + return mapFromSource(model->findIndex(name)); +} + +QModelIndex SortProxyModel::defaultIndex() const +{ + CursorThemeModel *model = static_cast(sourceModel()); + return mapFromSource(model->defaultIndex()); +} + +void SortProxyModel::removeTheme(const QModelIndex &index) +{ + CursorThemeModel *model = static_cast(sourceModel()); + model->removeTheme(mapToSource(index)); +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.cpp b/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.cpp new file mode 100644 index 0000000000..1f592fb1e1 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.cpp @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "config-X11.h" + +#include "cursortheme.h" + +#include "../../krdb/krdb.h" + +#include + +#include +#include + +#include +#ifdef HAVE_XFIXES +#include +#endif + +bool applyTheme(const CursorTheme *theme, const int size) +{ + // Require the Xcursor version that shipped with X11R6.9 or greater, since + // in previous versions the Xfixes code wasn't enabled due to a bug in the + // build system (freedesktop bug #975). +#if HAVE_XFIXES && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105 + if (!theme) { + return false; + } + + QByteArray themeName = QFile::encodeName(theme->name()); + + // Set up the proper launch environment for newly started apps + UpdateLaunchEnvJob launchEnvJob(QStringLiteral("XCURSOR_THEME"), themeName); + + // Update the Xcursor X resources + runRdb(0); + + // Reload the standard cursors + QStringList names; + + if (CursorTheme::haveXfixes()) { + // Qt cursors + names << "left_ptr" + << "up_arrow" + << "cross" + << "wait" + << "left_ptr_watch" + << "ibeam" + << "size_ver" + << "size_hor" + << "size_bdiag" + << "size_fdiag" + << "size_all" + << "split_v" + << "split_h" + << "pointing_hand" + << "openhand" + << "closedhand" + << "forbidden" + << "whats_this" + << "copy" + << "move" + << "link"; + + // X core cursors + names << "X_cursor" + << "right_ptr" + << "hand1" + << "hand2" + << "watch" + << "xterm" + << "crosshair" + << "left_ptr_watch" + << "center_ptr" + << "sb_h_double_arrow" + << "sb_v_double_arrow" + << "fleur" + << "top_left_corner" + << "top_side" + << "top_right_corner" + << "right_side" + << "bottom_right_corner" + << "bottom_side" + << "bottom_left_corner" + << "left_side" + << "question_arrow" + << "pirate"; + + foreach (const QString &name, names) { + XFixesChangeCursorByName(QX11Info::display(), theme->loadCursor(name, size), QFile::encodeName(name)); + } + } + + return true; +#else + Q_UNUSED(theme) + Q_UNUSED(size) + return false; +#endif +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.h b/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.h new file mode 100644 index 0000000000..4b94da3daf --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/themeapplicator.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +class CursorTheme; +/** Applies a given theme, using XFixes, XCursor and KGlobalSettings. + @param theme The cursor theme to be applied. It is save to pass 0 here + (will result in \e false as return value). + @param size The size hint that is used to select the cursor size. + @returns If the changes could be applied. Will return \e false if \e theme is + 0 or if the XFixes and XCursor libraries aren't available in the required + version, otherwise returns \e true. */ +bool applyTheme(const CursorTheme *theme, const int size); diff --git a/plasma/workspace/kcms/cursortheme/xcursor/thememodel.cpp b/plasma/workspace/kcms/cursortheme/xcursor/thememodel.cpp new file mode 100644 index 0000000000..cd8d930b3b --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/thememodel.cpp @@ -0,0 +1,406 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only +*/ + +#include +#include +#include +#include +#include + +#include "thememodel.h" +#include "xcursortheme.h" + +#include +#include + +// Check for older version +#if !defined(XCURSOR_LIB_MAJOR) && defined(XCURSOR_MAJOR) +#define XCURSOR_LIB_MAJOR XCURSOR_MAJOR +#define XCURSOR_LIB_MINOR XCURSOR_MINOR +#endif + +CursorThemeModel::CursorThemeModel(QObject *parent) + : QAbstractTableModel(parent) +{ + insertThemes(); +} + +CursorThemeModel::~CursorThemeModel() +{ + qDeleteAll(list); + list.clear(); +} + +QHash CursorThemeModel::roleNames() const +{ + QHash roleNames = QAbstractTableModel::roleNames(); + roleNames[CursorTheme::DisplayDetailRole] = "description"; + roleNames[CursorTheme::IsWritableRole] = "isWritable"; + roleNames[CursorTheme::PendingDeletionRole] = "pendingDeletion"; + + return roleNames; +} + +void CursorThemeModel::refreshList() +{ + beginResetModel(); + qDeleteAll(list); + list.clear(); + defaultName.clear(); + endResetModel(); + insertThemes(); +} + +QVariant CursorThemeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + // Only provide text for the headers + if (role != Qt::DisplayRole) + return QVariant(); + + // Horizontal header labels + if (orientation == Qt::Horizontal) { + switch (section) { + case NameColumn: + return i18n("Name"); + + case DescColumn: + return i18n("Description"); + + default: + return QVariant(); + } + } + + // Numbered vertical header labels + return QString(section); +} + +QVariant CursorThemeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= list.count()) + return QVariant(); + + CursorTheme *theme = list.at(index.row()); + + // Text label + if (role == Qt::DisplayRole) { + switch (index.column()) { + case NameColumn: + return theme->title(); + + case DescColumn: + return theme->description(); + + default: + return QVariant(); + } + } + + // Description for the first name column + if (role == CursorTheme::DisplayDetailRole && index.column() == NameColumn) + return theme->description(); + + // Icon for the name column + if (role == Qt::DecorationRole && index.column() == NameColumn) + return theme->icon(); + + if (role == CursorTheme::IsWritableRole) { + return theme->isWritable(); + } + + if (role == CursorTheme::PendingDeletionRole) { + return pendingDeletions.contains(theme); + } + + return QVariant(); +} + +bool CursorThemeModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return false; + } + if (role == CursorTheme::PendingDeletionRole) { + const bool shouldRemove = value.toBool(); + if (shouldRemove) { + pendingDeletions.push_back(list[index.row()]); + } else { + pendingDeletions.removeAll(list[index.row()]); + } + Q_EMIT dataChanged(index, index, {role}); + return true; + } + return false; +} + +void CursorThemeModel::sort(int column, Qt::SortOrder order) +{ + Q_UNUSED(column); + Q_UNUSED(order); + + // Sorting of the model isn't implemented, as the KCM currently uses + // a sorting proxy model. +} + +const CursorTheme *CursorThemeModel::theme(const QModelIndex &index) +{ + if (!index.isValid()) + return nullptr; + + if (index.row() < 0 || index.row() >= list.count()) + return nullptr; + + return list.at(index.row()); +} + +QModelIndex CursorThemeModel::findIndex(const QString &name) +{ + uint hash = qHash(name); + + for (int i = 0; i < list.count(); i++) { + const CursorTheme *theme = list.at(i); + if (theme->hash() == hash) + return index(i, 0); + } + + return QModelIndex(); +} + +QModelIndex CursorThemeModel::defaultIndex() +{ + return findIndex(defaultName); +} + +const QStringList CursorThemeModel::searchPaths() +{ + if (!baseDirs.isEmpty()) + return baseDirs; + +#if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1 + // These are the default paths Xcursor will scan for cursor themes + QString path("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons"); + + // If XCURSOR_PATH is set, use that instead of the default path + char *xcursorPath = std::getenv("XCURSOR_PATH"); + if (xcursorPath) + path = xcursorPath; +#else + // Get the search path from Xcursor + QString path = XcursorLibraryPath(); +#endif + + // Separate the paths + baseDirs = path.split(':', Qt::SkipEmptyParts); + + // Remove duplicates + QMutableStringListIterator i(baseDirs); + while (i.hasNext()) { + const QString path = i.next(); + QMutableStringListIterator j(i); + while (j.hasNext()) + if (j.next() == path) + j.remove(); + } + + // Expand all occurrences of ~/ to the home dir + baseDirs.replaceInStrings(QRegExp("^~\\/"), QDir::home().path() + '/'); + return baseDirs; +} + +bool CursorThemeModel::hasTheme(const QString &name) const +{ + const uint hash = qHash(name); + + foreach (const CursorTheme *theme, list) + if (theme->hash() == hash) + return true; + + return false; +} + +bool CursorThemeModel::isCursorTheme(const QString &theme, const int depth) +{ + // Prevent infinite recursion + if (depth > 10) + return false; + + // Search each icon theme directory for 'theme' + foreach (const QString &baseDir, searchPaths()) { + QDir dir(baseDir); + if (!dir.exists() || !dir.cd(theme)) + continue; + + // If there's a cursors subdir, we'll assume this is a cursor theme + if (dir.exists(QStringLiteral("cursors"))) + return true; + + // If the theme doesn't have an index.theme file, it can't inherit any themes. + if (!dir.exists(QStringLiteral("index.theme"))) + continue; + + // Open the index.theme file, so we can get the list of inherited themes + KConfig config(dir.path() + "/index.theme", KConfig::NoGlobals); + KConfigGroup cg(&config, "Icon Theme"); + + // Recurse through the list of inherited themes, to check if one of them + // is a cursor theme. + const QStringList inherits = cg.readEntry("Inherits", QStringList()); + for (const QString &inherit : inherits) { + // Avoid possible DoS + if (inherit == theme) + continue; + + if (isCursorTheme(inherit, depth + 1)) + return true; + } + } + + return false; +} + +bool CursorThemeModel::handleDefault(const QDir &themeDir) +{ + QFileInfo info(themeDir.path()); + + // If "default" is a symlink + if (info.isSymLink()) { + QFileInfo target(info.symLinkTarget()); + if (target.exists() && (target.isDir() || target.isSymLink())) + defaultName = target.fileName(); + + return true; + } + + // If there's no cursors subdir, or if it's empty + if (!themeDir.exists(QStringLiteral("cursors")) || QDir(themeDir.path() + "/cursors").entryList(QDir::Files | QDir::NoDotAndDotDot).isEmpty()) { + if (themeDir.exists(QStringLiteral("index.theme"))) { + XCursorTheme theme(themeDir); + if (!theme.inherits().isEmpty()) + defaultName = theme.inherits().at(0); + } + return true; + } + + defaultName = QStringLiteral("default"); + return false; +} + +void CursorThemeModel::processThemeDir(const QDir &themeDir) +{ + bool haveCursors = themeDir.exists(QStringLiteral("cursors")); + + // Special case handling of "default", since it's usually either a + // symlink to another theme, or an empty theme that inherits another + // theme. + if (defaultName.isNull() && themeDir.dirName() == QLatin1String("default")) { + if (handleDefault(themeDir)) + return; + } + + // If the directory doesn't have a cursors subdir and lacks an + // index.theme file it can't be a cursor theme. + if (!themeDir.exists(QStringLiteral("index.theme")) && !haveCursors) + return; + + // Create a cursor theme object for the theme dir + XCursorTheme *theme = new XCursorTheme(themeDir); + + // Skip this theme if it's hidden. + if (theme->isHidden()) { + delete theme; + return; + } + + // If there's no cursors subdirectory we'll do a recursive scan + // to check if the theme inherits a theme with one. + if (!haveCursors) { + bool foundCursorTheme = false; + + foreach (const QString &name, theme->inherits()) + if ((foundCursorTheme = isCursorTheme(name))) + break; + + if (!foundCursorTheme) { + delete theme; + return; + } + } + + // Append the theme to the list + beginInsertRows(QModelIndex(), list.size(), list.size()); + list.append(theme); + endInsertRows(); +} + +void CursorThemeModel::insertThemes() +{ + // Scan each base dir for Xcursor themes and add them to the list. + foreach (const QString &baseDir, searchPaths()) { + QDir dir(baseDir); + if (!dir.exists()) + continue; + + // Process each subdir in the directory + foreach (const QString &name, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + // Don't process the theme if a theme with the same name already exists + // in the list. Xcursor will pick the first one it finds in that case, + // and since we use the same search order, the one Xcursor picks should + // be the one already in the list. + if (hasTheme(name) || !dir.cd(name)) + continue; + + processThemeDir(dir); + dir.cdUp(); // Return to the base dir + } + } + + // The theme Xcursor will end up using if no theme is configured + if (defaultName.isNull() || !hasTheme(defaultName)) + defaultName = QStringLiteral("KDE_Classic"); +} + +bool CursorThemeModel::addTheme(const QDir &dir) +{ + XCursorTheme *theme = new XCursorTheme(dir); + + // Don't add the theme to the list if it's hidden + if (theme->isHidden()) { + delete theme; + return false; + } + + // ### If the theme is hidden, the user will probably find it strange that it + // doesn't appear in the list view. There also won't be a way for the user + // to delete the theme using the KCM. Perhaps a warning about this should + // be issued, and the user be given a chance to undo the installation. + + // If an item with the same name already exists in the list, + // we'll remove it before inserting the new one. + for (int i = 0; i < list.count(); i++) { + if (list.at(i)->hash() == theme->hash()) { + removeTheme(index(i, 0)); + break; + } + } + + // Append the theme to the list + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + list.append(theme); + endInsertRows(); + + return true; +} + +void CursorThemeModel::removeTheme(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + beginRemoveRows(QModelIndex(), index.row(), index.row()); + pendingDeletions.removeAll(list[index.row()]); + delete list.takeAt(index.row()); + endRemoveRows(); +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/thememodel.h b/plasma/workspace/kcms/cursortheme/xcursor/thememodel.h new file mode 100644 index 0000000000..292c50c5fe --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/thememodel.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2005-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#pragma once + +#include +#include + +class QDir; +class CursorTheme; + +// The two TableView/TreeView columns provided by the model +enum Columns { + NameColumn = 0, + DescColumn, +}; + +/** + * The CursorThemeModel class provides a model for all locally installed + * Xcursor themes, and the KDE/Qt legacy bitmap theme. + * + * This class automatically scans the locations in the file system from + * which Xcursor loads cursors, and creates an internal list of all + * available cursor themes. + * + * The model provides this theme list to item views in the form of a list + * of rows with two columns; the first column has the theme's descriptive + * name and its sample cursor as its icon, and the second column contains + * the theme's description. + * + * Additional Xcursor themes can be added to a model after it's been + * created, by calling addTheme(), which takes QDir as a parameter, + * with the themes location. The intention is for this function to be + * called when a new Xcursor theme has been installed, after the model + * was instantiated. + * + * The KDE legacy theme is a read-only entry, with the descriptive name + * "KDE Classic", and the internal name "#kde_legacy#". + * + * Calling defaultIndex() will return the index of the theme Xcursor + * will use if the user hasn't explicitly configured a cursor theme. + */ +class CursorThemeModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit CursorThemeModel(QObject *parent = nullptr); + ~CursorThemeModel() override; + QHash roleNames() const override; + inline int columnCount(const QModelIndex &parent = QModelIndex()) const override; + inline int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + + /// Returns the CursorTheme at @p index. + const CursorTheme *theme(const QModelIndex &index); + + /// Returns the index for the CursorTheme with the internal name @p name, + /// or an invalid index if no matching theme can be found. + QModelIndex findIndex(const QString &name); + + /// Returns the index for the default theme. + QModelIndex defaultIndex(); + + /// Adds the theme in @p dir, and returns @a true if successful or @a false otherwise. + bool addTheme(const QDir &dir); + void removeTheme(const QModelIndex &index); + + /// Returns the list of base dirs Xcursor looks for themes in. + const QStringList searchPaths(); + + /// Refresh the list of themes by checking what's on disk. + void refreshList(); + +private: + bool handleDefault(const QDir &dir); + void processThemeDir(const QDir &dir); + void insertThemes(); + bool hasTheme(const QString &theme) const; + bool isCursorTheme(const QString &theme, const int depth = 0); + +private: + QList list; + QStringList baseDirs; + QString defaultName; + QVector pendingDeletions; +}; + +int CursorThemeModel::rowCount(const QModelIndex &) const +{ + return list.count(); +} + +int CursorThemeModel::columnCount(const QModelIndex &) const +{ + return 2; +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/xcursor.knsrc b/plasma/workspace/kcms/cursortheme/xcursor/xcursor.knsrc new file mode 100644 index 0000000000..071b39bf05 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/xcursor.knsrc @@ -0,0 +1,50 @@ +[KNewStuff3] +Name=Cursors +Name[ar]=المؤشرات +Name[ast]=Cursores +Name[az]=Kursorlar +Name[ca]=Cursors +Name[cs]=Kurzory +Name[da]=Markører +Name[de]=Zeiger +Name[en_GB]=Cursors +Name[es]=Cursores +Name[et]=Kursorid +Name[eu]=Kurtsoreak +Name[fi]=Osoittimet +Name[fr]=Pointeurs +Name[hi]=कर्सर +Name[hsb]=Cursory +Name[hu]=Kurzorok +Name[ia]=Cursores +Name[id]=Kursor +Name[it]=Puntatori +Name[ja]=カーソル +Name[ko]=커서 +Name[lt]=Žymekliai +Name[ml]=ചൂണ്ടുവിരലുകൾ +Name[nl]=Cursors +Name[nn]=Peikarar +Name[pa]=ਕਰਸਰਾਂ +Name[pl]=Wskaźniki +Name[pt]=Cursores +Name[pt_BR]=Cursores +Name[ro]=Cursori +Name[ru]=Курсоры мыши +Name[sk]=Kurzory +Name[sl]=Kazalke +Name[sv]=Pekare +Name[ta]=சுட்டிக்குறிகள் +Name[tg]=Курсорҳои муш +Name[tr]=İmleçler +Name[uk]=Вказівники +Name[vi]=Con trỏ +Name[x-test]=xxCursorsxx +Name[zh_CN]=光标 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=X11 Mouse Theme +TargetDir=icons +Uncompress=always +RemoveDeadEntries=true +AdoptionCommand=plasma-apply-cursortheme %f diff --git a/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.cpp b/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.cpp new file mode 100644 index 0000000000..8e8e0c20b8 --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.cpp @@ -0,0 +1,207 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "xcursortheme.h" + +// Static variable holding alternative names for some cursors +QHash XCursorTheme::alternatives; + +XCursorTheme::XCursorTheme(const QDir &themeDir) + : CursorTheme(themeDir.dirName()) +{ + // Directory information + setName(themeDir.dirName()); + setPath(themeDir.path()); + setIsWritable(QFileInfo(themeDir.path()).isWritable()); // ### perhaps this shouldn't be cached + + if (themeDir.exists(QStringLiteral("index.theme"))) + parseIndexFile(); + + QString cursorFile = path() + "/cursors/left_ptr"; + QList sizeList; + XcursorImages *images = XcursorFilenameLoadAllImages(qPrintable(cursorFile)); + if (images) { + for (int i = 0; i < images->nimage; ++i) { + if (!sizeList.contains(images->images[i]->size)) + sizeList.append(images->images[i]->size); + }; + XcursorImagesDestroy(images); + std::sort(sizeList.begin(), sizeList.end()); + m_availableSizes = sizeList; + } + if (!sizeList.isEmpty()) { + QString sizeListString = QString::number(sizeList.takeFirst()); + while (!sizeList.isEmpty()) { + sizeListString.append(", "); + sizeListString.append(QString::number(sizeList.takeFirst())); + }; + QString tempString = i18nc( + "@info The argument is the list of available sizes (in pixel). Example: " + "'Available sizes: 24' or 'Available sizes: 24, 36, 48'", + "(Available sizes: %1)", + sizeListString); + if (m_description.isEmpty()) + m_description = tempString; + else + m_description = m_description + ' ' + tempString; + }; +} + +void XCursorTheme::parseIndexFile() +{ + KConfig config(path() + "/index.theme", KConfig::NoGlobals); + KConfigGroup cg(&config, "Icon Theme"); + + m_title = cg.readEntry("Name", m_title); + m_description = cg.readEntry("Comment", m_description); + m_sample = cg.readEntry("Example", m_sample); + m_hidden = cg.readEntry("Hidden", false); + m_inherits = cg.readEntry("Inherits", QStringList()); +} + +QString XCursorTheme::findAlternative(const QString &name) const +{ + if (alternatives.isEmpty()) { + alternatives.reserve(18); + + // Qt uses non-standard names for some core cursors. + // If Xcursor fails to load the cursor, Qt creates it with the correct name using the + // core protocol instead (which in turn calls Xcursor). We emulate that process here. + // Note that there's a core cursor called cross, but it's not the one Qt expects. + alternatives.insert(QStringLiteral("cross"), QStringLiteral("crosshair")); + alternatives.insert(QStringLiteral("up_arrow"), QStringLiteral("center_ptr")); + alternatives.insert(QStringLiteral("wait"), QStringLiteral("watch")); + alternatives.insert(QStringLiteral("ibeam"), QStringLiteral("xterm")); + alternatives.insert(QStringLiteral("size_all"), QStringLiteral("fleur")); + alternatives.insert(QStringLiteral("pointing_hand"), QStringLiteral("hand2")); + + // Precomputed MD5 hashes for the hardcoded bitmap cursors in Qt and KDE. + // Note that the MD5 hash for left_ptr_watch is for the KDE version of that cursor. + alternatives.insert(QStringLiteral("size_ver"), QStringLiteral("00008160000006810000408080010102")); + alternatives.insert(QStringLiteral("size_hor"), QStringLiteral("028006030e0e7ebffc7f7070c0600140")); + alternatives.insert(QStringLiteral("size_bdiag"), QStringLiteral("fcf1c3c7cd4491d801f1e1c78f100000")); + alternatives.insert(QStringLiteral("size_fdiag"), QStringLiteral("c7088f0f3e6c8088236ef8e1e3e70000")); + alternatives.insert(QStringLiteral("whats_this"), QStringLiteral("d9ce0ab605698f320427677b458ad60b")); + alternatives.insert(QStringLiteral("split_h"), QStringLiteral("14fef782d02440884392942c11205230")); + alternatives.insert(QStringLiteral("split_v"), QStringLiteral("2870a09082c103050810ffdffffe0204")); + alternatives.insert(QStringLiteral("forbidden"), QStringLiteral("03b6e0fcb3499374a867c041f52298f0")); + alternatives.insert(QStringLiteral("left_ptr_watch"), QStringLiteral("3ecb610c1bf2410f44200f48c40d3599")); + alternatives.insert(QStringLiteral("hand2"), QStringLiteral("e29285e634086352946a0e7090d73106")); + alternatives.insert(QStringLiteral("openhand"), QStringLiteral("9141b49c8149039304290b508d208c40")); + alternatives.insert(QStringLiteral("closedhand"), QStringLiteral("05e88622050804100c20044008402080")); + } + + return alternatives.value(name, QString()); +} + +XcursorImage *XCursorTheme::xcLoadImage(const QString &image, int size) const +{ + QByteArray cursorName = QFile::encodeName(image); + QByteArray themeName = QFile::encodeName(name()); + + return XcursorLibraryLoadImage(cursorName, themeName, size); +} + +XcursorImages *XCursorTheme::xcLoadImages(const QString &image, int size) const +{ + QByteArray cursorName = QFile::encodeName(image); + QByteArray themeName = QFile::encodeName(name()); + + return XcursorLibraryLoadImages(cursorName, themeName, size); +} + +int XCursorTheme::defaultCursorSize() const +{ + // TODO: manage Wayland + if (!QX11Info::isPlatformX11()) { + return 32; + } + /* This code is basically borrowed from display.c of the XCursor library + We can't use "int XcursorGetDefaultSize(Display *dpy)" because if + previously the cursor size was set to a custom value, it would return + this custom value. */ + int size = 0; + int dpi = 0; + Display *dpy = QX11Info::display(); + // The string "v" is owned and will be destroyed by Xlib + char *v = XGetDefault(dpy, "Xft", "dpi"); + if (v) + dpi = atoi(v); + if (dpi) + size = dpi * 16 / 72; + if (size == 0) { + int dim; + if (DisplayHeight(dpy, DefaultScreen(dpy)) < DisplayWidth(dpy, DefaultScreen(dpy))) { + dim = DisplayHeight(dpy, DefaultScreen(dpy)); + } else { + dim = DisplayWidth(dpy, DefaultScreen(dpy)); + } + size = dim / 48; + } + return size; +} + +qulonglong XCursorTheme::loadCursor(const QString &name, int size) const +{ + // TODO: manage Wayland + if (!QX11Info::isPlatformX11()) { + return None; + } + if (size <= 0) + size = defaultCursorSize(); + + // Load the cursor images + XcursorImages *images = xcLoadImages(name, size); + + if (!images) + images = xcLoadImages(findAlternative(name), size); + + if (!images) + return None; + + // Create the cursor + Cursor handle = XcursorImagesLoadCursor(QX11Info::display(), images); + XcursorImagesDestroy(images); + + setCursorName(handle, name); + return handle; +} + +QImage XCursorTheme::loadImage(const QString &name, int size) const +{ + if (size <= 0) + size = defaultCursorSize(); + + // Load the image + XcursorImage *xcimage = xcLoadImage(name, size); + + if (!xcimage) + xcimage = xcLoadImage(findAlternative(name), size); + + if (!xcimage) { + return QImage(); + } + + // Convert the XcursorImage to a QImage, and auto-crop it + QImage image((uchar *)xcimage->pixels, xcimage->width, xcimage->height, QImage::Format_ARGB32_Premultiplied); + + image = autoCropImage(image); + XcursorImageDestroy(xcimage); + + return image; +} diff --git a/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.h b/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.h new file mode 100644 index 0000000000..15608d9b0f --- /dev/null +++ b/plasma/workspace/kcms/cursortheme/xcursor/xcursortheme.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +*/ + +#pragma once + +#include + +#include "cursortheme.h" + +class QDir; + +struct _XcursorImage; +struct _XcursorImages; + +typedef _XcursorImage XcursorImage; +typedef _XcursorImages XcursorImages; + +/** + * The XCursorTheme class is a CursorTheme implementation for Xcursor themes. + */ +class XCursorTheme : public CursorTheme +{ +public: + /** + * Initializes itself from the @p dir information, and parses the + * index.theme file if the dir has one. + */ + XCursorTheme(const QDir &dir); + ~XCursorTheme() override + { + } + + const QStringList inherits() const + { + return m_inherits; + } + QImage loadImage(const QString &name, int size = 0) const override; + qulonglong loadCursor(const QString &name, int size = 0) const override; + + /** Returns the size that the XCursor library would use if no + cursor size is given. This depends mainly on Xft.dpi. */ + int defaultCursorSize() const override; + +protected: + XCursorTheme(const QString &title, const QString &desc) + : CursorTheme(title, desc) + { + } + void setInherits(const QStringList &val) + { + m_inherits = val; + } + +private: + XcursorImage *xcLoadImage(const QString &name, int size) const; + XcursorImages *xcLoadImages(const QString &name, int size) const; + void parseIndexFile(); + QString findAlternative(const QString &name) const; + + QStringList m_inherits; + static QHash alternatives; +}; diff --git a/plasma/workspace/kcms/desktoptheme/CMakeLists.txt b/plasma/workspace/kcms/desktoptheme/CMakeLists.txt new file mode 100644 index 0000000000..ed2e11007d --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/CMakeLists.txt @@ -0,0 +1,52 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_desktoptheme\") + +set(kcm_desktoptheme_SRCS + kcm.cpp + themesmodel.cpp + filterproxymodel.cpp +) + +kcmutils_generate_module_data( + kcm_desktoptheme_SRCS + MODULE_DATA_HEADER desktopthemedata.h + MODULE_DATA_CLASS_NAME DesktopThemeData + SETTINGS_HEADERS desktopthemesettings.h + SETTINGS_CLASSES DesktopThemeSettings +) + +kconfig_add_kcfg_files(kcm_desktoptheme_SRCS desktopthemesettings.kcfgc GENERATE_MOC) + +kcoreaddons_add_plugin(kcm_desktoptheme SOURCES ${kcm_desktoptheme_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + +target_link_libraries(kcm_desktoptheme + KF5::CoreAddons + KF5::KCMUtils + KF5::KIOCore + KF5::KIOWidgets + KF5::I18n + KF5::Plasma + KF5::Declarative + KF5::QuickAddons +) + +set(plasma-apply-desktoptheme_SRCS + plasma-apply-desktoptheme.cpp + themesmodel.cpp +) + +add_executable(plasma-apply-desktoptheme ${plasma-apply-desktoptheme_SRCS}) + +target_link_libraries(plasma-apply-desktoptheme + KF5::CoreAddons + KF5::KCMUtils + KF5::I18n + KF5::Plasma +) + +#this desktop file is installed only for retrocompatibility with sycoca +install(FILES kcm_desktoptheme.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES plasma-themes.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) +install(TARGETS plasma-apply-desktoptheme DESTINATION ${KDE_INSTALL_BINDIR}) + +kpackage_install_package(package kcm_desktoptheme kcms) diff --git a/plasma/workspace/kcms/desktoptheme/Messages.sh b/plasma/workspace/kcms/desktoptheme/Messages.sh new file mode 100644 index 0000000000..9c296ba5f6 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name "*.kcfg"` >> rc.cpp || exit 11 +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_desktoptheme.pot +rm -f rc.cpp diff --git a/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfg b/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfg new file mode 100644 index 0000000000..3127d36257 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfg @@ -0,0 +1,13 @@ + + + + + + + default + + + diff --git a/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfgc b/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfgc new file mode 100644 index 0000000000..d4c6582e8a --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/desktopthemesettings.kcfgc @@ -0,0 +1,6 @@ +File=desktopthemesettings.kcfg +ClassName=DesktopThemeSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/plasma/workspace/kcms/desktoptheme/filterproxymodel.cpp b/plasma/workspace/kcms/desktoptheme/filterproxymodel.cpp new file mode 100644 index 0000000000..06a6b325bd --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/filterproxymodel.cpp @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "filterproxymodel.h" + +#include "themesmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::selectedTheme() const +{ + return m_selectedTheme; +} + +void FilterProxyModel::setSelectedTheme(const QString &pluginName) +{ + if (m_selectedTheme == pluginName) { + return; + } + + const bool firstTime = m_selectedTheme.isNull(); + m_selectedTheme = pluginName; + + if (!firstTime) { + Q_EMIT selectedThemeChanged(); + } + Q_EMIT selectedThemeIndexChanged(); +} + +int FilterProxyModel::selectedThemeIndex() const +{ + // We must search in the source model and then map the index to our proxy model. + const auto results = sourceModel()->match(sourceModel()->index(0, 0), ThemesModel::PluginNameRole, m_selectedTheme, 1, Qt::MatchExactly); + + if (results.count() == 1) { + const QModelIndex result = mapFromSource(results.first()); + if (result.isValid()) { + return result.row(); + } + } + + return -1; +} + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + const int oldIndex = selectedThemeIndex(); + + m_query = query; + invalidateFilter(); + + Q_EMIT queryChanged(); + + if (selectedThemeIndex() != oldIndex) { + Q_EMIT selectedThemeIndexChanged(); + } + } +} + +FilterProxyModel::ThemeFilter FilterProxyModel::filter() const +{ + return m_filter; +} + +void FilterProxyModel::setFilter(ThemeFilter filter) +{ + if (m_filter != filter) { + const int oldIndex = selectedThemeIndex(); + + m_filter = filter; + invalidateFilter(); + + Q_EMIT filterChanged(); + + if (selectedThemeIndex() != oldIndex) { + Q_EMIT selectedThemeIndexChanged(); + } + } +} + +bool FilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!m_query.isEmpty()) { + if (!idx.data(Qt::DisplayRole).toString().contains(m_query, Qt::CaseInsensitive) + && !idx.data(ThemesModel::PluginNameRole).toString().contains(m_query, Qt::CaseInsensitive)) { + return false; + } + } + + const auto type = idx.data(ThemesModel::ColorTypeRole).value(); + switch (m_filter) { + case AllThemes: + return true; + case LightThemes: + return type == ThemesModel::LightTheme; + case DarkThemes: + return type == ThemesModel::DarkTheme; + case ThemesFollowingColors: + return type == ThemesModel::FollowsColorTheme; + } + + return true; +} diff --git a/plasma/workspace/kcms/desktoptheme/filterproxymodel.h b/plasma/workspace/kcms/desktoptheme/filterproxymodel.h new file mode 100644 index 0000000000..06a1d8cf62 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/filterproxymodel.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include "kcm.h" + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + enum ThemeFilter { + AllThemes, + LightThemes, + DarkThemes, + ThemesFollowingColors, + }; + Q_ENUM(ThemeFilter) + + Q_PROPERTY(QString selectedTheme READ selectedTheme WRITE setSelectedTheme NOTIFY selectedThemeChanged) + Q_PROPERTY(int selectedThemeIndex READ selectedThemeIndex NOTIFY selectedThemeIndexChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(ThemeFilter filter READ filter WRITE setFilter NOTIFY filterChanged) + + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString selectedTheme() const; + void setSelectedTheme(const QString &pluginName); + + int selectedThemeIndex() const; + + QString query() const; + void setQuery(const QString &query); + + ThemeFilter filter() const; + void setFilter(ThemeFilter filter); + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +Q_SIGNALS: + void filterChanged(); + void queryChanged(); + + void selectedThemeChanged(); + void selectedThemeIndexChanged(); + +private: + QString m_selectedTheme; + QString m_query; + ThemeFilter m_filter = AllThemes; +}; diff --git a/plasma/workspace/kcms/desktoptheme/kcm.cpp b/plasma/workspace/kcms/desktoptheme/kcm.cpp new file mode 100644 index 0000000000..8d2f3daf6f --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/kcm.cpp @@ -0,0 +1,254 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + SPDX-FileCopyrightText: 2014 Vishesh Handa + SPDX-FileCopyrightText: 2016 David Rosca + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "kcm.h" + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "desktopthemedata.h" +#include "desktopthemesettings.h" +#include "filterproxymodel.h" +#include "themesmodel.h" + +Q_LOGGING_CATEGORY(KCM_DESKTOP_THEME, "kcm_desktoptheme") + +K_PLUGIN_FACTORY_WITH_JSON(KCMDesktopThemeFactory, "kcm_desktoptheme.json", registerPlugin(); registerPlugin();) + +KCMDesktopTheme::KCMDesktopTheme(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + , m_data(new DesktopThemeData(this)) + , m_model(new ThemesModel(this)) + , m_filteredModel(new FilterProxyModel(this)) + , m_haveThemeExplorerInstalled(false) +{ + qmlRegisterAnonymousType("org.kde.private.kcms.desktoptheme", 1); + qmlRegisterUncreatableType("org.kde.private.kcms.desktoptheme", 1, 0, "ThemesModel", "Cannot create ThemesModel"); + qmlRegisterUncreatableType("org.kde.private.kcms.desktoptheme", 1, 0, "FilterProxyModel", "Cannot create FilterProxyModel"); + + setButtons(Apply | Default | Help); + + m_haveThemeExplorerInstalled = !QStandardPaths::findExecutable(QStringLiteral("plasmathemeexplorer")).isEmpty(); + + connect(m_model, &ThemesModel::pendingDeletionsChanged, this, &KCMDesktopTheme::settingsChanged); + + connect(m_model, &ThemesModel::selectedThemeChanged, this, [this](const QString &pluginName) { + desktopThemeSettings()->setName(pluginName); + }); + + connect(desktopThemeSettings(), &DesktopThemeSettings::nameChanged, this, [this] { + m_model->setSelectedTheme(desktopThemeSettings()->name()); + }); + + connect(m_model, &ThemesModel::selectedThemeChanged, m_filteredModel, &FilterProxyModel::setSelectedTheme); + + m_filteredModel->setSourceModel(m_model); +} + +KCMDesktopTheme::~KCMDesktopTheme() +{ +} + +DesktopThemeSettings *KCMDesktopTheme::desktopThemeSettings() const +{ + return m_data->settings(); +} + +ThemesModel *KCMDesktopTheme::desktopThemeModel() const +{ + return m_model; +} + +FilterProxyModel *KCMDesktopTheme::filteredModel() const +{ + return m_filteredModel; +} + +bool KCMDesktopTheme::downloadingFile() const +{ + return m_tempCopyJob; +} + +void KCMDesktopTheme::installThemeFromFile(const QUrl &url) +{ + if (url.isLocalFile()) { + installTheme(url.toLocalFile()); + return; + } + + if (m_tempCopyJob) { + return; + } + + m_tempInstallFile.reset(new QTemporaryFile()); + if (!m_tempInstallFile->open()) { + Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); + m_tempInstallFile.reset(); + return; + } + + m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + Q_EMIT downloadingFileChanged(); + + connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + if (job->error() != KJob::NoError) { + Q_EMIT showErrorMessage(i18n("Unable to download the theme: %1", job->errorText())); + return; + } + + installTheme(m_tempInstallFile->fileName()); + m_tempInstallFile.reset(); + }); + connect(m_tempCopyJob, &QObject::destroyed, this, &KCMDesktopTheme::downloadingFileChanged); +} + +void KCMDesktopTheme::installTheme(const QString &path) +{ + qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << path; + + const QString program = QStringLiteral("kpackagetool5"); + const QStringList arguments = {QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), path}; + + qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QLatin1Char(' ')); + QProcess *myProcess = new QProcess(this); + connect(myProcess, + static_cast(&QProcess::finished), + this, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitStatus) + if (exitCode == 0) { + Q_EMIT showSuccessMessage(i18n("Theme installed successfully.")); + load(); + } else { + Q_EMIT showErrorMessage(i18n("Theme installation failed.")); + } + }); + + connect(myProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError e) { + qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed: " << e; + Q_EMIT showErrorMessage(i18n("Theme installation failed.")); + }); + + myProcess->start(program, arguments); +} + +void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName) +{ + if (!item) { + return; + } + + Plasma::Theme *theme = m_themes[themeName]; + if (!theme) { + theme = new Plasma::Theme(themeName, this); + m_themes[themeName] = theme; + } + + Q_FOREACH (Plasma::Svg *svg, item->findChildren()) { + svg->setTheme(theme); + svg->setUsingRenderingCache(false); + } +} + +void KCMDesktopTheme::load() +{ + ManagedConfigModule::load(); + m_model->load(); + m_model->setSelectedTheme(desktopThemeSettings()->name()); +} + +void KCMDesktopTheme::save() +{ + ManagedConfigModule::save(); + Plasma::Theme().setThemeName(desktopThemeSettings()->name()); + processPendingDeletions(); +} + +void KCMDesktopTheme::defaults() +{ + ManagedConfigModule::defaults(); + + // can this be done more elegantly? + const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true); + for (const QModelIndex &idx : pendingDeletions) { + m_model->setData(idx, false, ThemesModel::PendingDeletionRole); + } +} + +bool KCMDesktopTheme::canEditThemes() const +{ + return m_haveThemeExplorerInstalled; +} + +void KCMDesktopTheme::editTheme(const QString &theme) +{ + QProcess::startDetached(QStringLiteral("plasmathemeexplorer"), {QStringLiteral("-t"), theme}); +} + +bool KCMDesktopTheme::isSaveNeeded() const +{ + return !m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true).isEmpty(); +} + +void KCMDesktopTheme::processPendingDeletions() +{ + const QString program = QStringLiteral("plasmapkg2"); + + const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true, -1 /*all*/); + QVector persistentPendingDeletions; + // turn into persistent model index so we can delete as we go + std::transform(pendingDeletions.begin(), pendingDeletions.end(), std::back_inserter(persistentPendingDeletions), [](const QModelIndex &idx) { + return QPersistentModelIndex(idx); + }); + + for (const QPersistentModelIndex &idx : persistentPendingDeletions) { + const QString pluginName = idx.data(ThemesModel::PluginNameRole).toString(); + const QString displayName = idx.data(Qt::DisplayRole).toString(); + + Q_ASSERT(pluginName != desktopThemeSettings()->name()); + + const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), pluginName}; + + QProcess *process = new QProcess(this); + connect(process, + static_cast(&QProcess::finished), + this, + [this, process, idx, pluginName, displayName](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitStatus) + if (exitCode == 0) { + m_model->removeRow(idx.row()); + } else { + Q_EMIT showErrorMessage(i18n("Removing theme failed: %1", QString::fromLocal8Bit(process->readAllStandardOutput().trimmed()))); + m_model->setData(idx, false, ThemesModel::PendingDeletionRole); + } + process->deleteLater(); + }); + + process->start(program, arguments); + process->waitForFinished(); // needed so it deletes fine when "OK" is clicked and the dialog destroyed + } +} + +#include "kcm.moc" diff --git a/plasma/workspace/kcms/desktoptheme/kcm.h b/plasma/workspace/kcms/desktoptheme/kcm.h new file mode 100644 index 0000000000..8988da83b1 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/kcm.h @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + SPDX-FileCopyrightText: 2014 Vishesh Handa + SPDX-FileCopyrightText: 2016 David Rosca + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +#include "desktopthemesettings.h" +#include "themesmodel.h" + +class QTemporaryFile; + +namespace Plasma +{ +class Theme; +} + +namespace KIO +{ +class FileCopyJob; +} + +class QQuickItem; +class DesktopThemeData; +class FilterProxyModel; + +class KCMDesktopTheme : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + + Q_PROPERTY(DesktopThemeSettings *desktopThemeSettings READ desktopThemeSettings CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) + Q_PROPERTY(ThemesModel *desktopThemeModel READ desktopThemeModel CONSTANT) + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + Q_PROPERTY(bool canEditThemes READ canEditThemes CONSTANT) + +public: + KCMDesktopTheme(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~KCMDesktopTheme() override; + + DesktopThemeSettings *desktopThemeSettings() const; + ThemesModel *desktopThemeModel() const; + FilterProxyModel *filteredModel() const; + + bool downloadingFile() const; + + bool canEditThemes() const; + + Q_INVOKABLE void installThemeFromFile(const QUrl &url); + + Q_INVOKABLE void applyPlasmaTheme(QQuickItem *item, const QString &themeName); + + Q_INVOKABLE void editTheme(const QString &themeName); + +Q_SIGNALS: + void downloadingFileChanged(); + + void showSuccessMessage(const QString &message); + void showErrorMessage(const QString &message); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +private: + bool isSaveNeeded() const override; + + void processPendingDeletions(); + + void installTheme(const QString &path); + + DesktopThemeData *m_data; + + ThemesModel *m_model; + FilterProxyModel *m_filteredModel; + QHash m_themes; + bool m_haveThemeExplorerInstalled; + + QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; +}; + +Q_DECLARE_LOGGING_CATEGORY(KCM_DESKTOP_THEME) diff --git a/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.desktop b/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.desktop new file mode 100644 index 0000000000..4dcaceff6b --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.desktop @@ -0,0 +1,47 @@ +[Desktop Entry] +Icon=preferences-desktop-plasma-theme +Type=Application +Exec=systemsettings kcm_desktoptheme +NoDisplay=true + +Name=Plasma Style +Name[ar]=نمط بلازما +Name[ast]=Estilu de Plasma +Name[az]=Plasma Üslubu +Name[ca]=Estil del Plasma +Name[cs]=Styl Plasma +Name[da]=Plasma-stil +Name[de]=Plasma-Stil +Name[en_GB]=Plasma Style +Name[es]=Estilo de Plasma +Name[et]=Plasma stiil +Name[eu]=Plasmaren estiloa +Name[fi]=Plasma-tyyli +Name[fr]=Style Plasma +Name[hi]=प्लाज़्मा शैली +Name[hsb]=Plasmowy stil +Name[hu]=Plasma stílus +Name[ia]=Stilo de Plasma +Name[id]=Gaya Plasma +Name[it]=Stile di Plasma +Name[ko]=Plasma 스타일 +Name[lt]=Plasma stilius +Name[ml]=പ്ലാസ്മ ശൈലി +Name[nl]=Plasma-stijl +Name[nn]=Plasma-stil +Name[pa]=ਪਲਾਜ਼ਮਾ ਸਟਾਈਲ +Name[pl]=Wygląd Plazmy +Name[pt]=Estilo do Plasma +Name[pt_BR]=Estilo do Plasma +Name[ro]=Stilul Plasma +Name[ru]=Оформление рабочего стола +Name[sk]=Plasma štýl +Name[sl]=Plasmin slog +Name[sv]=Plasmastil +Name[ta]=பிளாஸ்மா தோற்றத்திட்டம் +Name[tg]=Услуби плазма +Name[tr]=Plasma Biçemi +Name[uk]=Стиль Плазми +Name[vi]=Kiểu cách Plasma +Name[x-test]=xxPlasma Stylexx +Name[zh_CN]=Plasma 视觉风格 diff --git a/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.json b/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.json new file mode 100644 index 0000000000..3653905f6a --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/kcm_desktoptheme.json @@ -0,0 +1,114 @@ +{ + "KPlugin": { + "Description": "Choose Plasma style", + "Description[ar]": "اختر نمط بلازما", + "Description[az]": "Plasma üslubunu seçin", + "Description[ca]": "Trieu l'estil del Plasma", + "Description[cs]": "Zvolte styl Plasma", + "Description[de]": "Plasma-Stil auswählen", + "Description[en_GB]": "Choose Plasma style", + "Description[es]": "Escoger el estilo de Plasma", + "Description[eu]": "Aukeratu Plasmaren estiloa", + "Description[fi]": "Valitse Plasma-tyyli", + "Description[fr]": "Sélectionnez un style de Plasma", + "Description[hu]": "Plasma stílus kiválasztása", + "Description[ia]": "Selige le stilo de Plasma ", + "Description[it]": "Scegli lo stile di Plasma", + "Description[ko]": "Plasma 스타일 선택", + "Description[lt]": "Pasirinkti Plasma stilių", + "Description[nl]": "Plasma-stijl kiezen", + "Description[nn]": "Vel Plasma-stil", + "Description[pa]": "ਪਲਾਜ਼ਮਾ ਸਟਾਈਲ ਚੁਣੋ", + "Description[pl]": "Wybierz wygląd Plazmy", + "Description[pt_BR]": "Escolha o estilo do Plasma", + "Description[ro]": "Alege stilul Plasma", + "Description[ru]": "Выбор оформления рабочего стола", + "Description[sk]": "Vybrať Plasma štýl", + "Description[sl]": "Izberite Plasmin slog", + "Description[sv]": "Välj Plasmastil", + "Description[ta]": "பிளாஸ்மா தோற்றத்திட்டத்தை தேர்ந்தெடுங்கள்", + "Description[tr]": "Plazma biçemini seçin", + "Description[uk]": "Вибір стилю Плазми", + "Description[vi]": "Chọn kiểu cách Plasma", + "Description[x-test]": "xxChoose Plasma stylexx", + "Description[zh_CN]": "选择 Plasma 视觉风格", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-plasma-theme", + "Name": "Plasma Style", + "Name[ar]": "نمط بلازما", + "Name[ast]": "Estilu de Plasma", + "Name[az]": "Plasma Üslubu", + "Name[ca]": "Estil del Plasma", + "Name[cs]": "Styl Plasma", + "Name[da]": "Plasma-stil", + "Name[de]": "Plasma-Stil", + "Name[en_GB]": "Plasma Style", + "Name[es]": "Estilo de Plasma", + "Name[et]": "Plasma stiil", + "Name[eu]": "Plasmaren estiloa", + "Name[fi]": "Plasma-tyyli", + "Name[fr]": "Style Plasma", + "Name[hi]": "प्लाज़्मा शैली", + "Name[hsb]": "Plasmowy stil", + "Name[hu]": "Plasma stílus", + "Name[ia]": "Stilo de Plasma", + "Name[id]": "Gaya Plasma", + "Name[it]": "Stile di Plasma", + "Name[ko]": "Plasma 스타일", + "Name[lt]": "Plasma stilius", + "Name[ml]": "പ്ലാസ്മ ശൈലി", + "Name[nl]": "Plasma-stijl", + "Name[nn]": "Plasma-stil", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਸਟਾਈਲ", + "Name[pl]": "Wygląd Plazmy", + "Name[pt]": "Estilo do Plasma", + "Name[pt_BR]": "Estilo do Plasma", + "Name[ro]": "Stilul Plasma", + "Name[ru]": "Оформление рабочего стола", + "Name[sk]": "Plasma štýl", + "Name[sl]": "Plasmin slog", + "Name[sv]": "Plasmastil", + "Name[ta]": "பிளாஸ்மா தோற்றத்திட்டம்", + "Name[tg]": "Услуби плазма", + "Name[tr]": "Plazma Stili", + "Name[uk]": "Стиль Плазми", + "Name[vi]": "Kiểu cách Plasma", + "Name[x-test]": "xxPlasma Stylexx", + "Name[zh_CN]": "Plasma 视觉风格" + }, + "X-DocPath": "kcontrol/desktopthemedetails/index.html", + "X-KDE-Keywords": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme", + "X-KDE-Keywords[ar]": "سمة سطح المكتب, سمة بلازما,نمط بلازما,نمط سطح المكتب,مظهر بلازما, مظهر سطح المكتب,سمة", + "X-KDE-Keywords[az]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,İş masası mövzusu,Plasma mövzusu,plasma tərzi,iş masası tərzi,plasma örtüyü,iş masası örtüyü,mövzu", + "X-KDE-Keywords[ca]": "Tema d'escriptori,Tema del Plasma,estil del plasma,estil d'escriptori,pell del plasma,pell d'escriptori, tema", + "X-KDE-Keywords[en_GB]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme", + "X-KDE-Keywords[es]": "Tema del escritorio,tema de Plasma,estilo de Plasma,estilo del escritorio,piel de Plasma,piel del escritorio,tema", + "X-KDE-Keywords[eu]": "Mahaigaineko gaia,Plasmako gaia,Plasmako estiloa,mahaigainekoko estiloa,Plasmako azala,mahaigaineko azala,gaia", + "X-KDE-Keywords[fi]": "työpöytäteema,Plasma-teema,Plasma-tyyli,työpöytätyyli,teema", + "X-KDE-Keywords[fr]": "Thème de bureau, thème de Plasma, style de Plasma, style de bureau, habillage de Plasma, habillage de bureau, thème", + "X-KDE-Keywords[hi]": "डेस्कटॉप प्रसंग, प्लाज़्मा प्रसंग, प्लाज्मा शैली, डेस्कटॉप शैली, प्लाज़्मा त्वचा, डेस्कटॉप त्वचा, प्रसंग", + "X-KDE-Keywords[hu]": "Asztali téma,Plasma téma,plasma stílus,asztali stílus,plasma felület,asztali felület,téma", + "X-KDE-Keywords[ia]": "Thema de scriptorio, Thema de PLasma, stilo de plasma,stilo de scriptorio, apparentia de plasma, apprentia de scriptorio,thema", + "X-KDE-Keywords[it]": "Tema desktop,tema plasma,stile plasma,stile desktop,tema", + "X-KDE-Keywords[ko]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,데스크톱 테마,Plasma 테마,plasma 스타일,데스크톱 스타일,plasma 스킨,데스크톱 스킨,테마", + "X-KDE-Keywords[nl]": "Bureaubladthema,Plasma-thema,plasmastijl,bureabladstijl,uiterlijk van plasma,uiterlijk van bureaublad,thema", + "X-KDE-Keywords[nn]": "skrivebordstema,Plasma-tema,Plasma-stil,skrivebordsstil,Plasma-drakt,skrivebordsdrakt,tema,drakt", + "X-KDE-Keywords[pl]": "Wygląd pulpitu,Wygląd Plazmy,Styl Plazmy,Styl Pulpitu,skórka plazmy,skórka pulpitu,motyw", + "X-KDE-Keywords[pt]": "Tema do Ambiente de Trabalho,Tema do Plasma,estilo do plasma,estilo do ambiente de trabalho,visual do plasma,visual do ambiente de trabalho,tema", + "X-KDE-Keywords[pt_BR]": "tema da área de trabalho,tema do plasma,estilo do plasma,estilo da área de trabalho,pele de plasma,pele da área de trabalho,tema", + "X-KDE-Keywords[ru]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,Тема рабочего стола,оформление рабочего стола,оформление рабочей среды,оформление,тема", + "X-KDE-Keywords[sk]": "Téma pracovnej plochy, téma Plasmy, štýl Plasmy, štýl pracovnej plochy, skin Plasmy, skin pracovnej plochy,téma", + "X-KDE-Keywords[sl]": "Tema namizja,Tema Plasme,slog plasme,slog namizja,preobleka plasme,preobleka namizja,tema", + "X-KDE-Keywords[sv]": "Skrivbordstema,Plasmatema,plasmastil,skrivbordsstil,plasmaskal,skrivbordsskal,tema", + "X-KDE-Keywords[ta]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme, பிளாஸ்மா தோற்றத்திட்டம், பிளாஸ்மா திட்டம், பணிமேடை தோற்றத்திட்டம், பணிமேடை திட்டம்", + "X-KDE-Keywords[uk]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,тема стільниці,тема плазми,стиль плазми,стиль стільниці,оболонка плазми,шкірка плазми,скін плазми,оболонка стільниці,шкірка стільниці,скін стільниці,тема", + "X-KDE-Keywords[vi]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,Chủ đề Bàn làm việc,Chủ đề Plasma,kiểu cách plasma,kiểu cách bàn làm việc,da plasma,da bàn làm việc,chủ đề", + "X-KDE-Keywords[x-test]": "xxDesktop Themexx,xxPlasma Themexx,xxplasma stylexx,xxdesktop stylexx,xxplasma skinxx,xxdesktop skinxx,xxthemexx", + "X-KDE-Keywords[zh_CN]": "Desktop Theme,Plasma Theme,plasma style,desktop style,plasma skin,desktop skin,theme,桌面主题,Plasma 主题,plasma 风格,plasma 样式,plasma 皮肤,桌面皮肤,桌面风格,桌面样式,主题", + "X-KDE-System-Settings-Parent-Category": "appearance", + "X-KDE-Weight": 20 +} diff --git a/plasma/workspace/kcms/desktoptheme/package/contents/ui/Hand.qml b/plasma/workspace/kcms/desktoptheme/package/contents/ui/Hand.qml new file mode 100644 index 0000000000..1351c60cb1 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/package/contents/ui/Hand.qml @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2012 Viranch Mehta + SPDX-FileCopyrightText: 2012 Marco Martin + SPDX-FileCopyrightText: 2013 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.0 + +import org.kde.plasma.core 2.0 as PlasmaCore + +PlasmaCore.SvgItem { + id: handRoot + + property alias rotation: rotation.angle + property double svgScale + property double horizontalRotationOffset: 0 + property double verticalRotationOffset: 0 + property string rotationCenterHintId + readonly property double horizontalRotationCenter: { + if (svg.hasElement(rotationCenterHintId)) { + var hintedCenterRect = svg.elementRect(rotationCenterHintId), + handRect = svg.elementRect(elementId), + hintedX = hintedCenterRect.x - handRect.x + hintedCenterRect.width/2; + return Math.round(hintedX * svgScale) + Math.round(hintedX * svgScale) % 2; + } + return width/2; + } + readonly property double verticalRotationCenter: { + if (svg.hasElement(rotationCenterHintId)) { + var hintedCenterRect = svg.elementRect(rotationCenterHintId), + handRect = svg.elementRect(elementId), + hintedY = hintedCenterRect.y - handRect.y + hintedCenterRect.height/2; + return Math.round(hintedY * svgScale) + width % 2; + } + return width/2; + } + + width: Math.round(naturalSize.width * svgScale) + Math.round(naturalSize.width * svgScale) % 2 + height: Math.round(naturalSize.height * svgScale) + width % 2 + anchors { + top: clock.verticalCenter + topMargin: -verticalRotationCenter + verticalRotationOffset + left: clock.horizontalCenter + leftMargin: -horizontalRotationCenter + horizontalRotationOffset + } + + svg: clockSvg + transform: Rotation { + id: rotation + angle: 0 + origin { + x: handRoot.horizontalRotationCenter + y: handRoot.verticalRotationCenter + } + Behavior on angle { + RotationAnimation { + id: anim + duration: PlasmaCore.Units.longDuration + direction: RotationAnimation.Clockwise + easing.type: Easing.OutElastic + easing.overshoot: 0.5 + } + } + } +} diff --git a/plasma/workspace/kcms/desktoptheme/package/contents/ui/ThemePreview.qml b/plasma/workspace/kcms/desktoptheme/package/contents/ui/ThemePreview.qml new file mode 100644 index 0000000000..8f8173869e --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/package/contents/ui/ThemePreview.qml @@ -0,0 +1,178 @@ +/* + SPDX-FileCopyrightText: 2016 David Rosca + + SPDX-License-Identifier: LGPL-2.0-only +*/ +import QtQuick 2.4 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kirigami 2.4 as Kirigami +import org.kde.private.kcms.desktoptheme 1.0 as Private + +Item { + id: root + property string themeName + + Item { + id: backgroundMask + anchors.fill: parent + clip: true + + PlasmaCore.FrameSvgItem { + id: background + // Normalize margins around background. + // Some themes like "Air" have huge transparent margins which would result in too small container area. + // Sadly all of the breathing, shadow and border sizes are in one single margin value, + // but for typical themes the border is the smaller part the margin and should be in the size of + // Units.largeSpacing, to which we add another Units.largeSpacing for margin of the visual content + // Ideally Plasma::FrameSvg exposes the transparent margins one day. + readonly property int generalMargin: 2 * Kirigami.Units.largeSpacing + anchors { + fill: parent + topMargin: -margins.top + generalMargin + bottomMargin: -margins.bottom + generalMargin + leftMargin: -margins.left + generalMargin + rightMargin: -margins.right + generalMargin + } + imagePath: "widgets/background" + } + } + + RowLayout { + id: contents + spacing: 0 + anchors { + fill: parent + topMargin: background.generalMargin + bottomMargin: background.generalMargin + leftMargin: background.generalMargin + rightMargin: background.generalMargin + } + + // Icons + ColumnLayout { + id: icons + Layout.fillHeight: true + + PlasmaCore.IconItem { + id: computerIcon + Layout.fillHeight: true + source: "computer" + } + + PlasmaCore.IconItem { + id: applicationsIcon + Layout.fillHeight: true + source: "applications-other" + } + + PlasmaCore.IconItem { + id: logoutIcon + Layout.fillHeight: true + source: "system-log-out" + } + } + + // Analog clock + Item { + id: clock + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: height + Layout.alignment: Qt.AlignHCenter + property int hours: 9 + property int minutes: 5 + + readonly property double svgScale: face.width / face.naturalSize.width + readonly property double horizontalShadowOffset: + Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) % 2 + readonly property double verticalShadowOffset: + Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) % 2 + + PlasmaCore.Svg { + id: clockSvg + imagePath: "widgets/clock" + function estimateHorizontalHandShadowOffset() { + var id = "hint-hands-shadow-offset-to-west"; + if (hasElement(id)) { + return -elementSize(id).width; + } + id = "hint-hands-shadows-offset-to-east"; + if (hasElement(id)) { + return elementSize(id).width; + } + return 0; + } + function estimateVerticalHandShadowOffset() { + var id = "hint-hands-shadow-offset-to-north"; + if (hasElement(id)) { + return -elementSize(id).height; + } + id = "hint-hands-shadow-offset-to-south"; + if (hasElement(id)) { + return elementSize(id).height; + } + return 0; + } + property double naturalHorizontalHandShadowOffset: estimateHorizontalHandShadowOffset() + property double naturalVerticalHandShadowOffset: estimateVerticalHandShadowOffset() + onRepaintNeeded: { + naturalHorizontalHandShadowOffset = estimateHorizontalHandShadowOffset(); + naturalVerticalHandShadowOffset = estimateVerticalHandShadowOffset(); + } + } + + PlasmaCore.SvgItem { + id: face + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: Math.min(parent.width, parent.height) + svg: clockSvg + elementId: "ClockFace" + } + + Hand { + elementId: "HourHand" + rotationCenterHintId: "hint-hourhand-rotation-center-offset" + rotation: 180 + clock.hours * 30 + (clock.minutes/2) + svgScale: clock.svgScale + } + + Hand { + elementId: "MinuteHand" + rotationCenterHintId: "hint-minutehand-rotation-center-offset" + rotation: 180 + clock.minutes * 6 + svgScale: clock.svgScale + } + + PlasmaCore.SvgItem { + id: center + width: naturalSize.width * clock.svgScale + height: naturalSize.height * clock.svgScale + anchors.centerIn: clock + svg: clockSvg + elementId: "HandCenterScrew" + z: 1000 + } + + PlasmaCore.SvgItem { + anchors.fill: face + svg: clockSvg + elementId: "Glass" + width: naturalSize.width * clock.svgScale + height: naturalSize.height * clock.svgScale + } + } + Kirigami.Icon { + visible: model.colorType === Private.ThemesModel.FollowsColorTheme + source: "color-profile" + width: Kirigami.Units.iconSizes.smallMedium + height: width + Layout.alignment: Qt.AlignRight && Qt.AlignTop + } + } + + Component.onCompleted: { + kcm.applyPlasmaTheme(root, themeName); + } +} diff --git a/plasma/workspace/kcms/desktoptheme/package/contents/ui/main.qml b/plasma/workspace/kcms/desktoptheme/package/contents/ui/main.qml new file mode 100644 index 0000000000..89d67978df --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/package/contents/ui/main.qml @@ -0,0 +1,206 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + SPDX-FileCopyrightText: 2016 David Rosca + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.0 +import QtQuick.Controls 2.3 as QtControls +import QtQml 2.15 + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.newstuff 1.81 as NewStuff +import org.kde.kcm 1.3 as KCM +import org.kde.private.kcms.desktoptheme 1.0 as Private + + +KCM.GridViewKCM { + id: root + KCM.ConfigModule.quickHelp: i18n("This module lets you choose the Plasma style.") + + view.model: kcm.filteredModel + view.currentIndex: kcm.filteredModel.selectedThemeIndex + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + restoreMode: Binding.RestoreBinding + } + + Binding { + target: kcm.filteredModel + property: "filter" + value: filterCombo.model[filterCombo.currentIndex].filter + restoreMode: Binding.RestoreBinding + } + + KCM.SettingStateBinding { + configObject: kcm.desktopThemeSettings + settingName: "name" + extraEnabledConditions: !kcm.downloadingFile + } + + DropArea { + anchors.fill: parent + onEntered: { + if (!drag.hasUrls) { + drag.accepted = false; + } + } + onDropped: kcm.installThemeFromFile(drop.urls[0]) + } + header: RowLayout { + Layout.fillWidth: true + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + } + QtControls.ComboBox { + id: filterCombo + textRole: "text" + model: [ + {text: i18n("All Themes"), filter: Private.FilterProxyModel.AllThemes}, + {text: i18n("Light Themes"), filter: Private.FilterProxyModel.LightThemes}, + {text: i18n("Dark Themes"), filter: Private.FilterProxyModel.DarkThemes}, + {text: i18n("Color scheme compatible"), filter: Private.FilterProxyModel.ThemesFollowingColors} + ] + + // HACK QQC2 doesn't support icons, so we just tamper with the desktop style ComboBox's background + // and inject a nice little filter icon. + Component.onCompleted: { + if (!background || !background.hasOwnProperty("properties")) { + // not a KQuickStyleItem + return; + } + + var props = background.properties || {}; + + background.properties = Qt.binding(function() { + var newProps = props; + newProps.currentIcon = "view-filter"; + newProps.iconColor = Kirigami.Theme.textColor; + return newProps; + }); + } + } + } + + view.delegate: KCM.GridDelegate { + id: delegate + + text: model.display + subtitle: model.colorType == Private.ThemesModel.FollowsColorTheme + && view.model.filter != Private.FilterProxyModel.ThemesFollowingColors ? i18n("Follows color scheme") : "" + toolTip: model.description || model.display + + opacity: model.pendingDeletion ? 0.3 : 1 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + + thumbnailAvailable: true + thumbnail: ThemePreview { + id: preview + anchors.fill: parent + themeName: model.pluginName + } + + actions: [ + Kirigami.Action { + iconName: "document-edit" + tooltip: i18n("Edit Theme…") + enabled: !model.pendingDeletion + visible: kcm.canEditThemes + onTriggered: kcm.editTheme(model.pluginName) + }, + Kirigami.Action { + iconName: "edit-delete" + tooltip: i18n("Remove Theme") + enabled: model.isLocal + visible: !model.pendingDeletion + onTriggered: model.pendingDeletion = true; + }, + Kirigami.Action { + iconName: "edit-undo" + tooltip: i18n("Restore Theme") + visible: model.pendingDeletion + onTriggered: model.pendingDeletion = false; + } + ] + + onClicked: { + kcm.desktopThemeSettings.name = model.pluginName; + view.forceActiveFocus(); + } + onDoubleClicked: { + kcm.save(); + } + } + + footer: ColumnLayout { + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + showCloseButton: true + + Connections { + target: kcm + function onShowSuccessMessage(message) { + infoLabel.type = Kirigami.MessageType.Positive; + infoLabel.text = message; + infoLabel.visible = true; + } + function onShowErrorMessage(message) { + infoLabel.type = Kirigami.MessageType.Error; + infoLabel.text = message; + infoLabel.visible = true; + } + } + } + + Kirigami.ActionToolBar { + flat: false + alignment: Qt.AlignRight + actions: [ + Kirigami.Action { + text: i18n("Install from File…") + icon.name: "document-import" + onTriggered: fileDialogLoader.active = true + }, + NewStuff.Action { + text: i18n("Get New Plasma Styles…") + configFile: "plasma-themes.knsrc" + onEntryEvent: function (entry, event) { + kcm.load(); + } + } + ] + } + } + + Loader { + id: fileDialogLoader + active: false + sourceComponent: FileDialog { + title: i18n("Open Theme") + folder: shortcuts.home + nameFilters: [ i18n("Theme Files (*.zip *.tar.gz *.tar.bz2)") ] + Component.onCompleted: open() + onAccepted: { + kcm.installThemeFromFile(fileUrls[0]) + fileDialogLoader.active = false + } + onRejected: { + fileDialogLoader.active = false + } + } + } +} diff --git a/plasma/workspace/kcms/desktoptheme/plasma-apply-desktoptheme.cpp b/plasma/workspace/kcms/desktoptheme/plasma-apply-desktoptheme.cpp new file mode 100644 index 0000000000..5a78c03d0c --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/plasma-apply-desktoptheme.cpp @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "themesmodel.h" + +#include + +#include + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + // This is a CLI application, but we require at least a QGuiApplication for things + // in Plasma::Theme, so let's just roll with one of these + QApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("plasma-apply-desktoptheme")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + KLocalizedString::setApplicationDomain("plasma-apply-desktoptheme"); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->setApplicationDescription( + i18n("This tool allows you to set the theme of the current Plasma session, without accidentally setting it to one that is either not available, or " + "which is already set.")); + parser->addPositionalArgument( + QStringLiteral("themename"), + i18n("The name of the theme you wish to set for your current Plasma session (passing a full path will only use the last part of the path)")); + parser->addOption(QCommandLineOption(QStringLiteral("list-themes"), i18n("Show all the themes available on the system (and which is the current theme)"))); + parser->process(app); + + int errorCode{0}; + QTextStream ts(stdout); + ThemesModel *model{new ThemesModel(&app)}; + if (!parser->positionalArguments().isEmpty()) { + QString requestedTheme{parser->positionalArguments().first()}; + const QString dirSplit{"/"}; + if (requestedTheme.contains(dirSplit)) { + requestedTheme = requestedTheme.split(dirSplit, Qt::SkipEmptyParts).last(); + } + if (Plasma::Theme().themeName() == requestedTheme) { + ts << i18n("The requested theme \"%1\" is already set as the theme for the current Plasma session.", requestedTheme) << Qt::endl; + // Not an error condition really, let's just ignore that + } else { + bool found{false}; + QStringList availableThemes; + model->load(); + for (int i = 0; i < model->rowCount(); ++i) { + QString currentTheme{model->data(model->index(i), ThemesModel::PluginNameRole).toString()}; + if (currentTheme == requestedTheme) { + Plasma::Theme().setThemeName(requestedTheme); + found = true; + break; + } + availableThemes << currentTheme; + } + if (found) { + ts << i18n("The current Plasma session's theme has been set to %1", requestedTheme) << Qt::endl; + } else { + ts << i18n("Could not find theme \"%1\". The theme should be one of the following options: %2", + requestedTheme, + availableThemes.join(QLatin1String{", "})) + << Qt::endl; + errorCode = -1; + } + } + } else if (parser->isSet(QStringLiteral("list-themes"))) { + ts << i18n("You have the following Plasma themes on your system:") << Qt::endl; + model->load(); + for (int i = 0; i < model->rowCount(); ++i) { + QString themeName{model->data(model->index(i), ThemesModel::PluginNameRole).toString()}; + if (Plasma::Theme().themeName() == themeName) { + ts << QString(" * %1 (current theme for this Plasma session)").arg(themeName) << Qt::endl; + } else { + ts << QString(" * %1").arg(themeName) << Qt::endl; + } + } + } else { + parser->showHelp(); + } + QTimer::singleShot(0, &app, [&app, &errorCode]() { + app.exit(errorCode); + }); + + return app.exec(); +} diff --git a/plasma/workspace/kcms/desktoptheme/plasma-themes.knsrc b/plasma/workspace/kcms/desktoptheme/plasma-themes.knsrc new file mode 100644 index 0000000000..ae23b356e2 --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/plasma-themes.knsrc @@ -0,0 +1,49 @@ +[KNewStuff3] +Name=Plasma Styles +Name[ar]=أنماط بلازما +Name[ast]=Estilos de Plasma +Name[az]=Plasma Üslubu +Name[ca]=Estils del Plasma +Name[cs]=Styly Plasma +Name[da]=Plasma-stile +Name[de]=Plasma-Stile +Name[en_GB]=Plasma Styles +Name[es]=Estilos de Plasma +Name[et]=Plasma stiilid +Name[eu]=Plasmaren estiloak +Name[fi]=Plasma-tyylit +Name[fr]=Styles Plasma +Name[hi]=प्लाज़्मा शैलियाँ +Name[hu]=Plasma stílusok +Name[ia]=Stilos de Plasma +Name[id]=Gaya Plasma +Name[it]=Stili di Plasma +Name[ko]=Plasma 스타일 +Name[lt]=Plasma stiliai +Name[ml]=പ്ലാസ്മ ശൈലികൾ +Name[nl]=Plasma-stijlen +Name[nn]=Plasma-stilar +Name[pa]=ਪਲਾਜ਼ਮਾ ਸਟਾਈਲ +Name[pl]=Wyglądy Plazmy +Name[pt]=Estilos do Plasma +Name[pt_BR]=Estilos do Plasma +Name[ro]=Stiluri Plasma +Name[ru]=Оформления рабочего стола +Name[sk]=Plasma štýly +Name[sl]=Slogi Plasme +Name[sv]=Plasmastilar +Name[ta]=பிளாஸ்மா தோற்றத்திட்டங்கள் +Name[tr]=Plasma Biçemleri +Name[uk]=Стилі Плазми +Name[vi]=Kiểu cách Plasma +Name[x-test]=xxPlasma Stylesxx +Name[zh_CN]=Plasma 视觉风格 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Plasma Theme +StandardResource=tmp +Uncompress=kpackage +KPackageType=Plasma/Theme +TagFilter=ghns_excluded!=1,plasma##version==5 +DownloadTagFilter=plasma##version==5 +AdoptionCommand=plasma-apply-desktoptheme %f diff --git a/plasma/workspace/kcms/desktoptheme/themesmodel.cpp b/plasma/workspace/kcms/desktoptheme/themesmodel.cpp new file mode 100644 index 0000000000..6f0ecf4e3d --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/themesmodel.cpp @@ -0,0 +1,238 @@ +/* + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + SPDX-FileCopyrightText: 2016 Olivier Churlaud + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "themesmodel.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +ThemesModel::ThemesModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +ThemesModel::~ThemesModel() = default; + +int ThemesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant ThemesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return item.display; + case PluginNameRole: + return item.pluginName; + case DescriptionRole: + return item.description; + case ColorTypeRole: + return item.type; + case IsLocalRole: + return item.isLocal; + case PendingDeletionRole: + return item.pendingDeletion; + } + return QVariant(); +} + +bool ThemesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return false; + } + + if (role == PendingDeletionRole) { + auto &item = m_data[index.row()]; + + const bool pendingDeletion = value.toBool(); + + if (item.pendingDeletion != pendingDeletion) { + item.pendingDeletion = pendingDeletion; + Q_EMIT dataChanged(index, index, {PendingDeletionRole}); + + if (index.row() == selectedThemeIndex() && pendingDeletion) { + // move to the next non-pending theme + const auto nonPending = match(index, PendingDeletionRole, false); + if (!nonPending.isEmpty()) { + setSelectedTheme(nonPending.first().data(PluginNameRole).toString()); + } + } + + Q_EMIT pendingDeletionsChanged(); + return true; + } + } + + return false; +} + +QHash ThemesModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {PluginNameRole, QByteArrayLiteral("pluginName")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {ColorTypeRole, QByteArrayLiteral("colorType")}, + {IsLocalRole, QByteArrayLiteral("isLocal")}, + {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")}, + }; +} + +QString ThemesModel::selectedTheme() const +{ + return m_selectedTheme; +} + +void ThemesModel::setSelectedTheme(const QString &pluginName) +{ + if (m_selectedTheme == pluginName) { + return; + } + + m_selectedTheme = pluginName; + + Q_EMIT selectedThemeChanged(pluginName); + + Q_EMIT selectedThemeIndexChanged(); +} + +int ThemesModel::pluginIndex(const QString &pluginName) const +{ + const auto results = match(index(0, 0), PluginNameRole, pluginName, 1, Qt::MatchExactly); + if (results.count() == 1) { + return results.first().row(); + } + + return -1; +} + +int ThemesModel::selectedThemeIndex() const +{ + return pluginIndex(m_selectedTheme); +} + +void ThemesModel::load() +{ + beginResetModel(); + + const int oldCount = m_data.count(); + + m_data.clear(); + + // Get all desktop themes + QStringList themes; + const QStringList packs = + QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory); + for (const QString &ppath : packs) { + const QDir cd(ppath); + const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot); + for (const QString &pack : entries) { + const QString _metadata = ppath + QLatin1Char('/') + pack + QStringLiteral("/metadata.desktop"); + if (QFile::exists(_metadata)) { + themes << _metadata; + } + } + } + + for (const QString &theme : qAsConst(themes)) { + int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1); + const QString themeRoot = theme.left(themeSepIndex); + int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1); + const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); + + KDesktopFile df(theme); + + if (df.noDisplay()) { + continue; + } + + QString name = df.readName(); + if (name.isEmpty()) { + name = packageName; + } + const bool isLocal = QFileInfo(theme).isWritable(); + bool hasPluginName = std::any_of(m_data.begin(), m_data.end(), [&](const ThemesModelData &item) { + return item.pluginName == packageName; + }); + if (!hasPluginName) { + // Plasma Theme creates a KColorScheme out of the "color" file and falls back to system colors if there is none + const QString colorsPath = themeRoot + QLatin1String("/colors"); + const bool followsSystemColors = !QFileInfo::exists(colorsPath); + ColorType type = FollowsColorTheme; + if (!followsSystemColors) { + const KSharedConfig::Ptr config = KSharedConfig::openConfig(colorsPath); + const QPalette palette = KColorScheme::createApplicationPalette(config); + const int windowBackgroundGray = qGray(palette.window().color().rgb()); + if (windowBackgroundGray < 192) { + type = DarkTheme; + } else { + type = LightTheme; + } + } + ThemesModelData item{name, packageName, df.readComment(), type, isLocal, false}; + m_data.append(item); + } + } + + // Sort case-insensitively + QCollator collator; + collator.setCaseSensitivity(Qt::CaseInsensitive); + std::sort(m_data.begin(), m_data.end(), [&collator](const ThemesModelData &a, const ThemesModelData &b) { + return collator.compare(a.display, b.display) < 0; + }); + + endResetModel(); + + // an item might have been added before the currently selected one + if (oldCount != m_data.count()) { + Q_EMIT selectedThemeIndexChanged(); + } +} + +QStringList ThemesModel::pendingDeletions() const +{ + QStringList pendingDeletions; + + for (const auto &item : qAsConst(m_data)) { + if (item.pendingDeletion) { + pendingDeletions.append(item.pluginName); + } + } + + return pendingDeletions; +} + +void ThemesModel::removeRow(int row) +{ + beginRemoveRows(QModelIndex(), row, row); + m_data.erase(m_data.begin() + row); + endRemoveRows(); +} diff --git a/plasma/workspace/kcms/desktoptheme/themesmodel.h b/plasma/workspace/kcms/desktoptheme/themesmodel.h new file mode 100644 index 0000000000..18be11c27b --- /dev/null +++ b/plasma/workspace/kcms/desktoptheme/themesmodel.h @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2007 Matthew Woehlke + SPDX-FileCopyrightText: 2007 Jeremy Whiting + SPDX-FileCopyrightText: 2016 Olivier Churlaud + SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +struct ThemesModelData; + +class ThemesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedTheme READ selectedTheme WRITE setSelectedTheme NOTIFY selectedThemeChanged) + Q_PROPERTY(int selectedThemeIndex READ selectedThemeIndex NOTIFY selectedThemeChanged) + +public: + ThemesModel(QObject *parent); + ~ThemesModel() override; + + enum Roles { + PluginNameRole = Qt::UserRole + 1, + ThemeNameRole, + DescriptionRole, + FollowsSystemColorsRole, + ColorTypeRole, + IsLocalRole, + PendingDeletionRole, + }; + Q_ENUM(Roles) + enum ColorType { + LightTheme, + DarkTheme, + FollowsColorTheme, + }; + Q_ENUM(ColorType) + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QHash roleNames() const override; + + QString selectedTheme() const; + void setSelectedTheme(const QString &pluginName); + + int pluginIndex(const QString &pluginName) const; + int selectedThemeIndex() const; + + QStringList pendingDeletions() const; + void removeRow(int row); + + void load(); + +Q_SIGNALS: + void selectedThemeChanged(const QString &pluginName); + void selectedThemeIndexChanged(); + + void pendingDeletionsChanged(); + +private: + QString m_selectedTheme; + // Can't use QVector because unique_ptr causes deletion of copy-ctor + QVector m_data; +}; + +struct ThemesModelData { + QString display; + QString pluginName; + QString description; + ThemesModel::ColorType type; + bool isLocal; + bool pendingDeletion; +}; diff --git a/plasma/workspace/kcms/feedback/CMakeLists.txt b/plasma/workspace/kcms/feedback/CMakeLists.txt new file mode 100644 index 0000000000..e1b13c17ae --- /dev/null +++ b/plasma/workspace/kcms/feedback/CMakeLists.txt @@ -0,0 +1,31 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_feedback\") + +set(kcm_feedback_PART_SRCS feedback.cpp) + +kcmutils_generate_module_data( + kcm_feedback_PART_SRCS + MODULE_DATA_HEADER feedbackdata.h + MODULE_DATA_CLASS_NAME FeedbackData + SETTINGS_HEADERS feedbacksettings.h + SETTINGS_CLASSES FeedbackSettings +) + +kconfig_add_kcfg_files(kcm_feedback_PART_SRCS feedbacksettings.kcfgc GENERATE_MOC) +kcoreaddons_add_plugin(kcm_feedback SOURCES ${kcm_feedback_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + +target_link_libraries(kcm_feedback + KF5::I18n + KF5::KCMUtils + KF5::QuickAddons + KUserFeedbackCore +) + +ecm_qt_declare_logging_category(kcm_feedback + HEADER kcm_feedback_debug.h + IDENTIFIER KCM_FEEDBACK_DEBUG + CATEGORY_NAME org.kde.plasma.kcm_feedback +) + +install(FILES feedbacksettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES kcm_feedback.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +kpackage_install_package(package kcm_feedback kcms) diff --git a/plasma/workspace/kcms/feedback/Messages.sh b/plasma/workspace/kcms/feedback/Messages.sh new file mode 100644 index 0000000000..88a8770c90 --- /dev/null +++ b/plasma/workspace/kcms/feedback/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_feedback.pot diff --git a/plasma/workspace/kcms/feedback/feedback.cpp b/plasma/workspace/kcms/feedback/feedback.cpp new file mode 100644 index 0000000000..ad231d853d --- /dev/null +++ b/plasma/workspace/kcms/feedback/feedback.cpp @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "feedback.h" +#include "kcm_feedback_debug.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include "feedbackdata.h" +#include "feedbacksettings.h" + +K_PLUGIN_FACTORY_WITH_JSON(FeedbackFactory, "kcm_feedback.json", registerPlugin(); registerPlugin();) + +struct Information { + QString icon; + QString kuserfeedbackComponent; +}; +static QHash s_programs = { + { "plasmashell", {"plasmashell", "plasmashell"} }, + { "plasma-discover", {"plasmadiscover", "discover" } }, +}; + +inline void swap(QJsonValueRef v1, QJsonValueRef v2) +{ + QJsonValue temp(v1); + v1 = QJsonValue(v2); + v2 = temp; +} + +Feedback::Feedback(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + // UserFeedback.conf is used by KUserFeedback which uses QSettings and won't go through globals + , m_data(new FeedbackData(this)) +{ + qmlRegisterAnonymousType("org.kde.userfeedback.kcm", 1); + + QVector processes; + for (const auto &exec : s_programs.keys()) { + QProcess *p = new QProcess(this); + p->setProgram(exec); + p->setArguments({QStringLiteral("--feedback")}); + p->start(); + connect(p, &QProcess::finished, this, &Feedback::programFinished); + processes << p; + } +} + +Feedback::~Feedback() = default; + +void Feedback::programFinished(int exitCode) +{ + auto mo = KUserFeedback::Provider::staticMetaObject; + const int modeEnumIdx = mo.indexOfEnumerator("TelemetryMode"); + Q_ASSERT(modeEnumIdx >= 0); + const auto modeEnum = mo.enumerator(modeEnumIdx); + + QProcess *p = qobject_cast(sender()); + const QString program = p->program(); + + if (exitCode) { + qCWarning(KCM_FEEDBACK_DEBUG) << "Could not check" << program; + return; + } + + QTextStream stream(p); + for (QString line; stream.readLineInto(&line);) { + int sepIdx = line.indexOf(QLatin1String(": ")); + if (sepIdx < 0) { + break; + } + + const QString mode = line.left(sepIdx); + bool ok; + const int modeValue = modeEnum.keyToValue(qPrintable(mode), &ok); + if (!ok) { + qCWarning(KCM_FEEDBACK_DEBUG) << "error:" << mode << "is not a valid mode"; + continue; + } + + const QString description = line.mid(sepIdx + 1); + m_uses[modeValue][description] << s_programs[program].icon; + } + p->deleteLater(); + m_feedbackSources = {}; + for (auto it = m_uses.constBegin(), itEnd = m_uses.constEnd(); it != itEnd; ++it) { + const auto modeUses = *it; + for (auto itMode = modeUses.constBegin(), itModeEnd = modeUses.constEnd(); itMode != itModeEnd; ++itMode) { + m_feedbackSources << QJsonObject({{"mode", it.key()}, {"icons", *itMode}, {"description", itMode.key()}}); + } + } + std::sort(m_feedbackSources.begin(), m_feedbackSources.end(), [](const QJsonValue &valueL, const QJsonValue &valueR) { + const QJsonObject objL(valueL.toObject()), objR(valueR.toObject()); + const auto modeL = objL["mode"].toInt(), modeR = objR["mode"].toInt(); + return modeL < modeR || (modeL == modeR && objL["description"].toString() < objR["description"].toString()); + }); + Q_EMIT feedbackSourcesChanged(); +} + +bool Feedback::feedbackEnabled() const +{ + KUserFeedback::Provider p; + return p.isEnabled(); +} + +FeedbackSettings *Feedback::feedbackSettings() const +{ + return m_data->settings(); +} + +QJsonArray Feedback::audits() const +{ + QJsonArray ret; + for (auto it = s_programs.constBegin(); it != s_programs.constEnd(); ++it) { + QString feedbackLocation = + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + '/' + it->kuserfeedbackComponent + QStringLiteral("/kuserfeedback/audit"); + + if (QFileInfo::exists(feedbackLocation)) { + ret += QJsonObject{ + {"program", it.key()}, + {"audits", feedbackLocation}, + }; + } + } + return ret; +} + +#include "feedback.moc" diff --git a/plasma/workspace/kcms/feedback/feedback.h b/plasma/workspace/kcms/feedback/feedback.h new file mode 100644 index 0000000000..69595a0267 --- /dev/null +++ b/plasma/workspace/kcms/feedback/feedback.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class FeedbackSettings; +class FeedbackData; + +class Feedback : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + + Q_PROPERTY(QJsonArray feedbackSources MEMBER m_feedbackSources NOTIFY feedbackSourcesChanged) + Q_PROPERTY(QJsonArray audits READ audits CONSTANT) + Q_PROPERTY(bool feedbackEnabled READ feedbackEnabled CONSTANT) + Q_PROPERTY(FeedbackSettings *feedbackSettings READ feedbackSettings CONSTANT) + +public: + explicit Feedback(QObject *parent, const KPluginMetaData &data, const QVariantList &list = QVariantList()); + ~Feedback() override; + + bool feedbackEnabled() const; + FeedbackSettings *feedbackSettings() const; + + QJsonArray audits() const; + void programFinished(int exitCode); + +Q_SIGNALS: + void feedbackSourcesChanged(); + +private: + QHash> m_uses; + QJsonArray m_feedbackSources; + FeedbackData *m_data; +}; diff --git a/plasma/workspace/kcms/feedback/feedbacksettings.kcfg b/plasma/workspace/kcms/feedback/feedbacksettings.kcfg new file mode 100644 index 0000000000..7970f34959 --- /dev/null +++ b/plasma/workspace/kcms/feedback/feedbacksettings.kcfg @@ -0,0 +1,12 @@ + + + + + + KUserFeedback::Provider::NoTelemetry + + + diff --git a/plasma/workspace/kcms/feedback/feedbacksettings.kcfgc b/plasma/workspace/kcms/feedback/feedbacksettings.kcfgc new file mode 100644 index 0000000000..a488cb31d1 --- /dev/null +++ b/plasma/workspace/kcms/feedback/feedbacksettings.kcfgc @@ -0,0 +1,7 @@ +File=feedbacksettings.kcfg +ClassName=FeedbackSettings +IncludeFiles="KUserFeedback/Provider" +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/plasma/workspace/kcms/feedback/kcm_feedback.desktop b/plasma/workspace/kcms/feedback/kcm_feedback.desktop new file mode 100644 index 0000000000..365e58bc12 --- /dev/null +++ b/plasma/workspace/kcms/feedback/kcm_feedback.desktop @@ -0,0 +1,49 @@ +[Desktop Entry] +Icon=preferences-desktop-feedback +Type=Application +Exec=systemsettings kcm_feedback +NoDisplay=true + +Name=User Feedback +Name[ar]=مشاركة بياناتك +Name[ast]=Comentarios d'usuariu +Name[az]=İstifadəçi Rəyi +Name[ca]=Comentaris de l'usuari +Name[cs]=Zpětná vazba uživatele +Name[da]=Brugerfeedback +Name[de]=Benutzer-Feedback +Name[en_GB]=User Feedback +Name[es]=Comentarios del usuario +Name[et]=Kasutaja tagasiside +Name[eu]=Erabiltzaileen berrelikadura +Name[fi]=Käyttäjäpalaute +Name[fr]=Retours des utilisateurs +Name[gl]=Achegas de usuario +Name[hi]=उपयोक्ता प्रतिक्रिया +Name[hu]=Felhasználói visszajelzés +Name[ia]=Responsa del usator +Name[id]=Tanggapan Pengguna +Name[it]=Segnalazioni dell'utente +Name[ja]=ユーザ フィードバック +Name[ko]=사용자 피드백 +Name[lt]=Naudotojo grįžtamasis ryšys +Name[ml]=ഉപയോക്തൃ അഭിപ്രായം +Name[nl]=Terugkoppeling van gebruiker +Name[nn]=Tilbakemeldingar +Name[pa]=ਵਰਤੋਂਕਾਰ ਸੁਝਾਅ +Name[pl]=Informacja zwrotna +Name[pt]=Reacções do Utilizador +Name[pt_BR]=Comentários dos usuários +Name[ro]=Reacții utilizator +Name[ru]=Обратная связь +Name[sk]=Používateľská odozva +Name[sl]=Uporabnikov odziv +Name[sv]=Användaråterkoppling +Name[ta]=பயனர் பின்னூட்டம் +Name[tg]=Изҳори назари корбар +Name[tr]=Kullanıcı Geribildirimi +Name[uk]=Відгуки користувача +Name[vi]=Phản hồi của người dùng +Name[x-test]=xxUser Feedbackxx +Name[zh_CN]=用户反馈 +Name[zh_TW]=使用者意見回應 diff --git a/plasma/workspace/kcms/feedback/kcm_feedback.json b/plasma/workspace/kcms/feedback/kcm_feedback.json new file mode 100644 index 0000000000..e39403af5d --- /dev/null +++ b/plasma/workspace/kcms/feedback/kcm_feedback.json @@ -0,0 +1,115 @@ +{ + "KPlugin": { + "Description": "Configure user feedback settings", + "Description[ar]": "اضبط إعدادات مشاركة بياناتك", + "Description[az]": "İstifadəçi rəyi ayarlarını tənzimləmək", + "Description[ca]": "Configura les opcions dels comentaris de l'usuari", + "Description[cs]": "Nastavení voleb uživatelské zpětné vazby", + "Description[de]": "Benutzer-Feedback einrichten", + "Description[en_GB]": "Configure user feedback settings", + "Description[es]": "Configurar las preferencias de los comentarios del usuario", + "Description[eu]": "Konfiguratu erabiltzaileen berrelikadura ezarpenak", + "Description[fi]": "Käyttäjäpalauteasetukset", + "Description[fr]": "Configurer les paramètres de retours des utilisateurs", + "Description[hu]": "A felhasználói visszajelzések beállításai", + "Description[ia]": "Configura preferentias de retorno de usator", + "Description[it]": "Configura le impostazioni di segnalazione dell'utente", + "Description[ko]": "사용자 피드백 설정", + "Description[lt]": "Konfigūruoti naudotojo grįžtamojo ryšio nuostatas", + "Description[nl]": "Instellingen voor terugkoppeling van gebruikers configureren", + "Description[nn]": "Set opp tilbakemeldingar frå brukarar", + "Description[pa]": "ਵਰਤੋਂਕਾਰ ਫੀਡਬੈਕ ਸੈਟਿੰਗਾਂ ਦੀ ਸੰਰਚਨਾ", + "Description[pl]": "Ustawienia informacji zwrotnej", + "Description[pt_BR]": "Configure as definições dos comentários dos usuários", + "Description[ro]": "Configurează parametrii reacțiilor utilizatorului", + "Description[ru]": "Настройка параметров обратной связи", + "Description[sk]": "Nastaviť používateľskú odozvu", + "Description[sl]": "Nastavi uporabnikov odziv", + "Description[sv]": "Anpassa inställningar av användaråterkoppling", + "Description[ta]": "பயனர் பின்னூட்டத்தை அமையுங்கள்", + "Description[tr]": "Kullanıcı geri bildirim ayarlarını yapılandırın", + "Description[uk]": "Налаштовування системи відгуків користувача", + "Description[vi]": "Cấu hình các thiết lập phản hồi của người dùng", + "Description[x-test]": "xxConfigure user feedback settingsxx", + "Description[zh_CN]": "配置用户反馈设置", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-feedback", + "Name": "User Feedback", + "Name[ar]": "مشاركة بياناتك", + "Name[ast]": "Comentarios d'usuariu", + "Name[az]": "İstifadəçi Rəyi", + "Name[ca]": "Comentaris de l'usuari", + "Name[cs]": "Zpětná vazba uživatele", + "Name[da]": "Brugerfeedback", + "Name[de]": "Benutzer-Feedback", + "Name[en_GB]": "User Feedback", + "Name[es]": "Comentarios del usuario", + "Name[et]": "Kasutaja tagasiside", + "Name[eu]": "Erabiltzaileen berrelikadura", + "Name[fi]": "Käyttäjäpalaute", + "Name[fr]": "Retours des utilisateurs", + "Name[gl]": "Achegas de usuario", + "Name[hi]": "उपयोक्ता प्रतिक्रिया", + "Name[hu]": "Felhasználói visszajelzés", + "Name[ia]": "Responsa del usator", + "Name[id]": "Tanggapan Pengguna", + "Name[it]": "Segnalazioni dell'utente", + "Name[ja]": "ユーザ フィードバック", + "Name[ko]": "사용자 피드백", + "Name[lt]": "Naudotojo grįžtamasis ryšys", + "Name[ml]": "ഉപയോക്തൃ അഭിപ്രായം", + "Name[nl]": "Terugkoppeling van gebruiker", + "Name[nn]": "Tilbakemeldingar", + "Name[pa]": "ਵਰਤੋਂਕਾਰ ਸੁਝਾਅ", + "Name[pl]": "Informacja zwrotna", + "Name[pt]": "Reacções do Utilizador", + "Name[pt_BR]": "Comentários dos usuários", + "Name[ro]": "Reacții utilizator", + "Name[ru]": "Обратная связь", + "Name[sk]": "Používateľská odozva", + "Name[sl]": "Uporabnikov odziv", + "Name[sv]": "Användaråterkoppling", + "Name[ta]": "பயனர் பின்னூட்டம்", + "Name[tg]": "Изҳори назари корбар", + "Name[tr]": "Kullanıcı Geribildirimi", + "Name[uk]": "Відгуки користувача", + "Name[vi]": "Phản hồi của người dùng", + "Name[x-test]": "xxUser Feedbackxx", + "Name[zh_CN]": "用户反馈", + "Name[zh_TW]": "使用者意見回應" + }, + "X-DocPath": "kcontrol/feedback/index.html", + "X-KDE-Keywords": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry", + "X-KDE-Keywords[ar]": "ردود الفعل,التقرير,الاتصال,جمع البيانات,الإحصاءات,الاستخدام,السجل,التسجيل,القياس عن بعد,مشاركة بيانات", + "X-KDE-Keywords[az]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,geri rəy bildirişi,hesabat,əlaqə,verilənlərin toplanması,statistika, istifadə,jurnal,jurnalın yazılması,telemetriya", + "X-KDE-Keywords[ca]": "comentari,informe,contacte,recol·lecció de dades,estadístiques,ús,registre, enregistrament,telemetria", + "X-KDE-Keywords[en_GB]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry", + "X-KDE-Keywords[es]": "realimentación,comentarios y sugerencias,informe,contacto,recopilación de datos,estadísticas,uso,registro,telemetría", + "X-KDE-Keywords[eu]": "erreakzioa,txostena,kontaktua,datu-bilketa,estatistikak,erabilera,egunkaria,erregistratzea,telemetria", + "X-KDE-Keywords[fi]": "palaute,raportti,yhteystiedot,tiedon keruu,tilastot,käyttö,loki,telemetria", + "X-KDE-Keywords[fr]": "Retour utilisateur, rapport, contact, collecte de données, statistiques, utilisation, journal, connexion, télémétrie", + "X-KDE-Keywords[hi]": "प्रतिक्रिया, रिपोर्ट, संपर्क, डेटा संग्रह, आंकड़े, उपयोग, लॉग, लॉगिंग,टेलीमेटरी", + "X-KDE-Keywords[hu]": "visszajelzés,jelentés,kapcsolat,adatgyűjtés,statisztika,használat,napló,naplózás,telemetria", + "X-KDE-Keywords[ia]": "evalutation,reporto,contacto,collection de datos,statistica,uso,registro, registrar,telemetria", + "X-KDE-Keywords[it]": "segnalazione,report,contatto,raccolta dati,statistiche,utilizzo,registro,registrazione,telemetria", + "X-KDE-Keywords[ko]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,피드백,보고서,연락,데이터 수집,통계,사용량,사용 통계,로그,정보 수집", + "X-KDE-Keywords[nl]": "terugkoppeling,rapport,contactpersoon,gegevensverzameling,statistieken,gebruik,log,loggen,telemetrie", + "X-KDE-Keywords[nn]": "tilbakemelding,respons,rapport,respons,kontakt,datainnsamling,statistikk,bruk,logg,logging,telemetri", + "X-KDE-Keywords[pl]": "informacja zwrotna,zgłoszenie,kontakt,zbiór danych,statystyka,użycie,dziennik,rejestrowanie,telemetria", + "X-KDE-Keywords[pt]": "reacções,relatório,contacto,recolha de dados,estatísticas,utilização,registo,telemetria", + "X-KDE-Keywords[pt_BR]": "comentário,relatório,contato,coleção de dados,estatísticas,uso,log,registro,telemetria", + "X-KDE-Keywords[ru]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,обратная связь,отчёт,контакт,сбор данных,статистика,использование,журнал,журналирование,телеметрия", + "X-KDE-Keywords[sk]": "spätná väzba,hlásenie,kontakt,zber údajov,štatistiky,používanie,protokol,zaznamenávanie,telemetria", + "X-KDE-Keywords[sl]": "povratni odziv,poročilo,zbiranje podatkov,statistika,uporaba,dnevnik,beleženje,telemetrija", + "X-KDE-Keywords[sv]": "återkoppling,rapportera,kontakta,datainsamling,statistik,användning,logg,loggning,telemetri", + "X-KDE-Keywords[ta]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry, கருத்து, பின்னூட்டம், தொடர்பு, புள்ளிவிவரங்கள், தரவு, பயன்பாட்டு, பதிவு, சேகரிப்பு", + "X-KDE-Keywords[uk]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,відгук,звіт,зв'язок,контакт,збирання даних,статистика,користування,журнал,журналювання,телеметрія", + "X-KDE-Keywords[vi]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,phản hồi,báo cáo,liên hệ,thu thập dữ liệu,thống kê,sử dụng,nhật kí,ghi nhật kí,viễn trắc", + "X-KDE-Keywords[x-test]": "xxfeedbackxx,xxreportxx,xxcontactxx,xxdata collectionxx,xxstatisticsxx,xxusagexx,xxlogxx,xxloggingxx,xxtelemetryxx", + "X-KDE-Keywords[zh_CN]": "feedback,report,contact,data collection,statistics,usage,log,logging,telemetry,反馈,回馈,反映,报告,联系,数据收集,数据统计,统计数据,使用数据,用量,使用率,使用水平,日志,日志记录,上报,遥测", + "X-KDE-System-Settings-Parent-Category": "personalization" +} diff --git a/plasma/workspace/kcms/feedback/package/contents/ui/main.qml b/plasma/workspace/kcms/feedback/package/contents/ui/main.qml new file mode 100644 index 0000000000..19ca85d6c8 --- /dev/null +++ b/plasma/workspace/kcms/feedback/package/contents/ui/main.qml @@ -0,0 +1,184 @@ +/* + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QQC2 +import org.kde.kirigami 2.6 as Kirigami +import org.kde.userfeedback 1.0 as UserFeedback +import org.kde.userfeedback.kcm 1.0 +import org.kde.kcm 1.3 + +SimpleKCM { + id: root + + ConfigModule.buttons: ConfigModule.Default | ConfigModule.Apply + + implicitWidth: Kirigami.Units.gridUnit * 38 + implicitHeight: Kirigami.Units.gridUnit * 35 + + + ColumnLayout { + spacing: 0 + + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + type: Kirigami.MessageType.Information + visible: !form.enabled + text: i18n("User Feedback has been disabled centrally. Please contact your distributor.") + } + + // The system settings window likes to take over + // the cursor with a plain label. The TextEdit + // 'takes priority' over the system settings + // window trying to eat the mouse, allowing + // us to use the HoverHandler boilerplate for + // proper link handling + TextEdit { + Kirigami.FormData.label: i18n("Plasma:") + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.gridUnit + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignHCenter + wrapMode: Text.WordWrap + text: xi18nc("@info", "You can help KDE improve Plasma by contributing information on how you use it, so we can focus on things that matter to you.Contributing this information is optional and entirely anonymous. We never collect your personal data, files you use, websites you visit, or information that could identify you.You can read about our privacy policy here.") + textFormat: TextEdit.RichText + readOnly: true + + color: Kirigami.Theme.textColor + selectedTextColor: Kirigami.Theme.highlightedTextColor + selectionColor: Kirigami.Theme.highlightColor + + onLinkActivated: (url) => Qt.openUrlExternally(url) + + HoverHandler { + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + } + + Kirigami.FormLayout { + id: form + enabled: kcm.feedbackEnabled + QQC2.Slider { + id: statisticsModeSlider + Kirigami.FormData.label: i18n("Plasma:") + readonly property var currentMode: modeOptions[value] + Layout.fillWidth: true + Layout.minimumWidth: Kirigami.Units.gridUnit * 21 + Layout.maximumWidth: Kirigami.Units.gridUnit * 21 + + readonly property var modeOptions: [UserFeedback.Provider.NoTelemetry, UserFeedback.Provider.BasicSystemInformation, UserFeedback.Provider.BasicUsageStatistics, + UserFeedback.Provider.DetailedSystemInformation, UserFeedback.Provider.DetailedUsageStatistics] + from: 0 + to: modeOptions.length - 1 + stepSize: 1 + snapMode: QQC2.Slider.SnapAlways + + function findIndex(array, what, defaultValue) { + for (var v in array) { + if (array[v] == what) + return v; + } + return defaultValue; + } + + value: findIndex(modeOptions, kcm.feedbackSettings.feedbackLevel, 0) + + onMoved: { + kcm.feedbackSettings.feedbackLevel = modeOptions[value] + } + + SettingStateBinding { + configObject: kcm.feedbackSettings + settingName: "feedbackLevel" + extraEnabledConditions: kcm.feedbackEnabled + } + } + + UserFeedback.FeedbackConfigUiController { + id: feedbackController + applicationName: i18n("Plasma") + } + + Kirigami.Heading { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: Kirigami.Units.gridUnit * 21 + wrapMode: Text.WordWrap + level: 3 + text: feedbackController.telemetryName(statisticsModeSlider.currentMode) + } + Item { + Kirigami.FormData.isSection: true + } + QQC2.Label { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: Kirigami.Units.gridUnit * 21 + wrapMode: Text.WordWrap + + text: i18n("The following information will be sent:") + visible: statisticsModeSlider.value != 0 // This is "disabled" + } + ColumnLayout { + Layout.maximumWidth: parent.width * 0.5 + Repeater { + model: kcm.feedbackSources + delegate: QQC2.Label { + visible: modelData.mode <= statisticsModeSlider.currentMode + text: "· " + modelData.description + + MouseArea { + anchors.fill: parent + hoverEnabled: true + QQC2.ToolTip { + width: iconsLayout.implicitWidth + Kirigami.Units.largeSpacing * 2 + height: iconsLayout.implicitHeight + Kirigami.Units.smallSpacing * 2 + visible: parent.containsMouse + RowLayout { + id: iconsLayout + anchors.centerIn: parent + Repeater { + model: modelData.icons + delegate: Kirigami.Icon { + height: Kirigami.Units.gridUnit * 2 + width: Kirigami.Units.gridUnit * 2 + source: modelData + } + } + } + } + } + } + } + } + + Item { + implicitHeight: Kirigami.Units.largeSpacing + Layout.fillWidth: true + } + Repeater { + model: kcm.audits + delegate: Kirigami.LinkButton { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + Kirigami.FormData.label: model.index === 0 ? i18n("View sent data:") : "" + text: modelData.program + onClicked: Qt.openUrlExternally(modelData.audits) + } + } + } + } +} + diff --git a/plasma/workspace/kcms/fonts/CMakeLists.txt b/plasma/workspace/kcms/fonts/CMakeLists.txt new file mode 100644 index 0000000000..7e990f5a53 --- /dev/null +++ b/plasma/workspace/kcms/fonts/CMakeLists.txt @@ -0,0 +1,42 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_fonts\") + +########### next target ############### + +set(kcm_fonts_PART_SRCS + previewrenderengine.cpp + previewimageprovider.cpp + fonts.cpp + fontsaasettings.cpp + kxftconfig.cpp + fontinit.cpp + ../kfontinst/lib/FcEngine.cpp + ../kcms-common.cpp +) + +kcmutils_generate_module_data( + kcm_fonts_PART_SRCS + MODULE_DATA_HEADER fontsdata.h + MODULE_DATA_CLASS_NAME FontsData + SETTINGS_HEADERS fontssettings.h fontsaasettings.h + SETTINGS_CLASSES FontsSettings FontsAASettings +) + +kconfig_add_kcfg_files(kcm_fonts_PART_SRCS fontssettings.kcfgc fontsaasettingsbase.kcfgc GENERATE_MOC) +kcoreaddons_add_plugin(kcm_fonts SOURCES ${kcm_fonts_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + +target_link_libraries(kcm_fonts KF5::I18n KF5::WindowSystem KF5::KCMUtils KF5::QuickAddons KF5::Declarative kfontinst krdb) + +if(X11_FOUND) + target_link_libraries(kcm_fonts Qt::X11Extras X11::X11 X11::Xft XCB::IMAGE) +endif() + +########### install files ############### +install(FILES fontssettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES kcm_fonts.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +kpackage_install_package(package kcm_fonts kcms) + +add_custom_command(TARGET kcm_fonts POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink ../kcms/systemsettings/kcm_fonts.so kcm_fonts_init.so) + +install( FILES ${CMAKE_CURRENT_BINARY_DIR}/kcm_fonts_init.so DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/kcminit) diff --git a/plasma/workspace/kcms/fonts/Messages.sh b/plasma/workspace/kcms/fonts/Messages.sh new file mode 100644 index 0000000000..a4b197d61c --- /dev/null +++ b/plasma/workspace/kcms/fonts/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_fonts.pot diff --git a/plasma/workspace/kcms/fonts/fontinit.cpp b/plasma/workspace/kcms/fonts/fontinit.cpp new file mode 100644 index 0000000000..c06aa120b3 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontinit.cpp @@ -0,0 +1,35 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +extern "C" { +Q_DECL_EXPORT void kcminit() +{ + KConfig cfg(QStringLiteral("kcmfonts")); + KConfigGroup fontsCfg(&cfg, "General"); + + QString fontDpiKey = KWindowSystem::isPlatformWayland() ? QStringLiteral("forceFontDPIWayland") : QStringLiteral("forceFontDPI"); + + const int dpi = fontsCfg.readEntry(fontDpiKey, 0); + if (dpi <= 0) { + return; + } + + const QByteArray input = "Xft.dpi: " + QByteArray::number(dpi); + QProcess p; + p.start(QStringLiteral("xrdb"), {QStringLiteral("-quiet"), QStringLiteral("-merge"), QStringLiteral("-nocpp")}); + p.setProcessChannelMode(QProcess::ForwardedChannels); + p.write(input); + p.closeWriteChannel(); + p.waitForFinished(-1); +} +} diff --git a/plasma/workspace/kcms/fonts/fonts.cpp b/plasma/workspace/kcms/fonts/fonts.cpp new file mode 100644 index 0000000000..5798ea056c --- /dev/null +++ b/plasma/workspace/kcms/fonts/fonts.cpp @@ -0,0 +1,250 @@ +/* + SPDX-FileCopyrightText: 1997 Mark Donohoe + SPDX-FileCopyrightText: 1999 Lars Knoll + SPDX-FileCopyrightText: 2000 Rik Hemsley + SPDX-FileCopyrightText: 2015 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2017 Marco Martin + SPDX-FileCopyrightText: 2019 Benjamin Port + + Ported to kcontrol2: + SPDX-FileCopyrightText: Geert Jansen + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fonts.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../kcms-common_p.h" +#include "krdb.h" +#include "kxftconfig.h" +#include "previewimageprovider.h" + +#include "fontsaasettings.h" +#include "fontssettings.h" + +#include "fontsdata.h" + +/**** DLL Interface ****/ +K_PLUGIN_FACTORY_WITH_JSON(KFontsFactory, "kcm_fonts.json", registerPlugin(); registerPlugin();) + +/**** KFonts ****/ + +KFonts::KFonts(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, metaData, args) + , m_data(new FontsData(this)) + , m_subPixelOptionsModel(new QStandardItemModel(this)) + , m_hintingOptionsModel(new QStandardItemModel(this)) +{ + qmlRegisterAnonymousType("QStandardItemModel",1); + qmlRegisterAnonymousType("FontsSettings",1); + qmlRegisterAnonymousType("FontsAASettings",1); + + setButtons(Apply | Default | Help); + + for (KXftConfig::SubPixel::Type t : + {KXftConfig::SubPixel::None, KXftConfig::SubPixel::Rgb, KXftConfig::SubPixel::Bgr, KXftConfig::SubPixel::Vrgb, KXftConfig::SubPixel::Vbgr}) { + auto item = new QStandardItem(KXftConfig::description(t)); + m_subPixelOptionsModel->appendRow(item); + } + + for (KXftConfig::Hint::Style s : {KXftConfig::Hint::None, KXftConfig::Hint::Slight, KXftConfig::Hint::Medium, KXftConfig::Hint::Full}) { + auto item = new QStandardItem(KXftConfig::description(s)); + m_hintingOptionsModel->appendRow(item); + } + connect(fontsAASettings(), &FontsAASettings::hintingChanged, this, &KFonts::hintingCurrentIndexChanged); + connect(fontsAASettings(), &FontsAASettings::subPixelChanged, this, &KFonts::subPixelCurrentIndexChanged); +} + +KFonts::~KFonts() +{ +} + +FontsSettings *KFonts::fontsSettings() const +{ + return m_data->fontsSettings(); +} + +FontsAASettings *KFonts::fontsAASettings() const +{ + return m_data->fontsAASettings(); +} + +QAbstractItemModel *KFonts::subPixelOptionsModel() const +{ + return m_subPixelOptionsModel; +} + +QAbstractItemModel *KFonts::hintingOptionsModel() const +{ + return m_hintingOptionsModel; +} + +void KFonts::load() +{ + // first load all the settings + ManagedConfigModule::load(); + + // Load preview + // NOTE: This needs to be done AFTER AA settings is loaded + // otherwise AA settings will be reset in process of loading + // previews + engine()->addImageProvider("preview", new PreviewImageProvider(fontsSettings()->font())); + + // KCM expect save state to be false at this point (can be true because if a font setting loaded + // from the config isn't available on the system, font substitution may take place) + setNeedsSave(false); +} + +void KFonts::save() +{ + auto dpiItem = fontsAASettings()->findItem("forceFontDPI"); + auto dpiWaylandItem = fontsAASettings()->findItem("forceFontDPIWayland"); + auto antiAliasingItem = fontsAASettings()->findItem("antiAliasing"); + Q_ASSERT(dpiItem && dpiWaylandItem && antiAliasingItem); + if (dpiItem->isSaveNeeded() || dpiWaylandItem->isSaveNeeded() || antiAliasingItem->isSaveNeeded()) { + Q_EMIT aliasingChangeApplied(); + } + + auto forceFontDPIChanged = dpiItem->isSaveNeeded(); + + ManagedConfigModule::save(); + +#if HAVE_X11 + // if the setting is reset in the module, remove the dpi value, + // otherwise don't explicitly remove it and leave any possible system-wide value + if (fontsAASettings()->forceFontDPI() == 0 && forceFontDPIChanged && !KWindowSystem::isPlatformWayland()) { + QProcess proc; + proc.setProcessChannelMode(QProcess::ForwardedChannels); + proc.start("xrdb", + QStringList() << "-quiet" + << "-remove" + << "-nocpp"); + if (proc.waitForStarted()) { + proc.write(QByteArray("Xft.dpi\n")); + proc.closeWriteChannel(); + proc.waitForFinished(); + } + } + QApplication::processEvents(); +#endif + + // Notify the world about the font changes + if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) { + QDBusMessage message = QDBusMessage::createSignal("/KDEPlatformTheme", "org.kde.KDEPlatformTheme", "refreshFonts"); + QDBusConnection::sessionBus().send(message); + } + + runRdb(KRdbExportXftSettings | KRdbExportGtkTheme); +} + +void KFonts::adjustFont(const QFont &font, const QString &category) +{ + QFont selFont = font; + int ret = KFontChooserDialog::getFont(selFont, KFontChooser::NoDisplayFlags); + + if (ret == QDialog::Accepted) { + if (category == QLatin1String("font")) { + fontsSettings()->setFont(selFont); + } else if (category == QLatin1String("menuFont")) { + fontsSettings()->setMenuFont(selFont); + } else if (category == QLatin1String("toolBarFont")) { + fontsSettings()->setToolBarFont(selFont); + } else if (category == QLatin1String("activeFont")) { + fontsSettings()->setActiveFont(selFont); + } else if (category == QLatin1String("smallestReadableFont")) { + fontsSettings()->setSmallestReadableFont(selFont); + } else if (category == QLatin1String("fixed")) { + fontsSettings()->setFixed(selFont); + } + } + Q_EMIT fontsHaveChanged(); +} + +void KFonts::adjustAllFonts() +{ + QFont font = fontsSettings()->font(); + KFontChooser::FontDiffFlags fontDiffFlags; + int ret = KFontChooserDialog::getFontDiff(font, fontDiffFlags, KFontChooser::NoDisplayFlags); + + if (ret == QDialog::Accepted && fontDiffFlags) { + fontsSettings()->setFont(applyFontDiff(fontsSettings()->font(), font, fontDiffFlags)); + fontsSettings()->setMenuFont(applyFontDiff(fontsSettings()->menuFont(), font, fontDiffFlags)); + fontsSettings()->setToolBarFont(applyFontDiff(fontsSettings()->toolBarFont(), font, fontDiffFlags)); + fontsSettings()->setActiveFont(applyFontDiff(fontsSettings()->activeFont(), font, fontDiffFlags)); + + QFont smallestFont = font; + // Make the small font 2 points smaller than the general font, but only + // if the general font is 9pt or higher or else the small font would be + // borderline unreadable. Assume that if the user is making the font + // tiny, they want a tiny font everywhere. + const int generalFontPointSize = font.pointSize(); + if (generalFontPointSize >= 9) { + smallestFont.setPointSize(generalFontPointSize - 2); + } + fontsSettings()->setSmallestReadableFont(applyFontDiff(fontsSettings()->smallestReadableFont(), smallestFont, fontDiffFlags)); + + const QFont adjustedFont = applyFontDiff(fontsSettings()->fixed(), font, fontDiffFlags); + if (QFontInfo(adjustedFont).fixedPitch()) { + fontsSettings()->setFixed(adjustedFont); + } + } +} + +QFont KFonts::applyFontDiff(const QFont &fnt, const QFont &newFont, int fontDiffFlags) +{ + QFont font(fnt); + + if (fontDiffFlags & KFontChooser::FontDiffSize) { + font.setPointSizeF(newFont.pointSizeF()); + } + if ((fontDiffFlags & KFontChooser::FontDiffFamily)) { + font.setFamily(newFont.family()); + } + if (fontDiffFlags & KFontChooser::FontDiffStyle) { + font.setWeight(newFont.weight()); + font.setStyle(newFont.style()); + font.setUnderline(newFont.underline()); + font.setStyleName(newFont.styleName()); + } + + return font; +} + +int KFonts::subPixelCurrentIndex() const +{ + return fontsAASettings()->subPixel() - KXftConfig::SubPixel::None; +} + +void KFonts::setSubPixelCurrentIndex(int idx) +{ + fontsAASettings()->setSubPixel(static_cast(KXftConfig::SubPixel::None + idx)); +} + +int KFonts::hintingCurrentIndex() const +{ + return fontsAASettings()->hinting() - KXftConfig::Hint::None; +} + +void KFonts::setHintingCurrentIndex(int idx) +{ + fontsAASettings()->setHinting(static_cast(KXftConfig::Hint::None + idx)); +} + +#include "fonts.moc" diff --git a/plasma/workspace/kcms/fonts/fonts.h b/plasma/workspace/kcms/fonts/fonts.h new file mode 100644 index 0000000000..7a2956f520 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fonts.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 1997 Mark Donohoe + SPDX-FileCopyrightText: 1999 Lars Knoll + SPDX-FileCopyrightText: 2000 Rik Hemsley + SPDX-FileCopyrightText: 2015 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2017 Marco Martin + SPDX-FileCopyrightText: 2019 Benjamin Port + + Ported to kcontrol2: + SPDX-FileCopyrightText: Geert Jansen + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "fontsaasettings.h" +#include "fontssettings.h" + +class FontsData; + +/** + * The Desktop/fonts tab in kcontrol. + */ +class KFonts : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(FontsSettings *fontsSettings READ fontsSettings CONSTANT) + Q_PROPERTY(FontsAASettings *fontsAASettings READ fontsAASettings CONSTANT) + Q_PROPERTY(QAbstractItemModel *subPixelOptionsModel READ subPixelOptionsModel CONSTANT) + Q_PROPERTY(int subPixelCurrentIndex READ subPixelCurrentIndex WRITE setSubPixelCurrentIndex NOTIFY subPixelCurrentIndexChanged) + Q_PROPERTY(QAbstractItemModel *hintingOptionsModel READ hintingOptionsModel CONSTANT) + Q_PROPERTY(int hintingCurrentIndex READ hintingCurrentIndex WRITE setHintingCurrentIndex NOTIFY hintingCurrentIndexChanged) + +public: + KFonts(QObject *parent, const KPluginMetaData &metaData, const QVariantList &); + ~KFonts() override; + + FontsSettings *fontsSettings() const; + FontsAASettings *fontsAASettings() const; + + int subPixelCurrentIndex() const; + void setHintingCurrentIndex(int idx); + int hintingCurrentIndex() const; + void setSubPixelCurrentIndex(int idx); + QAbstractItemModel *subPixelOptionsModel() const; + QAbstractItemModel *hintingOptionsModel() const; + +public Q_SLOTS: + void load() override; + void save() override; + Q_INVOKABLE void adjustAllFonts(); + Q_INVOKABLE void adjustFont(const QFont &font, const QString &category); + +Q_SIGNALS: + void fontsHaveChanged(); + void hintingCurrentIndexChanged(); + void subPixelCurrentIndexChanged(); + void aliasingChangeApplied(); + void fontDpiSettingsChanged(); + +private: + QFont applyFontDiff(const QFont &fnt, const QFont &newFont, int fontDiffFlags); + + FontsData *m_data; + QStandardItemModel *m_subPixelOptionsModel; + QStandardItemModel *m_hintingOptionsModel; +}; diff --git a/plasma/workspace/kcms/fonts/fontsaasettings.cpp b/plasma/workspace/kcms/fonts/fontsaasettings.cpp new file mode 100644 index 0000000000..a7159b0e63 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontsaasettings.cpp @@ -0,0 +1,386 @@ +/* + SPDX-FileCopyrightText: 2020 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fontsaasettings.h" + +#include +#include + +namespace +{ +bool defaultExclude() +{ + return false; +} + +int defaultExcludeFrom() +{ + return 8; +} + +int defaultExcludeTo() +{ + return 15; +} + +bool defaultAntiAliasing() +{ + return true; +} + +KXftConfig::SubPixel::Type defaultSubPixel() +{ + return KXftConfig::SubPixel::Rgb; +} + +KXftConfig::Hint::Style defaultHinting() +{ + return KXftConfig::Hint::Slight; +} +} + +class FontAASettingsStore : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool exclude READ exclude WRITE setExclude) + Q_PROPERTY(int excludeFrom READ excludeFrom WRITE setExcludeFrom) + Q_PROPERTY(int excludeTo READ excludeTo WRITE setExcludeTo) + Q_PROPERTY(bool antiAliasing READ antiAliasing WRITE setAntiAliasing) + Q_PROPERTY(KXftConfig::SubPixel::Type subPixel READ subPixel WRITE setSubPixel) + Q_PROPERTY(KXftConfig::Hint::Style hinting READ hinting WRITE setHinting) +public: + FontAASettingsStore(FontsAASettings *parent = nullptr) + : QObject(parent) + { + load(); + } + + bool exclude() const + { + return m_exclude; + } + + void setExclude(bool exclude) + { + if (m_exclude != exclude) { + m_exclude = exclude; + } + } + + int excludeFrom() const + { + return m_excludeFrom; + } + + void setExcludeFrom(int excludeFrom) + { + if (m_excludeFrom != excludeFrom) { + m_excludeFrom = excludeFrom; + } + } + + int excludeTo() const + { + return m_excludeTo; + } + + void setExcludeTo(int excludeTo) + { + if (m_excludeTo != excludeTo) { + m_excludeTo = excludeTo; + } + } + + bool isImmutable() const + { + return m_isImmutable; + } + + bool antiAliasing() const + { + return m_antiAliasing; + } + + void setAntiAliasing(bool antiAliasing) + { + if (antiAliasing != m_antiAliasing) { + m_antiAliasingChanged = true; + m_antiAliasing = antiAliasing; + } + } + + KXftConfig::SubPixel::Type subPixel() const + { + return m_subPixel; + } + + void setSubPixel(KXftConfig::SubPixel::Type subPixel) + { + if (m_subPixel != subPixel) { + m_subPixelChanged = true; + m_subPixel = subPixel; + } + } + + KXftConfig::Hint::Style hinting() const + { + return m_hinting; + } + + void setHinting(KXftConfig::Hint::Style hinting) + { + if (m_hinting != hinting) { + m_hintingChanged = true; + m_hinting = hinting; + } + } + + void save() + { + KXftConfig xft; + KXftConfig::AntiAliasing::State aaState = KXftConfig::AntiAliasing::NotSet; + if (m_antiAliasingChanged || xft.antiAliasingHasLocalConfig()) { + aaState = m_antiAliasing ? KXftConfig::AntiAliasing::Enabled : KXftConfig::AntiAliasing::Disabled; + } + xft.setAntiAliasing(aaState); + + if (m_exclude) { + xft.setExcludeRange(m_excludeFrom, m_excludeTo); + } else { + xft.setExcludeRange(0, 0); + } + + if (m_subPixelChanged || xft.subPixelTypeHasLocalConfig()) { + xft.setSubPixelType(m_subPixel); + } else { + xft.setSubPixelType(KXftConfig::SubPixel::NotSet); + } + + if (m_hintingChanged || xft.hintStyleHasLocalConfig()) { + xft.setHintStyle(m_hinting); + } else { + xft.setHintStyle(KXftConfig::Hint::NotSet); + } + + // Write to KConfig to sync with krdb + KSharedConfig::Ptr config = KSharedConfig::openConfig("kdeglobals"); + KConfigGroup grp(config, "General"); + + grp.writeEntry("XftSubPixel", KXftConfig::toStr(m_subPixel)); + + if (aaState == KXftConfig::AntiAliasing::NotSet) { + grp.revertToDefault("XftAntialias"); + } else { + grp.writeEntry("XftAntialias", aaState == KXftConfig::AntiAliasing::Enabled); + } + + QString hs(KXftConfig::toStr(m_hinting)); + if (hs != grp.readEntry("XftHintStyle")) { + if (KXftConfig::Hint::NotSet == m_hinting) { + grp.revertToDefault("XftHintStyle"); + } else { + grp.writeEntry("XftHintStyle", hs); + } + } + + xft.apply(); + + m_subPixelChanged = false; + m_hintingChanged = false; + m_antiAliasingChanged = false; + } + + void load() + { + double from, to; + KXftConfig xft; + + if (xft.getExcludeRange(from, to)) { + setExclude(true); + setExcludeFrom(from); + setExcludeTo(to); + } else { + setExclude(defaultExclude()); + setExcludeFrom(defaultExcludeFrom()); + setExcludeTo(defaultExcludeTo()); + } + + // sub pixel + KXftConfig::SubPixel::Type spType = KXftConfig::SubPixel::NotSet; + xft.getSubPixelType(spType); + // if it is not set, we set it to rgb + if (spType == KXftConfig::SubPixel::NotSet) { + spType = KXftConfig::SubPixel::Rgb; + } + setSubPixel(spType); + + // hinting + KXftConfig::Hint::Style hStyle = KXftConfig::Hint::NotSet; + xft.getHintStyle(hStyle); + // if it is not set, we set it to slight hinting + if (hStyle == KXftConfig::Hint::NotSet) { + hStyle = KXftConfig::Hint::Slight; + } + setHinting(hStyle); + + KSharedConfig::Ptr config = KSharedConfig::openConfig("kdeglobals"); + KConfigGroup cg(config, "General"); + m_isImmutable = cg.isEntryImmutable("XftAntialias"); + + const auto aaState = xft.getAntiAliasing(); + setAntiAliasing(aaState != KXftConfig::AntiAliasing::Disabled); + + m_subPixelChanged = false; + m_hintingChanged = false; + m_antiAliasingChanged = false; + } + +private: + bool m_isImmutable; + bool m_antiAliasing; + bool m_antiAliasingChanged; + KXftConfig::SubPixel::Type m_subPixel; + bool m_subPixelChanged; + KXftConfig::Hint::Style m_hinting; + bool m_hintingChanged; + bool m_exclude; + int m_excludeFrom; + int m_excludeTo; +}; + +FontsAASettings::FontsAASettings(QObject *parent) + : FontsAASettingsBase(parent) + , m_fontAASettingsStore(new FontAASettingsStore(this)) +{ + addItemInternal("exclude", defaultExclude(), &FontsAASettings::excludeChanged); + addItemInternal("excludeFrom", defaultExcludeFrom(), &FontsAASettings::excludeFromChanged); + addItemInternal("excludeTo", defaultExcludeTo(), &FontsAASettings::excludeToChanged); + addItemInternal("antiAliasing", defaultAntiAliasing(), &FontsAASettings::antiAliasingChanged); + addItemInternal("subPixel", defaultSubPixel(), &FontsAASettings::subPixelChanged); + addItemInternal("hinting", defaultHinting(), &FontsAASettings::hintingChanged); + + connect(this, &FontsAASettings::forceFontDPIWaylandChanged, this, &FontsAASettings::dpiChanged); + connect(this, &FontsAASettings::forceFontDPIChanged, this, &FontsAASettings::dpiChanged); +} + +void FontsAASettings::addItemInternal(const QByteArray &propertyName, const QVariant &defaultValue, NotifySignalType notifySignal) +{ + auto item = new KPropertySkeletonItem(m_fontAASettingsStore, propertyName, defaultValue); + addItem(item, propertyName); + item->setNotifyFunction([this, notifySignal] { + Q_EMIT(this->*notifySignal)(); + }); +} + +bool FontsAASettings::exclude() const +{ + return findItem("exclude")->property().toBool(); +} + +void FontsAASettings::setExclude(bool exclude) +{ + findItem("exclude")->setProperty(exclude); +} + +int FontsAASettings::excludeFrom() const +{ + return findItem("excludeFrom")->property().toInt(); +} + +void FontsAASettings::setExcludeFrom(int excludeFrom) +{ + findItem("excludeFrom")->setProperty(excludeFrom); +} + +int FontsAASettings::excludeTo() const +{ + return findItem("excludeTo")->property().toInt(); +} + +void FontsAASettings::setExcludeTo(int excludeTo) +{ + findItem("excludeTo")->setProperty(excludeTo); +} + +bool FontsAASettings::antiAliasing() const +{ + return findItem("antiAliasing")->property().toBool(); +} + +void FontsAASettings::setAntiAliasing(bool enabled) +{ + if (antiAliasing() == enabled) { + return; + } + + findItem("antiAliasing")->setProperty(enabled); + if (!enabled) { + setSubPixel(KXftConfig::SubPixel::None); + } else if (subPixel() == KXftConfig::SubPixel::None) { + setSubPixel(defaultSubPixel()); + } +} + +int FontsAASettings::dpi() const +{ + return KWindowSystem::isPlatformWayland() ? forceFontDPIWayland() : forceFontDPI(); +} + +void FontsAASettings::setDpi(int newDPI) +{ + if (dpi() == newDPI) { + return; + } + + if (KWindowSystem::isPlatformWayland()) { + setForceFontDPIWayland(newDPI); + } else { + setForceFontDPI(newDPI); + } + Q_EMIT dpiChanged(); +} + +KXftConfig::SubPixel::Type FontsAASettings::subPixel() const +{ + return findItem("subPixel")->property().value(); +} + +void FontsAASettings::setSubPixel(KXftConfig::SubPixel::Type type) +{ + if (subPixel() == type) { + return; + } + + findItem("subPixel")->setProperty(type); +} + +KXftConfig::Hint::Style FontsAASettings::hinting() const +{ + return findItem("hinting")->property().value(); +} + +bool FontsAASettings::isAaImmutable() const +{ + return m_fontAASettingsStore->isImmutable(); +} + +bool FontsAASettings::excludeStateProxy() const +{ + return false; +} + +void FontsAASettings::setHinting(KXftConfig::Hint::Style hinting) +{ + findItem("hinting")->setProperty(hinting); +} + +bool FontsAASettings::usrSave() +{ + m_fontAASettingsStore->save(); + return FontsAASettingsBase::usrSave(); +} + +#include "fontsaasettings.moc" diff --git a/plasma/workspace/kcms/fonts/fontsaasettings.h b/plasma/workspace/kcms/fonts/fontsaasettings.h new file mode 100644 index 0000000000..7d5ea15935 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontsaasettings.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2020 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "fontsaasettingsbase.h" +#include "kxftconfig.h" + +class FontAASettingsStore; + +class FontsAASettings : public FontsAASettingsBase +{ + Q_OBJECT + + Q_PROPERTY(bool exclude READ exclude WRITE setExclude NOTIFY excludeChanged) + Q_PROPERTY(int excludeFrom READ excludeFrom WRITE setExcludeFrom NOTIFY excludeFromChanged) + Q_PROPERTY(int excludeTo READ excludeTo WRITE setExcludeTo NOTIFY excludeToChanged) + Q_PROPERTY(bool antiAliasing READ antiAliasing WRITE setAntiAliasing NOTIFY antiAliasingChanged) + Q_PROPERTY(int dpi READ dpi WRITE setDpi NOTIFY dpiChanged) + Q_PROPERTY(KXftConfig::SubPixel::Type subPixel READ subPixel WRITE setSubPixel NOTIFY subPixelChanged) + Q_PROPERTY(KXftConfig::Hint::Style hinting READ hinting WRITE setHinting NOTIFY hintingChanged) + Q_PROPERTY(bool isAaImmutable READ isAaImmutable CONSTANT) + Q_PROPERTY(bool excludeStateProxy READ excludeStateProxy NOTIFY excludeStateProxyChanged) + +public: + FontsAASettings(QObject *parent = nullptr); + + bool exclude() const; + int excludeFrom() const; + int excludeTo() const; + bool antiAliasing() const; + int dpi() const; + KXftConfig::SubPixel::Type subPixel() const; + KXftConfig::Hint::Style hinting() const; + bool isAaImmutable() const; + bool excludeStateProxy() const; + + void setExclude(bool exclude); + void setExcludeFrom(int excludeFrom); + void setExcludeTo(int excludeTo); + void setAntiAliasing(bool enabled); + void setDpi(int dpi); + void setSubPixel(KXftConfig::SubPixel::Type type); + void setHinting(KXftConfig::Hint::Style hinting); + +Q_SIGNALS: + void excludeChanged(); + void excludeFromChanged(); + void excludeToChanged(); + void antiAliasingChanged(); + void dpiChanged(); + void subPixelChanged(); + void hintingChanged(); + void aliasingChangeApplied(); + void excludeStateProxyChanged(); + +private: + FontAASettingsStore *m_fontAASettingsStore; + bool m_isAaImmutable = false; + bool usrSave() override; + + using NotifySignalType = void (FontsAASettings::*)(); + void addItemInternal(const QByteArray &propertyName, const QVariant &defaultValue, NotifySignalType notifySignal); +}; diff --git a/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfg b/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfg new file mode 100644 index 0000000000..7719538850 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfg @@ -0,0 +1,17 @@ + + + + + + + 0 + + + + 0 + + + diff --git a/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfgc b/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfgc new file mode 100644 index 0000000000..d0784f80ba --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontsaasettingsbase.kcfgc @@ -0,0 +1,6 @@ +File=fontsaasettingsbase.kcfg +ClassName=FontsAASettingsBase +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/plasma/workspace/kcms/fonts/fontssettings.kcfg b/plasma/workspace/kcms/fonts/fontssettings.kcfg new file mode 100644 index 0000000000..652e76b2a3 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontssettings.kcfg @@ -0,0 +1,83 @@ + + + + + + + + #ifdef Q_OS_MAC + QFont generalFont = QFont("Lucida Grande", 13); + #else + QFont generalFont = QFont("Noto Sans", 10); + generalFont.setStyleName("Regular"); + #endif + + generalFont + + + + + #ifdef Q_OS_MAC + QFont fixedWidthFont = QFont("Monaco", 10); + #else + QFont fixedWidthFont = QFont("Hack", 10); + fixedWidthFont.setStyleName("Regular"); + #endif + + fixedWidthFont + + + + + #ifdef Q_OS_MAC + QFont smallFont = QFont("Lucida Grande", 9); + #else + QFont smallFont = QFont("Noto Sans", 8); + smallFont.setStyleName("Regular"); + #endif + + smallFont + + + + + #ifdef Q_OS_MAC + QFont toolBarFont = QFont("Lucida Grande", 11); + #else + QFont toolBarFont = QFont("Noto Sans", 10); + toolBarFont.setStyleName("Regular"); + #endif + + toolBarFont + + + + + #ifdef Q_OS_MAC + QFont menuFont = QFont("Lucida Grande", 13); + #else + QFont menuFont = QFont("Noto Sans", 10); + menuFont.setStyleName("Regular"); + #endif + + menuFont + + + + + + + #ifdef Q_OS_MAC + QFont windowTitleFont = QFont("Lucida Grande", 14); + #else + QFont windowTitleFont = QFont("Noto Sans", 10); + windowTitleFont.setStyleName("Regular"); + #endif + + windowTitleFont + + + diff --git a/plasma/workspace/kcms/fonts/fontssettings.kcfgc b/plasma/workspace/kcms/fonts/fontssettings.kcfgc new file mode 100644 index 0000000000..20f7267771 --- /dev/null +++ b/plasma/workspace/kcms/fonts/fontssettings.kcfgc @@ -0,0 +1,7 @@ +File=fontssettings.kcfg +ClassName=FontsSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=font diff --git a/plasma/workspace/kcms/fonts/kcm_fonts.desktop b/plasma/workspace/kcms/fonts/kcm_fonts.desktop new file mode 100644 index 0000000000..040f2cf45d --- /dev/null +++ b/plasma/workspace/kcms/fonts/kcm_fonts.desktop @@ -0,0 +1,47 @@ +[Desktop Entry] +Icon=preferences-desktop-font +Type=Application +Exec=systemsettings kcm_fonts +NoDisplay=true + +Name=Fonts +Name[ar]=الخطوط +Name[az]=Şriftlər +Name[ca]=Tipus de lletra +Name[cs]=Písma +Name[da]=Skrifttyper +Name[de]=Schriftarten +Name[en_GB]=Fonts +Name[es]=Tipos de letra +Name[et]=Fondid +Name[eu]=Letra-tipoak +Name[fi]=Fontit +Name[fr]=Polices +Name[hi]=फ़ॉन्ट्स +Name[hsb]=Pisma +Name[hu]=Betűkészletek +Name[ia]=Fontes +Name[id]=Font +Name[it]=Caratteri +Name[ja]=フォント +Name[ko]=글꼴 +Name[lt]=Šriftai +Name[ml]=ഫോണ്ടുകൾ +Name[nl]=Lettertypen +Name[nn]=Skrifter +Name[pa]=ਫੋਂਟ +Name[pl]=Czcionki +Name[pt]=Tipos de Letra +Name[pt_BR]=Fontes +Name[ro]=Fonturi +Name[ru]=Шрифты +Name[sk]=Písma +Name[sl]=Pisave +Name[sv]=Teckensnitt +Name[ta]=எழுத்துருக்கள் +Name[tg]=Ҳуруф +Name[tr]=Yazıtipleri +Name[uk]=Шрифти +Name[vi]=Phông chữ +Name[x-test]=xxFontsxx +Name[zh_CN]=字体 diff --git a/plasma/workspace/kcms/fonts/kcm_fonts.json b/plasma/workspace/kcms/fonts/kcm_fonts.json new file mode 100644 index 0000000000..20d2ad5c3c --- /dev/null +++ b/plasma/workspace/kcms/fonts/kcm_fonts.json @@ -0,0 +1,117 @@ +{ + "KPlugin": { + "Description": "Configure user interface fonts", + "Description[ar]": "اضبط خطوط واجهة المستخدم", + "Description[az]": "İstifadəçi interfeysi şriftlərini tənzimləmək", + "Description[ca]": "Configura els tipus de lletra de la interfície d'usuari", + "Description[cs]": "Nastavte písmo uživatelského rozhraní", + "Description[de]": "Schriften der Benutzeroberfläche einrichten", + "Description[en_GB]": "Configure user interface fonts", + "Description[es]": "Configurar los tipos de letra de la interfaz de usuario", + "Description[eu]": "Konfiguratu erabiltzaile-interfazeko letra-tipoak", + "Description[fi]": "Käyttöliittymäfonttiasetukset", + "Description[fr]": "Configurer les polices de caractères pour l'interface utilisateur", + "Description[hu]": "A felhasználói betűkészleteinek beállításai", + "Description[ia]": "Configura Fontes de interfacie de usator", + "Description[it]": "Configura i caratteri dell'interfaccia utente", + "Description[ko]": "사용자 인터페이스 글꼴 설정", + "Description[lt]": "Konfigūruoti naudotojo sąsajos šriftus", + "Description[nl]": "Lettertypen van gebruikersinterface configureren", + "Description[nn]": "Set opp skrifter for brukargrensesnittet", + "Description[pa]": "ਵਰਤੋਂਕਾਰ ਇੰਟਰਫੇਸ ਫ਼ੋਂਟਾਂ ਦੀ ਸੰਰਚਨਾ", + "Description[pl]": "Ustawienia czcionek interfejsu użytkownika", + "Description[pt_BR]": "Configurar as fontes da interface", + "Description[ro]": "Configurează fonturile interfeței cu utilizatorul", + "Description[ru]": "Выбор шрифтов для элементов интерфейса", + "Description[sk]": "Nakonfigurujte písma používateľského rozhrania", + "Description[sl]": "Nastavi pisave uporabniškega vmesnika", + "Description[sv]": "Anpassa teckensnitt för användargränssnitt", + "Description[ta]": "பயனர் இடைமுகப்பு எழுத்துருக்களை அமையுங்கள்", + "Description[tr]": "Kullanıcı arayüzü yazıtiplerini yapılandırın", + "Description[uk]": "Налаштовування шрифтів у інтерфейсі користувача", + "Description[vi]": "Cấu hình phông chữ trong giao diện người dùng", + "Description[x-test]": "xxConfigure user interface fontsxx", + "Description[zh_CN]": "配置用户界面字体", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-font", + "Name": "Fonts", + "Name[ar]": "الخطوط", + "Name[az]": "Şriftlər", + "Name[ca]": "Tipus de lletra", + "Name[cs]": "Písma", + "Name[da]": "Skrifttyper", + "Name[de]": "Schriftarten", + "Name[en_GB]": "Fonts", + "Name[es]": "Tipos de letra", + "Name[et]": "Fondid", + "Name[eu]": "Letra-tipoak", + "Name[fi]": "Fontit", + "Name[fr]": "Polices", + "Name[hi]": "फ़ॉन्ट्स", + "Name[hsb]": "Pisma", + "Name[hu]": "Betűkészletek", + "Name[ia]": "Fontes", + "Name[id]": "Font", + "Name[it]": "Caratteri", + "Name[ja]": "フォント", + "Name[ko]": "글꼴", + "Name[lt]": "Šriftai", + "Name[ml]": "ഫോണ്ടുകൾ", + "Name[nl]": "Lettertypen", + "Name[nn]": "Skrifter", + "Name[pa]": "ਫੋਂਟ", + "Name[pl]": "Czcionki", + "Name[pt]": "Tipos de Letra", + "Name[pt_BR]": "Fontes", + "Name[ro]": "Fonturi", + "Name[ru]": "Шрифты", + "Name[sk]": "Písma", + "Name[sl]": "Pisave", + "Name[sv]": "Teckensnitt", + "Name[ta]": "எழுத்துருக்கள்", + "Name[tg]": "Ҳуруф", + "Name[tr]": "Yazı Tipleri", + "Name[uk]": "Шрифти", + "Name[vi]": "Phông chữ", + "Name[x-test]": "xxFontsxx", + "Name[zh_CN]": "字体" + }, + "X-DocPath": "kcontrol/fonts/index.html", + "X-KDE-Init-Phase": "0", + "X-KDE-Init-Symbol": "kcminit_fonts", + "X-KDE-Keywords": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size", + "X-KDE-Keywords[ar]": "خطوط,حجم خط,أنماط,مجموعات أحرف,مجموعات أحرف,لوحة,لوحة تحكم,أسطح مكتب,مدير ملفات,أشرطة أدوات,قائمة,عنوان نافذة,عنوان,DPI,صقل,خطوط سطح مكتب,خطوط أدوات,أحرف,خطوط عامة,حرف,نص,نص كبير,تغيير الخط,الحجم", + "X-KDE-Keywords[az]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size,şriftlər,şrift ölçüsü,üslublar,tablolar,simvol dəstləri,panel,idarəetmə paneli,iş masaları,Fayl Menecer,Alət panelləri,Menyu,Pəncərə başlığı,Başlıq, DPI,şrift hamarlaması,masaüstü şriftlər,alətlər paneli şriftləri,simvol,ümumi şriftlər,nəşriyyat şriftləri,simvollar,işarə etmək,alt-piksellər,mətn,böyük mətn,şrifti dəyişmək,ölçü", + "X-KDE-Keywords[ca]": "tipus de lletra,mida de tipus de lletra,estils,joc de caràcters,jocs de caràcters,plafó,plafó de control,escriptoris,Gestor de fitxers,Barres d'eines,Menú,Títol de la finestra,Títol,DPI,antialiàsing,tipus de lletra d'escriptori,tipus de lletra de barra d'eines,caràcter,tipus de lletra general,tipografia,tipus,lletres,caràcter,estil de correcció,subpíxel,text,text gran,canvi de tipus de lletra,mida", + "X-KDE-Keywords[cs]": "písma,velikost písma,styly,znakové sady,panel,kontrolní panel,plochy,správce souborů,panely nástrojů,nabídka,název okna,název,DPI,vyhlazování,písma plochy,písma panelů,znak,obecná písma, typografie,typ, znaky,písmeno,hinting,sub-pixel,text, velký text,změna písma,velikost", + "X-KDE-Keywords[en_GB]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size", + "X-KDE-Keywords[es]": "tipos de letra,fuentes,tamaño del tipo de letra,estilos,juegos de caracteres,panel,panel de control,escritorios,gestor de archivos,barras de herramientas,menú,título de la ventana,título,PPP,DPI,suavizado,antialiasing,tipos de letra del escritorio,tipos de letra de la barra de herramientas,carácter,caracteres,tipos de letra generales,tipografía,tipo,letras,carácter,hinting,subpíxel,texto,texto grande,cambiar tipo de letra,tamaño,fuente", + "X-KDE-Keywords[eu]": "letra-tipoak,letra-tamaina,estiloak,karaktere-jokoak,panela,aginte-panela,mahaigainak,fitxategi-kudeatzailea,tresna-barrak,menua,leihoaren titulua,titulua,DPI,anti-aliasing,ertzak-leuntzea,mahaigaineko letra-tipoak,tresna-barrako letra-tipoak,karakterea,letra-tipo orokorrak,tipografia,tipoa,letrak,karakterea,errendatzea doitzea,azpi-pixela,testua,testu handia,letra aldatzea,neurria", + "X-KDE-Keywords[fi]": "fontit,fonttikoko,tyyli,tyylit,merkistö,merkistöt,paneeli,hallintapaneeli,työpöydät,tiedoston hallinta,työkalurivit,valikko,ikkunan otsikko,otsikko,DPI,antialiasointi,työpöydän fontit,työkalurivin fontit,merkki,yleiset fontit,typografia,tyyppi,kirjaimet,merkki,vihjeet,alipikseli,iso teksti,suuri teksti,vaihda fonttia,koko", + "X-KDE-Keywords[fr]": "polices, taille de police, styles, tables de caractères, jeux de caractères, panneau, panneau de contrôle, bureaux, gestionnaire de fichiers, barres d'outils, menu, titre de fenêtre, titre, DPI, résolution, anti crénelage, polices de bureau, polices de barre d'outils, caractère, polices générales, typographie, type, lettres, caractères, astuces, sous-pixel, texte, texte large, modification de polices, taille", + "X-KDE-Keywords[hi]": "फ़ॉन्ट, फ़ॉन्ट आकार, शैलियाँ, वर्ण सेट, वर्ण सेट, पैनल, नियंत्रण कक्ष, डेस्कटॉप, फ़ाइल प्रबंधक, टूलबार, मेनू, विंडो शीर्षक, शीर्षक, डीपीआइ, एंटी-अलियासिंग, डेस्कटॉप फ़ॉन्ट, टूलबार फ़ॉन्ट, वर्ण, सामान्य फ़ॉन्ट, टाइपोग्राफी, प्रकार ,अक्षर,वर्ण, संकेत,उप-पिक्सेल,पाठ,बड़ा पाठ,फ़ॉन्ट,आकार बदलना", + "X-KDE-Keywords[hu]": "betűkészletek,betűméret,stílusok,karakterkészletek,karakterkészletek,panel,vezérlőpanel,asztalok,Fájlkezelő,Eszköztárak,Ablakcím,Cím,DPI,élsimítás,asztali betűkészletek,eszköztár betűkészletek,karakter,általános betűkészletek,tipográfia,gépelés,betűk,karakter,hinting,alpixel,szöveg,nagy szöveg,betűtípus módosítása,méret", + "X-KDE-Keywords[ia]": "fonts,grandor de font,stilos,insimules de characteres,insimules de characteres,pannello,pannello de controlo,scriptorios,Gerente de File,Barra de instrumentos,Menu,Titulo de Fenestra,Titulo,DPI,anti-aliasing,fonts de scriptorio, fonts de barra de titulo, character,fonts general,typographia,typo, litteras,character,insinuar,sub-pixel,texto,texto grande,cambia font,grandor", + "X-KDE-Keywords[it]": "caratteri,dimensione dei caratteri,stili,codifiche,insiemi di caratteri,pannello,pannello di controllo,desktop,gestore dei file,barre degli strumenti,menu,titolo della finestra,titolo,DPI,anti-aliasing,caratteri del desktop,caratteri della barra degli strumenti,carattere,caratteri generali,tipografia,tipo,lettere,carattere,hinting,sub-pixel,testo,testo grande,cambia carattere,dimensione", + "X-KDE-Keywords[ko]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size,글꼴,글꼴 스타일,문자 인코딩,패널,제어판,시스템 설정,데스크톱,바탕 화면,파일 관리자,도구 모음,메뉴,창 제목,제목,앤티에일리어싱,바탕 화면 글꼴,데스크톱 글꼴,도구 모음 글꼴,글자,힌팅,서브픽셀,큰 글꼴,글꼴 변경,글꼴 바꾸기,크기", + "X-KDE-Keywords[nl]": "lettertypes,lettertype,tekengrootte,stijlen,tekensets,paneel,besturingspaneel,bureaubladen,bestandsbeheerder,werkbalken,menu,venstertitel,titel,DPI,anti-aliasing,lettertypen van bureaublad,lettertypen van werkbalk,teken,algemene lettertypes,typografie,type,letter,teken,hinten,subpixel,tekst,grote tekst,lettertype wijzigen,grootte", + "X-KDE-Keywords[nn]": "skrifter,skriftstorleik,skriftstørrelse,stilar,teiknsett,teiknkodingar,panel,kontrollpanel,skrivebord,filhandsamar,verktøylinje,meny,vindaugstittel,tittel,DPI,PPT,kantutjamning,skrivebordsskrifter,verktøylinjeskrifter,generelle skrifter,typografi,bokstavar,teikn,hinting,delpiksel,tekst,stor skrift,større skrift,endra skriftstorleik,storleik,størrelse", + "X-KDE-Keywords[pl]": "czcionki,rozmiar czcionki,style,zestaw znaków,panel,panel sterowania,pulpity,Menadżer plików,Paski narzędzi,Menu,Tytuł okna,Tytuł,DPI,wygładzanie,czcionki pulpitu,czcionki pasków narzędzi,znak,czcionki ogólne,typografia,krój,litery,znak,hinting,podpikselowe,tekst,duży tekst,zmień czcionkę,rozmiar", + "X-KDE-Keywords[pt]": "tipos de letra,tamanho da letra,estilos,codificações,conjuntos de caracteres,painel,painel de controlo,ecrãs,Gestor de Ficheiros,Barras de Ferramentas,Menu,Título da Janela,Título,PPP,suavização,tipos de letra do ecrã,tipos de letra da barra de ferramentas,carácter,tipos de letra gerais,tipografia,escrita,letras,carácter,sugestões,sub-pixel,texto grande,mudar o tipo de letra,tamanho", + "X-KDE-Keywords[pt_BR]": "fontes,tamanho da fonte,estilos,codificações,codificações de caracteres,painel,painel de controle,áreas de trabalho,gerenciador de arquivos,barras de ferramentas,menu,título da janela,título,ppp,dpi,anti-aliasing,fontes da área de trabalho,fontes da barra de ferramentas,caractere,fontes gerais,tipografia,tipo,letras,caracteres,hinting,sub-pixel,texto,texto grande,alterar fonte,tamanho", + "X-KDE-Keywords[ru]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size,шрифты,размер шрифтов,стили,кодировки,панель,панель управления,рабочие столы,диспетчер файлов,панель инструментов,меню,заголовок окна,заголовок,сглаживание,шрифты рабочего стола,шрифты панели инструментов,символы,общие шрифты,типографика,тип,буквы,символ,хинтинг,суб-пискельное,текст,большой текст,изменить шрифт,размер", + "X-KDE-Keywords[sk]": "písma,veľkosť písma,štýly,znakové sady,znakové sady,panel,ovládací panel,pracovné plochy,Správca súborov,Panely nástrojov,Ponuka,Názov okna,Názov,DPI,antialiasing,písma na ploche,Písma panelov nástrojov,znakové,všeobecné písma,typografia,písmo,písmená,znak,hinting,subpixel,text,veľký text,zmena písma,veľkosť", + "X-KDE-Keywords[sl]": "pisave,velikosti pisav,znaki,zbirke znakov,kodne tabele,paneli,nadzorne plošče,namizja,upravljalnik datotek,meni,okno,naslov okna,DPI,glajenje pisav,pisave namizja,pisave orodnih vrstic, znak,splošne pisave,tipografija,vrsta,črke,znaki,namigi,besedilo,veliko besedilo,sprememba črkopisa,velikost", + "X-KDE-Keywords[sv]": "teckensnitt,teckenstorlek,stil,teckenuppsättningar,panel,kontrollpanel,skrivbord,Filhanterare,Verktygsrader,Meny,Fönsternamn,Namn,Punkter/tum,kantutjämning,skrivbordsteckensnitt,verktygsradsteckensnitt,tecken,allmänna teckensnitt,typografi,typ,bokstäver,tecken,tips,delbildpunkt,text,stor text, ändra teckensnitt,storlek", + "X-KDE-Keywords[ta]": "fonts,font size,styles,charsets, character sets,panel,control panel,desktops,FileManager, Toolbars,Menu, Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts, typography,type, letters,character, hinting,sub-pixel, text,big text, change font,size, எழுத்துரு,எழுத்து, அளவு, பெரிய உரை, தட்டச்சு, உரை", + "X-KDE-Keywords[uk]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size,шрифт,шрифти,розмір,розмір шрифту,стиль,стилі,гарнітура,гарнітури,кодування,набір,символ,символи,набір символів,панель,панель керування,стільниця,стільниці,файл,керування,керування файлами,менеджер,панель інструментів,меню,заголовок,заголовок вікна,роздільність,згладжування,шрифти стільниці,шрифти панелі,символ,загальні шрифти,типографія,друк,тип,літери,символ,хінтінг,гінтінґ,субпіксель,текст,великий текст,змінити шрифт,розмір", + "X-KDE-Keywords[vi]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,typography,type,letters,character,hinting,sub-pixel,text,big text,change font,size,phông chữ,cỡ phông chữ,kiểu cách,bộ mã tự,bảng,bảng điều khiển,bàn làm việc,trình quản lí tệp,thanh công cụ,trình đơn,tiêu đề cửa sổ,tiêu đề,khử răng cưa,phông chữ bàn làm việc,phông chữ thanh công cụ,chữ,phông chữ chung,thuật in,kiểu,chữ cái,dẫn phông,hạ điểm ảnh,chữ to,thay đổi phông chữ,kích cỡ", + "X-KDE-Keywords[x-test]": "xxfontsxx,xxfont sizexx,xxstylesxx,xxcharsetsxx,xxcharacter setsxx,xxpanelxx,xxcontrol panelxx,xxdesktopsxx,xxFileManagerxx,xxToolbarsxx,xxMenuxx,xxWindow Titlexx,xxTitlexx,xxDPIxx,xxanti-aliasingxx,xxdesktop fontsxx,xxtoolbar fontsxx,xxcharacterxx,xxgeneral fontsxx,xxtypographyxx,xxtypexx,xxlettersxx,xxcharacterxx,xxhintingxx,xxsub-pixelxx,xxtextxx,xxbig textxx,xxchange fontxx,xxsizexx", + "X-KDE-Keywords[zh_CN]": "fonts,font size,styles,charsets,character sets,panel,control panel,desktops,FileManager,Toolbars,Menu,Window Title,Title,DPI,anti-aliasing,desktop fonts,toolbar fonts,character,general fonts,字体,字体家族,字体族,字体大小,样式,风格,字符集,面板,控制面板,桌面,文件管理器,工具栏,菜单,窗口标题,标题,点每像素,抗锯齿,反锯齿,字体平滑,桌面字体,工具栏字体,字符,字母,符号,常规字体,排版,打字机,字体微调方式,全角字体,次像素渲染方式,大字体,小字体,衬线字体,非衬线字体,更改字体,字体大小", + "X-KDE-System-Settings-Parent-Category": "appearance", + "X-KDE-Weight": 50 +} diff --git a/plasma/workspace/kcms/fonts/kxftconfig.cpp b/plasma/workspace/kcms/fonts/kxftconfig.cpp new file mode 100644 index 0000000000..01edd6eb0b --- /dev/null +++ b/plasma/workspace/kcms/fonts/kxftconfig.cpp @@ -0,0 +1,884 @@ +/* + SPDX-FileCopyrightText: 2002 Craig Drummond + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kxftconfig.h" +#ifdef HAVE_FONTCONFIG + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace std; + +static int point2Pixel(double point) +{ + return (int)(((point * QX11Info::appDpiY()) / 72.0) + 0.5); +} + +static int pixel2Point(double pixel) +{ + return (int)(((pixel * 72.0) / (double)QX11Info::appDpiY()) + 0.5); +} + +static bool equal(double d1, double d2) +{ + return (fabs(d1 - d2) < 0.0001); +} + +static QString dirSyntax(const QString &d) +{ + if (d.isNull()) { + return d; + } + + QString ds(d); + ds.replace(QLatin1String("//"), QLatin1String("/")); + if (!ds.endsWith(QLatin1Char('/'))) { + ds += QLatin1Char('/'); + } + + return ds; +} + +inline bool fExists(const QString &p) +{ + return QFileInfo(p).isFile(); +} + +inline bool dWritable(const QString &p) +{ + QFileInfo info(p); + return info.isDir() && info.isWritable(); +} + +static QString getDir(const QString &path) +{ + QString str(path); + + const int slashPos = str.lastIndexOf(QLatin1Char('/')); + if (slashPos != -1) { + str.truncate(slashPos + 1); + } + + return dirSyntax(str); +} + +static QDateTime getTimeStamp(const QString &item) +{ + return QFileInfo(item).lastModified(); +} + +static QString getEntry(QDomElement element, const char *type, unsigned int numAttributes, ...) +{ + if (numAttributes == uint(element.attributes().length())) { + va_list args; + unsigned int arg; + bool ok = true; + + va_start(args, numAttributes); + + for (arg = 0; arg < numAttributes && ok; ++arg) { + const char *attr = va_arg(args, const char *); + const char *val = va_arg(args, const char *); + + if (!attr || !val || val != element.attribute(attr)) { + ok = false; + } + } + + va_end(args); + + if (ok) { + QDomNode n = element.firstChild(); + + if (!n.isNull()) { + QDomElement e = n.toElement(); + + if (!e.isNull() && type == e.tagName()) { + return e.text(); + } + } + } + } + + return QString(); +} + +static KXftConfig::SubPixel::Type strToType(const char *str) +{ + if (0 == strcmp(str, "rgb")) { + return KXftConfig::SubPixel::Rgb; + } else if (0 == strcmp(str, "bgr")) { + return KXftConfig::SubPixel::Bgr; + } else if (0 == strcmp(str, "vrgb")) { + return KXftConfig::SubPixel::Vrgb; + } else if (0 == strcmp(str, "vbgr")) { + return KXftConfig::SubPixel::Vbgr; + } else if (0 == strcmp(str, "none")) { + return KXftConfig::SubPixel::None; + } else { + return KXftConfig::SubPixel::NotSet; + } +} + +static KXftConfig::Hint::Style strToStyle(const char *str) +{ + if (0 == strcmp(str, "hintslight")) { + return KXftConfig::Hint::Slight; + } else if (0 == strcmp(str, "hintmedium")) { + return KXftConfig::Hint::Medium; + } else if (0 == strcmp(str, "hintfull")) { + return KXftConfig::Hint::Full; + } else { + return KXftConfig::Hint::None; + } +} + +KXftConfig::KXftConfig() + : m_doc("fontconfig") + , m_file(getConfigFile()) +{ + qDebug() << "Using fontconfig file:" << m_file; + reset(); +} + +KXftConfig::~KXftConfig() +{ +} + +// +// Obtain location of config file to use. +QString KXftConfig::getConfigFile() +{ + FcStrList *list = FcConfigGetConfigFiles(FcConfigGetCurrent()); + QStringList localFiles; + FcChar8 *file; + QString home(dirSyntax(QDir::homePath())); + + m_globalFiles.clear(); + + while ((file = FcStrListNext(list))) { + QString f((const char *)file); + + if (fExists(f) && 0 == f.indexOf(home)) { + localFiles.append(f); + } else { + m_globalFiles.append(f); + } + } + FcStrListDone(list); + + // + // Go through list of localFiles, looking for the preferred one... + if (!localFiles.isEmpty()) { + for (const QString &file : qAsConst(localFiles)) { + if (file.endsWith(QLatin1String("/fonts.conf")) || file.endsWith(QLatin1String("/.fonts.conf"))) { + return file; + } + } + return localFiles.front(); // Just return the 1st one... + } else { // Hmmm... no known localFiles? + if (FcGetVersion() >= 21000) { + const QString targetPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + QLatin1String("fontconfig")); + QDir target(targetPath); + if (!target.exists()) { + target.mkpath(targetPath); + } + return targetPath + QLatin1String("/fonts.conf"); + } else { + return home + QLatin1String("/.fonts.conf"); + } + } +} + +bool KXftConfig::reset() +{ + m_madeChanges = false; + m_hint.reset(); + m_hinting.reset(); + m_excludeRange.reset(); + m_excludePixelRange.reset(); + m_subPixel.reset(); + m_antiAliasing.reset(); + m_antiAliasingHasLocalConfig = false; + m_subPixelHasLocalConfig = false; + m_hintHasLocalConfig = false; + + bool ok = false; + std::for_each(m_globalFiles.cbegin(), m_globalFiles.cend(), [this, &ok](const QString &file) { + ok |= parseConfigFile(file); + }); + + AntiAliasing globalAntialiasing; + globalAntialiasing.state = m_antiAliasing.state; + SubPixel globalSubPixel; + globalSubPixel.type = m_subPixel.type; + Hint globalHint; + globalHint.style = m_hint.style; + Exclude globalExcludeRange; + globalExcludeRange.from = m_excludeRange.from; + globalExcludeRange.to = m_excludePixelRange.to; + Exclude globalExcludePixelRange; + globalExcludePixelRange.from = m_excludePixelRange.from; + globalExcludePixelRange.to = m_excludePixelRange.to; + Hinting globalHinting; + globalHinting.set = m_hinting.set; + + m_antiAliasing.reset(); + m_subPixel.reset(); + m_hint.reset(); + m_hinting.reset(); + m_excludeRange.reset(); + m_excludePixelRange.reset(); + + ok |= parseConfigFile(m_file); + + if (m_antiAliasing.node.isNull()) { + m_antiAliasing = globalAntialiasing; + } else { + m_antiAliasingHasLocalConfig = true; + } + + if (m_subPixel.node.isNull()) { + m_subPixel = globalSubPixel; + } else { + m_subPixelHasLocalConfig = true; + } + + if (m_hint.node.isNull()) { + m_hint = globalHint; + } else { + m_hintHasLocalConfig = true; + } + + if (m_hinting.node.isNull()) { + m_hinting = globalHinting; + } + if (m_excludeRange.node.isNull()) { + m_excludeRange = globalExcludeRange; + } + if (m_excludePixelRange.node.isNull()) { + m_excludePixelRange = globalExcludePixelRange; + } + + return ok; +} + +bool KXftConfig::apply() +{ + bool ok = true; + + if (m_madeChanges) { + // + // Check if file has been written since we last read it. If it has, then re-read and add any + // of our changes... + if (fExists(m_file) && getTimeStamp(m_file) != m_time) { + KXftConfig newConfig; + + newConfig.setExcludeRange(m_excludeRange.from, m_excludeRange.to); + newConfig.setSubPixelType(m_subPixel.type); + newConfig.setHintStyle(m_hint.style); + newConfig.setAntiAliasing(m_antiAliasing.state); + + ok = newConfig.changed() ? newConfig.apply() : true; + if (ok) { + reset(); + } else { + m_time = getTimeStamp(m_file); + } + } else { + // Ensure these are always equal... + m_excludePixelRange.from = (int)point2Pixel(m_excludeRange.from); + m_excludePixelRange.to = (int)point2Pixel(m_excludeRange.to); + + FcAtomic *atomic = FcAtomicCreate((const unsigned char *)(QFile::encodeName(m_file).data())); + + ok = false; + if (atomic) { + if (FcAtomicLock(atomic)) { + FILE *f = fopen((char *)FcAtomicNewFile(atomic), "w"); + + if (f) { + applySubPixelType(); + applyHintStyle(); + applyAntiAliasing(); + applyExcludeRange(false); + applyExcludeRange(true); + + // + // Check document syntax... + static const char qtXmlHeader[] = ""; + static const char xmlHeader[] = ""; + static const char qtDocTypeLine[] = ""; + static const char docTypeLine[] = + ""; + + QString str(m_doc.toString()); + int idx; + + if (0 != str.indexOf("= 0 || to >= 0) && foundFalse) { + m_excludeRange.from = from < to ? from : to; + m_excludeRange.to = from < to ? to : from; + m_excludeRange.node = n; + } else if ((pixelFrom >= 0 || pixelTo >= 0) && foundFalse) { + m_excludePixelRange.from = pixelFrom < pixelTo ? pixelFrom : pixelTo; + m_excludePixelRange.to = pixelFrom < pixelTo ? pixelTo : pixelFrom; + m_excludePixelRange.node = n; + } + } + break; + default: + break; + } + } + } + n = n.nextSibling(); + } +} + +void KXftConfig::applySubPixelType() +{ + if (SubPixel::NotSet == m_subPixel.type) { + if (!m_subPixel.node.isNull()) { + m_doc.documentElement().removeChild(m_subPixel.node); + m_subPixel.node.clear(); + } + } else { + QDomElement matchNode = m_doc.createElement("match"); + QDomElement typeNode = m_doc.createElement("const"); + QDomElement editNode = m_doc.createElement("edit"); + QDomText typeText = m_doc.createTextNode(toStr(m_subPixel.type)); + + matchNode.setAttribute("target", "font"); + editNode.setAttribute("mode", "assign"); + editNode.setAttribute("name", "rgba"); + editNode.appendChild(typeNode); + typeNode.appendChild(typeText); + matchNode.appendChild(editNode); + if (m_subPixel.node.isNull()) { + m_doc.documentElement().appendChild(matchNode); + } else { + m_doc.documentElement().replaceChild(matchNode, m_subPixel.node); + } + m_subPixel.node = matchNode; + } +} + +void KXftConfig::applyHintStyle() +{ + applyHinting(); + + if (Hint::NotSet == m_hint.style) { + if (!m_hint.node.isNull()) { + m_doc.documentElement().removeChild(m_hint.node); + m_hint.node.clear(); + } + if (!m_hinting.node.isNull()) { + m_doc.documentElement().removeChild(m_hinting.node); + m_hinting.node.clear(); + } + } else { + QDomElement matchNode = m_doc.createElement("match"), typeNode = m_doc.createElement("const"), editNode = m_doc.createElement("edit"); + QDomText typeText = m_doc.createTextNode(toStr(m_hint.style)); + + matchNode.setAttribute("target", "font"); + editNode.setAttribute("mode", "assign"); + editNode.setAttribute("name", "hintstyle"); + editNode.appendChild(typeNode); + typeNode.appendChild(typeText); + matchNode.appendChild(editNode); + if (m_hint.node.isNull()) { + m_doc.documentElement().appendChild(matchNode); + } else { + m_doc.documentElement().replaceChild(matchNode, m_hint.node); + } + m_hint.node = matchNode; + } +} + +void KXftConfig::applyHinting() +{ + QDomElement matchNode = m_doc.createElement("match"), typeNode = m_doc.createElement("bool"), editNode = m_doc.createElement("edit"); + QDomText typeText = m_doc.createTextNode(m_hinting.set ? "true" : "false"); + + matchNode.setAttribute("target", "font"); + editNode.setAttribute("mode", "assign"); + editNode.setAttribute("name", "hinting"); + editNode.appendChild(typeNode); + typeNode.appendChild(typeText); + matchNode.appendChild(editNode); + if (m_hinting.node.isNull()) { + m_doc.documentElement().appendChild(matchNode); + } else { + m_doc.documentElement().replaceChild(matchNode, m_hinting.node); + } + m_hinting.node = matchNode; +} + +void KXftConfig::applyExcludeRange(bool pixel) +{ + Exclude &range = pixel ? m_excludePixelRange : m_excludeRange; + + if (equal(range.from, 0) && equal(range.to, 0)) { + if (!range.node.isNull()) { + m_doc.documentElement().removeChild(range.node); + range.node.clear(); + } + } else { + QString fromString, toString; + + fromString.setNum(range.from); + toString.setNum(range.to); + + QDomElement matchNode = m_doc.createElement("match"), fromTestNode = m_doc.createElement("test"), fromNode = m_doc.createElement("double"), + toTestNode = m_doc.createElement("test"), toNode = m_doc.createElement("double"), editNode = m_doc.createElement("edit"), + boolNode = m_doc.createElement("bool"); + QDomText fromText = m_doc.createTextNode(fromString), toText = m_doc.createTextNode(toString), boolText = m_doc.createTextNode("false"); + + matchNode.setAttribute("target", "font"); // CPD: Is target "font" or "pattern" ???? + fromTestNode.setAttribute("qual", "any"); + fromTestNode.setAttribute("name", pixel ? "pixelsize" : "size"); + fromTestNode.setAttribute("compare", "more_eq"); + fromTestNode.appendChild(fromNode); + fromNode.appendChild(fromText); + toTestNode.setAttribute("qual", "any"); + toTestNode.setAttribute("name", pixel ? "pixelsize" : "size"); + toTestNode.setAttribute("compare", "less_eq"); + toTestNode.appendChild(toNode); + toNode.appendChild(toText); + editNode.setAttribute("mode", "assign"); + editNode.setAttribute("name", "antialias"); + editNode.appendChild(boolNode); + boolNode.appendChild(boolText); + matchNode.appendChild(fromTestNode); + matchNode.appendChild(toTestNode); + matchNode.appendChild(editNode); + + if (!m_antiAliasing.node.isNull()) { + m_doc.documentElement().removeChild(range.node); + } + if (range.node.isNull()) { + m_doc.documentElement().appendChild(matchNode); + } else { + m_doc.documentElement().replaceChild(matchNode, range.node); + } + range.node = matchNode; + } +} + +bool KXftConfig::antiAliasingHasLocalConfig() const +{ + return m_antiAliasingHasLocalConfig; +} + +KXftConfig::AntiAliasing::State KXftConfig::getAntiAliasing() const +{ + return m_antiAliasing.state; +} + +void KXftConfig::setAntiAliasing(AntiAliasing::State state) +{ + if (state != m_antiAliasing.state) { + m_antiAliasing.state = state; + m_madeChanges = true; + } +} + +void KXftConfig::applyAntiAliasing() +{ + if (AntiAliasing::NotSet == m_antiAliasing.state) { + if (!m_antiAliasing.node.isNull()) { + m_doc.documentElement().removeChild(m_antiAliasing.node); + m_antiAliasing.node.clear(); + } + } else { + QDomElement matchNode = m_doc.createElement("match"); + QDomElement typeNode = m_doc.createElement("bool"); + QDomElement editNode = m_doc.createElement("edit"); + QDomText typeText = m_doc.createTextNode(m_antiAliasing.state == AntiAliasing::Enabled ? "true" : "false"); + + matchNode.setAttribute("target", "font"); + editNode.setAttribute("mode", "assign"); + editNode.setAttribute("name", "antialias"); + editNode.appendChild(typeNode); + typeNode.appendChild(typeText); + matchNode.appendChild(editNode); + if (!m_antiAliasing.node.isNull()) { + m_doc.documentElement().removeChild(m_antiAliasing.node); + } + m_doc.documentElement().appendChild(matchNode); + m_antiAliasing.node = matchNode; + } +} + +// KXftConfig only parses one config file, user's .fonts.conf usually. +// If that one doesn't exist, then KXftConfig doesn't know if antialiasing +// is enabled or not. So try to find out the default value from the default font. +// Maybe there's a better way *shrug*. +bool KXftConfig::aliasingEnabled() +{ + FcPattern *pattern = FcPatternCreate(); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + FcResult result; + FcPattern *f = FcFontMatch(nullptr, pattern, &result); + FcBool antialiased = FcTrue; + FcPatternGetBool(f, FC_ANTIALIAS, 0, &antialiased); + FcPatternDestroy(f); + FcPatternDestroy(pattern); + return antialiased == FcTrue; +} + +#endif diff --git a/plasma/workspace/kcms/fonts/kxftconfig.h b/plasma/workspace/kcms/fonts/kxftconfig.h new file mode 100644 index 0000000000..aba2fee137 --- /dev/null +++ b/plasma/workspace/kcms/fonts/kxftconfig.h @@ -0,0 +1,232 @@ +#pragma once + +/* + SPDX-FileCopyrightText: 2002 Craig Drummond + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#ifdef HAVE_FONTCONFIG + +#include +#include +#include +#include + +class KXftConfig +{ +public: + struct Item { + Item(QDomNode &n) + : node(n) + , toBeRemoved(false) + { + } + Item() + : toBeRemoved(false) + { + } + virtual void reset() + { + node.clear(); + toBeRemoved = false; + } + bool added() + { + return node.isNull(); + } + + QDomNode node; + + virtual ~Item() + { + } + bool toBeRemoved; + }; + + struct SubPixel : public Item { + enum Type { + NotSet, + None, + Rgb, + Bgr, + Vrgb, + Vbgr, + }; + + SubPixel(Type t, QDomNode &n) + : Item(n) + , type(t) + { + } + SubPixel(Type t = NotSet) + : type(t) + { + } + + void reset() override + { + Item::reset(); + type = NotSet; + } + + Type type; + }; + + struct Exclude : public Item { + Exclude(double f, double t, QDomNode &n) + : Item(n) + , from(f) + , to(t) + { + } + Exclude(double f = 0, double t = 0) + : from(f) + , to(t) + { + } + + void reset() override + { + Item::reset(); + from = to = 0; + } + + double from, to; + }; + + struct Hint : public Item { + enum Style { + NotSet, + None, + Slight, + Medium, + Full, + }; + + Hint(Style s, QDomNode &n) + : Item(n) + , style(s) + { + } + Hint(Style s = NotSet) + : style(s) + { + } + + void reset() override + { + Item::reset(); + style = NotSet; + } + + Style style; + }; + + struct Hinting : public Item { + Hinting(bool s, QDomNode &n) + : Item(n) + , set(s) + { + } + Hinting(bool s = true) + : set(s) + { + } + + void reset() override + { + Item::reset(); + set = true; + } + + bool set; + }; + + struct AntiAliasing : public Item { + enum State { + NotSet, + Enabled, + Disabled, + }; + + AntiAliasing(State s, QDomNode &n) + : Item(n) + , state(s) + { + } + AntiAliasing(State s = NotSet) + : state(s) + { + } + + void reset() override + { + Item::reset(); + state = NotSet; + } + + enum State state; + }; + +public: + explicit KXftConfig(); + + virtual ~KXftConfig(); + + bool reset(); + bool apply(); + bool getSubPixelType(SubPixel::Type &type); + void setSubPixelType(SubPixel::Type type); // SubPixel::None => turn off sub-pixel rendering + bool getExcludeRange(double &from, double &to); + void setExcludeRange(double from, double to); // from:0, to:0 => turn off exclude range + bool getHintStyle(Hint::Style &style); + void setHintStyle(Hint::Style style); + void setAntiAliasing(AntiAliasing::State state); + AntiAliasing::State getAntiAliasing() const; + bool antiAliasingHasLocalConfig() const; + bool subPixelTypeHasLocalConfig() const; + bool hintStyleHasLocalConfig() const; + bool changed() + { + return m_madeChanges; + } + static QString description(SubPixel::Type t); + static const char *toStr(SubPixel::Type t); + static QString description(Hint::Style s); + static const char *toStr(Hint::Style s); + bool aliasingEnabled(); + +private: + bool parseConfigFile(const QString &filename); + void readContents(); + void applySubPixelType(); + void applyHintStyle(); + void applyAntiAliasing(); + void setHinting(bool set); + void applyHinting(); + void applyExcludeRange(bool pixel); + QString getConfigFile(); + +private: + QStringList m_globalFiles; + + SubPixel m_subPixel; + Exclude m_excludeRange, m_excludePixelRange; + Hint m_hint; + Hinting m_hinting; + AntiAliasing m_antiAliasing; + bool m_antiAliasingHasLocalConfig; + bool m_subPixelHasLocalConfig; + bool m_hintHasLocalConfig; + QDomDocument m_doc; + QString m_file; + bool m_madeChanges; + QDateTime m_time; +}; + +Q_DECLARE_METATYPE(KXftConfig::Hint::Style) +Q_DECLARE_METATYPE(KXftConfig::SubPixel::Type) +#endif diff --git a/plasma/workspace/kcms/fonts/package/contents/ui/FontWidget.qml b/plasma/workspace/kcms/fonts/package/contents/ui/FontWidget.qml new file mode 100644 index 0000000000..52562b5517 --- /dev/null +++ b/plasma/workspace/kcms/fonts/package/contents/ui/FontWidget.qml @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2015 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.0 as QtControls +import QtQuick.Dialogs 1.2 as QtDialogs +import org.kde.kirigami 2.3 as Kirigami +import org.kde.kcm 1.0 + + +FocusScope { + id: root + property string label + property alias tooltipText: tooltip.text + property string category + property font font + Kirigami.FormData.label: root.label + activeFocusOnTab: true + + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + RowLayout { + id: layout + + QtControls.TextField { + readOnly: true + Kirigami.Theme.inherit: true + text: root.font.family + " " + root.font.pointSize + "pt" + font: root.font + } + + QtControls.Button { + icon.name: "document-edit" + Layout.fillHeight: true + Kirigami.MnemonicData.enabled: false + focus: true + onClicked: { + fontDialog.adjustAllFonts = false + kcm.adjustFont(root.font, root.category) + } + QtControls.ToolTip { + id: tooltip + } + } + } +} + diff --git a/plasma/workspace/kcms/fonts/package/contents/ui/main.qml b/plasma/workspace/kcms/fonts/package/contents/ui/main.qml new file mode 100644 index 0000000000..152ca2d408 --- /dev/null +++ b/plasma/workspace/kcms/fonts/package/contents/ui/main.qml @@ -0,0 +1,419 @@ +/* + SPDX-FileCopyrightText: 2015 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2017 Marco Martin + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.0 as QtControls +import QtQuick.Dialogs 1.2 as QtDialogs + +import org.kde.kquickcontrolsaddons 2.0 // For KCMShell +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kcm 1.6 as KCM + +KCM.SimpleKCM { + id: root + + KCM.ConfigModule.quickHelp: i18n("This module lets you configure the system fonts.") + + Kirigami.Action { + id: kscreenAction + visible: KCMShell.authorize("kcm_kscreen.desktop").length > 0 + text: i18n("Change Display Scaling…") + iconName: "preferences-desktop-display" + onTriggered: KCMShell.open("kcm_kscreen") + } + + ColumnLayout { + + Kirigami.InlineMessage { + id: antiAliasingMessage + Layout.fillWidth: true + showCloseButton: true + text: i18n("Some changes such as anti-aliasing or DPI will only affect newly started applications.") + + Connections { + target: kcm + function onAliasingChangeApplied() { + antiAliasingMessage.visible = true + } + } + } + + Kirigami.InlineMessage { + id: hugeFontsMessage + Layout.fillWidth: true + showCloseButton: true + text: i18n("Very large fonts may produce odd-looking results. Consider adjusting the global screen scale instead of using a very large font size.") + + Connections { + target: kcm + function onFontsHaveChanged() { + hugeFontsMessage.visible = generalFontWidget.font.pointSize > 14 + || fixedWidthFontWidget.font.pointSize > 14 + || smallFontWidget.font.pointSize > 14 + || toolbarFontWidget.font.pointSize > 14 + || menuFontWidget.font.pointSize > 14 + } + } + + actions: [ kscreenAction ] + } + + Kirigami.InlineMessage { + id: dpiTwiddledMessage + Layout.fillWidth: true + showCloseButton: true + text: i18n("The recommended way to scale the user interface is using the global screen scaling feature.") + actions: [ kscreenAction ] + } + + Kirigami.FormLayout { + id: formLayout + readonly property int maxImplicitWidth: Math.max(adjustAllFontsButton.implicitWidth, excludeField.implicitWidth, subPixelCombo.implicitWidth, hintingCombo.implicitWidth) + + QtControls.Button { + id: adjustAllFontsButton + Layout.preferredWidth: formLayout.maxImplicitWidth + icon.name: "font-select-symbolic" + text: i18n("&Adjust All Fonts…") + + onClicked: kcm.adjustAllFonts(); + enabled: !kcm.fontsSettings.isImmutable("font") + || !kcm.fontsSettings.isImmutable("fixed") + || !kcm.fontsSettings.isImmutable("smallestReadableFont") + || !kcm.fontsSettings.isImmutable("toolBarFont") + || !kcm.fontsSettings.isImmutable("menuFont") + || !kcm.fontsSettings.isImmutable("activeFont") + } + + FontWidget { + id: generalFontWidget + label: i18n("General:") + tooltipText: i18n("Select general font") + category: "font" + font: kcm.fontsSettings.font + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "font" + } + } + FontWidget { + id: fixedWidthFontWidget + label: i18n("Fixed width:") + tooltipText: i18n("Select fixed width font") + category: "fixed" + font: kcm.fontsSettings.fixed + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "fixed" + } + } + FontWidget { + id: smallFontWidget + label: i18n("Small:") + tooltipText: i18n("Select small font") + category: "smallestReadableFont" + font: kcm.fontsSettings.smallestReadableFont + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "smallestReadableFont" + } + } + FontWidget { + id: toolbarFontWidget + label: i18n("Toolbar:") + tooltipText: i18n("Select toolbar font") + category: "toolBarFont" + font: kcm.fontsSettings.toolBarFont + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "toolBarFont" + } + } + FontWidget { + id: menuFontWidget + label: i18n("Menu:") + tooltipText: i18n("Select menu font") + category: "menuFont" + font: kcm.fontsSettings.menuFont + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "menuFont" + } + } + FontWidget { + label: i18n("Window title:") + tooltipText: i18n("Select window title font") + category: "activeFont" + font: kcm.fontsSettings.activeFont + + KCM.SettingStateBinding { + configObject: kcm.fontsSettings + settingName: "activeFont" + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + RowLayout { + Kirigami.FormData.label: i18n("Anti-Aliasing:") + QtControls.CheckBox { + id: antiAliasingCheckBox + checked: kcm.fontsAASettings.antiAliasing + onCheckedChanged: kcm.fontsAASettings.antiAliasing = checked + text: i18n("Enable") + Layout.fillWidth: true + } + KCM.ContextualHelpButton { + toolTipText: xi18nc("@info:tooltip Anti-Aliasing", "Pixels on displays are generally aligned in a grid. Therefore shapes of fonts that do not align with this grid will look blocky and wrong unless anti-aliasing techniques are used to reduce this effect. You generally want to keep this option enabled unless it causes problems.") + } + + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "antiAliasing" + extraEnabledConditions: !kcm.fontsAASettings.isAaImmutable + } + } + + QtControls.CheckBox { + id: excludeCheckBox + checked: kcm.fontsAASettings.exclude + onCheckedChanged: kcm.fontsAASettings.exclude = checked; + text: i18n("Exclude range from anti-aliasing") + Layout.fillWidth: true + + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "exclude" + extraEnabledConditions: !kcm.fontsAASettings.isAaImmutable && antiAliasingCheckBox.checked + } + } + + RowLayout { + id: excludeField + Layout.preferredWidth: formLayout.maxImplicitWidth + enabled: antiAliasingCheckBox.enabled && antiAliasingCheckBox.checked + + QtControls.SpinBox { + id: excludeFromSpinBox + stepSize: 1 + onValueChanged: kcm.fontsAASettings.excludeFrom = value + textFromValue: function(value, locale) { return i18n("%1 pt", value)} + valueFromText: function(text, locale) { return parseInt(text) } + editable: true + value: kcm.fontsAASettings.excludeFrom + + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "excludeFrom" + extraEnabledConditions: excludeCheckBox.checked + } + } + + QtControls.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: i18n("to") + enabled: excludeCheckBox.checked + } + + QtControls.SpinBox { + id: excludeToSpinBox + stepSize: 1 + onValueChanged: kcm.fontsAASettings.excludeTo = value + textFromValue: function(value, locale) { return i18n("%1 pt", value)} + valueFromText: function(text, locale) { return parseInt(text) } + editable: true + value: kcm.fontsAASettings.excludeTo + + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "excludeTo" + extraEnabledConditions: excludeCheckBox.checked + } + } + Connections { + target: kcm.fontsAASettings + function onExcludeFromChanged() { + excludeFromSpinBox.value = kcm.fontsAASettings.excludeFrom; + } + function onExcludeToChanged() { + excludeToSpinBox.value = kcm.fontsAASettings.excludeTo; + } + } + } + + RowLayout { + Kirigami.FormData.label: i18nc("Used as a noun, and precedes a combobox full of options", "Sub-pixel rendering:") + QtControls.ComboBox { + id: subPixelCombo + Layout.preferredWidth: formLayout.maxImplicitWidth + currentIndex: kcm.subPixelCurrentIndex + onCurrentIndexChanged: kcm.subPixelCurrentIndex = currentIndex; + model: kcm.subPixelOptionsModel + textRole: "display" + popup.height: popup.implicitHeight + delegate: QtControls.ItemDelegate { + id: subPixelDelegate + onWidthChanged: { + subPixelCombo.popup.width = Math.max(subPixelCombo.popup.width, width) + } + contentItem: ColumnLayout { + id: subPixelLayout + Kirigami.Heading { + id: subPixelComboText + text: model.display + level: 5 + } + Image { + id: subPixelComboImage + source: "image://preview/" + model.index + "_" + kcm.hintingCurrentIndex + ".png" + // Setting sourceSize here is necessary as a workaround for QTBUG-38127 + // + // With this bug, images requested from a QQuickImageProvider have an incorrect scale with devicePixelRatio != 1 when sourceSize is not set. + // + // TODO: Check if QTBUG-38127 is fixed and remove the next two lines. + sourceSize.width: 1 + sourceSize.height: 1 + } + } + } + + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "subPixel" + extraEnabledConditions: antiAliasingCheckBox.checked && !kcm.fontsAASettings.isAaImmutable + } + } + KCM.ContextualHelpButton { + toolTipText: xi18nc("@info:tooltip Sub-pixel rendering", "On TFT or LCD screens every single pixel is actually composed of three or four smaller monochrome lights. These sub-pixels can be changed independently to further improve the quality of displayed fonts. The rendering quality is only improved if the selection matches the manner in which the sub-pixels of your display are aligned. Most displays have a linear ordering of RGB sub-pixels, some have BGR and some exotic orderings are not supported by this feature.This does not work with CRT monitors.") + } + } + + RowLayout { + Kirigami.FormData.label: i18nc("Used as a noun, and precedes a combobox full of options", "Hinting:") + QtControls.ComboBox { + id: hintingCombo + Layout.preferredWidth: formLayout.maxImplicitWidth + currentIndex: kcm.hintingCurrentIndex + onCurrentTextChanged: kcm.hintingCurrentIndex = currentIndex; + model: kcm.hintingOptionsModel + textRole: "display" + popup.height: popup.implicitHeight + delegate: QtControls.ItemDelegate { + id: hintingDelegate + onWidthChanged: { + hintingCombo.popup.width = Math.max(hintingCombo.popup.width, width) + } + contentItem: ColumnLayout { + id: hintingLayout + Kirigami.Heading { + id: hintingComboText + text: model.display + level: 5 + } + Image { + id: hintingComboImage + source: "image://preview/" + kcm.subPixelCurrentIndex + "_" + model.index + ".png" + // Setting sourceSize here is necessary as a workaround for QTBUG-38127 + // + // With this bug, images requested from a QQuickImageProvider have an incorrect scale with devicePixelRatio != 1 when sourceSize is not set. + // + // TODO: Check if QTBUG-38127 is fixed and remove the next two lines. + sourceSize.width: 1 + sourceSize.height: 1 + } + } + } + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "hinting" + extraEnabledConditions: antiAliasingCheckBox.checked && !kcm.fontsAASettings.isAaImmutable + } + } + KCM.ContextualHelpButton { + toolTipText: xi18nc("@info:tooltip Hinting", "Hinting is a technique in which hints embedded in a font are used to enhance the rendering quality especially at small sizes. Stronger hinting generally leads to sharper edges but the small letters will less closely resemble their shape at big sizes.") + } + } + + RowLayout { + Layout.preferredWidth: formLayout.maxImplicitWidth + + QtControls.CheckBox { + id: dpiCheckBox + checked: kcm.fontsAASettings.dpi !== 0 + text: i18n("Force font DPI:") + onClicked: { + kcm.fontsAASettings.dpi = checked ? dpiSpinBox.value : 0 + dpiTwiddledMessage.visible = checked + } + + // dpiSpinBox will set forceFontDPI or forceFontDPIWayland, + // so only one SettingStateBinding will be activated at a time. + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "forceFontDPIWayland" + extraEnabledConditions: antiAliasingCheckBox.checked && !kcm.fontsAASettings.isAaImmutable + } + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "forceFontDPI" + extraEnabledConditions: antiAliasingCheckBox.checked && !kcm.fontsAASettings.isAaImmutable + } + } + + QtControls.SpinBox { + id: dpiSpinBox + editable: true + value: kcm.fontsAASettings.dpi !== 0 ? kcm.fontsAASettings.dpi : 96 + onValueModified: kcm.fontsAASettings.dpi = value + to: 999 + from: 1 + + // dpiSpinBox will set forceFontDPI or forceFontDPIWayland, + // so only one SettingStateBinding will be activated at a time. + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "forceFontDPIWayland" + extraEnabledConditions: dpiCheckBox.enabled && dpiCheckBox.checked + } + KCM.SettingStateBinding { + configObject: kcm.fontsAASettings + settingName: "forceFontDPI" + extraEnabledConditions: dpiCheckBox.enabled && dpiCheckBox.checked + } + } + KCM.ContextualHelpButton { + toolTipText: xi18nc("@info:tooltip Force fonts DPI", "Enter your screen's DPI here to make on-screen fonts match their physical sizes when printed. Changing this option from its default value will conflict with many apps; some icons and images may not scale as expected.To increase text size, change the size of the fonts above. To scale everything, use the scaling slider on the Display & Monitor page.") + } + } + + QtDialogs.FontDialog { + id: fontDialog + title: i18n("Select Font") + modality: Qt.WindowModal + property string currentCategory + property bool adjustAllFonts: false + onAccepted: { + if (adjustAllFonts) { + kcm.adjustAllFonts() + } else { + kcm.adjustFont(font, currentCategory) + } + } + } + } + } +} diff --git a/plasma/workspace/kcms/fonts/previewimageprovider.cpp b/plasma/workspace/kcms/fonts/previewimageprovider.cpp new file mode 100644 index 0000000000..dc75feac00 --- /dev/null +++ b/plasma/workspace/kcms/fonts/previewimageprovider.cpp @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2018 Julian Wolff + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include + +#include + +#include "kxftconfig.h" +#include "previewimageprovider.h" +#include "previewrenderengine.h" + +QImage combineImages(const QList &images, const QColor &bgnd, int spacing = 0) +{ + int width = 0; + int height = 0; + QImage::Format format = QImage::Format_Invalid; + int devicePixelRatio = 1; + for (const auto &image : images) { + if (width < image.width()) { + width = image.width(); + } + height += image.height() + spacing; + format = image.format(); + devicePixelRatio = image.devicePixelRatio(); + } + height -= spacing; + + // To correctly align the image pixels on a high dpi display, + // the image dimensions need to be a multiple of devicePixelRatio + width = (width + devicePixelRatio - 1) / devicePixelRatio * devicePixelRatio; + height = (height + devicePixelRatio - 1) / devicePixelRatio * devicePixelRatio; + + QImage combinedImage(width, height, format); + combinedImage.setDevicePixelRatio(devicePixelRatio); + combinedImage.fill(bgnd); + + int offset = 0; + QPainter p(&combinedImage); + for (const auto &image : images) { + p.drawImage(0, offset, image); + offset += (image.height() + spacing) / devicePixelRatio; + } + + return combinedImage; +} + +PreviewImageProvider::PreviewImageProvider(const QFont &font) + : QQuickImageProvider(QQuickImageProvider::Image) + , m_font(font) +{ +} + +QImage PreviewImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + Q_UNUSED(requestedSize) + if (!KWindowSystem::isPlatformX11()) { + return QImage(); + } + + int subPixelIndex = 0; + int hintingIndex = 0; + + const auto idpart = id.splitRef(QLatin1Char('.'))[0]; + const auto sections = idpart.split(QLatin1Char('_')); + + if (sections.size() >= 2) { + subPixelIndex = sections[0].toInt() + KXftConfig::SubPixel::None; + hintingIndex = sections[1].toInt() + KXftConfig::Hint::None; + } else { + return QImage(); + } + + KXftConfig xft; + + KXftConfig::AntiAliasing::State oldAntialiasing = xft.getAntiAliasing(); + double oldStart = 0; + double oldEnd = 0; + xft.getExcludeRange(oldStart, oldEnd); + KXftConfig::SubPixel::Type oldSubPixelType = KXftConfig::SubPixel::NotSet; + xft.getSubPixelType(oldSubPixelType); + KXftConfig::Hint::Style oldHintStyle = KXftConfig::Hint::NotSet; + xft.getHintStyle(oldHintStyle); + + xft.setAntiAliasing(KXftConfig::AntiAliasing::Enabled); + xft.setExcludeRange(0, 0); + + KXftConfig::SubPixel::Type subPixelType = (KXftConfig::SubPixel::Type)subPixelIndex; + xft.setSubPixelType(subPixelType); + + KXftConfig::Hint::Style hintStyle = (KXftConfig::Hint::Style)hintingIndex; + xft.setHintStyle(hintStyle); + + xft.apply(); + + QColor text(QApplication::palette().color(QPalette::Text)); + QColor bgnd(QApplication::palette().color(QPalette::Window)); + + PreviewRenderEngine eng(true); + QList lines; + + lines << eng.drawAutoSize(m_font, text, bgnd, eng.getDefaultPreviewString()); + + QImage img = combineImages(lines, bgnd, lines[0].height() * .25); + + xft.setAntiAliasing(oldAntialiasing); + xft.setExcludeRange(oldStart, oldEnd); + xft.setSubPixelType(oldSubPixelType); + xft.setHintStyle(oldHintStyle); + + xft.apply(); + + *size = img.size(); + + return img; +} diff --git a/plasma/workspace/kcms/fonts/previewimageprovider.h b/plasma/workspace/kcms/fonts/previewimageprovider.h new file mode 100644 index 0000000000..871ce7b569 --- /dev/null +++ b/plasma/workspace/kcms/fonts/previewimageprovider.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2018 Julian Wolff + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class PreviewImageProvider : public QQuickImageProvider +{ +public: + PreviewImageProvider(const QFont &font); + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + +private: + QFont m_font; +}; diff --git a/plasma/workspace/kcms/fonts/previewrenderengine.cpp b/plasma/workspace/kcms/fonts/previewrenderengine.cpp new file mode 100644 index 0000000000..00847aa64c --- /dev/null +++ b/plasma/workspace/kcms/fonts/previewrenderengine.cpp @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2018 Julian Wolff + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "previewrenderengine.h" +#include "Fc.h" + +#include +#include +#include + +#include + +#ifdef HAVE_FONTCONFIG + +static int qtToFcWeight(int weight) +{ + switch (weight) { + case 0: + return FC_WEIGHT_THIN; + case QFont::Light >> 1: + return FC_WEIGHT_EXTRALIGHT; + case QFont::Light: + return FC_WEIGHT_LIGHT; + default: + case QFont::Normal: + return FC_WEIGHT_REGULAR; + case (QFont::Normal + QFont::DemiBold) >> 1: +#ifdef KFI_HAVE_MEDIUM_WEIGHT + return FC_WEIGHT_MEDIUM; +#endif + case QFont::DemiBold: + return FC_WEIGHT_DEMIBOLD; + case QFont::Bold: + return FC_WEIGHT_BOLD; + case (QFont::Bold + QFont::Black) >> 1: + return FC_WEIGHT_EXTRABOLD; + case QFont::Black: + return FC_WEIGHT_BLACK; + } +} + +#ifndef KFI_FC_NO_WIDTHS +static int qtToFcWidth(int weight) +{ + switch (weight) { + case QFont::UltraCondensed: + return KFI_FC_WIDTH_ULTRACONDENSED; + case QFont::ExtraCondensed: + return KFI_FC_WIDTH_EXTRACONDENSED; + case QFont::Condensed: + return KFI_FC_WIDTH_CONDENSED; + case QFont::SemiCondensed: + return KFI_FC_WIDTH_SEMICONDENSED; + default: + case QFont::Unstretched: + return KFI_FC_WIDTH_NORMAL; + case QFont::SemiExpanded: + return KFI_FC_WIDTH_SEMIEXPANDED; + case QFont::Expanded: + return KFI_FC_WIDTH_EXPANDED; + case QFont::ExtraExpanded: + return KFI_FC_WIDTH_EXTRAEXPANDED; + case QFont::UltraExpanded: + return KFI_FC_WIDTH_ULTRAEXPANDED; + } +} +#endif + +static bool qtToFcSlant(int slant) +{ + switch (slant) { + default: + case QFont::StyleNormal: + return FC_SLANT_ROMAN; + case QFont::StyleItalic: + return FC_SLANT_ITALIC; + case QFont::StyleOblique: + return FC_SLANT_OBLIQUE; + } +} + +static quint32 qtToFcStyle(const QFont &font) +{ + return KFI::FC::createStyleVal(qtToFcWeight(font.weight()), qtToFcWidth(font.stretch()), qtToFcSlant(font.style())); +} + +PreviewRenderEngine::PreviewRenderEngine(bool init) + : CFcEngine(init) +{ + if (init) + FcInitReinitialize(); +} + +PreviewRenderEngine::~PreviewRenderEngine() +{ +} + +QImage PreviewRenderEngine::drawAutoSize(const QFont &font, const QColor &txt, const QColor &bgnd, const QString &text) +{ + const QString &name = font.family(); + const quint32 style = qtToFcStyle(font); + int faceNo = 0; + + double ratio = QGuiApplication::primaryScreen()->devicePixelRatio(); + double dpi = QX11Info::appDpiY(); + + int fSize((int)(((font.pointSizeF() * dpi * ratio) / 72.0) + 0.5)); + + QImage image(draw(name, style, faceNo, txt, bgnd, fSize, text)); + image.setDevicePixelRatio(ratio); + return image; +} + +#endif // HAVE_FONTCONFIG diff --git a/plasma/workspace/kcms/fonts/previewrenderengine.h b/plasma/workspace/kcms/fonts/previewrenderengine.h new file mode 100644 index 0000000000..4d98bea19d --- /dev/null +++ b/plasma/workspace/kcms/fonts/previewrenderengine.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2018 Julian Wolff + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "FcEngine.h" +#include "kxftconfig.h" + +#ifdef HAVE_FONTCONFIG + +#include + +class PreviewRenderEngine : public KFI::CFcEngine +{ +public: + PreviewRenderEngine(bool init = true); + ~PreviewRenderEngine() override; + + QImage drawAutoSize(const QFont &font, const QColor &txt, const QColor &bgnd, const QString &text); +}; + +#endif // HAVE_FONTCONFIG diff --git a/plasma/workspace/kcms/formats/CMakeLists.txt b/plasma/workspace/kcms/formats/CMakeLists.txt new file mode 100644 index 0000000000..85abf78789 --- /dev/null +++ b/plasma/workspace/kcms/formats/CMakeLists.txt @@ -0,0 +1,28 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmformats\") + +set(kcm_formats_PART_SRCS + kcmformats.cpp + localelistmodel.cpp + formatssettings.cpp + exampleutility.cpp + optionsmodel.cpp +) + +kconfig_add_kcfg_files(kcm_formats_PART_SRCS formatssettings.kcfgc GENERATE_MOC) + +add_library(kcm_formats MODULE ${kcm_formats_PART_SRCS}) + +target_link_libraries(kcm_formats + Qt5::Core + KF5::ConfigCore + KF5::ConfigGui + KF5::CoreAddons + KF5::I18n + KF5::QuickAddons) + + +########### install files ############### +install(TARGETS kcm_formats DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/kcms/systemsettings) +install(FILES kcm_formats.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +kpackage_install_package(package kcm_formats kcms) diff --git a/plasma/workspace/kcms/formats/Messages.sh b/plasma/workspace/kcms/formats/Messages.sh new file mode 100644 index 0000000000..0d4787b30e --- /dev/null +++ b/plasma/workspace/kcms/formats/Messages.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp +$XGETTEXT *.cpp -o $podir/kcmformats.pot +rm -f rc.cpp + diff --git a/plasma/workspace/kcms/formats/exampleutility.cpp b/plasma/workspace/kcms/formats/exampleutility.cpp new file mode 100644 index 0000000000..13b40d2e42 --- /dev/null +++ b/plasma/workspace/kcms/formats/exampleutility.cpp @@ -0,0 +1,66 @@ +/* + exampleutility.cpp + SPDX-FileCopyrightText: 2021 Han Young + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +class Utility +{ +public: + template + inline static QString collateExample(const T &collate); + inline static QString numericExample(const QLocale &locale); + inline static QString timeExample(const QLocale &locale); + inline static QString shortTimeExample(const QLocale &locale); + inline static QString measurementExample(const QLocale &locale); + inline static QString monetaryExample(const QLocale &locale); +}; + +template +QString Utility::collateExample(const T &collate) +{ + auto example{QStringLiteral("abcdefgxyzABCDEFGXYZÅåÄäÖöÅåÆæØø")}; + auto collator{QCollator{collate}}; + std::sort(example.begin(), example.end(), collator); + return example; +} + +QString Utility::monetaryExample(const QLocale &locale) +{ + return locale.toCurrencyString(24.00); +} + +QString Utility::timeExample(const QLocale &locale) +{ + return i18n("%1 (long format)", locale.toString(QDateTime::currentDateTime())) + QLatin1Char('\n') + + i18n("%1 (short format)", locale.toString(QDateTime::currentDateTime(), QLocale::ShortFormat)); +} + +QString Utility::shortTimeExample(const QLocale &locale) +{ + return locale.toString(QDateTime::currentDateTime(), QLocale::LongFormat); +} + +QString Utility::measurementExample(const QLocale &locale) +{ + QString measurementExample; + if (locale.measurementSystem() == QLocale::ImperialUKSystem) { + measurementExample = i18nc("Measurement combobox", "Imperial UK"); + } else if (locale.measurementSystem() == QLocale::ImperialUSSystem || locale.measurementSystem() == QLocale::ImperialSystem) { + measurementExample = i18nc("Measurement combobox", "Imperial US"); + } else { + measurementExample = i18nc("Measurement combobox", "Metric"); + } + return measurementExample; +} + +QString Utility::numericExample(const QLocale &locale) +{ + return locale.toString(1000.01); +} diff --git a/plasma/workspace/kcms/formats/formatssettings.kcfg b/plasma/workspace/kcms/formats/formatssettings.kcfg new file mode 100644 index 0000000000..e6110621d8 --- /dev/null +++ b/plasma/workspace/kcms/formats/formatssettings.kcfg @@ -0,0 +1,61 @@ + + + QtGlobal + KLocalizedString + + + + false + + + QString::fromLocal8Bit(qgetenv("LANG")) + + + + auto lc_numeric {QString::fromLocal8Bit(qgetenv("LC_NUMERIC"))}; + lc_numeric = lc_numeric.isEmpty() ? i18n("Default") : lc_numeric; + + + lc_numeric + + + + + auto lc_time {QString::fromLocal8Bit(qgetenv("LC_TIME"))}; + lc_time = lc_time.isEmpty() ? i18n("Default") : lc_time; + + lc_time + + + + auto lc_monetary {QString::fromLocal8Bit(qgetenv("LC_MONETARY"))}; + lc_monetary = lc_monetary.isEmpty() ? i18n("Default") : lc_monetary; + + lc_monetary + + + + auto lc_measurement {QString::fromLocal8Bit(qgetenv("LC_MEASUREMENT"))}; + lc_measurement = lc_measurement.isEmpty() ? i18n("Default") : lc_measurement; + + lc_measurement + + + + auto lc_ctype {QString::fromLocal8Bit(qgetenv("LC_CTYPE"))}; + lc_ctype = lc_ctype.isEmpty() ? i18n("Default") : lc_ctype; + + lc_ctype + + + + auto language {QString::fromLocal8Bit(qgetenv("LANGUAGE"))}; + language = language.isEmpty() ? i18n("Default") : language; + + language + + + diff --git a/plasma/workspace/kcms/formats/formatssettings.kcfgc b/plasma/workspace/kcms/formats/formatssettings.kcfgc new file mode 100644 index 0000000000..6a9d2e27b9 --- /dev/null +++ b/plasma/workspace/kcms/formats/formatssettings.kcfgc @@ -0,0 +1,6 @@ +File=formatssettings.kcfg +ClassName=FormatsSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/plasma/workspace/kcms/formats/kcm_formats.desktop b/plasma/workspace/kcms/formats/kcm_formats.desktop new file mode 100644 index 0000000000..46e0b26e31 --- /dev/null +++ b/plasma/workspace/kcms/formats/kcm_formats.desktop @@ -0,0 +1,44 @@ +[Desktop Entry] +Name=Formats +Name[ar]=التنسيقات +Name[az]=Formatlar +Name[ca]=Formats +Name[cs]=Formáty +Name[da]=Formater +Name[de]=Formate +Name[en_GB]=Formats +Name[es]=Formatos +Name[eu]=Formatuak +Name[fi]=Muotoilu +Name[fr]=Formats +Name[hi]=प्रारूप +Name[hsb]=Formaty +Name[hu]=Formátumok +Name[ia]=Formatos +Name[it]=Formati +Name[ja]=形式 +Name[ko]=형식 +Name[lt]=Formatai +Name[ml]=ഫോർമാറ്റുകൾ +Name[nl]=Formaten +Name[nn]=Format +Name[pa]=ਫਾਰਮੈਟ +Name[pl]=Formaty +Name[pt]=Formatos +Name[pt_BR]=Formatos +Name[ro]=Formate +Name[ru]=Форматы +Name[sk]=Formáty +Name[sl]=Formati +Name[sv]=Format +Name[ta]=படிவங்கள் +Name[tr]=Biçimler +Name[uk]=Формати +Name[vi]=Dạng thức +Name[x-test]=xxFormatsxx +Name[zh_CN]=格式 + +Icon=preferences-desktop-locale +Type=Application +Exec=systemsettings kcm_formats +NoDisplay=true diff --git a/plasma/workspace/kcms/formats/kcm_formats.json b/plasma/workspace/kcms/formats/kcm_formats.json new file mode 100644 index 0000000000..8ed2263bf7 --- /dev/null +++ b/plasma/workspace/kcms/formats/kcm_formats.json @@ -0,0 +1,113 @@ +{ + "KPlugin": { + "Description": "Numeric, Currency and Time Formats", + "Description[ar]": "تنسيقات الأرقام والعملات والوقت ", + "Description[az]": "Say, Valyuta və Vaxt formatları", + "Description[ca]": "Format numèric, monetari i horari", + "Description[cs]": "Formáty čísel, měny a času", + "Description[de]": "Formate für Zahlen, Währung und Zeitangaben", + "Description[en_GB]": "Numeric, Currency and Time Formats", + "Description[es]": "Formatos numérico, de moneda y de hora", + "Description[eu]": "Zenbakizko, diruzko eta denborazko formatuak", + "Description[fi]": "Lukujen, rahasummien ja ajanilmausten muotoilu", + "Description[fr]": "Format numérique, de monnaie et d'heure", + "Description[hu]": "Numerikus, pénznem- és időformátumok", + "Description[ia]": "Formatos numeric, de numerario e de tempore", + "Description[it]": "Formati numerici, delle valute e dell'orario", + "Description[ko]": "숫자, 통화, 시간 형식", + "Description[lt]": "Skaitmenų, valiutos ir laiko formatai", + "Description[nl]": "Indeling voor numerieke getallen, valuta en tijd", + "Description[nn]": "Format for tal, valuta og tid", + "Description[pa]": "ਅੰਕੀ, ਕਰੰਸੀ ਅਤੇ ਸਮੇਂ ਦੇ ਫਾਰਮੈਟ", + "Description[pl]": "Formaty liczb, waluty i czasu", + "Description[pt_BR]": "Formatos de data/hora, numérico e monetário", + "Description[ro]": "Formate numerice, valutare și orare", + "Description[ru]": "Форматы записи чисел, времени и денежных сумм", + "Description[sk]": "Numerické, mena a časové formáty", + "Description[sl]": "Oblika števil, valute in časa", + "Description[sv]": "Numeriska format, valutaformat och tidsformat", + "Description[ta]": "எண்களும், பண மதிப்புகளும், நேரங்களும் எழுதப்படும் விதம்", + "Description[tr]": "Sayısal, Para Birimi ve Zaman Biçimleri", + "Description[uk]": "Формати запису чисел, грошових сум та часу", + "Description[vi]": "Dạng thức số, tiền tệ và thời gian", + "Description[x-test]": "xxNumeric, Currency and Time Formatsxx", + "Description[zh_CN]": "数字、货币和时间格式", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-locale", + "Id": "kcm_formats", + "Name": "Formats", + "Name[ar]": "التنسيقات", + "Name[az]": "Formatlar", + "Name[ca]": "Formats", + "Name[cs]": "Formáty", + "Name[da]": "Formater", + "Name[de]": "Formate", + "Name[en_GB]": "Formats", + "Name[es]": "Formatos", + "Name[eu]": "Formatuak", + "Name[fi]": "Muotoilu", + "Name[fr]": "Formats", + "Name[hi]": "प्रारूप ", + "Name[hsb]": "Formaty", + "Name[hu]": "Formátumok", + "Name[ia]": "Formatos", + "Name[it]": "Formati", + "Name[ja]": "形式", + "Name[ko]": "형식", + "Name[lt]": "Formatai", + "Name[ml]": "ഫോർമാറ്റുകൾ", + "Name[nl]": "Formaten", + "Name[nn]": "Format", + "Name[pa]": "ਫਾਰਮੈਟ", + "Name[pl]": "Formaty", + "Name[pt]": "Formatos", + "Name[pt_BR]": "Formatos", + "Name[ro]": "Formate", + "Name[ru]": "Форматы", + "Name[sk]": "Formáty", + "Name[sl]": "Formati", + "Name[sv]": "Format", + "Name[ta]": "படிவங்கள்", + "Name[tr]": "Biçimler", + "Name[uk]": "Формати", + "Name[vi]": "Dạng thức", + "Name[x-test]": "xxFormatsxx", + "Name[zh_CN]": "格式" + }, + "X-DocPath": "kcontrol/formats/index.html", + "X-KDE-Keywords": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format", + "X-KDE-Keywords[ar]": "لغة,ترجمة,تنسيق رقم,لغة,بلد,أحرف,مجموعات أحرف,رمز عشري,فاصل آلاف,رمز,فاصل,علامة,موجب,سلبي,عملة,نقود,علامة جزئية,تقويم,وقت,تاريخ,تنسيقات,أسبوع,أسبوع ابدأ,أولا,ورقة,حجم,حرف,A4,قياس,متري,إنجليزي,إمبراطوري,منطقة,وحدة القياسات,تنسيق التاريخ", + "X-KDE-Keywords[az]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format,dil,tərcümə,say formatı,yerli, Ölkə,tablolar,simvol dəstləri,Onluqlar simvolu,Minlik ayırıcı,simvol,ayırıcı,işarə,müsbət,mənfi,valyuta,pul,kəsirrəqəmləri,təqvim,vaxt,tarix,formatlar,həftə, həftənin başlanğıcı,ilk,kağız,ölçü,məktub,A4,ölçü,metrik,İngilis,bölgə,ölçü vahisləri,toplama,çeşidləmə,tarix formatı", + "X-KDE-Keywords[ca]": "idioma,traducció,format numèric,configuració regional,país,jocs de caràcters,joc de caràcters,símbol decimal,separador de milers,símbol,separador,signe,positiu,negatiu,moneda,diners,dígits fraccionaris,calendari,hora,data,formats,setmana,inici de setmana,primer,paper,mida,carta,A4,mesura,mètric,Anglès,Imperial,regió,unitats de mesura,col·lació,ordenació,format de data", + "X-KDE-Keywords[cs]": "jazyk,překlad,formát čísel,locale,Země,znakové sady,sady znaků,desítkový symbol,Oddělovač tisíců,symbol,oddělovač,znaménko,kladné,záporné,měna,peníze,zlomky, kalendář,čas,datum,formáty,týden,začátek týdne,první,papír,velikost,dopis,A4,míry,metrické,Anglické,Imperiální,region,měrné jednotky,srovnání,řazení,formát dat", + "X-KDE-Keywords[en_GB]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format", + "X-KDE-Keywords[es]": "idioma,traducción,formato numérico,local,país,conjuntos de caracteres,símbolo decimal,separador de miles,separador de millares,símbolo,separador,signo,positivo,negativo,divisa,moneda,dígitos fraccionarios,calendario,hora,fecha,formatos,semana,inicio de la semana,primero,papel,tamaño,letter,folio,A4,medida,métrica,inglés,imperial,región,unidades de medida,recopilación,orden,formato de fecha", + "X-KDE-Keywords[eu]": "hizkuntza,itzulpena,zenbaki formatua,tokikoak,Herrialdea,karaktere-multzoak,karaktereen multzoak,ikur hamartarra,milakoen bereizlea,ikurra,bereizlea,zeinua,positiboa,negatiboa,moneta,dirua,zatikien digituak,egutegia,ordua,data, formatuak,astea,asteko hasiera,lehena,papera,neurria,gutuna,A4,neurria, metrika,ingelesa,Inperiala,eskualdea,neurri-unitateak,bilketa,sailkapena,data-formatua", + "X-KDE-Keywords[fi]": "kieli,käännös,kääntäminen,lukujen muotoilu,numeroiden muotoilu,locale,alue,alueasetukset,maa,merkistöt,desimaalierotin,tuhaterotin,symboli,erotin,merkki,positiivinen,negatiivinen,valuutta,raha,desimaalipaikkoja,desimaalipaikat,kalenteri,aika,kellonaika,päiväys,päivämäärä,muodot,viikko,viikon alku,viikon ensimmäinen päivä,ensimmäinen,paperi,koko,letter,A4,mittajärjestelmä,mittaaminen,metrinen,anglosaksinen,mittayksiköt,lajittelu,aakkostus,päivämäärän muoto", + "X-KDE-Keywords[fr]": "langue, traduction, format numérique, paramètres régionaux, pays, jeu de caractères, tables de caractères, symbole décimal, séparateur des milliers, symbole, séparateur, signe, positif, négatif, devise, monnaie, chiffres après la virgule, calendrier, heure, date, formats, semaine, début de semaine, premier, papier, taille, lettre, A4, mesure, métrique, anglais, français, impérial, région, unités de mesure, rassemblement, tri, format de date", + "X-KDE-Keywords[hi]": "भाषा,अनुवाद,संख्या प्रारूप,स्थान,देश,वर्ण सेट,वर्ण सेट,दशमलव प्रतीक,हज़ारों विभाजक,प्रतीक,विभाजक,चिह्न,धनात्मक,ऋणात्मक मुद्रा,धन,भिन्नात्मक अंक,कैलेंडर,समय,दिनांक,प्रारूप,सप्ताह,सप्ताह प्रारंभ,पहला, कागज,आकार,अक्षर,ए४,माप,मीट्रिक,अंग्रेजी,इंपीरियल,क्षेत्र, माप इकाइयाँ, संयोजन, छँटाई, दिनांक प्रारूप", + "X-KDE-Keywords[hu]": "nyelv,fordítás,számformátum,területi beállítás,ország,karakterkészlet,karakterkészletek,tizedesjel,ezres elválasztó,jel,elválasztó,előjel,pozitív,negatív,pénznem,pénz,tizedesjegyek,naptár,idő,dátum,formátumok,hét,hét kezdete,első,papír,méret,levél,A4,mértékegység,metrikus,angol,birodalmi,régió,mértékegységek,illesztés,rendezés,dátumformátum", + "X-KDE-Keywords[ia]": "linguage,traduction,formato de numero,locale,Pais,charsets,insimul de characteres, Symbolo decimal,separator de milles,symbolo,separator,signo,positive,negative,numerario,moneta,cifras fractional,calendario,tempore,data,formatos,septimana,initio de septimana, prime,papiro,grandor,littera,A4,mesura, metric,Anglese,Imperial,region,unitates de mesura,collation,ordinar,formato de data", + "X-KDE-Keywords[it]": "lingua,traduzione,formato dei numeri,localizzazione,nazione,codifica,insieme dei caratteri,simbolo decimale,separatore delle migliaia,simbolo,separatore,segno,positivo,negativo,valuta,moneta,cifre decimali,calendario,ora,data,formati,settimana,inizio settimana,primo,carta,dimensione,lettera,A4,misura,metrico,inglese,imperiale,regione,unità di misura,collazione,ordinamento,formato della data", + "X-KDE-Keywords[ko]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format,언어,번역,숫자,로캘,로케일,국가,나라,지역,문자셋,문자 인코딩,인코딩,소숫점,기호,구분자,양수,음수,통화,화폐,돈,달력,시간,시각,날짜,주,시작 요일,요일,종이,크기,미터법,지역,야드파운드법,단위,정렬,날짜 형식", + "X-KDE-Keywords[nl]": "taal,vertaling,getalformaat,locale,land,tekensets,decimaalsymbool,duizendscheidingsteken,symbool,scheidingsteken,teken,positief,negatief,valuta,geld,decimale cijfers,agenda,tijd,datum,formaten,week,weekbegin,eerst,papier,grootte,letter,A4,afmeting,metrisch,Engels,Imperiaal,regio,meeteenheden,vergelijking,sorteren,datumformaat", + "X-KDE-Keywords[nn]": "språk,omsetjing,talformat,lokale,locale,land,teiknkodingar,teiknsett,desimalskiljeteikn,tusenskiljeteikn,symbol,skiljeteikn,tein,positiv,negativ,valuta,pengar,siffer,kalender,tid,dato,klokkeslett,format,veke,vekestart,første,papir,storleik,letter,A4,mål,metrisk,engelsk,britisk,region,måleeiningar,kollatering,sortering,datoformat", + "X-KDE-Keywords[pl]": "język,tłumaczenie,format liczb,regionalność,kraj,zestaw znaków,symbol dziesiętny,separator tysięcy,symbol,separator,znak,dodatni,ujemny,waluta,pieniądze,ułamkowe cyfry,kalendarz,czas,data,formaty,tydzień,początek tygodnia,pierwszy,papier,rozmiar,list,A4,miara,metryczna,Angielski,Cesarskie,region,jednostki miary,grupowanie,sortowanie,format daty", + "X-KDE-Keywords[pt]": "língua,tradução,formato numérico,região,país,codificações,conjuntos de caracteres,símbolo decimal,separador dos milhares,símbolo,separador,sinal,positivo,negativo,moeda,dinheiro,casas decimais,calendário,hora,data,formatos,semana,início da semana,primeiro,papel,tamanho,carta,A4,medida,métrica,inglês,imperial,região,unidades de medida,ordenação,formato de datas", + "X-KDE-Keywords[pt_BR]": "idioma,tradução,formato de números,localização,país,codificações,codificações de caracteres,símbolo decimal,separador de milhares,símbolo,separador,sinal,positivo,negativo,moeda,monetário,casas decimais,calendário,hora,data,formatos,semana,início da semana,primeiro,papel,tamanho,carta,A4,medida,medir,inglês,imperial,região,unidades de medida,colação,ordenamento,formato de data", + "X-KDE-Keywords[ru]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format,язык,перевод,формат чисел,локаль,страна,кодировка,наборы символов,десятичный разделитель,тысячный разделитель,символ,разделитель,подпись,положительный, отрицательный,валюта, деньги, дробные цифры, календарь, время, дата,формат, неделя,начало недели,бумага,размер, буквы,измерение,система мера,метрика,Английский,Русский,единицы измерения,сопоставление,сортировка,формат даты", + "X-KDE-Keywords[sk]": "jazyk,preklad,formát čísla,lokál,krajina,znakové sady,znakové sady,desatinný symbol, oddeľovač tisícov,symbol,oddeľovač,znak,kladný,záporný,mena,peniaze,zlomkové číslice,kalendár,čas,dátum,formáty,týždeň,začiatok týždňa, prvý,papier,veľkosť,list,A4,miera,metrická,anglická,cisárska,región, merné jednotky,zoraďovanie,triedenie,formát dátumu", + "X-KDE-Keywords[sl]": "jezik,prevod,format števil,lokalno,država,nabor znakov,kodna tabela,decimalni simbol,ločilnik tisočic,simbol,ločilnik,predznak,pozitivno,negativno,valuta,denar,ulomek,koledar,čas,datum,formati,teden,začetni teden,prvi,papir,velikost,letter,A4,mera,metrično,angleško,imperialno,regija,enote mere,abecedni red,razvrščanje,oblika datuma", + "X-KDE-Keywords[sv]": "språk,översättning,nummerformat,plats,Land,teckenuppsättningar,decimalsymbol,tusentalsavgränsare,symbol,avgränsare,tecken,positivt,negativt,valuta,pengar,bråkdelssiffror,kalender,tid,datum,format,vecka, veckostart,först,papper,storlek,letter,A4,mått,meter,engelska,område,måttenheter,sorteringsordning,datumformat", + "X-KDE-Keywords[ta]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format, மொழி, மொழிபெயர்ப்பு, எண் வடிவம், நாடு, ஊர், ஆயிரம் பிரிப்பான், அளவு, அலகுகள், வாரம், நாள்காட்டி, நாட்காட்டி, மெட்ரிக், வரிசைப்படுத்தல், தொகுப்பு, தேதி", + "X-KDE-Keywords[uk]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format,мова,мови,переклад,переклади,числа,формат,число,локаль,країна,набір,символ,кодування,розряд,тисяч,дробовий,дробова частина,роздільник,символ,знак,додатне,додатний,додатній,від’ємний,від’ємне,валюта,гроші,грошова одиниця,календар,час,дата,формати,тиждень,робочий тиждень,початок тижня,перший,папір,аркуш,вимір,виміри,вимірювання,одиниці вимірювання,метричний,метричні,метр,англійські,імперські,регіон,одиниці виміру,об'єднання,упорядкування,упорядковування,формат дати", + "X-KDE-Keywords[vi]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,region,measurement units,collation,sorting,date format,ngôn ngữ,biên dịch,dạng thức số,vùng,quốc gia,bộ mã tự,bộ chữ,dấu thập phân,dấu phân cách hàng nghìn,kí hiệu,phần phân cách,dấu,dương,âm,tiền tệ,tiền,chữ số thập phân,lịch,thời gian,ngày,dạng thức,tuần,bắt đầu tuần,đầu tiên,giấy,cỡ,thư,đo,theo hệ mét,theo hệ Anh,vùng,đơn vị đo,đối chiếu,sắp xếp,dạng thức thời gian", + "X-KDE-Keywords[x-test]": "xxlanguagexx,xxtranslationxx,xxnumber formatxx,xxlocalexx,xxCountryxx,xxcharsetsxx,xxcharacter setsxx,xxDecimal symbolxx,xxThousands separatorxx,xxsymbolxx,xxseparatorxx,xxsignxx,xxpositivexx,xxnegativexx,xxcurrencyxx,xxmoneyxx,xxfractional digitsxx,xxcalendarxx,xxtimexx,xxdatexx,xxformatsxx,xxweekxx,xxweek startxx,xxfirstxx,xxpaperxx,xxsizexx,xxletterxx,xxA4xx,xxmeasurexx,xxmetricxx,xxEnglishxx,xxImperialxx,xxregionxx,xxmeasurement unitsxx,xxcollationxx,xxsortingxx,xxdate formatxx", + "X-KDE-Keywords[zh_CN]": "language,translation,number format,locale,Country,charsets,character sets,Decimal symbol,Thousands separator,symbol,separator,sign,positive,negative,currency,money,fractional digits,calendar,time,date,formats,week,week start,first,paper,size,letter,A4,measure,metric,English,Imperial,语言,翻译,数字格式,符号格式,语种,语系,书写系统,文字系统,国家,字符,字符集,千分符,十进制符号,符号,分隔符,正数,负数,现金,货币符号,货币,币种,钱,日历,分号,时间,日期,星期,周,每星期第一天,每周开始日,日期格式,纸张,大小,信件,单位,单位格式,度量,度量衡,英制,公制,美制,排序,排序规则,排序顺序,整理,顺序", + "X-KDE-System-Settings-Parent-Category": "regionalsettings", + "X-KDE-Weight": 50 +} diff --git a/plasma/workspace/kcms/formats/kcmformats.cpp b/plasma/workspace/kcms/formats/kcmformats.cpp new file mode 100644 index 0000000000..ef6bb55c0d --- /dev/null +++ b/plasma/workspace/kcms/formats/kcmformats.cpp @@ -0,0 +1,79 @@ +/* + kcmformats.cpp + SPDX-FileCopyrightText: 2014 Sebastian Kügler + SPDX-FileCopyrightText: 2021 Han Young + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include + +#include "formatssettings.h" +#include "kcmformats.h" +#include "localelistmodel.h" +#include "optionsmodel.h" + +K_PLUGIN_CLASS_WITH_JSON(KCMFormats, "kcm_formats.json") + +KCMFormats::KCMFormats(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + , m_optionsModel(new OptionsModel(this)) +{ + KAboutData *aboutData = new KAboutData(QStringLiteral("kcm_formats"), + i18nc("@title", "Formats"), + QStringLiteral("0.1"), + QLatin1String(""), + KAboutLicense::LicenseKey::GPL_V2, + i18nc("@info:credit", "Copyright 2021 Han Young")); + + aboutData->addAuthor(i18nc("@info:credit", "Han Young"), i18nc("@info:credit", "Author"), QStringLiteral("hanyoung@protonmail.com")); + + setAboutData(aboutData); + setQuickHelp(i18n("You can configure the formats used for time, dates, money and other numbers here.")); + + qmlRegisterAnonymousType("kcmformats", 1); + qmlRegisterType("LocaleListModel", 1, 0, "LocaleListModel"); + qmlRegisterAnonymousType("kcmformats_optionsmodel", 1); +} + +FormatsSettings *KCMFormats::settings() const +{ + return m_optionsModel->settings(); +} + +OptionsModel *KCMFormats::optionsModel() const +{ + return m_optionsModel; +} +QQuickItem *KCMFormats::getSubPage(int index) const +{ + return subPage(index); +} + +void KCMFormats::unset(const QString &setting) +{ + const char *entry; + if (setting == QStringLiteral("lang")) { + entry = "LANG"; + settings()->setLang(settings()->defaultLangValue()); + } else if (setting == QStringLiteral("numeric")) { + entry = "LC_NUMERIC"; + settings()->setNumeric(settings()->defaultNumericValue()); + } else if (setting == QStringLiteral("time")) { + entry = "LC_TIME"; + settings()->setTime(settings()->defaultTimeValue()); + } else if (setting == QStringLiteral("measurement")) { + entry = "LC_MEASUREMENT"; + settings()->setMeasurement(settings()->defaultMeasurementValue()); + } else { + entry = "LC_MONETARY"; + settings()->setMonetary(settings()->defaultMonetaryValue()); + } + settings()->config()->group(QStringLiteral("Formats")).deleteEntry(entry); +} +#include "kcmformats.moc" diff --git a/plasma/workspace/kcms/formats/kcmformats.h b/plasma/workspace/kcms/formats/kcmformats.h new file mode 100644 index 0000000000..0b6abaf42b --- /dev/null +++ b/plasma/workspace/kcms/formats/kcmformats.h @@ -0,0 +1,34 @@ +/* + kcmformats.h + SPDX-FileCopyrightText: 2014 Sebastian Kügler + SPDX-FileCopyrightText: 2021 Han Young + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class FormatsSettings; +class OptionsModel; +class KCMFormats : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(FormatsSettings *settings READ settings CONSTANT) + Q_PROPERTY(OptionsModel *optionsModel READ optionsModel CONSTANT) +public: + explicit KCMFormats(QObject *parent, const KPluginMetaData &data, const QVariantList &list = QVariantList()); + virtual ~KCMFormats() override = default; + + FormatsSettings *settings() const; + OptionsModel *optionsModel() const; + Q_INVOKABLE QQuickItem *getSubPage(int index) const; // proxy from KQuickAddons to Qml + Q_INVOKABLE void unset(const QString &setting); + +private: + QHash m_cachedFlags; + + OptionsModel *m_optionsModel = nullptr; +}; diff --git a/plasma/workspace/kcms/formats/localelistmodel.cpp b/plasma/workspace/kcms/formats/localelistmodel.cpp new file mode 100644 index 0000000000..e7418ed707 --- /dev/null +++ b/plasma/workspace/kcms/formats/localelistmodel.cpp @@ -0,0 +1,182 @@ +/* + * localelistmodel.cpp + * Copyright 2014 Sebastian Kügler + * Copyright 2021 Han Young + * + * 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 + */ +#include "localelistmodel.h" +#include "exampleutility.cpp" +#include "kcmformats.h" +#include +#include +LocaleListModel::LocaleListModel() +{ + QList m_locales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry); + m_localeTuples.reserve(m_locales.size() + 1); + m_localeTuples.push_back(std::tuple(i18n("Default"), i18n("Default"), i18n("Default"), QLocale())); + for (auto &locale : m_locales) { + m_localeTuples.push_back( + std::tuple(locale.nativeLanguageName(), locale.nativeCountryName(), locale.name(), locale)); + } +} +int LocaleListModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + if (m_noFilter) { + return m_localeTuples.size(); + } else { + return m_filteredLocales.size(); + } +} +QVariant LocaleListModel::data(const QModelIndex &index, int role) const +{ + int tupleIndex; + if (m_noFilter) { + tupleIndex = index.row(); + } else { + tupleIndex = m_filteredLocales.at(index.row()); + } + + const auto &[lang, country, name, locale] = m_localeTuples.at(tupleIndex); + switch (role) { + case FlagIcon: { + QString flagCode; + const QStringList split = name.split(QLatin1Char('_')); + if (split.count() > 1) { + flagCode = split[1].toLower(); + } + auto flagIconPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/locale/countries/%1/flag.png").arg(flagCode)); + return flagIconPath; + } + case DisplayName: { + const QString clabel = !country.isEmpty() ? country : QLocale::countryToString(locale.country()); + if (!lang.isEmpty()) { + return lang + QStringLiteral(" (") + clabel + QStringLiteral(")"); + } else { + return name + QStringLiteral(" (") + clabel + QStringLiteral(")"); + } + } + case LocaleName: { + QString cvalue = name; + if (!cvalue.contains(QLatin1Char('.')) && cvalue != QLatin1Char('C')) { + // explicitly add the encoding, + // otherwise Qt doesn't accept dead keys and garbles the output as well + cvalue.append(QLatin1Char('.') + QTextCodec::codecForLocale()->name()); + } + return cvalue; + } + case Example: { + switch (m_configType) { + case Lang: + return QVariant(); + case Numeric: + return Utility::numericExample(locale); + case Time: + return Utility::shortTimeExample(locale); + case Currency: + return Utility::monetaryExample(locale); + case Measurement: + return Utility::measurementExample(locale); + case Collate: + return Utility::collateExample(locale); + default: + return QVariant(); + } + } + default: + return QVariant(); + } +} + +QHash LocaleListModel::roleNames() const +{ + return {{LocaleName, "localeName"}, {DisplayName, "display"}, {FlagIcon, "flag"}, {Example, "example"}}; +} + +const QString &LocaleListModel::filter() const +{ + return m_filter; +} + +void LocaleListModel::setFilter(const QString &filter) +{ + if (m_filter == filter) { + return; + } + m_filter = filter; + filterLocale(); +} + +void LocaleListModel::filterLocale() +{ + beginResetModel(); + if (!m_filter.isEmpty()) { + m_filteredLocales.clear(); + int i{0}; + for (const auto &[lang, country, name, _locale] : m_localeTuples) { + if (lang.indexOf(m_filter, 0, Qt::CaseInsensitive) != -1) { + m_filteredLocales.push_back(i); + } else if (country.indexOf(m_filter, 0, Qt::CaseInsensitive) != -1) { + m_filteredLocales.push_back(i); + } else if (name.indexOf(m_filter, 0, Qt::CaseInsensitive) != -1) { + m_filteredLocales.push_back(i); + } + i++; + } + m_noFilter = false; + } else { + m_noFilter = true; + } + endResetModel(); +} + +QString LocaleListModel::selectedConfig() const +{ + switch (m_configType) { + case Lang: + return QStringLiteral("lang"); + case Numeric: + return QStringLiteral("numeric"); + case Time: + return QStringLiteral("time"); + case Currency: + return QStringLiteral("currency"); + case Measurement: + return QStringLiteral("measurement"); + case Collate: + return QStringLiteral("collate"); + } + // won't reach here + return QString(); +} + +void LocaleListModel::setSelectedConfig(const QString &config) +{ + if (config == QStringLiteral("lang")) { + m_configType = Lang; + } else if (config == QStringLiteral("numeric")) { + m_configType = Numeric; + } else if (config == QStringLiteral("time")) { + m_configType = Time; + } else if (config == QStringLiteral("measurement")) { + m_configType = Measurement; + } else if (config == QStringLiteral("currency")) { + m_configType = Currency; + } else { + m_configType = Collate; + } + Q_EMIT selectedConfigChanged(); + Q_EMIT dataChanged(createIndex(0, 0), createIndex(rowCount(), 0), QVector(1, Example)); +} diff --git a/plasma/workspace/kcms/formats/localelistmodel.h b/plasma/workspace/kcms/formats/localelistmodel.h new file mode 100644 index 0000000000..c53ea2fbeb --- /dev/null +++ b/plasma/workspace/kcms/formats/localelistmodel.h @@ -0,0 +1,58 @@ +/* + * localelistmodel.h + * Copyright 2014 Sebastian Kügler + * Copyright 2021 Han Young + * + * 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 + */ +#ifndef LOCALELISTMODEL_H +#define LOCALELISTMODEL_H + +#include +#include +class LocaleListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) + Q_PROPERTY(QString selectedConfig READ selectedConfig WRITE setSelectedConfig NOTIFY selectedConfigChanged) +public: + enum RoleName { DisplayName = Qt::DisplayRole, LocaleName, FlagIcon, Example }; + enum ConfigType { Lang, Numeric, Time, Currency, Measurement, Collate }; + LocaleListModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + const QString &filter() const; + void setFilter(const QString &filter); + QString selectedConfig() const; + void setSelectedConfig(const QString &config); + +Q_SIGNALS: + void filterChanged(); + void selectedConfigChanged(); + +private: + void filterLocale(); + void getExample(); + + QString m_filter; + std::vector> m_localeTuples; // lang, country, name + std::vector m_filteredLocales; + bool m_noFilter = true; + ConfigType m_configType = Lang; +}; + +#endif // LOCALELISTMODEL_H diff --git a/plasma/workspace/kcms/formats/optionsmodel.cpp b/plasma/workspace/kcms/formats/optionsmodel.cpp new file mode 100644 index 0000000000..7110f10795 --- /dev/null +++ b/plasma/workspace/kcms/formats/optionsmodel.cpp @@ -0,0 +1,140 @@ +/* + optionsmodel.cpp + SPDX-FileCopyrightText: 2021 Han Young + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include + +#include "exampleutility.cpp" +#include "formatssettings.h" +#include "optionsmodel.h" + +OptionsModel::OptionsModel(QObject *parent) + : QAbstractListModel(parent) + , m_settings(new FormatsSettings(this)) +{ + m_staticNames = {{{i18n("Region"), QStringLiteral("lang")}, + {i18n("Numbers"), QStringLiteral("numeric")}, + {i18n("Time"), QStringLiteral("time")}, + {i18n("Currency"), QStringLiteral("currency")}, + {i18n("Measurement"), QStringLiteral("measurement")}}}; + connect(m_settings, &FormatsSettings::langChanged, this, &OptionsModel::handleLangChange); + connect(m_settings, &FormatsSettings::numericChanged, this, [this] { + Q_EMIT dataChanged(createIndex(1, 0), createIndex(1, 0), {Subtitle, Example}); + }); + connect(m_settings, &FormatsSettings::timeChanged, this, [this] { + Q_EMIT dataChanged(createIndex(2, 0), createIndex(2, 0), {Subtitle, Example}); + }); + connect(m_settings, &FormatsSettings::monetaryChanged, this, [this] { + Q_EMIT dataChanged(createIndex(3, 0), createIndex(3, 0), {Subtitle, Example}); + }); + connect(m_settings, &FormatsSettings::measurementChanged, this, [this] { + Q_EMIT dataChanged(createIndex(4, 0), createIndex(4, 0), {Subtitle, Example}); + }); +} +int OptionsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_staticNames.size(); +} +QVariant OptionsModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (row < 0 || row >= (int)m_staticNames.size()) + return QVariant(); + + switch (role) { + case Name: + return m_staticNames[row].first; + case Subtitle: { + switch (row) { + case 0: + return m_settings->lang(); + case 1: + return m_settings->numeric(); + case 2: + return m_settings->time(); + case 3: + return m_settings->monetary(); + case 4: + return m_settings->measurement(); + default: + return QVariant(); + } + } + case Example: { + switch (row) { + case 0: + return QString(); + case 1: + return numberExample(); + case 2: + return timeExample(); + case 3: + return currencyExample(); + case 4: + return measurementExample(); + default: + return QVariant(); + } + } + case Page: + return m_staticNames[row].second; + default: + return QVariant(); + } +} + +QHash OptionsModel::roleNames() const +{ + return {{Name, "name"}, {Subtitle, "localeName"}, {Example, "example"}, {Page, "page"}}; +} + +void OptionsModel::handleLangChange() +{ + Q_EMIT dataChanged(createIndex(0, 0), createIndex(0, 0), {Subtitle, Example}); + + QString defaultVal = i18n("Default"); + if (m_settings->numeric() == defaultVal) { + Q_EMIT dataChanged(createIndex(1, 0), createIndex(1, 0), {Subtitle, Example}); + } + if (m_settings->time() == defaultVal) { + Q_EMIT dataChanged(createIndex(2, 0), createIndex(2, 0), {Subtitle, Example}); + } + if (m_settings->measurement() == defaultVal) { + Q_EMIT dataChanged(createIndex(3, 0), createIndex(3, 0), {Subtitle, Example}); + } + if (m_settings->monetary() == defaultVal) { + Q_EMIT dataChanged(createIndex(4, 0), createIndex(4, 0), {Subtitle, Example}); + } +} + +QString OptionsModel::numberExample() const +{ + return Utility::numericExample(localeWithDefault(m_settings->numeric())); +} +QString OptionsModel::timeExample() const +{ + return Utility::timeExample(localeWithDefault(m_settings->time())); +} +QString OptionsModel::currencyExample() const +{ + return Utility::monetaryExample(localeWithDefault(m_settings->monetary())); +} +QString OptionsModel::measurementExample() const +{ + return Utility::measurementExample(localeWithDefault(m_settings->measurement())); +} +QLocale OptionsModel::localeWithDefault(const QString &val) const +{ + if (val != i18n("Default")) { + return QLocale(val); + } else { + return QLocale(m_settings->lang()); + } +} +FormatsSettings *OptionsModel::settings() const +{ + return m_settings; +} diff --git a/plasma/workspace/kcms/formats/optionsmodel.h b/plasma/workspace/kcms/formats/optionsmodel.h new file mode 100644 index 0000000000..6319848f70 --- /dev/null +++ b/plasma/workspace/kcms/formats/optionsmodel.h @@ -0,0 +1,34 @@ +/* + optionsmodel.h + SPDX-FileCopyrightText: 2021 Han Young + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include + +class FormatsSettings; +class OptionsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { Name = Qt::DisplayRole, Subtitle, Example, Page }; + OptionsModel(QObject *parent); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + FormatsSettings *settings() const; +public Q_SLOTS: + void handleLangChange(); + +private: + QString numberExample() const; + QString timeExample() const; + QString currencyExample() const; + QString measurementExample() const; + QLocale localeWithDefault(const QString &val) const; + FormatsSettings *m_settings = nullptr; + std::array, 5> m_staticNames; // title, page +}; diff --git a/plasma/workspace/kcms/formats/package/contents/ui/main.qml b/plasma/workspace/kcms/formats/package/contents/ui/main.qml new file mode 100644 index 0000000000..43dedfd3c8 --- /dev/null +++ b/plasma/workspace/kcms/formats/package/contents/ui/main.qml @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightLabel: 2021 Han Young + + SPDX-License-Identifier: LGPL-3.0-or-later +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.15 as Kirigami +import org.kde.kcm 1.2 as KCM +import LocaleListModel 1.0 + +KCM.ScrollViewKCM { + id: root + implicitHeight: Kirigami.Units.gridUnit * 40 + implicitWidth: Kirigami.Units.gridUnit * 20 + header: Kirigami.InlineMessage { + id: helpMsg + text: i18n("Your changes will take effect the next time you log in.") + } + + view: ListView { + model: kcm.optionsModel + delegate: Kirigami.BasicListItem { + text: model.name + subtitle: model.localeName + trailing: QQC2.Label { + text: model.example + } + reserveSpaceForSubtitle: true + onClicked: { + if (kcm.depth === 1) { + localeListPage.active = true; + localeListPage.item.setting = page; + kcm.push(localeListPage.item); + } else { + kcm.getSubPage(0).setting = page; + kcm.getSubPage(0).filterText = ''; + kcm.currentIndex = 1; + } + } + } + } + + Loader { + id: localeListPage + active: false + sourceComponent: KCM.ScrollViewKCM { + property string setting: "lang" + property alias filterText: searchField.text + title: { + localeListView.currentIndex = -1; + localeListModel.selectedConfig = setting; + switch (setting) { + case "lang": + return i18n("Region"); + case "numeric": + return i18n("Numbers"); + case "time": + return i18n("Time"); + case "currency": + return i18n("Currency"); + case "measurement": + return i18n("Measurement"); + case "collate": + return i18n("Collate"); + } + } + + LocaleListModel { + id: localeListModel + } + + header: Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + onTextChanged: localeListModel.filter = text + } + + view: ListView { + id: localeListView + clip: true + model: localeListModel + delegate: Kirigami.BasicListItem { + icon: model.flag + text: model.display + subtitle: model.localeName + trailing: QQC2.Label { + color: Kirigami.Theme.disabledTextColor + text: model.example ? model.example : '' + } + onClicked: { + if (model.localeName !== i18n("Default")) { + switch (setting) { + case "lang": + kcm.settings.lang = localeName; + break; + case "numeric": + kcm.settings.numeric = localeName; + break; + case "time": + kcm.settings.time = localeName; + break; + case "currency": + kcm.settings.monetary = localeName; + break; + case "measurement": + kcm.settings.measurement = localeName; + break; + case "collate": + kcm.settings.collate = localeName; + break; + } + } else { + kcm.unset(setting); + } + + kcm.currentIndex = 0; + helpMsg.visible = true; + } + } + } + } + } +} diff --git a/plasma/workspace/kcms/icons/CMakeLists.txt b/plasma/workspace/kcms/icons/CMakeLists.txt new file mode 100644 index 0000000000..dc55555e2a --- /dev/null +++ b/plasma/workspace/kcms/icons/CMakeLists.txt @@ -0,0 +1,53 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_icons\") + +########### next target ############### + +set(kcm_icons_PART_SRCS main.cpp iconsmodel.cpp iconsizecategorymodel.cpp iconssettings.cpp ../kcms-common.cpp) + +kcmutils_generate_module_data( + kcm_icons_PART_SRCS + MODULE_DATA_HEADER iconsdata.h + MODULE_DATA_CLASS_NAME IconsData + SETTINGS_HEADERS iconssettings.h + SETTINGS_CLASSES IconsSettings +) + +kconfig_add_kcfg_files(kcm_icons_PART_SRCS iconssettingsbase.kcfgc GENERATE_MOC) +kcoreaddons_add_plugin(kcm_icons SOURCES ${kcm_icons_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings") + +target_link_libraries(kcm_icons + Qt::Widgets + Qt::Svg + KF5::KCMUtils + KF5::I18n + KF5::IconThemes + KF5::Archive + KF5::KIOWidgets + KF5::QuickAddons +) + +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/config.h CONTENT "#define CMAKE_INSTALL_FULL_LIBEXECDIR \"${CMAKE_INSTALL_FULL_LIBEXECDIR}\"") + +set(changeicons_SRCS changeicons.cpp iconssettings.cpp) + +kconfig_add_kcfg_files(changeicons_SRCS iconssettingsbase.kcfgc GENERATE_MOC) + +add_executable(plasma-changeicons ${changeicons_SRCS}) +target_link_libraries(plasma-changeicons PRIVATE Qt::Core KF5::KIOWidgets KF5::KCMUtils KF5::IconThemes KF5::I18n) + +ecm_qt_declare_logging_category(plasma-changeicons + HEADER plasma_changeicons_debug.h + IDENTIFIER PLASMA_CHANGEICONS_DEBUG + CATEGORY_NAME org.kde.plasma.changeicons +) + +install(FILES iconssettingsbase.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES icons_remove_effects.upd DESTINATION ${KDE_INSTALL_DATADIR}/kconf_update) +install(FILES kcm_icons.desktop DESTINATION ${KDE_INSTALL_APPDIR}) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/icons.knsrc ${CMAKE_BINARY_DIR}/icons.knsrc) +install( FILES ${CMAKE_BINARY_DIR}/icons.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR} ) + +install(TARGETS plasma-changeicons DESTINATION ${KDE_INSTALL_LIBEXECDIR} ) + +kpackage_install_package(package kcm_icons kcms) diff --git a/plasma/workspace/kcms/icons/Messages.sh b/plasma/workspace/kcms/icons/Messages.sh new file mode 100644 index 0000000000..4584d245c9 --- /dev/null +++ b/plasma/workspace/kcms/icons/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_icons.pot diff --git a/plasma/workspace/kcms/icons/changeicons.cpp b/plasma/workspace/kcms/icons/changeicons.cpp new file mode 100644 index 0000000000..3d171388b8 --- /dev/null +++ b/plasma/workspace/kcms/icons/changeicons.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 20016 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "iconssettings.h" +#include "plasma_changeicons_debug.h" + +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + if (argc != 2) { + return 1; + } + + // KNS will give us a path + const QStringList args = app.arguments(); + QString themeName = args.last(); + int idx = themeName.lastIndexOf('/'); + if (idx >= 0) { + themeName = themeName.mid(idx + 1); + } + + IconsSettings settings; + if (settings.theme() == themeName) { + // In KNS this will be displayed as a warning in the UI + qCWarning(PLASMA_CHANGEICONS_DEBUG).noquote() << "Icon theme is already used"; + } else { + settings.setTheme(themeName); + settings.save(); + } + return 0; +} diff --git a/plasma/workspace/kcms/icons/icons.knsrc b/plasma/workspace/kcms/icons/icons.knsrc new file mode 100644 index 0000000000..c6a1cf3a6c --- /dev/null +++ b/plasma/workspace/kcms/icons/icons.knsrc @@ -0,0 +1,51 @@ +[KNewStuff3] +Name=Icons +Name[ar]=الأيقونات +Name[ast]=Iconos +Name[az]=Nişanlar +Name[ca]=Icones +Name[cs]=Ikony +Name[da]=Ikoner +Name[de]=Symbole +Name[en_GB]=Icons +Name[es]=Iconos +Name[et]=Ikoonid +Name[eu]=Ikonoak +Name[fi]=Kuvakkeet +Name[fr]=Icônes +Name[hi]=प्रतीक +Name[hsb]=Piktogramy +Name[hu]=Ikonok +Name[ia]=Icones +Name[id]=Ikon +Name[it]=Icone +Name[ja]=アイコン +Name[ko]=아이콘 +Name[lt]=Piktogramos +Name[ml]=ചിഹ്നങ്ങൾ +Name[nl]=Pictogrammen +Name[nn]=Ikon +Name[pa]=ਆਈਕਾਨ +Name[pl]=Ikony +Name[pt]=Ícones +Name[pt_BR]=Ícones +Name[ro]=Pictograme +Name[ru]=Значки +Name[sk]=Ikony +Name[sl]=Ikone +Name[sv]=Ikoner +Name[ta]=சின்னங்கள் +Name[tg]=Нишонаҳо +Name[tr]=Simgeler +Name[uk]=Піктограми +Name[vi]=Biểu tượng +Name[x-test]=xxIconsxx +Name[zh_CN]=图标 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=KDE Icon Theme +TargetDir=icons +Uncompress=true +RemoveDeadEntries=true + +AdoptionCommand=@CMAKE_INSTALL_FULL_LIBEXECDIR@/plasma-changeicons %d diff --git a/plasma/workspace/kcms/icons/icons_remove_effects.upd b/plasma/workspace/kcms/icons/icons_remove_effects.upd new file mode 100644 index 0000000000..5abd73633e --- /dev/null +++ b/plasma/workspace/kcms/icons/icons_remove_effects.upd @@ -0,0 +1,105 @@ +Version=5 +Id=IconsRemoveEffects +File=kdeglobals +Group=DesktopIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent +Group=ToolbarIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent +Group=MainToolbarIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent +Group=SmallIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent +Group=PanelIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent +Group=DialogIcons +RemoveKey=Animated +RemoveKey=DefaultEffect +RemoveKey=DefaultValue +RemoveKey=DefaultColor +RemoveKey=DefaultColor2 +RemoveKey=DefaultSemiTransparent +RemoveKey=ActiveEffect +RemoveKey=ActiveValue +RemoveKey=ActiveColor +RemoveKey=ActiveColor2 +RemoveKey=ActiveSemiTransparent +RemoveKey=DisabledEffect +RemoveKey=DisabledValue +RemoveKey=DisabledColor +RemoveKey=DisabledColor2 +RemoveKey=DisabledSemiTransparent diff --git a/plasma/workspace/kcms/icons/iconsizecategorymodel.cpp b/plasma/workspace/kcms/icons/iconsizecategorymodel.cpp new file mode 100644 index 0000000000..c173ff4586 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconsizecategorymodel.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "iconsizecategorymodel.h" +#include +#include + +IconSizeCategoryModel::IconSizeCategoryModel(QObject *parent) + : QAbstractListModel(parent) + , m_data({ + {QStringLiteral("toolbarSize"), i18n("Toolbar"), QStringLiteral("Toolbar"), KIconLoader::Toolbar}, + {QStringLiteral("mainToolbarSize"), i18n("Main Toolbar"), QStringLiteral("MainToolbar"), KIconLoader::MainToolbar}, + {QStringLiteral("smallSize"), i18n("Small Icons"), QStringLiteral("Small"), KIconLoader::Small}, + {QStringLiteral("panelSize"), i18n("Panel"), QStringLiteral("Panel"), KIconLoader::Panel}, + {QStringLiteral("dialogSize"), i18n("Dialogs"), QStringLiteral("Dialog"), KIconLoader::Dialog}, + }) +{ +} + +IconSizeCategoryModel::~IconSizeCategoryModel() = default; + +int IconSizeCategoryModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant IconSizeCategoryModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return item.display; + case ConfigKeyRole: + return item.configKey; + case ConfigSectionRole: + return item.configSection; + case KIconLoaderGroupRole: + return item.kIconloaderGroup; + } + + return QVariant(); +} + +QHash IconSizeCategoryModel::roleNames() const +{ + QHash roleNames = QAbstractListModel::roleNames(); + roleNames[ConfigKeyRole] = QByteArrayLiteral("configKey"); + roleNames[ConfigSectionRole] = QByteArrayLiteral("configSectionRole"); + roleNames[KIconLoaderGroupRole] = QByteArrayLiteral("KIconLoaderGroup"); + return roleNames; +} diff --git a/plasma/workspace/kcms/icons/iconsizecategorymodel.h b/plasma/workspace/kcms/icons/iconsizecategorymodel.h new file mode 100644 index 0000000000..3897764213 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconsizecategorymodel.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +struct IconSizeCategoryModelData { + QString configKey; + QString display; + QString configSection; + int kIconloaderGroup; +}; + +Q_DECLARE_TYPEINFO(IconSizeCategoryModelData, Q_MOVABLE_TYPE); + +class IconSizeCategoryModel : public QAbstractListModel +{ + Q_OBJECT + +public: + IconSizeCategoryModel(QObject *parent); + ~IconSizeCategoryModel() override; + + enum Roles { + ConfigKeyRole = Qt::UserRole + 1, + ConfigSectionRole, + KIconLoaderGroupRole, + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void load(); + +Q_SIGNALS: + void categorySelectedIndexChanged(); + +private: + QVector m_data; +}; diff --git a/plasma/workspace/kcms/icons/iconsmodel.cpp b/plasma/workspace/kcms/icons/iconsmodel.cpp new file mode 100644 index 0000000000..ce7a6d44ab --- /dev/null +++ b/plasma/workspace/kcms/icons/iconsmodel.cpp @@ -0,0 +1,164 @@ +/* + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + SPDX-FileCopyrightText: 2000 Antonio Larrosa + SPDX-FileCopyrightText: 2000 Geert Jansen + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + KDE Frameworks 5 port + SPDX-FileCopyrightText: 2013 Jonathan Riddell + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "iconsmodel.h" + +#include +#include + +#include + +#include "iconssettings.h" + +IconsModel::IconsModel(IconsSettings *iconsSettings, QObject *parent) + : QAbstractListModel(parent) + , m_settings(iconsSettings) +{ +} + +IconsModel::~IconsModel() = default; + +int IconsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant IconsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return item.display; + case ThemeNameRole: + return item.themeName; + case DescriptionRole: + return item.description; + case RemovableRole: + return item.removable; + case PendingDeletionRole: + return item.pendingDeletion; + } + + return QVariant(); +} + +bool IconsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return false; + } + + if (role == PendingDeletionRole) { + auto &item = m_data[index.row()]; + + const bool pendingDeletion = value.toBool(); + + if (item.pendingDeletion != pendingDeletion) { + item.pendingDeletion = pendingDeletion; + Q_EMIT dataChanged(index, index, {PendingDeletionRole}); + + // if we delete current selected theme move to the next non-pending theme + const auto nonPending = match(index, PendingDeletionRole, false); + if (m_settings->theme() == index.data(ThemeNameRole) && !nonPending.isEmpty()) { + m_settings->setTheme(nonPending.first().data(ThemeNameRole).toString()); + } + Q_EMIT pendingDeletionsChanged(); + return true; + } + } + + return false; +} + +QHash IconsModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {ThemeNameRole, QByteArrayLiteral("themeName")}, + {RemovableRole, QByteArrayLiteral("removable")}, + {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")}, + }; +} + +void IconsModel::load() +{ + beginResetModel(); + + m_data.clear(); + + const QStringList themes = KIconTheme::list(); + + m_data.reserve(themes.count()); + + for (const QString &themeName : themes) { + KIconTheme theme(themeName); + if (!theme.isValid()) { + // qCWarning(KCM_ICONS) << "Not a valid theme" << themeName; + } + if (theme.isHidden()) { + continue; + } + + IconsModelData item{ + theme.name(), + themeName, + theme.description(), + themeName != KIconTheme::defaultThemeName() && QFileInfo(theme.dir()).isWritable(), + false // pending deletion + }; + + m_data.append(item); + } + + // Sort case-insensitively + QCollator collator; + collator.setCaseSensitivity(Qt::CaseInsensitive); + std::sort(m_data.begin(), m_data.end(), [&collator](const IconsModelData &a, const IconsModelData &b) { + return collator.compare(a.display, b.display) < 0; + }); + + endResetModel(); +} + +QStringList IconsModel::pendingDeletions() const +{ + QStringList pendingDeletions; + + for (const auto &item : m_data) { + if (item.pendingDeletion) { + pendingDeletions.append(item.themeName); + } + } + + return pendingDeletions; +} + +void IconsModel::removeItemsPendingDeletion() +{ + for (int i = m_data.count() - 1; i >= 0; --i) { + if (m_data.at(i).pendingDeletion) { + beginRemoveRows(QModelIndex(), i, i); + m_data.remove(i); + endRemoveRows(); + } + } +} diff --git a/plasma/workspace/kcms/icons/iconsmodel.h b/plasma/workspace/kcms/icons/iconsmodel.h new file mode 100644 index 0000000000..218f36e169 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconsmodel.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + KDE Frameworks 5 port: + SPDX-FileCopyrightText: 2013 Jonathan Riddell + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class IconsSettings; + +struct IconsModelData { + QString display; + QString themeName; + QString description; + bool removable; + bool pendingDeletion; +}; +Q_DECLARE_TYPEINFO(IconsModelData, Q_MOVABLE_TYPE); + +class IconsModel : public QAbstractListModel +{ + Q_OBJECT + +public: + IconsModel(IconsSettings *iconsSettings, QObject *parent = nullptr); + ~IconsModel() override; + + enum Roles { + ThemeNameRole = Qt::UserRole + 1, + DescriptionRole, + RemovableRole, + PendingDeletionRole, + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QHash roleNames() const override; + + QStringList pendingDeletions() const; + void removeItemsPendingDeletion(); + + void load(); + +Q_SIGNALS: + void pendingDeletionsChanged(); + +private: + QVector m_data; + IconsSettings *m_settings; +}; diff --git a/plasma/workspace/kcms/icons/iconssettings.cpp b/plasma/workspace/kcms/icons/iconssettings.cpp new file mode 100644 index 0000000000..9f9b473722 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconssettings.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include +#include + +#include "iconssettings.h" + +IconsSettings::IconsSettings(QObject *parent) + : IconsSettingsBase(parent) + , m_themeDirty(false) +{ + connect(this, &IconsSettings::configChanged, this, &IconsSettings::updateIconTheme); + connect(this, &IconsSettings::ThemeChanged, this, &IconsSettings::updateThemeDirty); +} + +IconsSettings::~IconsSettings() +{ +} + +void IconsSettings::updateThemeDirty() +{ + m_themeDirty = theme() != KIconTheme::current(); +} + +void IconsSettings::updateIconTheme() +{ + if (m_themeDirty) { + KIconTheme::reconfigure(); + + KSharedDataCache::deleteCache(QStringLiteral("icon-cache")); + + for (int i = 0; i < KIconLoader::LastGroup; i++) { + KIconLoader::emitChange(KIconLoader::Group(i)); + } + + KBuildSycocaProgressDialog::rebuildKSycoca(nullptr); + } +} diff --git a/plasma/workspace/kcms/icons/iconssettings.h b/plasma/workspace/kcms/icons/iconssettings.h new file mode 100644 index 0000000000..4875e91a63 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconssettings.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2019 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "iconssettingsbase.h" + +class IconsSettings : public IconsSettingsBase +{ + Q_OBJECT +public: + IconsSettings(QObject *parent = nullptr); + ~IconsSettings() override; +public Q_SLOTS: + void updateIconTheme(); + void updateThemeDirty(); + +private: + bool m_themeDirty; +}; diff --git a/plasma/workspace/kcms/icons/iconssettingsbase.kcfg b/plasma/workspace/kcms/icons/iconssettingsbase.kcfg new file mode 100644 index 0000000000..4c50f0fb3d --- /dev/null +++ b/plasma/workspace/kcms/icons/iconssettingsbase.kcfg @@ -0,0 +1,49 @@ + + + + + + + breeze + + + + + + 32 + + + + + + 22 + + + + + + 22 + + + + + + 16 + + + + + + 48 + + + + + + 32 + + + diff --git a/plasma/workspace/kcms/icons/iconssettingsbase.kcfgc b/plasma/workspace/kcms/icons/iconssettingsbase.kcfgc new file mode 100644 index 0000000000..008f6c3e87 --- /dev/null +++ b/plasma/workspace/kcms/icons/iconssettingsbase.kcfgc @@ -0,0 +1,7 @@ +File=iconssettingsbase.kcfg +ClassName=IconsSettingsBase +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=Theme diff --git a/plasma/workspace/kcms/icons/kcm_icons.desktop b/plasma/workspace/kcms/icons/kcm_icons.desktop new file mode 100644 index 0000000000..3d41b5849c --- /dev/null +++ b/plasma/workspace/kcms/icons/kcm_icons.desktop @@ -0,0 +1,48 @@ +[Desktop Entry] +Icon=preferences-desktop-icons +Type=Application +Exec=systemsettings kcm_icons +NoDisplay=true + +Name=Icons +Name[ar]=الأيقونات +Name[ast]=Iconos +Name[az]=Nişanlar +Name[ca]=Icones +Name[cs]=Ikony +Name[da]=Ikoner +Name[de]=Symbole +Name[en_GB]=Icons +Name[es]=Iconos +Name[et]=Ikoonid +Name[eu]=Ikonoak +Name[fi]=Kuvakkeet +Name[fr]=Icônes +Name[hi]=प्रतीक +Name[hsb]=Piktogramy +Name[hu]=Ikonok +Name[ia]=Icones +Name[id]=Ikon +Name[it]=Icone +Name[ja]=アイコン +Name[ko]=아이콘 +Name[lt]=Piktogramos +Name[ml]=ചിഹ്നങ്ങൾ +Name[nl]=Pictogrammen +Name[nn]=Ikon +Name[pa]=ਆਈਕਾਨ +Name[pl]=Ikony +Name[pt]=Ícones +Name[pt_BR]=Ícones +Name[ro]=Pictograme +Name[ru]=Значки +Name[sk]=Ikony +Name[sl]=Ikone +Name[sv]=Ikoner +Name[ta]=சின்னங்கள் +Name[tg]=Нишонаҳо +Name[tr]=Simgeler +Name[uk]=Піктограми +Name[vi]=Biểu tượng +Name[x-test]=xxIconsxx +Name[zh_CN]=图标 diff --git a/plasma/workspace/kcms/icons/kcm_icons.json b/plasma/workspace/kcms/icons/kcm_icons.json new file mode 100644 index 0000000000..0d1cf65333 --- /dev/null +++ b/plasma/workspace/kcms/icons/kcm_icons.json @@ -0,0 +1,115 @@ +{ + "KPlugin": { + "Description": "Choose icon theme", + "Description[ar]": "اختر سمة الأيقونة", + "Description[az]": "İkon mövzusunu seçin", + "Description[ca]": "Trieu el tema d'icones", + "Description[cs]": "Vyberte motiv ikon", + "Description[de]": "Symbol-Design auswählen", + "Description[en_GB]": "Choose icon theme", + "Description[es]": "Escoger un tema de iconos", + "Description[eu]": "Aukeratu ikono-gaia", + "Description[fi]": "Valitse kuvaketeema", + "Description[fr]": "Sélectionner un thème d'icônes", + "Description[hu]": "Ikontéma kiválasztása", + "Description[ia]": "Selige le thema de icone", + "Description[it]": "Scegli il tema di icone", + "Description[ko]": "아이콘 테마 선택", + "Description[lt]": "Pasirinkti piktogramų apipavidalinimą", + "Description[nl]": "Pictogramthema kiezen", + "Description[nn]": "Vel ikontema", + "Description[pa]": "ਆਈਕਾਨ ਥੀਮ ਚੁਣੋ", + "Description[pl]": "Wybierz zestaw ikon", + "Description[pt_BR]": "Escolha o tema de ícones", + "Description[ro]": "Alege tematica pictogramelor", + "Description[ru]": "Выбор набора значков", + "Description[sk]": "Vybrať tému ikon", + "Description[sl]": "Izberite temo ikon", + "Description[sv]": "Välj ikontema", + "Description[ta]": "சின்னங்களுக்கான திட்டத்தை தேர்ந்தெடுங்கள்", + "Description[tr]": "Simge teması seçin", + "Description[uk]": "Вибір теми піктограм", + "Description[vi]": "Chọn chủ đề của biểu tượng", + "Description[x-test]": "xxChoose icon themexx", + "Description[zh_CN]": "选择图标主题", + "FormFactors": [ + "tablet", + "handset", + "desktop" + ], + "Icon": "preferences-desktop-icons", + "Name": "Icons", + "Name[ar]": "الأيقونات", + "Name[ast]": "Iconos", + "Name[az]": "Nişanlar", + "Name[ca]": "Icones", + "Name[cs]": "Ikony", + "Name[da]": "Ikoner", + "Name[de]": "Symbole", + "Name[en_GB]": "Icons", + "Name[es]": "Iconos", + "Name[et]": "Ikoonid", + "Name[eu]": "Ikonoak", + "Name[fi]": "Kuvakkeet", + "Name[fr]": "Icônes", + "Name[hi]": "प्रतीक", + "Name[hsb]": "Piktogramy", + "Name[hu]": "Ikonok", + "Name[ia]": "Icones", + "Name[id]": "Ikon", + "Name[it]": "Icone", + "Name[ja]": "アイコン", + "Name[ko]": "아이콘", + "Name[lt]": "Piktogramos", + "Name[ml]": "ചിഹ്നങ്ങൾ", + "Name[nl]": "Pictogrammen", + "Name[nn]": "Ikon", + "Name[pa]": "ਆਈਕਾਨ", + "Name[pl]": "Ikony", + "Name[pt]": "Ícones", + "Name[pt_BR]": "Ícones", + "Name[ro]": "Pictograme", + "Name[ru]": "Значки", + "Name[sk]": "Ikony", + "Name[sl]": "Ikone", + "Name[sv]": "Ikoner", + "Name[ta]": "சின்னங்கள்", + "Name[tg]": "Нишонаҳо", + "Name[tr]": "Simgeler", + "Name[uk]": "Піктограми", + "Name[vi]": "Biểu tượng", + "Name[x-test]": "xxIconsxx", + "Name[zh_CN]": "图标" + }, + "X-DocPath": "kcontrol/icons/index.html", + "X-KDE-Keywords": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography", + "X-KDE-Keywords[ar]": "أيقونات,تأثيرات,حجم,تغيير الرموز,أزرار,سمة زر,سمة رمز,سمة,أيقونات", + "X-KDE-Keywords[az]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,nişanlar,effektlər,kəskin rənglər,zəif rəng,nişanı dəyişmək,düymələr,düymə mövzusu,nişan mövzusu,mövzu,ikonaqrafiya", + "X-KDE-Keywords[ca]": "icones,efectes,mida,color alt,color baix,canvi d'icones,botons,tema de botons,tema d'icones,tema,iconografia", + "X-KDE-Keywords[en_GB]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography", + "X-KDE-Keywords[es]": "iconos,efectos,tamaño,hicolor,locolor,cambiar iconos,botones,tema de botones,tema de iconos,tema,iconografía", + "X-KDE-Keywords[eu]": "ikonoak,efektuak,neurria,sakonera handiko kolorea,sakonera txikiko kolorea,aldatu ikonoak,botoiak,botoien gaia,ikonoen gaia,gaia,ikonografia", + "X-KDE-Keywords[fi]": "kuvake,kuvakkeet,tehosteet,koko,täysväri,vaihda kuvakkeet,painikkeet,painiketeema,kuvaketeema,teema,ikoni,ikonit", + "X-KDE-Keywords[fr]": "icônes, effets, taille, couleurs haute définition, couleurs basse définition, changement d'icônes, boutons, thème de boutons, thème d'icônes, thème, iconographie", + "X-KDE-Keywords[hi]": "प्रतीक,प्रभाव,आकार,है-कलर,लो-कलर,चिह्न बदलना,बटन,बटन प्रसंग,आइकन प्रसंग,थीम,आइकनोग्राफी", + "X-KDE-Keywords[hu]": "ikonok,effektusok,méret,hicolor,locolor,ikonok cseréje,gombok,gombtéma,ikontéma,téma,ikonográfia", + "X-KDE-Keywords[ia]": "icones,effectos,grandor,hicolor,locolor,cambia icones,buttones,thema de button, thema de icone,thema,iconographia", + "X-KDE-Keywords[it]": "icone,effetti,dimensioni,hicolor,locolor,cambia icone,pulsanti,tema pulsanti,tema icone,tema,iconografia", + "X-KDE-Keywords[ko]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,아이콘,효과,크기,색상,아이콘 변경,버튼,단추,단추 테마,아이콘 테마,테마", + "X-KDE-Keywords[nl]": "pictogrammen,effecten,grootte,hi-kleur,lo-kleur,pictogrammen wijzigen,knoppen,knoppenthema,thema,iconografie", + "X-KDE-Keywords[nn]": "ikon,effektar,storleik,hicolor,locolor,endra ikon,knappar,knappetema,ikontema,tema,ikonografi", + "X-KDE-Keywords[pl]": "ikony,efekty,rozmiar,hicolor,locolor,zmiana ikon,przyciski,wygląd przycisków,wygląd ikon,wygląd,ikonografia", + "X-KDE-Keywords[pt]": "ícones,efeitos,tamanho,muitas cores,poucas cores,mudar os ícones,botões,tema de botões,tema de ícones,tema,iconografia", + "X-KDE-Keywords[pt_BR]": "ícones,efeitos,tamanho,hicolor,locolor,alterar ícones,botão,tema de botões,tema de ícones,tema,iconografia", + "X-KDE-Keywords[ru]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,значки,эффекты,размер,изменение значков,кнопки,оформление кнопок,темы,оформление", + "X-KDE-Keywords[sk]": "ikony,efekty,veľkosť,hicolor,locolor,zmena ikon,tlačidlá,téma tlačidiel,téma ikon,téma,ikonografia", + "X-KDE-Keywords[sl]": "ikone,učinki,svetli toni,temni toni,sprememba ikon,gumbi,tema gumbov,tema ikon,tema,ikonografija", + "X-KDE-Keywords[sv]": "ikoner,effekter,storlek,många färger,få färger,ändra ikoner,knappar,knapptema,ikontema,tema,ikonografi", + "X-KDE-Keywords[ta]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography, சின்னம், சின்னங்கள், பட்டன், பட்டன்கள், மென்மேடு, பொத்தான், தோற்றத்திட்டம், திட்டம், திட்டமுறை", + "X-KDE-Keywords[uk]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,піктограми,значки,іконки,ефекти,розмір,колір,змінити піктограми,кнопки,тема кнопок,тема піктограм,тема значків,тема,піктографія", + "X-KDE-Keywords[vi]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,biểu tượng,hiệu ứng,cỡ,màu cao,màu thấp,thay đổi biểu tượng,nút,chủ đề nút,chủ đề biểu tượng,chủ đề,biểu tượng học", + "X-KDE-Keywords[x-test]": "xxiconsxx,xxeffectsxx,xxsizexx,xxhicolorxx,xxlocolorxx,xxchange iconsxx,xxbuttonsxx,xxbutton themexx,xxicon themexx,xxthemexx,xxiconographyxx", + "X-KDE-Keywords[zh_CN]": "icons,effects,size,hicolor,locolor,change icons,buttons,button theme,icon theme,theme,iconography,图标,效果,特效,动效,大小,图标大小,高彩色,低彩色,多颜色,少颜色,更改图标,修改图标,替换图标,按钮,按钮主题,图标主题,主题,图像学", + "X-KDE-System-Settings-Parent-Category": "appearance", + "X-KDE-Weight": 60 +} diff --git a/plasma/workspace/kcms/icons/main.cpp b/plasma/workspace/kcms/icons/main.cpp new file mode 100644 index 0000000000..3ded340075 --- /dev/null +++ b/plasma/workspace/kcms/icons/main.cpp @@ -0,0 +1,477 @@ +/* + main.cpp + + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + SPDX-FileCopyrightText: 2000 Antonio Larrosa + SPDX-FileCopyrightText: 2000 Geert Jansen + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Benjamin Port + + KDE Frameworks 5 port: + SPDX-FileCopyrightText: 2013 Jonathan Riddell + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" +#include "../kcms-common_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include // for unlink + +#include "iconsdata.h" +#include "iconsizecategorymodel.h" +#include "iconsmodel.h" +#include "iconssettings.h" + +#include "config.h" // for CMAKE_INSTALL_FULL_LIBEXECDIR + +K_PLUGIN_FACTORY_WITH_JSON(IconsFactory, "kcm_icons.json", registerPlugin(); registerPlugin();) + +IconModule::IconModule(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KQuickAddons::ManagedConfigModule(parent, data, args) + , m_data(new IconsData(this)) + , m_model(new IconsModel(m_data->settings(), this)) + , m_iconSizeCategoryModel(new IconSizeCategoryModel(this)) +{ + auto uri = "org.kde.private.kcms.icons"; + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + qmlRegisterAnonymousType(uri, 1); + + // to be able to access its enums + qmlRegisterUncreatableType(uri, 1, 0, "KIconLoader", QString()); + + setButtons(Apply | Default); + + connect(m_model, &IconsModel::pendingDeletionsChanged, this, &IconModule::settingsChanged); + + // When user has a lot of themes installed, preview pixmaps might get evicted prematurely + QPixmapCache::setCacheLimit(50 * 1024); // 50 MiB +} + +IconModule::~IconModule() +{ +} + +IconsSettings *IconModule::iconsSettings() const +{ + return m_data->settings(); +} + +IconsModel *IconModule::iconsModel() const +{ + return m_model; +} + +IconSizeCategoryModel *IconModule::iconSizeCategoryModel() const +{ + return m_iconSizeCategoryModel; +} + +bool IconModule::downloadingFile() const +{ + return m_tempCopyJob; +} + +QList IconModule::availableIconSizes(int group) const +{ + const auto themeName = iconsSettings()->theme(); + if (!m_kiconThemeCache.contains(iconsSettings()->theme())) { + m_kiconThemeCache.insert(themeName, new KIconTheme(themeName)); + } + return m_kiconThemeCache[themeName]->querySizes(static_cast(group)); +} + +void IconModule::load() +{ + ManagedConfigModule::load(); + m_model->load(); + // Model has been cleared so pretend the theme name changed to force view update + Q_EMIT iconsSettings()->ThemeChanged(); +} + +void IconModule::save() +{ + bool needToExportToKDE4 = iconsSettings()->isSaveNeeded(); + + // keep track of Group of icons size that has changed + QList notifyList; + for (int i = 0; i < m_iconSizeCategoryModel->rowCount(); ++i) { + const QModelIndex index = m_iconSizeCategoryModel->index(i, 0); + const QString key = index.data(IconSizeCategoryModel::ConfigKeyRole).toString(); + if (iconsSettings()->findItem(key)->isSaveNeeded()) { + notifyList << index.data(IconSizeCategoryModel::KIconLoaderGroupRole).toInt(); + } + } + + ManagedConfigModule::save(); + + if (needToExportToKDE4) { + // Is this still needed? + exportToKDE4(); + } + + processPendingDeletions(); + + // Notify the group(s) where icon sizes have changed + for (auto group : qAsConst(notifyList)) { + KIconLoader::emitChange(KIconLoader::Group(group)); + } +} + +bool IconModule::isSaveNeeded() const +{ + return !m_model->pendingDeletions().isEmpty(); +} + +void IconModule::processPendingDeletions() +{ + const QStringList pendingDeletions = m_model->pendingDeletions(); + + for (const QString &themeName : pendingDeletions) { + Q_ASSERT(themeName != iconsSettings()->theme()); + + KIconTheme theme(themeName); + auto *job = KIO::del(QUrl::fromLocalFile(theme.dir()), KIO::HideProgressInfo); + // needs to block for it to work on "OK" where the dialog (kcmshell) closes + job->exec(); + } + + m_model->removeItemsPendingDeletion(); +} + +void IconModule::ghnsEntriesChanged() +{ + // reload the display icontheme items + KIconTheme::reconfigure(); + KIconLoader::global()->newIconLoader(); + load(); + QPixmapCache::clear(); +} + +void IconModule::installThemeFromFile(const QUrl &url) +{ + if (url.isLocalFile()) { + installThemeFile(url.toLocalFile()); + return; + } + + if (m_tempCopyJob) { + return; + } + + m_tempInstallFile.reset(new QTemporaryFile()); + if (!m_tempInstallFile->open()) { + Q_EMIT showErrorMessage(i18n("Unable to create a temporary file.")); + m_tempInstallFile.reset(); + return; + } + + m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + Q_EMIT downloadingFileChanged(); + + connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + if (job->error() != KJob::NoError) { + Q_EMIT showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText())); + return; + } + + installThemeFile(m_tempInstallFile->fileName()); + m_tempInstallFile.reset(); + }); + connect(m_tempCopyJob, &QObject::destroyed, this, &IconModule::downloadingFileChanged); +} + +void IconModule::installThemeFile(const QString &path) +{ + const QStringList themesNames = findThemeDirs(path); + if (themesNames.isEmpty()) { + Q_EMIT showErrorMessage(i18n("The file is not a valid icon theme archive.")); + return; + } + + if (!installThemes(themesNames, path)) { + Q_EMIT showErrorMessage(i18n("A problem occurred during the installation process; however, most of the themes in the archive have been installed")); + return; + } + + Q_EMIT showSuccessMessage(i18n("Theme installed successfully.")); + + KIconLoader::global()->newIconLoader(); + m_model->load(); +} + +void IconModule::exportToKDE4() +{ + // TODO: killing the kde4 icon cache: possible? (kde4migration doesn't let access the cache folder) + Kdelibs4Migration migration; + QString configFilePath = migration.saveLocation("config"); + if (configFilePath.isEmpty()) { + return; + } + + configFilePath += QLatin1String("kdeglobals"); + + KSharedConfigPtr kglobalcfg = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); + KConfig kde4config(configFilePath, KConfig::SimpleConfig); + + KConfigGroup kde4IconGroup(&kde4config, "Icons"); + kde4IconGroup.writeEntry("Theme", iconsSettings()->theme()); + + // Synchronize icon effects + for (int row = 0; row < m_iconSizeCategoryModel->rowCount(); row++) { + QModelIndex idx(m_iconSizeCategoryModel->index(row, 0)); + QString group = m_iconSizeCategoryModel->data(idx, IconSizeCategoryModel::ConfigSectionRole).toString(); + const QString groupName = group + QLatin1String("Icons"); + KConfigGroup cg(kglobalcfg, groupName); + KConfigGroup kde4Cg(&kde4config, groupName); + + // HACK copyTo only copies keys, it doesn't replace the entire group + // which means if we removed the effects in our config it won't remove + // them from the kde4 config, hence revert all of them prior to copying + const QStringList keys = cg.keyList() + kde4Cg.keyList(); + for (const QString &key : keys) { + kde4Cg.revertToDefault(key); + } + // now copy over the new values + cg.copyTo(&kde4Cg); + } + + kde4config.sync(); + + QProcess *cachePathProcess = new QProcess(this); + connect(cachePathProcess, &QProcess::finished, this, [cachePathProcess](int exitCode, QProcess::ExitStatus status) { + if (status == QProcess::NormalExit && exitCode == 0) { + QString path = cachePathProcess->readAllStandardOutput().trimmed(); + path.append(QLatin1String("icon-cache.kcache")); + QFile::remove(path); + } + + // message kde4 apps that icon theme has changed + for (int i = 0; i < KIconLoader::LastGroup; ++i) { + notifyKcmChange(GlobalChangeType::IconChanged, KIconLoader::Group(i)); + } + + cachePathProcess->deleteLater(); + }); + cachePathProcess->start(QStringLiteral("kde4-config"), {QStringLiteral("--path"), QStringLiteral("cache")}); +} + +QStringList IconModule::findThemeDirs(const QString &archiveName) +{ + QStringList foundThemes; + + KTar archive(archiveName); + archive.open(QIODevice::ReadOnly); + const KArchiveDirectory *themeDir = archive.directory(); + + KArchiveEntry *possibleDir = nullptr; + KArchiveDirectory *subDir = nullptr; + + // iterate all the dirs looking for an index.theme or index.desktop file + const QStringList entries = themeDir->entries(); + for (const QString &entry : entries) { + possibleDir = const_cast(themeDir->entry(entry)); + if (!possibleDir->isDirectory()) { + continue; + } + + subDir = dynamic_cast(possibleDir); + if (!subDir) { + continue; + } + + if (subDir->entry(QStringLiteral("index.theme")) || subDir->entry(QStringLiteral("index.desktop"))) { + foundThemes.append(subDir->name()); + } + } + + archive.close(); + return foundThemes; +} + +bool IconModule::installThemes(const QStringList &themes, const QString &archiveName) +{ + bool everythingOk = true; + const QString localThemesDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/icons/./")); + + Q_EMIT showProgress(i18n("Installing icon themes…")); + + KTar archive(archiveName); + archive.open(QIODevice::ReadOnly); + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + + const KArchiveDirectory *rootDir = archive.directory(); + + KArchiveDirectory *currentTheme = nullptr; + for (const QString &theme : themes) { + Q_EMIT showProgress(i18n("Installing %1 theme…", theme)); + + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + + currentTheme = dynamic_cast(const_cast(rootDir->entry(theme))); + if (!currentTheme) { + // we tell back that something went wrong, but try to install as much + // as possible + everythingOk = false; + continue; + } + + currentTheme->copyTo(localThemesDir + theme); + } + + archive.close(); + + Q_EMIT hideProgress(); + return everythingOk; +} + +QVariantList IconModule::previewIcons(const QString &themeName, int size, qreal dpr, int limit) +{ + static QVector s_previewIcons{ + {QStringLiteral("system-run"), QStringLiteral("exec")}, + {QStringLiteral("folder")}, + {QStringLiteral("document"), QStringLiteral("text-x-generic")}, + {QStringLiteral("user-trash"), QStringLiteral("user-trash-empty")}, + {QStringLiteral("help-browser"), QStringLiteral("system-help"), QStringLiteral("help-about"), QStringLiteral("help-contents")}, + {QStringLiteral("preferences-system"), QStringLiteral("systemsettings"), QStringLiteral("configure")}, + + {QStringLiteral("text-html")}, + {QStringLiteral("image-x-generic"), QStringLiteral("image-png"), QStringLiteral("image-jpeg")}, + {QStringLiteral("video-x-generic"), QStringLiteral("video-x-theora+ogg"), QStringLiteral("video-mp4")}, + {QStringLiteral("x-office-document")}, + {QStringLiteral("x-office-spreadsheet")}, + {QStringLiteral("x-office-presentation"), QStringLiteral("application-presentation")}, + + {QStringLiteral("user-home")}, + {QStringLiteral("user-desktop"), QStringLiteral("desktop")}, + {QStringLiteral("folder-image"), QStringLiteral("folder-images"), QStringLiteral("folder-pictures"), QStringLiteral("folder-picture")}, + {QStringLiteral("folder-documents")}, + {QStringLiteral("folder-download"), QStringLiteral("folder-downloads")}, + {QStringLiteral("folder-video"), QStringLiteral("folder-videos")}}; + + // created on-demand as it is quite expensive to do and we don't want to do it every loop iteration either + QScopedPointer theme; + + QVariantList pixmaps; + + for (const QStringList &iconNames : s_previewIcons) { + const QString cacheKey = themeName + QLatin1Char('@') + QString::number(size) + QLatin1Char('@') + QString::number(dpr, 'f', 1) + QLatin1Char('@') + + iconNames.join(QLatin1Char(',')); + + QPixmap pix; + if (!QPixmapCache::find(cacheKey, &pix)) { + if (!theme) { + theme.reset(new KIconTheme(themeName)); + } + + pix = getBestIcon(*theme.data(), iconNames, size, dpr); + + // Inserting a pixmap even if null so we know whether we searched for it already + QPixmapCache::insert(cacheKey, pix); + } + + if (pix.isNull()) { + continue; + } + + pixmaps.append(pix); + + if (limit > -1 && pixmaps.count() >= limit) { + break; + } + } + + return pixmaps; +} + +QPixmap IconModule::getBestIcon(KIconTheme &theme, const QStringList &iconNames, int size, qreal dpr) +{ + QSvgRenderer renderer; + + const int iconSize = size * dpr; + + // not using initializer list as we want to unwrap inherits() + const QStringList themes = QStringList() << theme.internalName() << theme.inherits(); + for (const QString &themeName : themes) { + KIconTheme theme(themeName); + + for (const QString &iconName : iconNames) { + QString path = theme.iconPath(QStringLiteral("%1.png").arg(iconName), iconSize, KIconLoader::MatchBest); + if (!path.isEmpty()) { + QPixmap pixmap(path); + pixmap.setDevicePixelRatio(dpr); + return pixmap; + } + + // could not find the .png, try loading the .svg or .svgz + path = theme.iconPath(QStringLiteral("%1.svg").arg(iconName), iconSize, KIconLoader::MatchBest); + if (path.isEmpty()) { + path = theme.iconPath(QStringLiteral("%1.svgz").arg(iconName), iconSize, KIconLoader::MatchBest); + } + + if (path.isEmpty()) { + continue; + } + + if (!renderer.load(path)) { + continue; + } + + QPixmap pixmap(iconSize, iconSize); + pixmap.setDevicePixelRatio(dpr); + pixmap.fill(QColor(Qt::transparent)); + QPainter p(&pixmap); + p.setViewport(0, 0, size, size); + renderer.render(&p); + return pixmap; + } + } + + return QPixmap(); +} + +int IconModule::pluginIndex(const QString &themeName) const +{ + const auto results = m_model->match(m_model->index(0, 0), ThemeNameRole, themeName, 1, Qt::MatchExactly); + if (results.count() == 1) { + return results.first().row(); + } + return -1; +} + +void IconModule::defaults() +{ + for (int i = 0, count = m_model->rowCount(QModelIndex()); i < count; ++i) { + m_model->setData(m_model->index(i), false, IconsModel::Roles::PendingDeletionRole); + } + ManagedConfigModule::defaults(); +} + +#include "main.moc" diff --git a/plasma/workspace/kcms/icons/main.h b/plasma/workspace/kcms/icons/main.h new file mode 100644 index 0000000000..fdad6a616f --- /dev/null +++ b/plasma/workspace/kcms/icons/main.h @@ -0,0 +1,110 @@ +/* + main.h + + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 Benjamin Port + + KDE Frameworks 5 port: + SPDX-FileCopyrightText: 2013 Jonathan Riddell + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +class KIconTheme; +class IconsSettings; +class IconsData; + +class QQuickItem; +class QTemporaryFile; + +namespace KIO +{ +class FileCopyJob; +} + +class IconsModel; +class IconSizeCategoryModel; + +class IconModule : public KQuickAddons::ManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(IconsSettings *iconsSettings READ iconsSettings CONSTANT) + Q_PROPERTY(IconsModel *iconsModel READ iconsModel CONSTANT) + Q_PROPERTY(IconSizeCategoryModel *iconSizeCategoryModel READ iconSizeCategoryModel CONSTANT) + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + +public: + IconModule(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + ~IconModule() override; + + enum Roles { + ThemeNameRole = Qt::UserRole + 1, + DescriptionRole, + RemovableRole, + PendingDeletionRole, + }; + + IconsSettings *iconsSettings() const; + IconsModel *iconsModel() const; + IconSizeCategoryModel *iconSizeCategoryModel() const; + + bool downloadingFile() const; + + void load() override; + Q_INVOKABLE void reloadConfig() + { + ManagedConfigModule::load(); + } + + void save() override; + void defaults() override; + + Q_INVOKABLE void ghnsEntriesChanged(); + Q_INVOKABLE void installThemeFromFile(const QUrl &url); + + Q_INVOKABLE QList availableIconSizes(int group) const; + + Q_INVOKABLE int pluginIndex(const QString &pluginName) const; + + // QML doesn't understand QList, hence wrapped in a QVariantList + Q_INVOKABLE QVariantList previewIcons(const QString &themeName, int size, qreal dpr, int limit = -1); + +Q_SIGNALS: + void downloadingFileChanged(); + + void showSuccessMessage(const QString &message); + void showErrorMessage(const QString &message); + + void showProgress(const QString &message); + void hideProgress(); + +private: + bool isSaveNeeded() const override; + + void processPendingDeletions(); + + static QStringList findThemeDirs(const QString &archiveName); + bool installThemes(const QStringList &themes, const QString &archiveName); + void installThemeFile(const QString &path); + + void exportToKDE4(); + + static QPixmap getBestIcon(KIconTheme &theme, const QStringList &iconNames, int size, qreal dpr); + + IconsData *m_data; + IconsModel *m_model; + IconSizeCategoryModel *m_iconSizeCategoryModel; + + mutable QCache m_kiconThemeCache; + + QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; +}; diff --git a/plasma/workspace/kcms/icons/package/contents/ui/IconSizePopup.qml b/plasma/workspace/kcms/icons/package/contents/ui/IconSizePopup.qml new file mode 100644 index 0000000000..a5fa0dac10 --- /dev/null +++ b/plasma/workspace/kcms/icons/package/contents/ui/IconSizePopup.qml @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kcm 1.3 as KCM + +QtControls.Popup { + id: iconSizePopup + + width: 400 + modal: true + + onOpened: { + // can we do this automatically with "focus: true" somewhere? + iconTypeList.forceActiveFocus(); + } + + onVisibleChanged: { + if (visible) { + iconSizeSlider.sizes = kcm.availableIconSizes(iconTypeList.currentIndex); + iconSizeSlider.updateSizes() + } + } + + Connections { + target: iconTypeList + function onCurrentIndexChanged() { + iconSizeSlider.sizes = kcm.availableIconSizes(iconTypeList.currentIndex); + } + } + + RowLayout { + anchors.fill: parent + + ColumnLayout { + id: iconSizeColumn + Layout.fillWidth: true + + QtControls.ItemDelegate { // purely for metrics... + id: measureDelegate + visible: false + } + + QtControls.ScrollView { + id: iconTypeScroll + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: iconTypeList.count * measureDelegate.height + 4 + activeFocusOnTab: false + + Component.onCompleted: iconTypeScroll.background.visible = true; + + ListView { + id: iconTypeList + activeFocusOnTab: true + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveDuration: 0 + clip: true + + model: kcm.iconSizeCategoryModel + currentIndex: 0 // Initialize with the first item + + Keys.onLeftPressed: { + LayoutMirroring.enabled ? iconSizeSlider.increase() : iconSizeSlider.decrease() + iconSizeSlider.moved(); + } + Keys.onRightPressed: { + LayoutMirroring.enabled ? iconSizeSlider.decrease() : iconSizeSlider.increase() + iconSizeSlider.moved(); + } + + delegate: QtControls.ItemDelegate { + width: ListView.view.width + highlighted: ListView.isCurrentItem + text: model.display + readonly property string configKey: model.configKey + onClicked: { + ListView.view.currentIndex = index; + ListView.view.forceActiveFocus(); + } + } + } + } + + QtControls.Slider { + id: iconSizeSlider + property var sizes: kcm.availableIconSizes(iconTypeList.currentIndex) + + Layout.fillWidth: true + from: 0 + to: sizes.length - 1 + stepSize: 1.0 + snapMode: QtControls.Slider.SnapAlways + + KCM.SettingStateBinding { + configObject: kcm.iconsSettings + settingName: iconTypeList.currentItem.configKey + extraEnabledConditions: parent.sizes.length > 0 + } + + onMoved: { + kcm.iconsSettings[iconTypeList.currentItem.configKey] = iconSizeSlider.sizes[iconSizeSlider.value] || 0 + } + + function updateSizes() { + // since the icon sizes are queried using invokables, always force an update when opening + // in case the user clicked Default or something + value = Qt.binding(function() { + var iconSize = kcm.iconsSettings[iconTypeList.currentItem.configKey] + + // I have no idea what this code does but it works and is just copied from the old KCM + var index = -1; + var delta = 1000; + for (var i = 0, length = sizes.length; i < length; ++i) { + var dw = Math.abs(iconSize - sizes[i]); + if (dw < delta) { + delta = dw; + index = i; + } + } + + return index; + }); + } + } + } + + ColumnLayout { + Layout.fillHeight: true + Layout.minimumWidth: Math.round(parent.width / 2) + Layout.maximumWidth: Math.round(parent.width / 2) + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + Kirigami.Icon { + anchors.centerIn: parent + width: kcm.iconsSettings[iconTypeList.currentItem.configKey] + height: width + source: "folder" + } + } + + QtControls.Label { + id: iconSizeLabel + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: kcm.iconsSettings[iconTypeList.currentItem.configKey] + } + } + } +} diff --git a/plasma/workspace/kcms/icons/package/contents/ui/main.qml b/plasma/workspace/kcms/icons/package/contents/ui/main.qml new file mode 100644 index 0000000000..f3958cc4a0 --- /dev/null +++ b/plasma/workspace/kcms/icons/package/contents/ui/main.qml @@ -0,0 +1,292 @@ +/* + SPDX-FileCopyrightText: 2018 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtQuick.Dialogs 1.0 as QtDialogs +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.14 as Kirigami +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons +import org.kde.newstuff 1.81 as NewStuff +import org.kde.kcm 1.3 as KCM + +import org.kde.private.kcms.icons 1.0 as Private + +KCM.GridViewKCM { + id: root + KCM.ConfigModule.quickHelp: i18n("This module allows you to choose the icons for your desktop.") + + view.model: kcm.iconsModel + view.currentIndex: kcm.pluginIndex(kcm.iconsSettings.theme) + enabled: !kcm.downloadingFile + + KCM.SettingStateBinding { + configObject: kcm.iconsSettings + settingName: "Theme" + } + + DropArea { + enabled: view.enabled + anchors.fill: parent + onEntered: { + if (!drag.hasUrls) { + drag.accepted = false; + } + } + onDropped: kcm.installThemeFromFile(drop.urls[0]) + } + + view.delegate: KCM.GridDelegate { + id: delegate + + text: model.display + toolTip: model.description + + thumbnailAvailable: typeof thumbFlow.previews === "undefined" || thumbFlow.previews.length > 0 + thumbnail: MouseArea { + id: thumbArea + + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + clip: thumbFlow.y < 0 + + opacity: model.pendingDeletion ? 0.3 : 1 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + + Timer { + interval: 1000 + repeat: true + running: thumbArea.containsMouse + onRunningChanged: { + if (!running) { + thumbFlow.currentPage = 0; + } + } + onTriggered: { + if (!thumbFlow.allPreviesLoaded) { + thumbFlow.loadPreviews(-1 /*no limit*/); + thumbFlow.allPreviesLoaded = true; + } + + ++thumbFlow.currentPage; + if (thumbFlow.currentPage >= thumbFlow.pageCount) { + stop(); + } + } + } + + Flow { + id: thumbFlow + + // undefined is "didn't load preview yet" + // empty array is "no preview available" + property var previews + // initially we only load 6 and when the animation starts we'll load the rest + property bool allPreviesLoaded: false + + property int currentPage + readonly property int pageCount: Math.ceil(thumbRepeater.count / (thumbFlow.columns * thumbFlow.rows)) + + readonly property int iconWidth: Math.floor(thumbArea.width / thumbFlow.columns) + readonly property int iconHeight: Math.floor(thumbArea.height / thumbFlow.rows) + + readonly property int columns: 3 + readonly property int rows: 2 + + function loadPreviews(limit) { + previews = kcm.previewIcons(model.themeName, Math.min(thumbFlow.iconWidth, thumbFlow.iconHeight), Screen.devicePixelRatio, limit); + } + + width: parent.width + y: -currentPage * iconHeight * rows + + Behavior on y { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + + Repeater { + id: thumbRepeater + model: thumbFlow.previews + + Item { + width: thumbFlow.iconWidth + height: thumbFlow.iconHeight + + KQCAddons.QPixmapItem { + anchors.centerIn: parent + width: Math.min(parent.width, nativeWidth) + height: Math.min(parent.height, nativeHeight) + // load on demand and avoid leaking a tiny corner of the icon + pixmap: thumbFlow.y < 0 || index < (thumbFlow.columns * thumbFlow.rows) ? modelData : undefined + smooth: true + fillMode: KQCAddons.QPixmapItem.PreserveAspectFit + } + } + } + + Component.onCompleted: { + // avoid reloading it when icon sizes or dpr changes on startup + Qt.callLater(function() { + // We show 6 icons initially (3x2 grid), only load those + thumbFlow.loadPreviews(6 /*limit*/); + }); + } + } + } + + actions: [ + Kirigami.Action { + iconName: "edit-delete" + tooltip: i18n("Remove Icon Theme") + enabled: model.removable + visible: !model.pendingDeletion + onTriggered: model.pendingDeletion = true + }, + Kirigami.Action { + iconName: "edit-undo" + tooltip: i18n("Restore Icon Theme") + visible: model.pendingDeletion + onTriggered: model.pendingDeletion = false + } + ] + onClicked: { + if (!model.pendingDeletion) { + kcm.iconsSettings.theme = model.themeName; + } + view.forceActiveFocus(); + } + onDoubleClicked: { + kcm.save(); + } + } + + footer: ColumnLayout { + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + showCloseButton: true + + Connections { + target: kcm + function onShowSuccessMessage(message) { + infoLabel.type = Kirigami.MessageType.Positive; + infoLabel.text = message; + infoLabel.visible = true; + } + function onShowErrorMessage(message) { + infoLabel.type = Kirigami.MessageType.Error; + infoLabel.text = message; + infoLabel.visible = true; + } + } + } + + RowLayout { + id: progressRow + visible: false + + QtControls.BusyIndicator { + id: progressBusy + } + + QtControls.Label { + id: progressLabel + Layout.fillWidth: true + textFormat: Text.PlainText + wrapMode: Text.WordWrap + } + + Connections { + target: kcm + onShowProgress: { + progressLabel.text = message; + progressBusy.running = true; + progressRow.visible = true; + } + onHideProgress: { + progressBusy.running = false; + progressRow.visible = false; + } + } + } + + RowLayout { + Layout.fillWidth: true + // Using a non-flat toolbutton here, so it matches the items in the actiontoolbar + // (a Button is just ever so slightly smaller than a ToolButton, and it would look + // kind of silly if the buttons aren't the same size) + QtControls.ToolButton { + id: iconSizesButton + text: i18n("Configure Icon Sizes") + icon.name: "transform-scale" // proper icon? + display: QtControls.ToolButton.TextBesideIcon + flat: false + checkable: true + checked: iconSizePopupLoader.item && iconSizePopupLoader.item.opened + onClicked: { + iconSizePopupLoader.active = true; + iconSizePopupLoader.item.open(); + } + } + + Kirigami.ActionToolBar { + flat: false + alignment: Qt.AlignRight + actions: [ + Kirigami.Action { + enabled: root.view.enabled + text: i18n("Install from File…") + icon.name: "document-import" + onTriggered: fileDialogLoader.active = true + }, + NewStuff.Action { + text: i18n("Get New Icons…") + configFile: "icons.knsrc" + onEntryEvent: function (entry, event) { + if (event == 1) { // StatusChangedEvent + kcm.ghnsEntriesChanged(); + } else if (event == 2) { // AdoptedEvent + kcm.reloadConfig(); + } + } + } + ] + } + } + } + + Loader { + id: iconSizePopupLoader + active: false + sourceComponent: IconSizePopup { + parent: iconSizesButton + y: -height + } + } + + Loader { + id: fileDialogLoader + active: false + sourceComponent: QtDialogs.FileDialog { + title: i18n("Open Theme") + folder: shortcuts.home + nameFilters: [ i18n("Theme Files (*.tar.gz *.tar.bz2)") ] + Component.onCompleted: open() + onAccepted: { + kcm.installThemeFromFile(fileUrls[0]) + fileDialogLoader.active = false + } + onRejected: { + fileDialogLoader.active = false + } + } + } +} diff --git a/plasma/workspace/kcms/kcms-common.cpp b/plasma/workspace/kcms/kcms-common.cpp new file mode 100644 index 0000000000..e319abe226 --- /dev/null +++ b/plasma/workspace/kcms/kcms-common.cpp @@ -0,0 +1,15 @@ +/* + SPDX-FileCopyrightText: 2021 Ahmad Samir + + SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include "kcms-common_p.h" + +void notifyKcmChange(GlobalChangeType changeType, int arg) +{ + QDBusMessage message = + QDBusMessage::createSignal(QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), QStringLiteral("notifyChange")); + message.setArguments({changeType, arg}); + QDBusConnection::sessionBus().send(message); +} diff --git a/plasma/workspace/kcms/kcms-common_p.h b/plasma/workspace/kcms/kcms-common_p.h new file mode 100644 index 0000000000..b96d5296c3 --- /dev/null +++ b/plasma/workspace/kcms/kcms-common_p.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2021 Ahmad Samir + + SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#pragma once + +#include +#include + +// These two enums are copied from KHintSettings (which copied them from KGlobalSettings) +enum GlobalChangeType { + PaletteChanged = 0, + FontChanged, + StyleChanged, // 2 + SettingsChanged, + IconChanged, + CursorChanged, // 5 + ToolbarStyleChanged, + ClipboardConfigChanged, + BlockShortcuts, + NaturalSortingChanged, +}; + +enum GlobalSettingsCategory { + SETTINGS_MOUSE, + SETTINGS_COMPLETION, + SETTINGS_PATHS, + SETTINGS_POPUPMENU, + SETTINGS_QT, + SETTINGS_SHORTCUTS, + SETTINGS_LOCALE, + SETTINGS_STYLE, +}; + +void notifyKcmChange(GlobalChangeType changeType, int arg = 0); diff --git a/plasma/workspace/kcms/kfontinst/CMakeLists.txt b/plasma/workspace/kcms/kfontinst/CMakeLists.txt new file mode 100644 index 0000000000..b3f7bf793d --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/CMakeLists.txt @@ -0,0 +1,37 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kfontinst\") + +if (X11_Xft_FOUND) + check_include_files(locale.h HAVE_LOCALE_H) + configure_file(config-fontinst.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-fontinst.h) + include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/lib + ${CMAKE_CURRENT_BINARY_DIR}/lib + ${CMAKE_CURRENT_SOURCE_DIR}/dbus + ${CMAKE_CURRENT_BINARY_DIR}/dbus + ${CMAKE_CURRENT_SOURCE_DIR}/viewpart + ${CMAKE_CURRENT_SOURCE_DIR}/kcmfontinst + ${CMAKE_CURRENT_BINARY_DIR}/kcmfontinst) + + set(libkfontinstdbusiface_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/dbus/FontinstIface.cpp) + set(libkfontinstview_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/viewpart/FontPreview.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/viewpart/PreviewSelectAction.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/viewpart/CharTip.cpp ) + set(libkfontinstactionlabel_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/kcmfontinst/ActionLabel.cpp) + set(libkfontinstjobrunner_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/kcmfontinst/JobRunner.cpp + ${libkfontinstactionlabel_SRCS} + ${CMAKE_CURRENT_SOURCE_DIR}/kcmfontinst/FontsPackage.cpp) + + add_subdirectory( lib ) + add_subdirectory( dbus ) + add_subdirectory( kcmfontinst ) + add_subdirectory( apps ) + add_subdirectory( kio ) + add_subdirectory( thumbnail ) + add_subdirectory( viewpart ) + + ecm_install_icons(ICONS sc-apps-preferences-desktop-font-installer.svgz DESTINATION ${KDE_INSTALL_ICONDIR}) + +endif () diff --git a/plasma/workspace/kcms/kfontinst/ChangeLog b/plasma/workspace/kcms/kfontinst/ChangeLog new file mode 100644 index 0000000000..fe87b58aea --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/ChangeLog @@ -0,0 +1,562 @@ +KDE4.4 +====== +1. Create a dbus daemon for handling font installtion, and have the kcm and ioslave + use this. +2. Use policykit to control installation of system-wide fonts. +3. Merged progress, skip, cancel, and error dialogs used when installing, etc., into + 1 dialog. +4. Simplify layout. +5. Remove simple mode - always use font management mode. +6. When multiple fonts are selected, use a list style preview of all selected fonts. +7. Because of the above, remove the in-line previews from the font list. + +KDE4.3 +====== +1. Dont cache previews to disk. + +KDE4.2 +====== +1. Added zoom controls to font preview. +2. Made viewer application a KUniqueApplication + +KDE3.5 -> KDE4.0 +================ + 1. Enabling/disabling of fonts. + 2. Creation of "Font Groups" + 3. Fonts are now grouped via family name, i.e. shown as: + + Courier [4] + - Times [2] + Regular + Italic + ...where the number in brackets indicates the number of styles. Clicking + on the expand icon (+) will then show the list of styles. + 4. Add ability in font view part to show unicode tables. + 5. Remove all references to Speedo fonts - haven't been supported since + KDE3.3! + 6. Only one view - list view. However, each item now has a small preview. + 7. Xft is now required. + 8. Remove Fontmap creation. + 9. Creation of fonts.dir & fonts.scale will be done via mkfontscale and + mkfontdir. +10. Legacy X is only configured for a folder if it already contains a fonts.dir + file. +11. Font installtion is *much* faster - as config files are now updated after + all fonts are installed, as opposed to every 50 fonts. +12. Folders are no longer added to X's config files - because of 10 above. + The only system config file that will be altered is either + /etc/fonts/local.conf or /etc/fonts/conf.d/00kde.conf (dependant upon + your fontconfig version) +13. When installing font files, install into a sub folder named after the 1st + character. e.g. + wibble.ttf -> ~/.fonts/w/wibble.ttf +14. Better unique names when creating font packages. +15. Use zip and not tar.gz for font packages. +16. Allow import of fonts/package. +17. Allow fonts/package to be installed via konqueror service menu. +18. Better TTC handling. +19. When installing to fonts:/, as non-root, automatically install to fonts:/Personal. + To install to fonts:/System need to explicity copy to fonts:/System. +20. Hide kfontview from KMenu - its only required by kcm, and when cliking on a font. +21. Renamed kcmfontinst to just fontinst. +22. When printing, use external kfontprint app - that way newly installed fonts can + also be printed. +23. When run as non-root, the kcontrol module will have a combo box allowing the user + to select Personal or System fonts. +24. Better bitmap font previews - list of sizes obtained via fontconfig. +25. Regular fonts listed as fonts:/, Regular - e.g. fonts:/Times, Regular +26. Use a kio_font_helper app when accesing fonts:/System - much faster, as kdesu is + not required to be called for each action. +27. Add a "Find Duplicates" tool, that looks for multiple installtions of the same font. + i.e. different locations, or the same location but different filename case + (e.g. times.ttf / times.TTF) + +KDE3.4 -> KDE3.5 +================ +1. When copying a file out of fonts sub-system, copy as filename, or .fonts.tar.gz + in the case of multiple font files mapped to the same font name. + + e.g. Times New Roman -> times.ttf + Helvetica, Bold Oblique-> Hevetica, Bold Oblique.fonts.tar.gz + Which contains: + 75dpi_helvBO10.pcf.gz + 75dpi_helvBO12.pcf.gz + 100dpi_helvBO10.pcf.gz + 100dpi_helvBO12.pcf.gz + ...etc + +2. New mimetype: fonts/package - to cater for the above. +3. Add settings to enable/disable configuring fonts for legacy X, and Ghostscript + (X defaults to true, and Ghostscript defaults to false) +4. Show mime-type in detailed view. +5. Simple font sample printing - but only of installed fonts! +6. Add toggle button to control display of bitmap fonts. + +KDE3.3 -> KDE3.4 +================ +1. Font listing comes from fontconfig. This means that fonts will be grouped, i.e. + previously each size of a bitmap font was shown seperately, now only 1 font + will be displayed which represents all sizes. +2. Only fonts, and not folders (except System and Personal), are now shown. +3. Creation of afms from pfa/pfb and a pfm file. +4. Previews are drawn via Xft - previously FreeType was called directly. +5. New font preview look. +6. No longer dependant upon file extension. +7. Check for FPE of "fontconfig" -> if set, then no need to configure X core fonts. + +KDE3.2 -> KDE3.3 +================ +1. List fonts as "Full Name" -> i.e. "Times New Roman". +2. Preview of bitmap fonts. +3. No fontname-title in thumbnails - as fonts:/ lists the fontnames! +4. When copying to fonts:/ (as non-root) only ask for destination if more than 5 seconds + since previously asked. +5. Add a konqueror service menu "Install" +6. Use FreeType2 for reading Type1 - instead of parsing the pfa/pfb header. +7. FamilyName is now the fonts *real* family name - no adding of style information. +8. Add extra style information (which was previously added to FamilyName) into the XLFD. +9. Consider regular weight to be medium (same as mkfontscale). +10. Default to width=normal, weight=medium if not set. +11. Allow change of preview string. +12. Allow zooming in/out of preview. +13. Waterfall font preview. +14. Use font preview part in the KControl module - less code duplication. +15. Include simple fontviwer app - basically just an application wrapper for the viewpart. + +KDE3.1 -> KDE3.2 +================ +1. Re-designed (yet again...) to be a kio slave. As a user, starting fonts:/ will display + + Personal Lists contents of $HOME/.fonts and $KDEHOME/share/fonts (where previous installer installed to) + Fonts are installed to $HOME/.fonts + + System Lists contents of /usr/local/share/fonts, /usr/share/fonts, and /usr/X11R6/lib/X11/fonts + Fonts are installed to /usr/local/share/fonts (as per FHS) + + To install fonts system wide, just drop onto "System" and root's password will be asked + for. + + As root, fonts:/ will show the same as fonts:/System (but without the System part...) + +2. New kcontrol module that uses fonts:/ +3. Removed: + AFM creation -- only really required (TTF wise) for SO <6.0 + StarOffice configuration (S0 6.0 / OO.o is *much* better anyway) +4. X font server (xfs) - if used - is refreshed by sending a SIGUSR1 instead of + relying on a /etc/init.d/xfs script. This is much more portable. +5. Simple FontView part for konqueror - this is basically a big re-sizable preview of the font. +6. Speed up creation of fonts.dir and fonts.scale - by reading in any existing files, and using the + entries from these instead of loading and testing the font (if listed). +7. Better font preview and thumbnails. +8. Only add a dir to fontpath if fonts.dir has greater than 0 entries! +9. Use /etc/fonts/local.conf as root fontconfig file. +10. Only add dirs to fontconfig if *not* a sub-dir of an existing dir. +11. Ensure that top-level fonts dir is always in fontpath. +12. A Fontmap file is created in each sub dir, which is then combined into 1 top level Fontmap file. + ~/.fonts/Fontmap for normal users, and /etc/fonts/Fontmap for root. +13. Modify /Fontmap to contain: + (/etc/fonts/Fontmap) .runlibfile + ...as this is the system-wide Fontmap file created. As for the per-user, one, hmmm... +14. When a folder is configured, ensure fonts.dir/fonts.scale/Fontmap/.fonts-config-timestamp (SuSE + specific) all have the same timestamp (if they exist). Helps to discover if a folder has been + modified - in which case it needs to be reconfigured (and should happen automatically). +15. Add support for TrueType Collections (.ttc), and OpenType (.otf) fonts. Currently TTC's are only + configured for X - need to also configure GS to see other faces. +16. CID fonts are *not* handled - therefore don't list the X11 CID directory, and don't let users + try to create this. +17. Don't list "encodings" in fonts:/System - and don't allow users to create this. +18. Use XFree86's libfontenc (if found) to read font-encodings. +19. Handle 1bpp glyphs in thumbnail code. +20. Ensure X fontpaths *never* end in "/" - i.e. when write XF86Config, xfs/config, + or fontpaths remove any trailing "/" +21. When adding/removing an unscaled dir from X font path, ensure ":unscaled" is + part of the path! +22. Call fc-cache on top-level dir, not on each dir. +23. Use "~" in Xft config and user X config files -> e.g. /home/user/.fonts -> ~/.fonts +24. Remove top-level dir spec from top-level fontmap, e.g. + + TimesNewRomanPSMT (/home/user/.fonts/wibble/times.ttf); + + ...becomes... + + TimesNewRomanPSMT (wibble/times.ttf); + +25. Add meta-data for AFM files to KFile plugin. + +0.11-> KDE3.1 +============= +1. Re-design of UI - removed "Install From" view. +2. Created a KIO/thumbnail font preview class. +3. Fonts are installed/uninstalled on "Apply". +4. Settings are saved on "Apply". +5. Settings tab simplified - some uneccesary settings removed. +6. Removal of Anti-Alias tab - relevant settings moved to kcmfonts. +7. Add kfile-plugin to display Meta data for fonts. +8. Remove settings wizard. +9. Drop use of internal version numbering - not tagging CVS anyway, so whats the point? +10. DCOP interface. +11. Remember size of main window when run via kcmshell. + +0.10->0.11 (KDE3.0) +=================== +1. Port to KDE3/Qt3. +2. Add support for CUPS's Fontmap. +3. Create backups of system files. +4. When install symbol encoding fonts, set encoding to "glyphs-fontspecific" in XftConfig. +5. When install monospaced fonts, set spacing to mono in XftConfig. +6. When first run (as root) - checks XFree86 config file to see if a font server is being used, if + so then fs/config is used as the config file, and "/etc/rc.d/init.d/xfs restart" is selected as + as the X refresh command. +7. Only install fonts that are useable. +8. Add checkbox to enable overwriting of existing AFMs. +9. Remember open directories in advanced mode. +10. Add support for .Z compressed Bitmap fonts. +11. Read Type1 encodings from .afm files if listed as "array" in pfa/pfb. + +0.10b11->0.10 +============= +1. Version added to KDE CVS. +2. Modified some keyboard shortcuts to remove conflicts. +3. Disable "Touch" and "Delete" folder if top-level X fonts dir is selected. + +0.10b10->0.10b11 (Test version...) +================ +1. Removed "root"/"Modify" and "Help" buttons - this gives more space to font lists, plus when using "root"/"Modify" root's + config files are not being saved. +2. "IsFixedPitch" flag in AFMs produced incorrectly - was outputing "false" for monospaced fonts! +3. Added rounding to AFM metric scaling. +4. When creating AFMs, check that each characters' BBox is within the main BBox - this is a quick fix for wingdings.afm, + which seems to be giving incorrect results. +5. StarOffice 6 / OpenOffice only need AFM files for Type1 fonts - plus no config files need to be altered. Therefore, added the ability to + select which font types AFMs should be created for. +6. Output *all* characters from a font into the AFM file. +7. Fixed a bug with Full/Family name in Speedo fonts. +8. For TrueType, Type1, and Speedo fonts - family name is obtained by using the fonts' FullName, remove FamilyName (read from file), remove + any weight, width, or italic designation, and re-add FamilyName. (This is because some fonts are named + , and was previously being lost). +9. When adding encodings to lists, check that they aren't alredy inserted. + +0.10b9->0.10b10 (Test version...) +=============== +1. Fixed a problem with non-enabled install button in basic mode - again, thanks to Hardy Griech for spotting this. + +0.10b8->0.10b9 (Test version...) +============== + +*** NOTE +*** Please remove any Kfontinst generated StarOffice psstd.fonts and Ghostscript Fontmap output before using this version + +1. StarOffice psstd.fonts generated output is no longer marked line-by line, instead it is marked as a section, e.g. + + # kfontinst /usr/X11R6/lib/X11/fonts/TrueType + + # kfontinst /usr/X11R6/lib/X11/fonts/TrueType + + ...Likewise for Ghostscript's Fontmap + +2. Limited generated StarOffice psstd.fonts lines to 126 characters, and lines longher than this will not be output. It appears + as if this is the max line len StarOffice will accept - thanks to Hardy Griech for discovering this. +3. Fixed a bug where a static pointer was not reset to NULL when module was unloaded. +4. When chekcing ps-fonts, I was looking for the string "%!PS-Adbobe", however the hershey fonts just has "%!FontType" - therefore + I've change the code to just look for "%!" +5. For pcf fonts, look for FAMILY as well as FAMILY_NAME +6. Construct name from xlfd for bitmap fonts where can't get seperate components + +0.10b7->0.10b8 (Test version...) +============== + +*** NOTE +*** Please remove any Kfontinst generated Ghostscript and/or StarOffice output before using this version + +1. Forgot to extract foundry from bitmap fonts - however, changed bitmap Xlfd creation, see below. +2. Extract Xlfd from Bitmap fonts directly - not all fonts have each seperate component available. Thanks to Claudio Bandaloukas + for helping me discover the various bugs with Bitmap output. +3. When displaying bitmap details, if individual entries (family, point size, etc) can't be read, then the + Xlfd will be displayed. +4. Ghostscript & StarOffice include guards changed from "kfontinst" to "kfi" -- this will *require" removing of any previous output! +5. Shortened generated TrueType foundry fields - to help with StarOffice + +0.10b6->0.10b7 (Test version...) +============== +1. Fixed a bug with string-to-width conversion for Type1 and bitmap fonts +2. Fixed some compile bugs if no Xft.h found +3. Remove any fonts.alias when deleting a dir +4. When try to open Type1 or Speedo fonts, check magic numbers - for Speedo check char[0]=='D' or 'd', char[1]==num, char[2]=='.', and char[3]==num + +0.10b5->0.10b6 (Test version...) +============== +1. Fonts with "Normal" weight now installed as "Medium" +2. Added support for "Oblique" in xlfd +3. Check is performed to see if destination is writeable before enabling "Install" button, likewise for the "Remove" button. + +0.10b4->0.10b5 (Test version...) +============== +1. Forgot to reset the made-changes state of XftConfig when saved! +2. Add a validator to math & edit line-edits to disallow usage of double-quotes & tabs +3. Select correct default entries for field-name combos when adding an XftRule. +4. Removed check for number of items in match list, as these are not always required (such as for the sub-pixel hinting + rule.) +5. When setting rgba - use symbolic name - previously always setting to 0! +6. Modified help a little +7. Reduced Advanced mode list-view treeStepSize to 10 pixels (from default of 20), this makes it easier for browsing + /usr/X11R6/lib/X11/fonts/etc... +8. Fixed bug where could not remove Xft exclude range! +9. Fixed a bug where uninstalled items could cause duplicates in "Install from" list. +10. Renamed the "Configure System" button to "Apply" - this should make the neccessity of the option more obvious. +11. Fixed display of uninstall folder. +12. Added "include" and "includeif" directives from XftConfig to editor. + +0.10b3->0.10b4 (Test version...) +============== + +*** NOTE +*** Please delete your existing ~/.kde/share/config/kfontinstrc -or- ~/.kde2/share/config/kfontinstrc file +*** before using this version + +1. Added support for X font server config files. +2. Show "unscaled" directories in italic. +3. Abilty to set directories as scaled/unscaled. +4. Modified GUI slightly so that it works beter with Liquid style. +5. Added chack to make sure XftConfig file exists before trying to parse. +6. Advanced editing of XftConfig. +7. Added help on XftConfig - from Danny Tholen (obiwan@mailmij.org) +8. Removed some memory leaks +10. Default folders changed for non-root users. KFontinst will now (upon initial start-up) select the following: + + X fonts dir: $KDEHOME/share/fonts + XConfig file: $KDEHOME/share/fonts/fontpaths + XftConfig file: $HOME/.xftconfig + Fontmap file: $KDEHOME/share/fonts/Fontmap + + ...This should make it possible for users to install fonts without being root. However, problems may arrise when + configuring StarOffice - as this requires some other files to be modified, which won't be possible if SO has been installed + by root. + + ...Also to accomplish this, some changes are needed to 'startkde' - see file README.startkde + +11. Because of the above, when started as non-root, KFontinst will create Type1 and TrueType sub-folders in + $KDEHOME/share/fonts - if they do not already exist. +12. Moved XftConfig stuff from a sub-page of settings tab into its own tab. +13. Added question dialog if module is unloaded before system has been configured. +14. Added ability to "touch" a X font folder - marking it as being modified, so that a re-configure of the that folder + can be done in order to create AFMs, modify encoding, etc. + +0.10b2->0.10b3 (Test version...) +============== +1. When locating Ghostscript's Fontmap file, sub-directories (up to a level of 4) are searched - this allows + for the possiblity of users using a different version of Ghostscript. +2. Create a fonts.scale as well as fonts.dir - just incase another program runs mkfontdir. +3. Fixed an error when creating AFMs for some symbol-encoded fonts. +4. Re-worded SettingsWizard "Folders/Files" tab. +5. Added a checkbox to Ghostscript configuration. +6. Added support for XftConfig. +7. Added/fixed support of Type1 fonts with no FullName or FamilyName fields - such as the hershey fonts. +8. Fixed some bugs when configuring with --enable-final. +9. Basic html help added. + +0.10b1->0.10b2 (Test version...) +============== +1. Minor compile error (struct declared as private, but used elsewhere!) + +0.9.2->0.10b1 (Test version...) +============= +1. Almost a complete re-write, +2. Handles Speedo, and Bitmap fonts +3. fonts.dir & encodings.dir are now created internally - no need for ttmkfdir +4. Re-design of GUI. +5. Advanced mode where X11 folder structure is displayed and all font types may be installed - and a Basic + mode where the X11 folder structure is hidden, and only TrueType and Type1 fonts may be installed. +6. Settings wizard. +7. Application is now a kcontrol module. +8. Complete X11 fonts directory structure is now managed - no need for seperate 'Managed' directory. +9. X11.PS is no longer created, instead the StarOffice printer file (*.PS) is now modified. +10. Ported to FreeType2. +11. Support more encodings - encodings combos now list standard encodings as well as those read from + .enc(.gz) files. +12. Internal AMF creator for Type1 and TrueType fonts - ttf2pt1 and pf2afm.ps are no longer used/supplied. +13. Removed the 'Process AFMs' & 'Delete AFMs' options - all AFMs are created be KFontinst, therefore they + should be OK for StarOffice and AbiWord. +14. If a writable XF86Config file is found - then complete folders may be installed, and folders in the X11 + directory may be uninstalled or disabled (i.e. the folder is not deleted, but it's entry is removed from + the XF86Config file). +15. No longer supply .enc files with KFontinst - they should be provided by the distro. + +0.9.1->0.9.2 +============ +1. Removed a bug where the "Configure System" menu entry was always disabled! +2. Spelling error in Settings dialog. +3. t1lib has problems with some of the fonts supplied with Adobe acrobat - therefore, if t1lib fails to load the + font, then KFontinst itself will try to read the header information (although no preview will be available, + everything else should still work). +4. Fixed multiple installing of programs in other/ directory. For instance KFontinst's version of ttmkfdir + was being installed into $(PREFIX) (usually /usr/bin) as well as $(KDE_DATADIR)/kfontinst - this was incorrect + as KFontinst will only use the version in $(KDE_DATADIR)/kfontinst, and it was possible that a previous version + of ttmkfdir (such as that supplied with XFree86) would have been overwritten. +5. Fixed bug where the user was allowed to select (and subsequently install) fonts which could not be loaded correctly. +6. Added the ability to enter a custom preview string. + +0.9->0.9.1 +========== +1. Fixed a few compile errors. +2. Fix to html formatting error. +3. Fixed a ./configure error if t1lib was not found (the string NO was being used as the + librarary name, instead of an empty string!) +4. Modified the reading of Type1 header information. + +0.8.3->0.9 +========== +1. Converted to KDE2. +2. Rearranged this file! +3. Removed command line interface - KDE2's command line stuff is way different! +4. Re-created dialogs with Qt designer. +5. Removed ProgressDialog, and replaced with a progress bar on a new statusbar. +6. Modified configure script to check for FreeType & t1lib. +7. If an encoding (not unicode) is selected, then the .enc file is copied to the X11 fonts directory. +8. As with the .enc files, the StarOffice .xpp files are also copied, and no longer just sym linked. +9. Removed enabling/disabing of Configure System button. +10. Fontmap.X11 is no longer created, instead the real Fontmap file itself is modified. + +0.8.2->0.8.3 +============ +1. Modified 'kfontinst.kdelnk' so that kdesu is now used - so that a user will automatically be prompted + for the root password. +2. Modified dialogs to use the KDE caption ("Font Installer") instead of the app name ("kfontinst") +3. Corrected size of Configure dialog. +4. Fixed a minor bug where if all fonts were uninstalled, the 'Configure System' button was disabled - therefore not + allowing you to activate the changes! +5. Added a command line interface. (type 'kfontinst --help' for details) +6. Added option to automatically fix TTF postscript names upon install. + +0.8.1->0.8.2 +============ +1. Fixed a bug which always had SO configuration disabled! +2. Fixed a bug when selecting Unicode encoding. +3. Changed "Fonts/Uninstalled" menu entry to "Fonts/Disk" +4. Added keyboard short-cuts to dialogs + +0.8->0.8.1 +========== +1. Fixed a bug where X configuration would fail if no TT fonts present. +2. If no fonts are installed, then the system configuration button/menu-entry is now disabled. +3. Changed menu structure to add 'Fonts' menu. + +0.7.4->0.8 +========== +1. Changed location of StarOffice stuf from /xp3 to just + -- As StarOffice 5.2 has 'xp3' within a 'share' sub-dir. +2. Changed structure of config file to be more modular. +3. Modified internal code structure to allow easier additon of extra apps to be configured. (NOTE: If any + apps need to be configured, then I'll also [later on] modify the Settings & Configure dialogs to + accomodate these.) +4. Because of 3, added a 'StarOffice' check to the settings dialog. If this is not seleted, then no check + is performed to make sure the SO dir is OK - and the option to config SO is diabled on the config dialog. +5. Added check when installing font to make sure that it's not already installed. + +0.7.3->0.7.4 +============ +1. Changed location of Fontmap.X11 -- from /lib/Fontmap.X11 to + /Fontmap.X11. As it seems some ghostscript installations don't + have the 'lib' sub directory. +2. Improved the documentation a little - added a FAQ section + +0.7.2->0.7.3 +============ +1. Very minor bug fix. + +0.7.1->0.7.2 +============ +1. Added more detailed error messages when system configuration fails. + +0.7->0.7.1 +========== +1. Removed lots of debug info from ttf2pt1, and afm.pl -- this should drastically speed up afm creation. +2. Modified ttf2pt1 to accept a parameter to just create .afm files +3. Added option to modify a .afm file when installing. +4. Added "Unicode" to list of encodings that can be used. +5. Removed kfontinst-cp1252.enc, kfontinst-cp1252.xpp -- these were hacks anyway, and seing as Qt2 is going to + support cp1252 by a hard-coded codec, there's no real point... +6. Rearranged the Configure System dialog - so that Force AFM regeneration is grouped next to the Generate AFMs option. +7. Encoding files now stored in /share/apps/kfontinst/Encodings + +0.6.1->0.7 +========== +1. Modified ttmkfdir & ttf2pt1 to allow usage of X11 style font re-encoding files. +2. Because .enc files are now used by kfontinst, removed the possibility of using gzipped encodings. +3. Added the ability to delete an installed font's .afm file. +4. Fixed a bug in the TtfPsNameFixer class - this would cause ttf2pt1 to creash when accessing a modified font! +5. Font encodings are now stored in /share/x11encodings +6. Removed the reencode shell script, as the encodng is all done by ttmkfdir. +7. Removed xfinst shell script - handled internally. +8. Supplied a kfontinst-cp1252 encoding - with the ugly single-quotes normaly found in .ttf files remapped to + the nice looking ones. +9. Added functionality, when configuring StarOffice, to select an appropriate xprinter.prolog for the selected + encoding (if one exists)... + +0.6->0.6.1 +========== +1. Fixed a display bug in the 'Un/Exclude from StarOffice" options. + +0.5->0.6 +======== +1. Added the ability to 'fix' the postscript names in a ttf file. +2. Fixed some missing changes to help files. + +0.4->0.5 +======== +1. Discovered a patch that modifies StarOffice's xprinter.prolog so that font's don't need to be modified + to use the microsoft cp1252 enocding scheme. (Previosuly the PS output from StarOffice would not print + OK with ghostscript - when using extra characters - unless the .ttf file was modified.) +2. Because of 1, removed the abilty to modify a TrueType font's internal charactermap - this was a hack anyway. +3. xfinst now uses mkfontdir to create encodings.dir - instead of kfontinst's install procedure copying a standard + one in (this didn't actually work...) +4. As kfontinst no longer reads the .enc files themselves, added the ability to use .enc.gz files as well + when selecting an encoding for X. +5. Re-wrote xfinst & reencode to be plain 'sh' scripts, as opposed to 'tcsh' scripts. + +0.3->0.4 +======== +1. All X fonts will now be placed with in a directory - "Managed" - this makes things easier for + AbiWord, and maybe others. +2. encodings.dir & Encodings/ will now be placed within this new "Managed" dir. +3. Only 1 StarOffice .PS file will be created - X11.PS +4. Only 1 Fontmap will be created - Fontmap.X11 - and this will be placed within + /lib +5. Because of 4, an option has been added to the Settings dialog to specify the location of + Ghostscript. +6. Because of 1, removed the font option from the Configure dialog. +7. Fixed an error with getting PS name from TT font - PS names are not allowed to have spaces, but in + fences.ttf it does. FontEngine.cpp will now check for, and fix, this - using the same 'algorithm' as that + of ttf2pt1 (which means the names will tie up with those in the .afm files). +8. Added some improvements to control of dialogs. + +0.2.1->0.3 +========== +1. Reverted back to naming .afm files .afm - and renaming any conflicting fonts. +2. Speeded up copying of files - by copying preview bitmap as opposed to regenerating it! +3. Removed need for FontMetrics directory - .afm files now placed within TrueType or Type1 dir, + and sym links are produced for StarOffice. + + 1. & 3. should now make things easier for AbiWord. + +4. Fixed output of Fontmap so that "URW Gothic" will be aliased as "UrwGothic-Roman" (etc.) as + this is what Qt will output. +5. Added more processing of .afm files - this makes them OK for AbiWord. + +0.2->0.2.1 +========== +1. Modified start-up progress dailog, and added progress dialogs to main window when scanning + fonts. These will only appear if numTTfonts>X || numT1fonts>Y + +0.1->0.2 +======== +1. Combined views of installed TrueType and Type1 fonts into 1 list. +2. When uninstalling a font, can now move the font to another directory - or delete. +3. Used t1lib so that Type1 fonts can also be previewed. +4. Changed Fontmap creator to dynamically allocate memory for each font-category. +5. .afm files are now named as ..afm - this removes the need + for renaming the .afm file if there exists Type1 and TrueType fonts with the same + fontname. +6. Removed the re-scanning of the install directories whenever a font is added. +7. Added support for extra Type1 font weights. +8. Added a start-up screen to inform the user that the installed/disk fonts are being scanned. +9. When exiting, confirmation is now only asked if the system has been changed and not + reconfigured. diff --git a/plasma/workspace/kcms/kfontinst/Messages.sh b/plasma/workspace/kcms/kfontinst/Messages.sh new file mode 100644 index 0000000000..0634d49b0a --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.rc` >> rc.cpp +$XGETTEXT rc.cpp */*.cpp */*.h -o $podir/kfontinst.pot +rm -f rc.cpp diff --git a/plasma/workspace/kcms/kfontinst/apps/16-apps-kfontview.png b/plasma/workspace/kcms/kfontinst/apps/16-apps-kfontview.png new file mode 100644 index 0000000000000000000000000000000000000000..3691f9068b5e297632061a9f1772e1d3ed17b8d9 GIT binary patch literal 719 zcmV;=0xk@#$4V#1-U z(mz~FtarYD^OAV}kjc~Qx^n!O^Ewf|V;DAwdSa&sAPUz#G!Fy6MQK=}MPE)a1 zB%|4+XI0g`!C=rp0MFmwZ7#vm4w~v3)0W7oGkKBX$uAU*M(NeuJPFwXN$NUT6s11e z2j$;gB&i_cjY}JOTAEv+`N?roYW0%N?n^c@qJZ;Sr*igA@FTl5V^C)V1uN4^JSk02 zKc`qMR?unLPipCVk>fN^D+UY>Uc4%9u6*cj6ovK{Lxup~bWk)aKdTfZY6L-W(LN|f zM@MNFdq;cV?dum>zK(0_>asVq+FF0|hWfrpB+}sZdT)n9AD-0;{lW&cnjKKiEHo)LY4_&9fi;5 zdyHw$*lf1dy#YNH{>&gke$fs%{4uPv^e5nd_zgIR1@P-;wx0k1002ovPDHLkV1hS!s8V~3H!vQ}|Ps!Q4!#h5pFqfM9OP2N3+1b!Bgu=PtmJn!?q@Oi_R z_kAe<>Kg#?!v9ZDQ5w%$3xixje29XqAaO)QDu@TUi~wc}xs6bW39%wWh#i6Een`wC z2gv()#~3uTva(_ihaY?)ih0OvW#F%Pd@r@RISE84O2y$=Mg|f7&^7$6;`{-n)6H|L zhYufpR|n>!%uZe#i$<(QgAv?aCoAX9p7{wt3H0~(i-df*Dfgq}5h+gK{IJ!}z4;#_ zg89U|7FUl=RmlTgEr6QmVK-j{TIV>ZN!u@8q&Lrxj*iv{`4b%D=y*U@5ok&0=I-9q znd}~u%VRO?Szt8=P}L|91OYg;23S+4ZX;QBSgob`X7{apj1++ggVJAHOSxPQ>fb(P zY~Zn6Z-6A0ppZ=f!!STGJalMJD7KC+ZAQ#oASLKXz-v*Fi&~>GNDYCmp)`LWL#}uL z@puAOS6AWL#@|X+R`%-(oo9KWHak0e7LEx+E-cH?d#@DWqzZ5h4f$LVVzEDfrfD$S zI%MxaW2aP>vkXJIDT-p-7MCPRbh>KhicE9mO0^MT}4zAQssuGS0!vuyy;DM`gY0n@hl z#`WvdyV>Vy6BKseaqyJGtbGNoC=J=d?04;DQ)f&@Lu6uN!hW2Xo}8QnG7IsDwhH z&c}~`zLgjA!JX}GNTpIw5{dZvp`jsTcXxN9udmPHcDrk9YipkcgTeW>#Kar*366Ws z^*4N8eSjGm896;LFc9H+-s$yvm1HtGJ~lS?_2G{HLzb79n}2)1ty!?qJ_n)9LiLfZ{g_zBre05*@$c{0r$k%@^W* R(LDeF002ovPDHLkV1h!A>9_y@ literal 0 HcmV?d00001 diff --git a/plasma/workspace/kcms/kfontinst/apps/32-apps-kfontview.png b/plasma/workspace/kcms/kfontinst/apps/32-apps-kfontview.png new file mode 100644 index 0000000000000000000000000000000000000000..c3063f15b993f67c7e93963ee5d386d482b78894 GIT binary patch literal 1545 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 literal 0 HcmV?d00001 diff --git a/plasma/workspace/kcms/kfontinst/apps/CMakeLists.txt b/plasma/workspace/kcms/kfontinst/apps/CMakeLists.txt new file mode 100644 index 0000000000..eae0aa5059 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/CMakeLists.txt @@ -0,0 +1,54 @@ +set(kfontinst_bin_SRCS ${libkfontinstjobrunner_SRCS} ${libkfontinstdbusiface_SRCS} Installer.cpp ) +# qt_add_dbus_interface(kfontinst_bin_SRCS ../dbus/org.kde.fontinst.xml FontInstInterfaceBase) + +set(kfontprint_bin_SRCS ${libkfontinstactionlabel_SRCS} Printer.cpp ) +set(kfontview_bin_SRCS Viewer.cpp ) + +add_executable(kfontinst_bin ${kfontinst_bin_SRCS}) +add_executable(kfontprint_bin ${kfontprint_bin_SRCS}) +add_executable(kfontview_bin ${kfontview_bin_SRCS}) + +set_target_properties(kfontinst_bin PROPERTIES OUTPUT_NAME kfontinst) +set_target_properties(kfontprint_bin PROPERTIES OUTPUT_NAME kfontprint) +set_target_properties(kfontview_bin PROPERTIES OUTPUT_NAME kfontview) + +target_link_libraries(kfontinst_bin + Qt::X11Extras + KF5::Archive + KF5::IconThemes + KF5::KIOCore + KF5::KIOWidgets + KF5::XmlGui + X11::X11 + kfontinst +) +target_link_libraries(kfontprint_bin + Qt::PrintSupport + KF5::IconThemes + kfontinstui + kfontinst +) +target_link_libraries(kfontview_bin KF5::Parts KF5::XmlGui KF5::DBusAddons kfontinstui kfontinst ) + + +ecm_qt_declare_logging_category(kfontview_bin + HEADER kfontview_debug.h + IDENTIFIER KFONTVIEW_DEBUG + CATEGORY_NAME org.kde.plasma.kfontview +) + +install(TARGETS kfontinst_bin ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) +install(TARGETS kfontprint_bin DESTINATION ${KDE_INSTALL_LIBEXECDIR} ) +install(TARGETS kfontview_bin ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) +install(FILES kfontviewui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kfontview ) +install(PROGRAMS org.kde.kfontview.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) +install(FILES installfont.desktop DESTINATION +${KDE_INSTALL_KSERVICES5DIR}/ServiceMenus ) + +ecm_install_icons( ICONS + 16-apps-kfontview.png + 22-apps-kfontview.png + 32-apps-kfontview.png + 48-apps-kfontview.png + 64-apps-kfontview.png + DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor) diff --git a/plasma/workspace/kcms/kfontinst/apps/CreateParent.h b/plasma/workspace/kcms/kfontinst/apps/CreateParent.h new file mode 100644 index 0000000000..5c0a9ea0ce --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/CreateParent.h @@ -0,0 +1,48 @@ +#pragma once + +/* + * SPDX-FileCopyrightText: 2003-2007 Craig Drummond + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include +#include + +// +// *Very* hacky way to get some KDE dialogs to appear to be transient +// for 'xid' +// +// Create's a QWidget with size 0/0 and no border, makes this transient +// for xid, and all other widgets can use this as their parent... +static QWidget *createParent(int xid) +{ + if (!xid) + return nullptr; + + QWidget *parent = new QWidget(nullptr, Qt::FramelessWindowHint); + + parent->resize(1, 1); + parent->show(); + + XWindowAttributes attr; + int rx, ry; + Window junkwin; + + XSetTransientForHint(QX11Info::display(), parent->winId(), xid); + if (XGetWindowAttributes(QX11Info::display(), xid, &attr)) { + XTranslateCoordinates(QX11Info::display(), xid, attr.root, -attr.border_width, -16, &rx, &ry, &junkwin); + + rx = (rx + (attr.width / 2)); + if (rx < 0) + rx = 0; + ry = (ry + (attr.height / 2)); + if (ry < 0) + ry = 0; + parent->move(rx, ry); + } + parent->setWindowOpacity(0); + parent->setWindowTitle(QLatin1String("KFI")); + + return parent; +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Installer.cpp b/plasma/workspace/kcms/kfontinst/apps/Installer.cpp new file mode 100644 index 0000000000..0084bf72bf --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Installer.cpp @@ -0,0 +1,141 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "Installer.h" +#include "FontsPackage.h" +#include "JobRunner.h" +#include "Misc.h" +#include "config-workspace.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// This include must be at the end +#include "CreateParent.h" + +namespace KFI +{ +int CInstaller::install(const QSet &urls) +{ + QSet::ConstIterator it(urls.begin()), end(urls.end()); + bool sysInstall(false); + CJobRunner *jobRunner = new CJobRunner(m_parent); + + CJobRunner::startDbusService(); + + if (!Misc::root()) { + switch (KMessageBox::questionYesNoCancel(m_parent, + i18n("Do you wish to install the font(s) for personal use " + "(only available to you), or " + "system-wide (available to all users)?"), + i18n("Where to Install"), + KGuiItem(i18n(KFI_KIO_FONTS_USER)), + KGuiItem(i18n(KFI_KIO_FONTS_SYS)))) { + case KMessageBox::No: + sysInstall = true; + break; + case KMessageBox::Cancel: + return -1; + default: + break; + } + } + + QSet instUrls; + + for (; it != end; ++it) { + auto job = KIO::mostLocalUrl(*it); + job->exec(); + QUrl local = job->mostLocalUrl(); + bool package(false); + + if (local.isLocalFile()) { + QString localFile(local.toLocalFile()); + + if (Misc::isPackage(localFile)) { + instUrls += FontsPackage::extract(localFile, &m_tempDir); + package = true; + } + } + if (!package) { + QList associatedUrls; + + CJobRunner::getAssociatedUrls(*it, associatedUrls, false, m_parent); + instUrls.insert(*it); + + QList::Iterator aIt(associatedUrls.begin()), aEnd(associatedUrls.end()); + + for (; aIt != aEnd; ++aIt) { + instUrls.insert(*aIt); + } + } + } + + if (!instUrls.isEmpty()) { + CJobRunner::ItemList list; + QSet::ConstIterator it(instUrls.begin()), end(instUrls.end()); + + for (; it != end; ++it) { + list.append(*it); + } + + return jobRunner->exec(CJobRunner::CMD_INSTALL, list, Misc::root() || sysInstall); + } else { + return -1; + } +} + +CInstaller::~CInstaller() +{ + delete m_tempDir; +} + +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); + + KLocalizedString::setApplicationDomain(KFI_CATALOGUE); + KAboutData aboutData("kfontinst", + i18n("Font Installer"), + WORKSPACE_VERSION_STRING, + i18n("Simple font installer"), + KAboutLicense::GPL, + i18n("(C) Craig Drummond, 2007")); + KAboutData::setApplicationData(aboutData); + + QGuiApplication::setWindowIcon(QIcon::fromTheme("preferences-desktop-font-installer")); + + QCommandLineParser parser; + const QCommandLineOption embedOption(QLatin1String("embed"), i18n("Makes the dialog transient for an X app specified by winid"), QLatin1String("winid")); + parser.addOption(embedOption); + parser.addPositionalArgument(QLatin1String("[URL]"), i18n("URL to install")); + + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + QSet urls; + + foreach (const QString &arg, parser.positionalArguments()) + urls.insert(QUrl::fromUserInput(arg, QDir::currentPath())); + + if (!urls.isEmpty()) { + QString opt(parser.value(embedOption)); + KFI::CInstaller inst(createParent(opt.size() ? opt.toInt(nullptr, 16) : 0)); + + return inst.install(urls); + } + + return -1; +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Installer.h b/plasma/workspace/kcms/kfontinst/apps/Installer.h new file mode 100644 index 0000000000..7c2f8e1207 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Installer.h @@ -0,0 +1,33 @@ +#pragma once + +/* + * SPDX-FileCopyrightText: 2003-2007 Craig Drummond + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +class QWidget; +class QTemporaryDir; + +namespace KFI +{ +class CInstaller +{ +public: + CInstaller(QWidget *p) + : m_parent(p) + , m_tempDir(nullptr) + { + } + ~CInstaller(); + + int install(const QSet &urls); + +private: + QWidget *m_parent; + QTemporaryDir *m_tempDir; +}; + +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Printer.cpp b/plasma/workspace/kcms/kfontinst/apps/Printer.cpp new file mode 100644 index 0000000000..cd8c93c05f --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Printer.cpp @@ -0,0 +1,403 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "Printer.h" +#include "ActionLabel.h" +#include "FcEngine.h" +#include "config-fontinst.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config-workspace.h" + +#ifdef HAVE_LOCALE_H +#include +#include +#include +#include +#endif +#include "CreateParent.h" + +// Enable the following to allow printing of non-installed fonts. Does not seem to work :-( +//#define KFI_PRINT_APP_FONTS + +using namespace KFI; + +static const int constMarginLineBefore = 1; +static const int constMarginLineAfter = 2; +static const int constMarginFont = 4; + +inline bool sufficientSpace(int y, int pageHeight, const QFontMetrics &fm) +{ + return (y + constMarginFont + fm.height()) < pageHeight; +} + +static bool sufficientSpace(int y, QPainter *painter, QFont font, const int *sizes, int pageHeight, int size) +{ + int titleFontHeight = painter->fontMetrics().height(), required = titleFontHeight + constMarginLineBefore + constMarginLineAfter; + + for (unsigned int s = 0; sizes[s]; ++s) { + font.setPointSize(sizes[s]); + required += QFontMetrics(font, painter->device()).height(); + if (sizes[s + 1]) { + required += constMarginFont; + } + } + + if (0 == size) { + font.setPointSize(CFcEngine::constDefaultAlphaSize); + int fontHeight = QFontMetrics(font, painter->device()).height(); + + required += (3 * (constMarginFont + fontHeight)) + constMarginLineBefore + constMarginLineAfter; + } + return (y + required) < pageHeight; +} + +static QString usableStr(QFont &font, const QString &str) +{ + Q_UNUSED(font) + return str; +} + +static bool hasStr(QFont &font, const QString &str) +{ + Q_UNUSED(font) + Q_UNUSED(str) + return true; +} + +static QString previewString(QFont &font, const QString &text, bool onlyDrawChars) +{ + Q_UNUSED(font) + Q_UNUSED(onlyDrawChars) + return text; +} + +CPrintThread::CPrintThread(QPrinter *printer, const QList &items, int size, QObject *parent) + : QThread(parent) + , m_printer(printer) + , m_items(items) + , m_size(size) + , m_cancelled(false) +{ +} + +CPrintThread::~CPrintThread() +{ +} + +void CPrintThread::cancel() +{ + m_cancelled = true; +} + +void CPrintThread::run() +{ + QPainter painter; + QFont sans("sans", 12, QFont::Bold); + bool changedFontEmbeddingSetting(false); + QString str(CFcEngine(false).getPreviewString()); + + if (!m_printer->fontEmbeddingEnabled()) { + m_printer->setFontEmbeddingEnabled(true); + changedFontEmbeddingSetting = true; + } + + m_printer->setResolution(72); + painter.begin(m_printer); + + int pageWidth = painter.device()->width(), pageHeight = painter.device()->height(), y = 0, oneSize[2] = {m_size, 0}; + const int *sizes = oneSize; + bool firstFont(true); + + if (0 == m_size) { + sizes = CFcEngine::constScalableSizes; + } + + painter.setClipping(true); + painter.setClipRect(0, 0, pageWidth, pageHeight); + + QList::ConstIterator it(m_items.constBegin()), end(m_items.constEnd()); + + for (int i = 0; it != end && !m_cancelled; ++it, ++i) { + QString name(FC::createName((*it).family, (*it).styleInfo)); + Q_EMIT progress(i, name); + + unsigned int s = 0; + QFont font; + +#ifdef KFI_PRINT_APP_FONTS + QString family; + + if (-1 != appFont[(*it).family]) { + family = QFontDatabase::applicationFontFamilies(appFont[(*it).family]).first(); + font = QFont(family); + } +#else + font = CFcEngine::getQFont((*it).family, (*it).styleInfo, CFcEngine::constDefaultAlphaSize); +#endif + painter.setFont(sans); + + if (!firstFont && !sufficientSpace(y, &painter, font, sizes, pageHeight, m_size)) { + m_printer->newPage(); + y = 0; + } + painter.setFont(sans); + y += painter.fontMetrics().height(); + painter.drawText(0, y, name); + + y += constMarginLineBefore; + painter.drawLine(0, y, pageWidth, y); + y += constMarginLineAfter; + + bool onlyDrawChars = false; + Qt::TextElideMode em = Qt::LeftToRight == QApplication::layoutDirection() ? Qt::ElideRight : Qt::ElideLeft; + + if (0 == m_size) { + font.setPointSize(CFcEngine::constDefaultAlphaSize); + painter.setFont(font); + + QFontMetrics fm(font, painter.device()); + bool lc = hasStr(font, CFcEngine::getLowercaseLetters()), uc = hasStr(font, CFcEngine::getUppercaseLetters()); + + onlyDrawChars = !lc && !uc; + + if (lc || uc) { + y += CFcEngine::constDefaultAlphaSize; + } + + if (lc) { + painter.drawText(0, y, fm.elidedText(CFcEngine::getLowercaseLetters(), em, pageWidth)); + y += constMarginFont + CFcEngine::constDefaultAlphaSize; + } + + if (uc) { + painter.drawText(0, y, fm.elidedText(CFcEngine::getUppercaseLetters(), em, pageWidth)); + y += constMarginFont + CFcEngine::constDefaultAlphaSize; + } + + if (lc || uc) { + QString validPunc(usableStr(font, CFcEngine::getPunctuation())); + if (validPunc.length() >= (CFcEngine::getPunctuation().length() / 2)) { + painter.drawText(0, y, fm.elidedText(CFcEngine::getPunctuation(), em, pageWidth)); + y += constMarginFont + constMarginLineBefore; + } + painter.drawLine(0, y, pageWidth, y); + y += constMarginLineAfter; + } + } + + for (; sizes[s]; ++s) { + y += sizes[s]; + font.setPointSize(sizes[s]); + painter.setFont(font); + + QFontMetrics fm(font, painter.device()); + + if (sufficientSpace(y, pageHeight, fm)) { + painter.drawText(0, y, fm.elidedText(previewString(font, str, onlyDrawChars), em, pageWidth)); + if (sizes[s + 1]) { + y += constMarginFont; + } + } else { + break; + } + } + y += (s < 1 || sizes[s - 1] < 25 ? 14 : 28); + firstFont = false; + } + Q_EMIT progress(m_items.count(), QString()); + painter.end(); + + // + // Did we change the users font settings? If so, reset to their previous values... + if (changedFontEmbeddingSetting) { + m_printer->setFontEmbeddingEnabled(false); + } +} + +CPrinter::CPrinter(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(i18n("Print")); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::rejected, this, &CPrinter::slotCancelClicked); + + QVBoxLayout *mainLayout = new QVBoxLayout; + setLayout(mainLayout); + + QFrame *page = new QFrame(this); + QGridLayout *layout = new QGridLayout(page); + m_statusLabel = new QLabel(page); + m_progress = new QProgressBar(page); + layout->addWidget(m_actionLabel = new CActionLabel(this), 0, 0, 2, 1); + layout->addWidget(m_statusLabel, 0, 1); + layout->addWidget(m_progress, 1, 1); + m_progress->setRange(0, 100); + layout->addItem(new QSpacerItem(0, 0, QSizePolicy::Fixed, QSizePolicy::Expanding), 2, 0); + + mainLayout->addWidget(page); + mainLayout->addWidget(buttonBox); + setMinimumSize(420, 80); +} + +CPrinter::~CPrinter() +{ +} + +void CPrinter::print(const QList &items, int size) +{ +#ifdef HAVE_LOCALE_H + char *oldLocale = setlocale(LC_NUMERIC, "C"); +#endif + + QPrinter printer; + QPrintDialog *dialog = new QPrintDialog(&printer, parentWidget()); + + if (dialog->exec()) { + CPrintThread *thread = new CPrintThread(&printer, items, size, this); + + m_progress->setRange(0, items.count()); + m_progress->setValue(0); + progress(0, QString()); + connect(thread, &CPrintThread::progress, this, &CPrinter::progress); + connect(thread, &QThread::finished, this, &QDialog::accept); + connect(this, &CPrinter::cancelled, thread, &CPrintThread::cancel); + m_actionLabel->startAnimation(); + thread->start(); + exec(); + delete thread; + } + + delete dialog; + +#ifdef HAVE_LOCALE_H + if (oldLocale) { + setlocale(LC_NUMERIC, oldLocale); + } +#endif +} + +void CPrinter::progress(int p, const QString &label) +{ + if (!label.isEmpty()) { + m_statusLabel->setText(label); + } + m_progress->setValue(p); +} + +void CPrinter::slotCancelClicked() +{ + m_statusLabel->setText(i18n("Canceling…")); + Q_EMIT cancelled(); +} + +void CPrinter::closeEvent(QCloseEvent *e) +{ + Q_UNUSED(e) + e->ignore(); + slotCancelClicked(); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + KLocalizedString::setApplicationDomain(KFI_CATALOGUE); + KAboutData aboutData("kfontprint", + i18n("Font Printer"), + WORKSPACE_VERSION_STRING, + i18n("Simple font printer"), + KAboutLicense::GPL, + i18n("(C) Craig Drummond, 2007")); + KAboutData::setApplicationData(aboutData); + + QGuiApplication::setWindowIcon(QIcon::fromTheme("kfontprint")); + + QCommandLineParser parser; + const QCommandLineOption embedOption(QLatin1String("embed"), i18n("Makes the dialog transient for an X app specified by winid"), QLatin1String("winid")); + parser.addOption(embedOption); + const QCommandLineOption sizeOption(QLatin1String("size"), i18n("Size index to print fonts"), QLatin1String("index")); + parser.addOption(sizeOption); + const QCommandLineOption pfontOption( + QLatin1String("pfont"), + i18n("Font to print, specified as \"Family,Style\" where Style is a 24-bit decimal number composed as: "), + QLatin1String("font")); + parser.addOption(pfontOption); + const QCommandLineOption listfileOption(QLatin1String("listfile"), i18n("File containing list of fonts to print"), QLatin1String("file")); + parser.addOption(listfileOption); + const QCommandLineOption deletefileOption(QLatin1String("deletefile"), i18n("Remove file containing list of fonts to print")); + parser.addOption(deletefileOption); + + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + QList fonts; + int size(parser.value(sizeOption).toInt()); + + if (size > -1 && size < 256) { + QString listFile(parser.value(listfileOption)); + + if (!listFile.isEmpty()) { + QFile f(listFile); + + if (f.open(QIODevice::ReadOnly)) { + QTextStream str(&f); + + while (!str.atEnd()) { + QString family(str.readLine()), style(str.readLine()); + + if (!family.isEmpty() && !style.isEmpty()) { + fonts.append(Misc::TFont(family, style.toUInt())); + } else { + break; + } + } + f.close(); + } + + if (parser.isSet(deletefileOption)) { + ::unlink(listFile.toLocal8Bit().constData()); + } + } else { + QStringList fl(parser.values(pfontOption)); + QStringList::ConstIterator it(fl.begin()), end(fl.end()); + + for (; it != end; ++it) { + QString f(*it); + + int commaPos = f.lastIndexOf(','); + + if (-1 != commaPos) { + fonts.append(Misc::TFont(f.left(commaPos), f.mid(commaPos + 1).toUInt())); + } + } + } + + if (!fonts.isEmpty()) { + CPrinter(createParent(parser.value(embedOption).toInt(nullptr, 16))).print(fonts, size); + + return 0; + } + } + + return -1; +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Printer.h b/plasma/workspace/kcms/kfontinst/apps/Printer.h new file mode 100644 index 0000000000..4e75b49b68 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Printer.h @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2011 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "Misc.h" +#include +#include +#include +#include + +class QLabel; +class QProgressBar; +class QPrinter; + +namespace KFI +{ +class CActionLabel; + +class CPrintThread : public QThread +{ + Q_OBJECT + +public: + CPrintThread(QPrinter *printer, const QList &items, int size, QObject *parent); + ~CPrintThread() override; + + void run() override; + +Q_SIGNALS: + + void progress(int p, const QString &f); + +public Q_SLOTS: + + void cancel(); + +private: + QPrinter *m_printer; + QList m_items; + int m_size; + bool m_cancelled; +}; + +class CPrinter : public QDialog +{ + Q_OBJECT + +public: + explicit CPrinter(QWidget *parent); + ~CPrinter() override; + + void print(const QList &items, int size); + +Q_SIGNALS: + + void cancelled(); + +public Q_SLOTS: + + void progress(int p, const QString &label); + void slotCancelClicked(); + +private: + void closeEvent(QCloseEvent *e) override; + +private: + QLabel *m_statusLabel; + QProgressBar *m_progress; + CActionLabel *m_actionLabel; +}; + +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Viewer.cpp b/plasma/workspace/kcms/kfontinst/apps/Viewer.cpp new file mode 100644 index 0000000000..9ad644d100 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Viewer.cpp @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2003-2007 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "Viewer.h" +#include "KfiConstants.h" +#include "config-workspace.h" +#include "kfontview_debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KFI +{ +CViewer::CViewer() +{ + const auto result = KPluginFactory::instantiatePlugin(KPluginMetaData(QStringLiteral("kf5/parts/kfontviewpart")), this); + + if (!result) { + qCWarning(KFONTVIEW_DEBUG) << "Error loading kfontviewpart:" << result.errorString; + exit(1); + } + + m_preview = result.plugin; + + m_openAct = actionCollection()->addAction(KStandardAction::Open, this, SLOT(fileOpen())); + actionCollection()->addAction(KStandardAction::Quit, this, SLOT(close())); + actionCollection()->addAction(KStandardAction::KeyBindings, this, SLOT(configureKeys())); + m_printAct = actionCollection()->addAction(KStandardAction::Print, m_preview, SLOT(print())); + + // Make tooltips more specific, instead of "document". + m_openAct->setToolTip(i18n("Open an existing font file")); + m_printAct->setToolTip(i18n("Print font preview")); + + m_printAct->setEnabled(false); + + if (m_preview->browserExtension()) { + connect(m_preview->browserExtension(), &KParts::BrowserExtension::enableAction, this, &CViewer::enableAction); + } + + setCentralWidget(m_preview->widget()); + createGUI(m_preview); + + setAutoSaveSettings(); + applyMainWindowSettings(KSharedConfig::openConfig()->group("MainWindow")); +} + +void CViewer::fileOpen() +{ + QFileDialog dlg(this, i18n("Select Font to View")); + dlg.setFileMode(QFileDialog::ExistingFile); + dlg.setMimeTypeFilters(QStringList() << "application/x-font-ttf" + << "application/x-font-otf" + << "application/x-font-type1" + << "application/x-font-bdf" + << "application/x-font-pcf"); + if (dlg.exec() == QDialog::Accepted) { + QUrl url = dlg.selectedUrls().value(0); + if (url.isValid()) { + showUrl(url); + } + } +} + +void CViewer::showUrl(const QUrl &url) +{ + if (url.isValid()) { + m_preview->openUrl(url); + } +} + +void CViewer::configureKeys() +{ + KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); + + dlg.addCollection(actionCollection()); + dlg.configure(); +} + +void CViewer::enableAction(const char *name, bool enable) +{ + if (0 == qstrcmp("print", name)) { + m_printAct->setEnabled(enable); + } +} + +class ViewerApplication : public QApplication +{ +public: + ViewerApplication(int &argc, char **argv) + : QApplication(argc, argv) + { + cmdParser.addPositionalArgument(QLatin1String("[URL]"), i18n("URL to open")); + } + + QCommandLineParser *parser() + { + return &cmdParser; + } + +public Q_SLOTS: + void activate(const QStringList &args, const QString &workingDirectory) + { + KFI::CViewer *viewer = new KFI::CViewer; + viewer->show(); + + if (!args.isEmpty()) { + cmdParser.process(args); + bool first = true; + foreach (const QString &arg, cmdParser.positionalArguments()) { + QUrl url(QUrl::fromUserInput(arg, workingDirectory)); + + if (!first) { + viewer = new KFI::CViewer; + viewer->show(); + } + viewer->showUrl(url); + first = false; + } + } + } + +private: + QCommandLineParser cmdParser; +}; + +} + +int main(int argc, char **argv) +{ + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + KFI::ViewerApplication app(argc, argv); + + KLocalizedString::setApplicationDomain(KFI_CATALOGUE); + KAboutData aboutData("kfontview", + i18n("Font Viewer"), + WORKSPACE_VERSION_STRING, + i18n("Simple font viewer"), + KAboutLicense::GPL, + i18n("(C) Craig Drummond, 2004-2007")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser *parser = app.parser(); + aboutData.setupCommandLine(parser); + parser->process(app); + aboutData.processCommandLine(parser); + + KDBusService dbusService(KDBusService::Unique); + QGuiApplication::setWindowIcon(QIcon::fromTheme("kfontview")); + app.activate(app.arguments(), QDir::currentPath()); + QObject::connect(&dbusService, &KDBusService::activateRequested, &app, &KFI::ViewerApplication::activate); + + return app.exec(); +} diff --git a/plasma/workspace/kcms/kfontinst/apps/Viewer.h b/plasma/workspace/kcms/kfontinst/apps/Viewer.h new file mode 100644 index 0000000000..c2dcec1f6c --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/Viewer.h @@ -0,0 +1,39 @@ +#pragma once + +/* + * SPDX-FileCopyrightText: 2003-2007 Craig Drummond + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +class QAction; +class QUrl; + +namespace KFI +{ +class CViewer : public KParts::MainWindow +{ + Q_OBJECT + +public: + CViewer(); + ~CViewer() override + { + } + void showUrl(const QUrl &url); + +public Q_SLOTS: + + void fileOpen(); + void configureKeys(); + void enableAction(const char *name, bool enable); + +private: + KParts::ReadOnlyPart *m_preview; + QAction *m_printAct; + QAction *m_openAct; +}; + +} diff --git a/plasma/workspace/kcms/kfontinst/apps/hisc-apps-kfontview.svgz b/plasma/workspace/kcms/kfontinst/apps/hisc-apps-kfontview.svgz new file mode 100644 index 0000000000000000000000000000000000000000..6a6a4f8d6b2d9b69a23443cd52105366efe87a78 GIT binary patch literal 9292 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 + + + diff --git a/plasma/workspace/kcms/kfontinst/apps/org.kde.kfontview.desktop b/plasma/workspace/kcms/kfontinst/apps/org.kde.kfontview.desktop new file mode 100644 index 0000000000..ca8b7fd98f --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/apps/org.kde.kfontview.desktop @@ -0,0 +1,89 @@ +[Desktop Entry] +Name=KFontView +Name[ar]=عارض الخط ك +Name[ast]=KFontView +Name[az]=KFontView +Name[ca]=KFontView +Name[cs]=Prohlížeč písem +Name[da]=KFontView +Name[de]=KFontView +Name[en_GB]=KFontView +Name[es]=KFontView +Name[et]=KFontView +Name[eu]=KFontView +Name[fi]=KFontView +Name[fr]=KFontView +Name[hi]=केफॉन्टवीव +Name[hu]=KFontView +Name[ia]=KFontView +Name[id]=KFontView +Name[it]=KFontView +Name[ko]=KFontView +Name[lt]=KFontView +Name[ml]=കെഫോണ്ട്‍വ്യൂ +Name[nl]=KFontView +Name[nn]=KFontView +Name[pa]=KFontView +Name[pl]=KFontView +Name[pt]=KFontView +Name[pt_BR]=KFontView +Name[ro]=KFontView +Name[ru]=KFontView +Name[sk]=KFontView +Name[sl]=KFontView +Name[sv]=Kfontview +Name[tr]=KFontView +Name[uk]=KFontView +Name[vi]=KFontView +Name[x-test]=xxKFontViewxx +Name[zh_CN]=KFontView +Exec=kfontview %U +Icon=kfontview +X-KDE-StartupNotify=true +Type=Application +MimeType=application/x-font-ttf;application/x-font-type1;application/x-font-otf;application/x-font-pcf;application/x-font-bdf;application/vnd.kde.fontspackage;font/otf;font/ttf;font/collection; +GenericName=Font Viewer +GenericName[ar]=عارض الخط +GenericName[az]=Şriftə baxış vasitəsi +GenericName[ca]=Visor de tipus de lletra +GenericName[cs]=Prohlížeč písem +GenericName[da]=Skrifttypevisning +GenericName[de]=Schriftartenbetrachter +GenericName[en_GB]=Font Viewer +GenericName[es]=Visor de tipos de letra +GenericName[et]=Fontide näitaja +GenericName[eu]=Letra-tipoak ikustekoa +GenericName[fi]=Fonttikatselin +GenericName[fr]=Afficheur de polices de caractères +GenericName[hi]=फ़ॉन्ट प्रदर्शक +GenericName[hu]=Betűtípus-böngésző +GenericName[ia]=Visor de font +GenericName[id]=Penampil Font +GenericName[it]=Visore di caratteri +GenericName[ko]=글꼴 뷰어 +GenericName[lt]=Šriftų žiūryklė +GenericName[ml]=അക്ഷരസഞ്ചയ ദർശിനി +GenericName[nl]=Lettertypeweergave +GenericName[nn]=Skriftvisar +GenericName[pa]=ਫੋਂਟ ਦਰਸ਼ਕ +GenericName[pl]=Przeglądarka czcionek +GenericName[pt]=Visualizador de Tipos de Letra +GenericName[pt_BR]=Visualizador de fontes +GenericName[ro]=Vizualizator de fonturi +GenericName[ru]=Просмотр шрифтов +GenericName[sk]=Prehliadač písiem +GenericName[sl]=Prikazovalnik pisav +GenericName[sv]=Teckensnittsvisning +GenericName[ta]=எழுத்துரு காட்டி +GenericName[tr]=Yazıtipi Görüntüleyici +GenericName[uk]=Переглядач шрифтів +GenericName[vi]=Trình xem phông chữ +GenericName[x-test]=xxFont Viewerxx +GenericName[zh_CN]=字体查看器 +Terminal=false +InitialPreference=1 +NoDisplay=true +Categories=Qt;KDE;Utility; +X-DBUS-StartupType=Unique +X-KDE-HasTempFileOption=true +X-DBUS-ServiceName=org.kde.kfontview diff --git a/plasma/workspace/kcms/kfontinst/config-fontinst.h.cmake b/plasma/workspace/kcms/kfontinst/config-fontinst.h.cmake new file mode 100644 index 0000000000..22721eaf48 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/config-fontinst.h.cmake @@ -0,0 +1,6 @@ +#pragma once + +#define KFONTINST_LIB_EXEC_DIR "${KAUTH_HELPER_INSTALL_ABSOLUTE_DIR}" + +/* Define to 1 if you have the header file. */ +#cmakedefine HAVE_LOCALE_H 1 diff --git a/plasma/workspace/kcms/kfontinst/dbus/CMakeLists.txt b/plasma/workspace/kcms/kfontinst/dbus/CMakeLists.txt new file mode 100644 index 0000000000..1008f49c0b --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/CMakeLists.txt @@ -0,0 +1,33 @@ +set(fontinst_bin_SRCS FcConfig.cpp FontInst.cpp Folder.cpp Main.cpp Utils.cpp ${libkfontinstdbusiface_SRCS} ) +set(fontinst_helper_SRCS FcConfig.cpp Helper.cpp Folder.cpp Utils.cpp ${libkfontinstdbusiface_SRCS} ) + +# qt5_generate_dbus_interface(FontInst.h org.kde.fontinst.xml) +qt_add_dbus_adaptor(fontinst_bin_SRCS org.kde.fontinst.xml FontInst.h KFI::FontInst) +# qt_add_dbus_interface(fontinst_bin_SRCS org.kde.fontinst.xml FontinstIface) + +add_executable(fontinst_bin ${fontinst_bin_SRCS}) +add_executable(fontinst_helper ${fontinst_helper_SRCS}) + +set_target_properties(fontinst_bin PROPERTIES OUTPUT_NAME fontinst) +target_link_libraries(fontinst_bin + Qt::DBus Qt::Xml KF5::AuthCore KF5::KIOCore kfontinst) + +set_target_properties(fontinst_helper PROPERTIES OUTPUT_NAME fontinst_helper) +target_link_libraries(fontinst_helper + Qt::DBus Qt::Xml KF5::AuthCore KF5::KIOCore kfontinst) + +ecm_qt_declare_logging_category(fontinst_bin + HEADER kfontinst_debug.h + IDENTIFIER KFONTINST_DEBUG + CATEGORY_NAME org.kde.plasma.kfontinst +) + +install(TARGETS fontinst_bin DESTINATION ${KAUTH_HELPER_INSTALL_DIR} ) +install(TARGETS fontinst_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR} ) +install(PROGRAMS fontinst_x11 DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) + +configure_file(org.kde.fontinst.service.cmake ${CMAKE_CURRENT_BINARY_DIR}/session/org.kde.fontinst.service) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/session/org.kde.fontinst.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} ) + +kauth_install_helper_files(fontinst_helper org.kde.fontinst root) +kauth_install_actions(org.kde.fontinst fontinst.actions) diff --git a/plasma/workspace/kcms/kfontinst/dbus/FcConfig.cpp b/plasma/workspace/kcms/kfontinst/dbus/FcConfig.cpp new file mode 100644 index 0000000000..b7de943306 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/FcConfig.cpp @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2003-2009 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "FcConfig.h" +#include "Misc.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KFI +{ +namespace FcConfig +{ +inline QString xDirSyntax(const QString &d) +{ + return Misc::fileSyntax(d); +} + +// +// Obtain location of config file to use. +// +// For system, prefer the following: +// +// <...>/config.d/00kde.conf = preferred method from FCConfig >= 2.3 +// <...>/local.conf +// +// Non-system, prefer: +// +// $HOME/<...>/.fonts.conf +// $HOME/<...>/fonts.conf +// +QString getConfigFile(bool system) +{ +#if (FC_VERSION >= 20300) + static const char constKdeRootFcFile[] = "00kde.conf"; +#endif + + FcStrList *list = FcConfigGetConfigFiles(FcConfigGetCurrent()); + QStringList files; + FcChar8 *file; + QString home(Misc::dirSyntax(QDir::homePath())); + + while ((file = FcStrListNext(list))) { + QString f((const char *)file); + + if (Misc::fExists(f)) { + // For nonsystem, only consider file within $HOME + if (system || 0 == Misc::fileSyntax(f).indexOf(home)) { + files.append(f); + } + } +#if (FC_VERSION >= 20300) + if (system && Misc::dExists(f) && (f.contains(QRegExp("/conf\\.d/?$")) || f.contains(QRegExp("/conf\\.d?$")))) { + return Misc::dirSyntax(f) + constKdeRootFcFile; // This ones good enough for me! + } +#endif + } + + // + // Go through list of files, looking for the preferred one... + if (!files.isEmpty()) { + QStringList::const_iterator it(files.begin()), end(files.end()); + + for (; it != end; ++it) { + if (-1 != (*it).indexOf(QRegExp(system ? "/local\\.conf$" : "/\\.?fonts\\.conf$"))) { + return *it; + } + } + return files.front(); // Just return the 1st one... + } else { // Hmmm... no known files? + return system ? "/etc/fonts/local.conf" : Misc::fileSyntax(home + "/.fonts.conf"); + } +} + +void addDir(const QString &dir, bool system) +{ + QDomDocument doc("fontconfig"); + QString fileName = getConfigFile(system); + QFile f(fileName); + bool hasDir(false); + + // qDebug() << "Using fontconfig file:" << fileName; + + // Load existing file - and check to see whether it has the dir... + if (f.open(QIODevice::ReadOnly)) { + doc.clear(); + + if (doc.setContent(&f)) { + QDomNode n = doc.documentElement().firstChild(); + + while (!n.isNull() && !hasDir) { + QDomElement e = n.toElement(); + + if (!e.isNull() && "dir" == e.tagName()) { + if (0 == Misc::expandHome(Misc::dirSyntax(e.text())).indexOf(dir)) { + hasDir = true; + } + } + n = n.nextSibling(); + } + } + f.close(); + } + + // Add dir, and save, if config does not already have this dir. + if (!hasDir) { + if (doc.documentElement().isNull()) { + doc.appendChild(doc.createElement("fontconfig")); + } + + QDomElement newNode = doc.createElement("dir"); + QDomText text = doc.createTextNode(Misc::contractHome(xDirSyntax(dir))); + + newNode.appendChild(text); + doc.documentElement().appendChild(newNode); + + FcAtomic *atomic = FcAtomicCreate((const unsigned char *)(QFile::encodeName(fileName).data())); + + if (atomic) { + if (FcAtomicLock(atomic)) { + FILE *f = fopen((char *)FcAtomicNewFile(atomic), "w"); + + if (f) { + // + // Check document syntax... + static const char qtXmlHeader[] = ""; + static const char xmlHeader[] = ""; + static const char qtDocTypeLine[] = ""; + static const char docTypeLine[] = + ""; + + QString str(doc.toString()); + int idx; + + if (0 != str.indexOf(" + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +class QString; + +namespace KFI +{ +namespace FcConfig +{ +void addDir(const QString &dir, bool system); +} + +} diff --git a/plasma/workspace/kcms/kfontinst/dbus/Folder.cpp b/plasma/workspace/kcms/kfontinst/dbus/Folder.cpp new file mode 100644 index 0000000000..43c8a154c9 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/Folder.cpp @@ -0,0 +1,362 @@ +/* + SPDX-FileCopyrightText: 2003-2009 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "Folder.h" +#include "FcConfig.h" +#include "KfiConstants.h" +#include "XmlStrings.h" +#include "config-fontinst.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DISABLED_FONTS "disabledfonts" + +namespace KFI +{ +bool Folder::CfgFile::modified() +{ + return timestamp != Misc::getTimeStamp(name); +} + +void Folder::CfgFile::updateTimeStamp() +{ + timestamp = Misc::getTimeStamp(name); +} + +Folder::~Folder() +{ + saveDisabled(); +} + +void Folder::init(bool system, bool systemBus) +{ + m_isSystem = system; + if (!system) { + FcStrList *list = FcConfigGetFontDirs(FcInitLoadConfigAndFonts()); + QStringList dirs; + FcChar8 *fcDir; + + while ((fcDir = FcStrListNext(list))) { + dirs.append(Misc::dirSyntax((const char *)fcDir)); + } + + m_location = Misc::getFolder(Misc::dirSyntax(QDir::homePath() + "/.fonts/"), Misc::dirSyntax(QDir::homePath()), dirs); + } else { + m_location = KFI_DEFAULT_SYS_FONTS_FOLDER; + } + + if ((!system && !systemBus) || (system && systemBus)) { + FcConfig::addDir(m_location, system); + } + + m_disabledCfg.dirty = false; + if (m_disabledCfg.name.isEmpty()) { + QString fileName("/" DISABLED_FONTS ".xml"); + + if (system) { + m_disabledCfg.name = QString::fromLatin1(KFI_ROOT_CFG_DIR) + fileName; + } else { + QString path = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/'); + + if (!Misc::dExists(path)) { + Misc::createDir(path); + } + m_disabledCfg.name = path + fileName; + } + m_disabledCfg.timestamp = 0; + } +} + +bool Folder::allowToggling() const +{ + return Misc::fExists(m_disabledCfg.name) ? Misc::fWritable(m_disabledCfg.name) : Misc::dWritable(Misc::getDir(m_disabledCfg.name)); +} + +void Folder::loadDisabled() +{ + if (m_disabledCfg.dirty) { + saveDisabled(); + } + + QFile f(m_disabledCfg.name); + + // qDebug() << m_disabledCfg.name; + m_disabledCfg.dirty = false; + if (f.open(QIODevice::ReadOnly)) { + QDomDocument doc; + + if (doc.setContent(&f)) { + for (QDomNode n = doc.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + + if (FONT_TAG == e.tagName()) { + Family fam(e, false); + + if (!fam.name().isEmpty()) { + Style style(e, false); + + if (KFI_NO_STYLE_INFO != style.value()) { + QList files; + + if (e.hasAttribute(PATH_ATTR)) { + File file(e, true); + + if (!file.path().isEmpty()) { + files.append(file); + } else { + m_disabledCfg.dirty = true; + } + } else { + for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement ent = n.toElement(); + + if (FILE_TAG == ent.tagName()) { + File file(ent, true); + + if (!file.path().isEmpty()) { + files.append(file); + } else { + // qDebug() << "Set dirty from load"; + m_disabledCfg.dirty = true; + } + } + } + } + + if (files.count() > 0) { + QList::ConstIterator it(files.begin()), end(files.end()); + + FamilyCont::ConstIterator f(m_fonts.insert(fam)); + StyleCont::ConstIterator s((*f).add(style)); + + for (; it != end; ++it) { + (*s).add(*it); + } + } + } + } + } + } + } + + f.close(); + m_disabledCfg.updateTimeStamp(); + } + + saveDisabled(); +} + +void Folder::saveDisabled() +{ + if (m_disabledCfg.dirty) { + if (!m_isSystem || Misc::root()) { + // qDebug() << m_disabledCfg.name; + + QSaveFile file; + + file.setFileName(m_disabledCfg.name); + + if (!file.open(QIODevice::WriteOnly)) { + // qDebug() << "Exit - cant open save file"; + qApp->exit(0); + } + + QTextStream str(&file); + + str << "<" DISABLED_FONTS ">" << Qt::endl; + + FamilyCont::ConstIterator it(m_fonts.begin()), end(m_fonts.end()); + + for (; it != end; ++it) { + (*it).toXml(true, str); + } + str << "" << Qt::endl; + str.flush(); + + if (!file.commit()) { + // qDebug() << "Exit - cant finalize save file"; + qApp->exit(0); + } + } + m_disabledCfg.updateTimeStamp(); + m_disabledCfg.dirty = false; + } +} + +QStringList Folder::toXml(int max) +{ + QStringList rv; + FamilyCont::ConstIterator it(m_fonts.begin()), end(m_fonts.end()); + QString string; + QTextStream str(&string); + + for (int i = 0; it != end; ++it, ++i) { + if (0 == (i % max)) { + if (i) { + str << "" << Qt::endl; + rv.append(string); + string = QString(); + } + str << "<" FONTLIST_TAG " " << SYSTEM_ATTR "=\"" << (m_isSystem ? "true" : "false") << "\">" << Qt::endl; + } + + (*it).toXml(false, str); + } + + if (!string.isEmpty()) { + str << "" << Qt::endl; + rv.append(string); + } + return rv; +} + +Families Folder::list() +{ + Families fam(m_isSystem); + FamilyCont::ConstIterator it(m_fonts.begin()), end(m_fonts.end()); + + for (int i = 0; it != end; ++it, ++i) { + fam.items.insert(*it); + } + + return fam; +} + +bool Folder::contains(const QString &family, quint32 style) +{ + FamilyCont::ConstIterator fam = m_fonts.find(Family(family)); + + if (fam == m_fonts.end()) { + return false; + } + + StyleCont::ConstIterator st = (*fam).styles().find(Style(style)); + + return st != (*fam).styles().end(); +} + +void Folder::add(const Family &family) +{ + FamilyCont::ConstIterator existingFamily = m_fonts.find(family); + + if (existingFamily == m_fonts.end()) { + m_fonts.insert(family); + } else { + StyleCont::ConstIterator it(family.styles().begin()), end(family.styles().end()); + + for (; it != end; ++it) { + StyleCont::ConstIterator existingStyle = (*existingFamily).styles().find(*it); + + if (existingStyle == (*existingFamily).styles().end()) { + (*existingFamily).add(*it); + } else { + FileCont::ConstIterator fit((*it).files().begin()), fend((*it).files().end()); + + for (; fit != fend; ++fit) { + FileCont::ConstIterator f = (*existingStyle).files().find(*fit); + + if (f == (*existingStyle).files().end()) { + (*existingStyle).add(*fit); + } + } + + (*existingStyle).setWritingSystems((*existingStyle).writingSystems() | (*it).writingSystems()); + if (!(*existingStyle).scalable() && (*it).scalable()) { + (*existingStyle).setScalable(true); + } + } + } + } +} + +void Folder::configure(bool force) +{ + // qDebug() << "EMPTY MODIFIED " << m_modifiedDirs.isEmpty(); + + if (force || !m_modifiedDirs.isEmpty()) { + saveDisabled(); + + QSet::ConstIterator it(m_modifiedDirs.constBegin()), end(m_modifiedDirs.constEnd()); + QSet dirs; + + for (; it != end; ++it) { + if (Misc::fExists((*it) + "fonts.dir")) { + dirs.insert(KShell::quoteArg(*it)); + } + } + + if (!dirs.isEmpty()) { + QProcess::startDetached(QStringLiteral(KFONTINST_LIB_EXEC_DIR "/fontinst_x11"), dirs.values()); + } + + m_modifiedDirs.clear(); + + // qDebug() << "RUN FC"; + Misc::doCmd("fc-cache"); + // qDebug() << "DONE"; + } +} + +Folder::Flat Folder::flatten() const +{ + FamilyCont::ConstIterator fam = m_fonts.begin(), famEnd = m_fonts.end(); + Flat rv; + + for (; fam != famEnd; ++fam) { + StyleCont::ConstIterator style((*fam).styles().begin()), styleEnd((*fam).styles().end()); + + for (; style != styleEnd; ++style) { + FileCont::ConstIterator file((*style).files().begin()), fileEnd((*style).files().end()); + + for (; file != fileEnd; ++file) { + rv.insert(FlatFont(*fam, *style, *file)); + } + } + } + + return rv; +} + +Families Folder::Flat::build(bool system) const +{ + ConstIterator it(begin()), e(end()); + Families families(system); + + for (; it != e; ++it) { + Family f((*it).family); + Style s((*it).styleInfo, (*it).scalable, (*it).writingSystems); + FamilyCont::ConstIterator fam = families.items.constFind(f); + + if (families.items.constEnd() == fam) { + s.add((*it).file); + f.add(s); + families.items.insert(f); + } else { + StyleCont::ConstIterator st = (*fam).styles().constFind(s); + + if ((*fam).styles().constEnd() == st) { + s.add((*it).file); + (*fam).add(s); + } else { + (*st).add((*it).file); + } + } + } + + return families; +} + +} diff --git a/plasma/workspace/kcms/kfontinst/dbus/Folder.h b/plasma/workspace/kcms/kfontinst/dbus/Folder.h new file mode 100644 index 0000000000..7b2d436c8e --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/Folder.h @@ -0,0 +1,125 @@ +#pragma once + +/* + * SPDX-FileCopyrightText: 2003-2009 Craig Drummond + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "Family.h" +#include "File.h" +#include "Misc.h" +#include "Style.h" +#include +#include +#include + +namespace KFI +{ +class Folder +{ + struct CfgFile { + bool modified(); + void updateTimeStamp(); + + bool dirty; + QString name; + time_t timestamp; + }; + +public: + struct FlatFont : Misc::TFont { + FlatFont(const Family &fam, const Style &style, const File &f) + : Misc::TFont(fam.name(), style.value()) + , writingSystems(style.writingSystems()) + , scalable(style.scalable()) + , file(f) + { + } + bool operator==(const FlatFont &o) const + { + return file.path() == o.file.path(); + } + + qulonglong writingSystems; + bool scalable; + File file; + }; + + struct Flat : public QSet { + Families build(bool system) const; + }; + + Folder() + { + } + ~Folder(); + + void init(bool system, bool systemBus); + const QString &location() const + { + return m_location; + } + bool allowToggling() const; + void loadDisabled(); + void saveDisabled(); + void setDisabledDirty() + { + m_disabledCfg.dirty = true; + } + bool disabledDirty() const + { + return m_disabledCfg.dirty; + } + QStringList toXml(int max = 0); + Families list(); + bool contains(const QString &family, quint32 style); + void add(const Family &family); + void addModifiedDir(const QString &dir) + { + m_modifiedDirs.insert(dir); + } + void addModifiedDirs(const QSet &dirs) + { + m_modifiedDirs += dirs; + } + bool isModified() const + { + return !m_modifiedDirs.isEmpty(); + } + void clearModified() + { + m_modifiedDirs.clear(); + } + void configure(bool force = false); + Flat flatten() const; + const FamilyCont &fonts() const + { + return m_fonts; + } + FamilyCont::ConstIterator addFont(const Family &fam) + { + return m_fonts.insert(fam); + } + void removeFont(const Family &fam) + { + m_fonts.remove(fam); + } + void clearFonts() + { + m_fonts.clear(); + } + +private: + bool m_isSystem; + FamilyCont m_fonts; + CfgFile m_disabledCfg; + QString m_location; + QSet m_modifiedDirs; +}; + +inline Q_DECL_EXPORT uint qHash(const Folder::FlatFont &key) +{ + return qHash(key.file); // +qHash(key.index()); +} + +} diff --git a/plasma/workspace/kcms/kfontinst/dbus/FontInst.cpp b/plasma/workspace/kcms/kfontinst/dbus/FontInst.cpp new file mode 100644 index 0000000000..80ba82278b --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/FontInst.cpp @@ -0,0 +1,889 @@ +/* + SPDX-FileCopyrightText: 2003-2009 Craig Drummond + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "FontInst.h" +#include "Fc.h" +#include "Misc.h" +#include "Utils.h" +#include "WritingSystems.h" +#include "fontinstadaptor.h" +#include "kfontinst_debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KFI +{ +static void decompose(const QString &name, QString &family, QString &style) +{ + int commaPos = name.lastIndexOf(','); + + family = -1 == commaPos ? name : name.left(commaPos); + style = -1 == commaPos ? KFI_WEIGHT_REGULAR : name.mid(commaPos + 2); +} + +static bool isSystem = false; +static Folder theFolders[FontInst::FOLDER_COUNT]; +static const int constSystemReconfigured = -1; +static const int constConnectionsTimeout = 30 * 1000; +static const int constFontListTimeout = 10 * 1000; + +typedef void (*SignalHandler)(int); + +static void registerSignalHandler(SignalHandler handler) +{ + if (!handler) { + handler = SIG_DFL; + } + + sigset_t mask; + sigemptyset(&mask); + +#ifdef SIGSEGV + signal(SIGSEGV, handler); + sigaddset(&mask, SIGSEGV); +#endif +#ifdef SIGFPE + signal(SIGFPE, handler); + sigaddset(&mask, SIGFPE); +#endif +#ifdef SIGILL + signal(SIGILL, handler); + sigaddset(&mask, SIGILL); +#endif +#ifdef SIGABRT + signal(SIGABRT, handler); + sigaddset(&mask, SIGABRT); +#endif + + sigprocmask(SIG_UNBLOCK, &mask, nullptr); +} + +void signalHander(int) +{ + static bool inHandler = false; + + if (!inHandler) { + inHandler = true; + theFolders[isSystem ? FontInst::FOLDER_SYS : FontInst::FOLDER_USER].saveDisabled(); + inHandler = false; + } +} + +FontInst::FontInst() +{ + isSystem = Misc::root(); + registerTypes(); + + new FontinstAdaptor(this); + QDBusConnection bus = QDBusConnection::sessionBus(); + + // qDebug() << "Connecting to session bus"; + if (!bus.registerService(OrgKdeFontinstInterface::staticInterfaceName())) { + // qDebug() << "Failed to register service!"; + ::exit(-1); + } + if (!bus.registerObject(FONTINST_PATH, this)) { + // qDebug() << "Failed to register object!"; + ::exit(-1); + } + + registerSignalHandler(signalHander); + m_connectionsTimer = new QTimer(this); + m_fontListTimer = new QTimer(this); + connect(m_connectionsTimer, &QTimer::timeout, this, &FontInst::connectionsTimeout); + connect(m_fontListTimer, &QTimer::timeout, this, &FontInst::fontListTimeout); + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); + + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + theFolders[i].init(FOLDER_SYS == i, isSystem); + } + + updateFontList(false); +} + +FontInst::~FontInst() +{ + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + theFolders[i].saveDisabled(); + } +} + +void FontInst::list(int folders, int pid) +{ + // qDebug() << folders << pid; + + m_connections.insert(pid); + updateFontList(false); + QList fonts; + + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + if (0 == folders || folders & (1 << i)) { + fonts += theFolders[i].list(); + } + } + + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); + Q_EMIT fontList(pid, fonts); +} + +void FontInst::statFont(const QString &name, int folders, int pid) +{ + // qDebug() << name << folders << pid; + + bool checkSystem = 0 == folders || folders & SYS_MASK || isSystem, checkUser = 0 == folders || (folders & USR_MASK && !isSystem); + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + + m_connections.insert(pid); + if ((checkSystem && findFont(name, FOLDER_SYS, fam, st)) || (checkUser && findFont(name, FOLDER_USER, fam, st, !checkSystem))) { + Family rv((*fam).name()); + rv.add(*st); + // qDebug() << "Found font, Q_EMIT details..."; + Q_EMIT fontStat(pid, rv); + } else { + // qDebug() << "Font not found, Q_EMIT empty details..."; + Q_EMIT fontStat(pid, Family(name)); + } +} + +void FontInst::install(const QString &file, bool createAfm, bool toSystem, int pid, bool checkConfig) +{ + // qDebug() << file << createAfm << toSystem << pid << checkConfig; + + m_connections.insert(pid); + + if (checkConfig) { + updateFontList(); + } + + EFolder folder = isSystem || toSystem ? FOLDER_SYS : FOLDER_USER; + Family font; + Utils::EFileType type = Utils::check(file, font); + + int result = Utils::FILE_BITMAP == type && !FC::bitmapsEnabled() ? STATUS_BITMAPS_DISABLED : Utils::FILE_INVALID == type ? STATUS_NOT_FONT_FILE : STATUS_OK; + + if (STATUS_OK == result) { + if (Utils::FILE_AFM != type && Utils::FILE_PFM != type) { + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT) && STATUS_OK == result; ++i) { + if (theFolders[i].contains(font.name(), (*font.styles().begin()).value())) { + result = STATUS_ALREADY_INSTALLED; + } + } + } + + if (STATUS_OK == result) { + QString name(Misc::modifyName(Misc::getFile(file))), destFolder(Misc::getDestFolder(theFolders[folder].location(), name)); + + result = Utils::FILE_AFM != type && Utils::FILE_PFM != type && Misc::fExists(destFolder + name) ? (int)KIO::ERR_FILE_ALREADY_EXIST : (int)STATUS_OK; + if (STATUS_OK == result) { + if (toSystem && !isSystem) { + // qDebug() << "Send request to system helper" << file << destFolder << name; + QVariantMap args; + args["method"] = "install"; + args["file"] = file; + args["name"] = name; + args["destFolder"] = destFolder; + args["createAfm"] = createAfm; + args["type"] = (int)type; + result = performAction(args); + } else { + if (!Misc::dExists(destFolder)) { + result = Misc::createDir(destFolder) ? (int)STATUS_OK : (int)KIO::ERR_WRITE_ACCESS_DENIED; + } + + if (STATUS_OK == result) { + result = QFile::copy(file, destFolder + name) ? (int)STATUS_OK : (int)KIO::ERR_WRITE_ACCESS_DENIED; + } + + if (STATUS_OK == result) { + Misc::setFilePerms(QFile::encodeName(destFolder + name)); + if ((Utils::FILE_SCALABLE == type || Utils::FILE_PFM == type) && createAfm) { + Utils::createAfm(destFolder + name, type); + } + } + } + + if (STATUS_OK == result && Utils::FILE_AFM != type && Utils::FILE_PFM != type) { + StyleCont::ConstIterator st(font.styles().begin()); + FileCont::ConstIterator f((*st).files().begin()); + File df(destFolder + name, (*f).foundry(), (*f).index()); + + (*st).clearFiles(); + (*st).add(df); + theFolders[folder].add(font); + theFolders[folder].addModifiedDir(destFolder); + Q_EMIT fontsAdded(Families(font, FOLDER_SYS == folder)); + } + } + } + } + + Q_EMIT status(pid, result); + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); +} + +void FontInst::uninstall(const QString &family, quint32 style, bool fromSystem, int pid, bool checkConfig) +{ + // qDebug() << family << style << fromSystem << pid << checkConfig; + + m_connections.insert(pid); + + if (checkConfig) { + updateFontList(); + } + + EFolder folder = isSystem || fromSystem ? FOLDER_SYS : FOLDER_USER; + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + int result = findFont(family, style, folder, fam, st) ? (int)STATUS_OK : (int)KIO::ERR_DOES_NOT_EXIST; + + if (STATUS_OK == result) { + Family del((*fam).name()); + Style s((*st).value(), (*st).scalable(), (*st).writingSystems()); + FileCont files((*st).files()); + FileCont::ConstIterator it(files.begin()), end(files.end()); + + if (fromSystem && !isSystem) { + QStringList fileList; + bool wasDisabled(false); + + for (; it != end; ++it) { + fileList.append((*it).path()); + theFolders[FOLDER_SYS].addModifiedDir(Misc::getDir((*it).path())); + if (!wasDisabled && Misc::isHidden(Misc::getFile((*it).path()))) { + wasDisabled = true; + } + } + QVariantMap args; + args["method"] = "uninstall"; + args["files"] = fileList; + result = performAction(args); + + if (STATUS_OK == result) { + FileCont empty; + s.setFiles(files); + (*st).setFiles(empty); + if (wasDisabled) { + theFolders[FOLDER_SYS].setDisabledDirty(); + } + } + } else { + for (; it != end; ++it) { + if (!Misc::fExists((*it).path()) || QFile::remove((*it).path())) { + // Also remove any AFM or PFM files... + QStringList other; + Misc::getAssociatedFiles((*it).path(), other); + QStringList::ConstIterator oit(other.constBegin()), oend(other.constEnd()); + for (; oit != oend; ++oit) { + QFile::remove(*oit); + } + + theFolders[folder].addModifiedDir(Misc::getDir((*it).path())); + (*st).remove(*it); + s.add(*it); + if (!theFolders[folder].disabledDirty() && Misc::isHidden(Misc::getFile((*it).path()))) { + theFolders[folder].setDisabledDirty(); + } + } + } + } + + if (STATUS_OK == result) { + if ((*st).files().isEmpty()) { + (*fam).remove(*st); + if ((*fam).styles().isEmpty()) { + theFolders[folder].removeFont(*fam); + } + } else { + result = STATUS_PARTIAL_DELETE; + } + del.add(s); + } + Q_EMIT fontsRemoved(Families(del, FOLDER_SYS == folder)); + } + // qDebug() << "status" << result; + Q_EMIT status(pid, result); + + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); +} + +void FontInst::uninstall(const QString &name, bool fromSystem, int pid, bool checkConfig) +{ + // qDebug() << name << fromSystem << pid << checkConfig; + + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + if (findFont(name, fromSystem || isSystem ? FOLDER_SYS : FOLDER_USER, fam, st)) { + uninstall((*fam).name(), (*st).value(), fromSystem, pid, checkConfig); + } else { + Q_EMIT status(pid, KIO::ERR_DOES_NOT_EXIST); + } +} + +void FontInst::move(const QString &family, quint32 style, bool toSystem, int pid, bool checkConfig) +{ + // qDebug() << family << style << toSystem << pid << checkConfig; + + m_connections.insert(pid); + if (checkConfig) { + updateFontList(); + } + + if (isSystem) { + Q_EMIT status(pid, KIO::ERR_UNSUPPORTED_ACTION); + } else { + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + EFolder from = toSystem ? FOLDER_USER : FOLDER_SYS, to = toSystem ? FOLDER_SYS : FOLDER_USER; + + if (findFont(family, style, from, fam, st)) { + FileCont::ConstIterator it((*st).files().begin()), end((*st).files().end()); + QStringList files; + + for (; it != end; ++it) { + files.append((*it).path()); + theFolders[from].addModifiedDir(Misc::getDir((*it).path())); + // Actual 'to' folder does not really matter, as we only want to call fc-cache + // ...actual folders only matter for xreating fonts.dir, etc, and we wont be doing this... + theFolders[to].addModifiedDir(theFolders[to].location()); + } + + QVariantMap args; + args["method"] = "move"; + args["files"] = files; + args["toSystem"] = toSystem; + args["dest"] = theFolders[to].location(); + args["uid"] = getuid(); + args["gid"] = getgid(); + int result = performAction(args); + + if (STATUS_OK == result) { + updateFontList(); + } + Q_EMIT status(pid, result); + } else { + // qDebug() << "does not exist"; + Q_EMIT status(pid, KIO::ERR_DOES_NOT_EXIST); + } + } + + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); +} + +static bool renameFontFile(const QString &from, const QString &to, int uid = -1, int gid = -1) +{ + if (!QFile::rename(from, to)) { + return false; + } + + QByteArray dest(QFile::encodeName(to)); + Misc::setFilePerms(dest); + if (-1 != uid && -1 != gid) { + ::chown(dest.data(), uid, gid); + } + return true; +} + +void FontInst::enable(const QString &family, quint32 style, bool inSystem, int pid, bool checkConfig) +{ + // qDebug() << family << style << inSystem << pid << checkConfig; + toggle(true, family, style, inSystem, pid, checkConfig); +} + +void FontInst::disable(const QString &family, quint32 style, bool inSystem, int pid, bool checkConfig) +{ + // qDebug() << family << style << inSystem << pid << checkConfig; + toggle(false, family, style, inSystem, pid, checkConfig); +} + +void FontInst::removeFile(const QString &family, quint32 style, const QString &file, bool fromSystem, int pid, bool checkConfig) +{ + // qDebug() << family << style << file << fromSystem << pid << checkConfig; + + m_connections.insert(pid); + + if (checkConfig) { + updateFontList(); + } + + // First find the family/style + EFolder folder = isSystem || fromSystem ? FOLDER_SYS : FOLDER_USER; + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + int result = findFont(family, style, folder, fam, st) ? (int)STATUS_OK : (int)KIO::ERR_DOES_NOT_EXIST; + + if (STATUS_OK == result) { + // Family/style found - now check that the requested file is *within* the same folder as one + // of the files linked to this font... + FileCont files((*st).files()); + FileCont::ConstIterator it(files.begin()), end(files.end()); + QString dir(Misc::getDir(file)); + + result = KIO::ERR_DOES_NOT_EXIST; + for (; it != end && STATUS_OK != result; ++it) { + if (Misc::getDir((*it).path()) == dir) { + result = STATUS_OK; + } + } + + if (STATUS_OK == result) { + // OK, found folder - so can now proceed to delete the file... + if (fromSystem && !isSystem) { + QVariantMap args; + args["method"] = "removeFile"; + args["file"] = file; + result = performAction(args); + } else { + result = Misc::fExists(file) ? QFile::remove(file) ? (int)STATUS_OK : (int)KIO::ERR_WRITE_ACCESS_DENIED : (int)KIO::ERR_DOES_NOT_EXIST; + } + + if (STATUS_OK == result) { + theFolders[folder].addModifiedDir(dir); + } + } + } + + Q_EMIT status(pid, result); +} + +void FontInst::reconfigure(int pid, bool force) +{ + // qDebug() << pid << force; + bool sysModified(theFolders[FOLDER_SYS].isModified()); + + saveDisabled(); + + // qDebug() << theFolders[FOLDER_USER].isModified() << sysModified; + if (!isSystem && (force || theFolders[FOLDER_USER].isModified())) { + theFolders[FOLDER_USER].configure(force); + } + + if (sysModified) { + if (isSystem) { + theFolders[FOLDER_SYS].configure(); + } else { + QVariantMap args; + args["method"] = "reconfigure"; + performAction(args); + theFolders[FOLDER_SYS].clearModified(); + } + } + + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); + + updateFontList(); + Q_EMIT status(pid, isSystem ? constSystemReconfigured : STATUS_OK); +} + +QString FontInst::folderName(bool sys) +{ + return theFolders[sys || isSystem ? FOLDER_SYS : FOLDER_USER].location(); +} + +void FontInst::saveDisabled() +{ + if (isSystem) { + theFolders[FOLDER_SYS].saveDisabled(); + } else { + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + if (FOLDER_SYS == i && !isSystem) { + if (theFolders[i].disabledDirty()) { + QVariantMap args; + args["method"] = "saveDisabled"; + performAction(args); + theFolders[i].saveDisabled(); + } + } else { + theFolders[i].saveDisabled(); + } + } + } +} + +void FontInst::connectionsTimeout() +{ + bool canExit(true); + + // qDebug() << "exiting"; + checkConnections(); + + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + if (theFolders[i].disabledDirty()) { + canExit = false; + } + theFolders[i].saveDisabled(); + } + + if (0 == m_connections.count()) { + if (canExit) { + qApp->exit(0); + } else { // Try again later... + m_connectionsTimer->start(constConnectionsTimeout); + } + } +} + +void FontInst::fontListTimeout() +{ + updateFontList(true); + m_fontListTimer->start(constFontListTimeout); +} + +void FontInst::updateFontList(bool emitChanges) +{ + // For some reason just the "!FcConfigUptoDate(0)" check does not always work :-( + FcBool fcModified = !FcConfigUptoDate(nullptr); + + if (fcModified || theFolders[FOLDER_SYS].fonts().isEmpty() || (!isSystem && theFolders[FOLDER_USER].fonts().isEmpty()) + || theFolders[FOLDER_SYS].disabledDirty() || (!isSystem && theFolders[FOLDER_USER].disabledDirty())) { + // qDebug() << "Need to refresh font lists"; + if (fcModified) { + // qDebug() << "Re-init FC"; + if (!FcInitReinitialize()) { + // qDebug() << "Re-init failed????"; + } + } + + Folder::Flat old[FOLDER_COUNT]; + + if (emitChanges) { + // qDebug() << "Flatten existing font lists"; + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + old[i] = theFolders[i].flatten(); + } + } + + saveDisabled(); + + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + theFolders[i].clearFonts(); + } + + // qDebug() << "update list of fonts"; + + FcPattern *pat = FcPatternCreate(); + FcObjectSet *os = FcObjectSetBuild(FC_FILE, + FC_FAMILY, + FC_FAMILYLANG, + FC_WEIGHT, + FC_LANG, + FC_CHARSET, + FC_SCALABLE, +#ifndef KFI_FC_NO_WIDTHS + FC_WIDTH, +#endif + FC_SLANT, + FC_INDEX, + FC_FOUNDRY, + (void *)nullptr); + + FcFontSet *list = FcFontList(nullptr, pat, os); + + FcPatternDestroy(pat); + FcObjectSetDestroy(os); + + theFolders[FOLDER_SYS].loadDisabled(); + if (!isSystem) { + theFolders[FOLDER_USER].loadDisabled(); + } + + if (list) { + QString home(Misc::dirSyntax(QDir::homePath())); + + for (int i = 0; i < list->nfont; i++) { + QString fileName(Misc::fileSyntax(FC::getFcString(list->fonts[i], FC_FILE))); + + if (!fileName.isEmpty() && Misc::fExists(fileName)) // && 0!=fileName.indexOf(constDefomaLocation)) + { + QString family, foundry; + quint32 styleVal; + int index; + qulonglong writingSystems(WritingSystems::instance()->get(list->fonts[i])); + FcBool scalable = FcFalse; + + if (FcResultMatch != FcPatternGetBool(list->fonts[i], FC_SCALABLE, 0, &scalable)) { + scalable = FcFalse; + } + + FC::getDetails(list->fonts[i], family, styleVal, index, foundry); + FamilyCont::ConstIterator fam = theFolders[isSystem || 0 != fileName.indexOf(home) ? FOLDER_SYS : FOLDER_USER].addFont(Family(family)); + StyleCont::ConstIterator style = (*fam).add(Style(styleVal)); + FileCont::ConstIterator file = (*style).add(File(fileName, foundry, index)); + Q_UNUSED(file); + + (*style).setWritingSystems((*style).writingSystems() | writingSystems); + if (scalable) { + (*style).setScalable(); + } + } + } + + FcFontSetDestroy(list); + } + + if (emitChanges) { + // qDebug() << "Look for differences"; + for (int i = 0; i < (isSystem ? 1 : FOLDER_COUNT); ++i) { + // qDebug() << "Flatten, and take copies..."; + Folder::Flat newList = theFolders[i].flatten(), onlyNew = newList; + + // qDebug() << "Determine differences..."; + onlyNew.subtract(old[i]); + old[i].subtract(newList); + + // qDebug() << "Emit changes..."; + Families families = onlyNew.build(isSystem || i == FOLDER_SYS); + + if (!families.items.isEmpty()) { + Q_EMIT fontsAdded(families); + } + + families = old[i].build(isSystem || i == FOLDER_SYS); + if (!families.items.isEmpty()) { + Q_EMIT fontsRemoved(families); + } + } + } + // qDebug() << "updated list of fonts"; + } +} + +void FontInst::toggle(bool enable, const QString &family, quint32 style, bool inSystem, int pid, bool checkConfig) +{ + m_connections.insert(pid); + + if (checkConfig) { + updateFontList(); + } + + EFolder folder = isSystem || inSystem ? FOLDER_SYS : FOLDER_USER; + FamilyCont::ConstIterator fam; + StyleCont::ConstIterator st; + int result = findFont(family, style, folder, fam, st) ? (int)STATUS_OK : (int)KIO::ERR_DOES_NOT_EXIST; + + if (STATUS_OK == result) { + FileCont files((*st).files()), toggledFiles; + FileCont::ConstIterator it(files.begin()), end(files.end()); + QHash movedFonts; + QHash movedAssoc; + QSet modifiedDirs; + + for (; it != end && STATUS_OK == result; ++it) { + QString to = Misc::getDir((*it).path()) + QString(enable ? Misc::unhide(Misc::getFile((*it).path())) : Misc::hide(Misc::getFile((*it).path()))); + if (to != (*it).path()) { + // qDebug() << "MOVE:" << (*it).path() << " to " << to; + // If the font is a system font, and we're not root, then just go through the actions here - so + // that we can build the list of changes that would happen... + if ((inSystem && !isSystem) || renameFontFile((*it).path(), to)) { + modifiedDirs.insert(Misc::getDir(enable ? to : (*it).path())); + toggledFiles.insert(File(to, (*it).foundry(), (*it).index())); + // Now try to move an associated AFM or PFM files... + QStringList assoc; + + movedFonts[*it] = to; + Misc::getAssociatedFiles((*it).path(), assoc); + + QStringList::ConstIterator ait(assoc.constBegin()), aend(assoc.constEnd()); + + for (; ait != aend && STATUS_OK == result; ++ait) { + to = Misc::getDir(*ait) + QString(enable ? Misc::unhide(Misc::getFile(*ait)) : Misc::hide(Misc::getFile(*ait))); + + if (to != *ait) { + if ((inSystem && !isSystem) || renameFontFile(*ait, to)) { + movedAssoc[*ait] = to; + } else { + result = KIO::ERR_WRITE_ACCESS_DENIED; + } + } + } + } else { + result = KIO::ERR_WRITE_ACCESS_DENIED; + } + } + } + + if (inSystem && !isSystem) { + Family toggleFam((*fam).name()); + + toggleFam.add(*st); + QVariantMap args; + args["method"] = "toggle"; + QString xml; + QTextStream str(&xml); + toggleFam.toXml(false, str); + args["xml"] = xml; + args["enable"] = enable; + result = performAction(args); + } + + if (STATUS_OK == result) { + Family addFam((*fam).name()), delFam((*fam).name()); + Style addStyle((*st).value(), (*st).scalable(), (*st).writingSystems()), delStyle((*st).value(), (*st).scalable(), (*st).writingSystems()); + + addStyle.setFiles(toggledFiles); + addFam.add(addStyle); + delStyle.setFiles(files); + delFam.add(delStyle); + (*st).setFiles(toggledFiles); + + theFolders[folder].addModifiedDirs(modifiedDirs); + Q_EMIT fontsAdded(Families(addFam, FOLDER_SYS == folder)); + Q_EMIT fontsRemoved(Families(delFam, FOLDER_SYS == folder)); + + theFolders[folder].setDisabledDirty(); + } else // un-move fonts! + { + QHash::ConstIterator fit(movedFonts.constBegin()), fend(movedFonts.constEnd()); + QHash::ConstIterator ait(movedAssoc.constBegin()), aend(movedAssoc.constEnd()); + + for (; fit != fend; ++fit) { + renameFontFile(fit.value(), fit.key().path()); + } + for (; ait != aend; ++ait) { + renameFontFile(ait.value(), ait.key()); + } + } + } + Q_EMIT status(pid, result); + + m_connectionsTimer->start(constConnectionsTimeout); + m_fontListTimer->start(constFontListTimeout); +} + +void FontInst::addModifedSysFolders(const Family &family) +{ + StyleCont::ConstIterator style(family.styles().begin()), styleEnd(family.styles().end()); + + for (; style != styleEnd; ++style) { + FileCont::ConstIterator file((*style).files().begin()), fileEnd((*style).files().end()); + + for (; file != fileEnd; ++file) { + theFolders[FOLDER_SYS].addModifiedDir(Misc::getDir((*file).path())); + } + } +} + +void FontInst::checkConnections() +{ + QSet::ConstIterator it(m_connections.begin()), end(m_connections.end()); + QSet remove; + + for (; it != end; ++it) { + if (0 != kill(*it, 0)) { + remove.insert(*it); + } + } + m_connections.subtract(remove); +} + +bool FontInst::findFontReal(const QString &family, const QString &style, EFolder folder, FamilyCont::ConstIterator &fam, StyleCont::ConstIterator &st) +{ + Family f(family); + fam = theFolders[folder].fonts().find(f); + if (theFolders[folder].fonts().end() == fam) { + return false; + } + + StyleCont::ConstIterator end((*fam).styles().end()); + for (st = (*fam).styles().begin(); st != end; ++st) { + if (FC::createStyleName((*st).value()) == style) { + return true; + } + } + + return false; +} + +bool FontInst::findFont(const QString &font, EFolder folder, FamilyCont::ConstIterator &fam, StyleCont::ConstIterator &st, bool updateList) +{ + QString family, style; + + decompose(font, family, style); + + if (!findFontReal(family, style, folder, fam, st)) { + if (updateList) { + // Not found, so refresh font list and try again... + updateFontList(); + return findFontReal(family, style, folder, fam, st); + } else { + return false; + } + } + + return true; +} + +bool FontInst::findFontReal(const QString &family, quint32 style, EFolder folder, FamilyCont::ConstIterator &fam, StyleCont::ConstIterator &st) +{ + fam = theFolders[folder].fonts().find(Family(family)); + + if (theFolders[folder].fonts().end() == fam) { + return false; + } else { + st = (*fam).styles().find(style); + + return (*fam).styles().end() != st; + } +} + +bool FontInst::findFont(const QString &family, quint32 style, EFolder folder, FamilyCont::ConstIterator &fam, StyleCont::ConstIterator &st, bool updateList) +{ + if (!findFontReal(family, style, folder, fam, st)) { + if (updateList) { + // Not found, so refresh font list and try again... + updateFontList(); + return findFontReal(family, style, folder, fam, st); + } else { + return false; + } + } + return true; +} + +int FontInst::performAction(const QVariantMap &args) +{ + KAuth::Action action("org.kde.fontinst.manage"); + + action.setHelperId("org.kde.fontinst"); + action.setArguments(args); + // qDebug() << "Call " << args["method"].toString() << " on helper"; + m_fontListTimer->stop(); + m_connectionsTimer->stop(); + + KAuth::ExecuteJob *j = action.execute(); + j->exec(); + if (j->error()) { + qCWarning(KFONTINST_DEBUG) << "kauth action failed" << j->errorString() << j->errorText(); + // error is a KAuth::ActionReply::Error rest of this code expects KIO error codes which are extended by EStatus + switch (j->error()) { + case KAuth::ActionReply::Error::UserCancelledError: + return KIO::ERR_USER_CANCELED; + case KAuth::ActionReply::Error::AuthorizationDeniedError: + /*fall through*/ + case KAuth::ActionReply::Error::NoSuchActionError: + return KIO::ERR_CANNOT_AUTHENTICATE; + default: + return KIO::ERR_INTERNAL; + } + return KIO::ERR_INTERNAL; + } + // qDebug() << "Success!"; + return STATUS_OK; +} + +} diff --git a/plasma/workspace/kcms/kfontinst/dbus/FontInst.h b/plasma/workspace/kcms/kfontinst/dbus/FontInst.h new file mode 100644 index 0000000000..21229d3553 --- /dev/null +++ b/plasma/workspace/kcms/kfontinst/dbus/FontInst.h @@ -0,0 +1,124 @@ +#pragma once + +/* + * SPDX-FileCopyrightText: 2003-2009 Craig Drummond + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "Family.h" +#include "Folder.h" +#include "FontinstIface.h" +#include "kfontinst_export.h" +#include +#include +#include +#include +#include + +#define FONTINST_PATH "/FontInst" + +class QTimer; + +namespace KFI +{ +class KFONTINST_EXPORT FontInst : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.fontinst") + +public: + enum EStatus { + STATUS_OK = 0, + STATUS_SERVICE_DIED = KJob::UserDefinedError + 500, + STATUS_BITMAPS_DISABLED, + STATUS_ALREADY_INSTALLED, + STATUS_NOT_FONT_FILE, + STATUS_PARTIAL_DELETE, + STATUS_NO_SYS_CONNECTION, + }; + + enum EFolder { + FOLDER_SYS, + FOLDER_USER, + + FOLDER_COUNT, + }; + + enum { + SYS_MASK = 0x01, + USR_MASK = 0x02, + }; + + static void registerTypes() + { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType + + + diff --git a/plasma/workspace/sddm-theme/metadata.desktop b/plasma/workspace/sddm-theme/metadata.desktop new file mode 100644 index 0000000000..91a818fa9a --- /dev/null +++ b/plasma/workspace/sddm-theme/metadata.desktop @@ -0,0 +1,108 @@ +[SddmGreeterTheme] +Name=Breeze +Name[ar]=نسيم +Name[ast]=Breeze +Name[az]=Breeze +Name[bs]=Breeze +Name[ca]=Brisa +Name[ca@valencia]=Brisa +Name[cs]=Breeze +Name[da]=Breeze +Name[de]=Breeze +Name[el]=Breeze +Name[en_GB]=Breeze +Name[es]=Brisa +Name[et]=Breeze +Name[eu]=Breeze +Name[fi]=Breeze +Name[fr]=Breeze +Name[gl]=Breeze +Name[hi]=ब्रीज़ +Name[hu]=Breeze +Name[ia]=Brisa +Name[id]=Breeze +Name[is]=Breeze +Name[it]=Brezza +Name[ja]=Breeze +Name[ko]=Breeze +Name[lt]=Breeze +Name[ml]=ബ്രീസ് +Name[nb]=Breeze +Name[nds]=Breeze +Name[nl]=Breeze +Name[nn]=Breeze +Name[pa]=ਬਰੀਜ਼ +Name[pl]=Bryza +Name[pt]=Brisa +Name[pt_BR]=Breeze +Name[ro]=Briză +Name[ru]=Breeze +Name[sk]=Vánok +Name[sl]=Sapica +Name[sr]=Поветарац +Name[sr@ijekavian]=Поветарац +Name[sr@ijekavianlatin]=Povetarac +Name[sr@latin]=Povetarac +Name[sv]=Breeze +Name[tg]=Насим +Name[tr]=Esinti +Name[uk]=Breeze +Name[vi]=Breeze +Name[x-test]=xxBreezexx +Name[zh_CN]=Breeze 微风 +Name[zh_TW]=微風 +Description=Breeze +Description[ar]=بريز +Description[ast]=Breeze +Description[az]=Breeze +Description[ca]=Brisa +Description[ca@valencia]=Brisa +Description[cs]=Breeze +Description[da]=Breeze +Description[de]=Breeze +Description[en_GB]=Breeze +Description[es]=Brisa +Description[et]=Breeze +Description[eu]=Breeze +Description[fi]=Breeze +Description[fr]=Breeze +Description[gl]=Breeze +Description[hi]=ब्रीज़ +Description[hu]=Breeze +Description[ia]=Brisa +Description[id]=Breeze +Description[it]=Brezza +Description[ko]=Breeze +Description[lt]=Breeze +Description[ml]=ബ്രീസ് +Description[nl]=Breeze +Description[nn]=Breeze +Description[pa]=ਬਰੀਜ਼ +Description[pl]=Bryza +Description[pt]=Brisa +Description[pt_BR]=Breeze +Description[ro]=Briză +Description[ru]=Breeze +Description[sk]=Vánok +Description[sl]=Sapica +Description[sv]=Breeze +Description[tg]=Насим +Description[tr]=Esinti +Description[uk]=Breeze +Description[vi]=Breeze +Description[x-test]=xxBreezexx +Description[zh_CN]=Breeze 微风 +Description[zh_TW]=Breeze +Author=KDE Visual Design Group +Copyright=(c) 2014, David Edmundson +License=CC-BY-SA +Type=sddm-theme +Version=0.1 +Website=https://github.com/sddm/sddm +Screenshot=preview.png +MainScript=Main.qml +ConfigFile=theme.conf +TranslationsDirectory=translations +Email=plasma-devel@kde.org +Theme-Id=breeze +Theme-API=2.0 diff --git a/plasma/workspace/sddm-theme/preview.png b/plasma/workspace/sddm-theme/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..33e2ba65d358ca8627191b45857242875154ec97 GIT binary patch literal 717013 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­Ab^{02oss53hzVv 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%0AlR7Sz!mJ%iZ*a1wuL9egcei}>s)+-oOx8l2U= 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 ~t^q|5*h&PutIVocHJkUBEj2K2wu1_rwPI3^ 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%0DNF*rK>8VP#lpxSlgj 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�~)7;NjyU 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%0AE9f1#!w_jrdE@x?AhX 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

&(`WX}5=z#>pvFKaNM)}KgFW&^1 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 literal 0 HcmV?d00001 diff --git a/plasma/workspace/sddm-theme/theme.conf.cmake b/plasma/workspace/sddm-theme/theme.conf.cmake new file mode 100644 index 0000000000..8494a5c8a2 --- /dev/null +++ b/plasma/workspace/sddm-theme/theme.conf.cmake @@ -0,0 +1,8 @@ +[General] +showlogo=hidden +logo=${KDE_INSTALL_FULL_DATADIR}/sddm/themes/breeze/default-logo.svg +type=image +color=#1d99f3 +fontSize=10 +background=${KDE_INSTALL_FULL_WALLPAPERDIR}/Next/contents/images/5120x2880.jpg +needsFullUserModel=false diff --git a/plasma/workspace/sddm-wayland-session/plasma-wayland.conf b/plasma/workspace/sddm-wayland-session/plasma-wayland.conf new file mode 100644 index 0000000000..9c97613b97 --- /dev/null +++ b/plasma/workspace/sddm-wayland-session/plasma-wayland.conf @@ -0,0 +1,7 @@ +[General] +DisplayServer=wayland +GreeterEnvironment=QT_WAYLAND_SHELL_INTEGRATION=layer-shell +InputMethod= + +[Wayland] +CompositorCommand=kwin_wayland --no-lockscreen --inputmethod maliit-keyboard diff --git a/plasma/workspace/shell/CMakeLists.txt b/plasma/workspace/shell/CMakeLists.txt new file mode 100644 index 0000000000..c355b94faa --- /dev/null +++ b/plasma/workspace/shell/CMakeLists.txt @@ -0,0 +1,126 @@ +configure_file(config-ktexteditor.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-ktexteditor.h ) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-plasma.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-plasma.h) + +add_definitions(-DPLASMA_DEPRECATED=) + +set(scripting_SRC + scripting/appinterface.cpp + scripting/applet.cpp + scripting/containment.cpp + scripting/configgroup.cpp + scripting/panel.cpp + scripting/scriptengine.cpp + scripting/scriptengine_v1.cpp + scripting/widget.cpp +) + +set(plasmashell_dbusXML dbus/org.kde.PlasmaShell.xml) +qt_add_dbus_adaptor(scripting_SRC ${plasmashell_dbusXML} shellcorona.h ShellCorona plasmashelladaptor) + +ecm_qt_declare_logging_category(plasmashell HEADER debug.h + IDENTIFIER PLASMASHELL + CATEGORY_NAME kde.plasmashell + DEFAULT_SEVERITY Info) + +ecm_qt_declare_logging_category(plasmashell HEADER screenpool-debug.h + IDENTIFIER SCREENPOOL + CATEGORY_NAME kde.plasmashell.screenpool + DEFAULT_SEVERITY Info) +set (plasma_shell_SRCS + alternativeshelper.cpp + main.cpp + containmentconfigview.cpp + currentcontainmentactionsmodel.cpp + desktopview.cpp + panelview.cpp + panelconfigview.cpp + panelshadows.cpp + primaryoutputwatcher.cpp + shellcorona.cpp + standaloneappcorona.cpp + osd.cpp + coronatesthelper.cpp + strutmanager.cpp + debug.cpp + screenpool-debug.cpp + screenpool.cpp + softwarerendernotifier.cpp + shellcontainmentconfig.cpp + ${scripting_SRC} +) + +if (TARGET KUserFeedbackCore) + set(plasma_shell_SRCS + ${plasma_shell_SRCS} + userfeedback.cpp + ) +endif() + +ecm_add_qtwayland_client_protocol(plasma_shell_SRCS + PROTOCOL ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-primary-output-v1.xml + BASENAME kde-primary-output-v1 +) + +set(krunner_xml ${plasma-workspace_SOURCE_DIR}/krunner/dbus/org.kde.krunner.App.xml) +qt_add_dbus_interface(plasma_shell_SRCS ${krunner_xml} krunner_interface) + + +add_executable(plasmashell + ${plasma_shell_SRCS} +) + +target_link_libraries(plasmashell + Qt::Quick + Qt::DBus + KF5::KIOCore + KF5::WindowSystem + KF5::Crash + KF5::Plasma + KF5::PlasmaQuick + KF5::Solid + KF5::Declarative + KF5::I18n + KF5::Activities + KF5::GlobalAccel + KF5::CoreAddons + KF5::DBusAddons + KF5::QuickAddons + KF5::XmlGui + KF5::Package + KF5::WaylandClient + KF5::Notifications + PW::KWorkspace + Wayland::Client +) +if (TARGET KUserFeedbackCore) + target_link_libraries(plasmashell KUserFeedbackCore) + target_compile_definitions(plasmashell PRIVATE -DWITH_KUSERFEEDBACKCORE) +endif() + +target_include_directories(plasmashell PRIVATE "${CMAKE_BINARY_DIR}") +target_compile_definitions(plasmashell PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}") + +if(HAVE_X11) + target_link_libraries(plasmashell XCB::XCB XCB::RANDR) + target_link_libraries(plasmashell Qt::X11Extras) +endif() + +configure_file(org.kde.plasmashell.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.desktop @ONLY) + +install(TARGETS plasmashell ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) +install( FILES dbus/org.kde.PlasmaShell.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} ) + +ecm_install_configured_files(INPUT plasma-plasmashell.service.in @ONLY DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) + +install(FILES + scripting/plasma-layouttemplate.desktop + DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) + +add_subdirectory(packageplugins) +if(BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() diff --git a/plasma/workspace/shell/Messages.sh b/plasma/workspace/shell/Messages.sh new file mode 100644 index 0000000000..b38d1de35b --- /dev/null +++ b/plasma/workspace/shell/Messages.sh @@ -0,0 +1,2 @@ +#! /bin/sh +$XGETTEXT *.cpp *.h -o $podir/plasmashell.pot diff --git a/plasma/workspace/shell/alternativeshelper.cpp b/plasma/workspace/shell/alternativeshelper.cpp new file mode 100644 index 0000000000..4d8e9bb587 --- /dev/null +++ b/plasma/workspace/shell/alternativeshelper.cpp @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "alternativeshelper.h" + +#include +#include + +#include +#include + +AlternativesHelper::AlternativesHelper(Plasma::Applet *applet, QObject *parent) + : QObject(parent) + , m_applet(applet) +{ +} + +AlternativesHelper::~AlternativesHelper() +{ +} + +QStringList AlternativesHelper::appletProvides() const +{ + return m_applet->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList()); +} + +QString AlternativesHelper::currentPlugin() const +{ + return m_applet->pluginMetaData().pluginId(); +} + +QQuickItem *AlternativesHelper::applet() const +{ + return m_applet->property("_plasma_graphicObject").value(); +} + +void AlternativesHelper::loadAlternative(const QString &plugin) +{ + if (plugin == m_applet->pluginMetaData().pluginId() || m_applet->isContainment()) { + return; + } + + Plasma::Containment *cont = m_applet->containment(); + if (!cont) { + return; + } + + QQuickItem *appletItem = m_applet->property("_plasma_graphicObject").value(); + QQuickItem *contItem = cont->property("_plasma_graphicObject").value(); + if (!appletItem || !contItem) { + return; + } + + // ensure the global shortcut is moved to the new applet + const QKeySequence &shortcut = m_applet->globalShortcut(); + m_applet->setGlobalShortcut(QKeySequence()); // need to unmap the old one first + + const QPoint newPos = appletItem->mapToItem(contItem, QPointF(0, 0)).toPoint(); + + m_applet->destroy(); + + connect(m_applet, &QObject::destroyed, contItem, [=]() { + Plasma::Applet *newApplet = nullptr; + QMetaObject::invokeMethod(contItem, + "createApplet", + Q_RETURN_ARG(Plasma::Applet *, newApplet), + Q_ARG(QString, plugin), + Q_ARG(QVariantList, QVariantList()), + Q_ARG(QPoint, newPos)); + + if (newApplet) { + newApplet->setGlobalShortcut(shortcut); + } + }); +} + +#include "moc_alternativeshelper.cpp" diff --git a/plasma/workspace/shell/alternativeshelper.h b/plasma/workspace/shell/alternativeshelper.h new file mode 100644 index 0000000000..954bb44dd8 --- /dev/null +++ b/plasma/workspace/shell/alternativeshelper.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class AlternativesHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList appletProvides READ appletProvides CONSTANT) + Q_PROPERTY(QString currentPlugin READ currentPlugin CONSTANT) + Q_PROPERTY(QQuickItem *applet READ applet CONSTANT) + +public: + explicit AlternativesHelper(Plasma::Applet *applet, QObject *parent = nullptr); + ~AlternativesHelper() override; + + QQuickItem *applet() const; + QStringList appletProvides() const; + QString currentPlugin() const; + + Q_INVOKABLE void loadAlternative(const QString &plugin); + +private: + Plasma::Applet *m_applet; +}; diff --git a/plasma/workspace/shell/autotests/CMakeLists.txt b/plasma/workspace/shell/autotests/CMakeLists.txt new file mode 100644 index 0000000000..d013675277 --- /dev/null +++ b/plasma/workspace/shell/autotests/CMakeLists.txt @@ -0,0 +1,38 @@ +add_subdirectory(mockserver) + +include(ECMAddTests) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/..) + +MACRO(PLASMASHELL_UNIT_TESTS) + FOREACH(_testname ${ARGN}) + set(test_SRCS + ${_testname}.cpp ../screenpool.cpp ${CMAKE_CURRENT_BINARY_DIR}/../screenpool-debug.cpp ../primaryoutputwatcher.cpp + ) + include_directories(${CMAKE_CURRENT_BINARY_DIR}/../mockserver) + add_executable(${_testname} ${test_SRCS}) + target_link_libraries(${_testname} + Qt::Test + Qt::Gui + KF5::Service + KF5::WaylandClient + KF5::WindowSystem + Wayland::Client + Wayland::Server + SharedClientTest + ) + if(HAVE_X11) + target_link_libraries(${_testname} XCB::XCB XCB::RANDR) + target_link_libraries(${_testname} Qt::X11Extras) + endif() + if(QT_QTOPENGL_FOUND) + target_link_libraries(${_testname} Qt::OpenGL) + endif() + add_test(NAME ${_testname} COMMAND ${_testname}) + ecm_mark_as_test(${_testname}) + ENDFOREACH(_testname) +ENDMACRO(PLASMASHELL_UNIT_TESTS) + +PLASMASHELL_UNIT_TESTS( + screenpooltest +) diff --git a/plasma/workspace/shell/autotests/desktopview.cpp b/plasma/workspace/shell/autotests/desktopview.cpp new file mode 100644 index 0000000000..319bbf1033 --- /dev/null +++ b/plasma/workspace/shell/autotests/desktopview.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopview.h" + +#include + +DesktopView::DesktopView(Plasma::Corona *c, QScreen *targetScreen) + : QWindow(targetScreen) +{ + if (targetScreen) { + setScreenToFollow(targetScreen); + setScreen(targetScreen); + setGeometry(targetScreen->geometry()); + } +} + +DesktopView::~DesktopView() +{ +} + +void DesktopView::setScreenToFollow(QScreen *screen) +{ + if (screen == m_screenToFollow) { + return; + } + + m_screenToFollow = screen; + setScreen(screen); +} + +QScreen *DesktopView::screenToFollow() const +{ + return m_screenToFollow; +} diff --git a/plasma/workspace/shell/autotests/desktopview.h b/plasma/workspace/shell/autotests/desktopview.h new file mode 100644 index 0000000000..5699b514b3 --- /dev/null +++ b/plasma/workspace/shell/autotests/desktopview.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class QScreen; + +class DesktopView : public QWindow +{ + Q_OBJECT + +public: + explicit DesktopView(Plasma::Corona *c, QScreen *targetScreen = nullptr); + ~DesktopView() override; + + /*This is different from screen() as is always there, even if the window is + temporarily outside the screen or if is hidden: only plasmashell will ever + change this property, unlike QWindow::screen()*/ + void setScreenToFollow(QScreen *screen); + QScreen *screenToFollow() const; + +private: + QPointer m_screenToFollow; +}; diff --git a/plasma/workspace/shell/autotests/mockserver/CMakeLists.txt b/plasma/workspace/shell/autotests/mockserver/CMakeLists.txt new file mode 100644 index 0000000000..b01661b6a3 --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/CMakeLists.txt @@ -0,0 +1,47 @@ +project(waylandmockservertest) + +find_package(WaylandProtocols 1.24) +set_package_properties(WaylandProtocols PROPERTIES TYPE REQUIRED) +find_package(Threads) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_USE_NANOSLEEP" ) + +set(SharedClientTest_LIB_SRCS + corecompositor.cpp corecompositor.h + coreprotocol.cpp coreprotocol.h + mockcompositor.cpp mockcompositor.h + xdgoutputv1.cpp xdgoutputv1.h + primaryoutput.cpp primaryoutput.h +) + +ecm_add_qtwayland_server_protocol(SharedClientTest_LIB_SRCS + PROTOCOL ${Wayland_DATADIR}/wayland.xml + BASENAME wayland +) + +ecm_add_qtwayland_server_protocol(SharedClientTest_LIB_SRCS + PROTOCOL ${WaylandProtocols_DATADIR}/unstable/xdg-output/xdg-output-unstable-v1.xml + BASENAME xdg-output-unstable-v1 +) + +ecm_add_qtwayland_client_protocol(SharedClientTest_LIB_SRCS + PROTOCOL ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-primary-output-v1.xml + BASENAME kde-primary-output-v1 +) +ecm_add_qtwayland_server_protocol(SharedClientTest_LIB_SRCS + PROTOCOL ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-primary-output-v1.xml + BASENAME kde-primary-output-v1 +) + +add_library(SharedClientTest OBJECT ${SharedClientTest_LIB_SRCS}) + +target_link_libraries(SharedClientTest + PUBLIC + Qt::Test + Qt::Gui + Qt::WaylandClientPrivate + Wayland::Server + Threads::Threads +) + +target_include_directories(SharedClientTest PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/plasma/workspace/shell/autotests/mockserver/corecompositor.cpp b/plasma/workspace/shell/autotests/mockserver/corecompositor.cpp new file mode 100644 index 0000000000..c29261987a --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/corecompositor.cpp @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "corecompositor.h" +#include + +namespace MockCompositor +{ +CoreCompositor::CoreCompositor() + : m_display(wl_display_create()) + , m_socketName(wl_display_add_socket_auto(m_display)) + , m_eventLoop(wl_display_get_event_loop(m_display)) + + // Start dispatching + , m_dispatchThread([this]() { + while (m_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + dispatch(); + } + }) +{ + qputenv("WAYLAND_DISPLAY", m_socketName); + Q_ASSERT(isClean()); +} + +CoreCompositor::~CoreCompositor() +{ + m_running = false; + m_dispatchThread.join(); + wl_display_destroy(m_display); +} + +bool CoreCompositor::isClean() +{ + Lock lock(this); + for (auto *global : qAsConst(m_globals)) { + if (!global->isClean()) + return false; + } + return true; +} + +QString CoreCompositor::dirtyMessage() +{ + Lock lock(this); + QStringList messages; + for (auto *global : qAsConst(m_globals)) { + if (!global->isClean()) + messages << (global->metaObject()->className() % QLatin1String(": ") % global->dirtyMessage()); + } + return messages.join(", "); +} + +void CoreCompositor::dispatch() +{ + Lock lock(this); + wl_display_flush_clients(m_display); + constexpr int timeout = 0; // immediate return + wl_event_loop_dispatch(m_eventLoop, timeout); +} + +/*! + * \brief Adds a new global interface for the compositor + * + * Takes ownership of \a global + */ +void CoreCompositor::add(Global *global) +{ + warnIfNotLockedByThread(Q_FUNC_INFO); + m_globals.append(global); +} + +void CoreCompositor::remove(Global *global) +{ + warnIfNotLockedByThread(Q_FUNC_INFO); + m_globals.removeAll(global); + delete global; +} + +uint CoreCompositor::nextSerial() +{ + warnIfNotLockedByThread(Q_FUNC_INFO); + return wl_display_next_serial(m_display); +} + +wl_client *CoreCompositor::client(int index) +{ + warnIfNotLockedByThread(Q_FUNC_INFO); + wl_list *clients = wl_display_get_client_list(m_display); + wl_client *client = nullptr; + int i = 0; + wl_client_for_each(client, clients) + { + if (i++ == index) + return client; + } + return nullptr; +} + +void CoreCompositor::warnIfNotLockedByThread(const char *caller) +{ + if (!m_lock || !m_lock->isOwnedByCurrentThread()) { + qWarning() << caller << "called without locking the compositor to the current thread." + << "This means the compositor can start dispatching at any moment," + << "potentially leading to threading issues." + << "Unless you know what you are doing you should probably fix the test" + << "by locking the compositor before accessing it (see mutex())."; + } +} + +} // namespace MockCompositor diff --git a/plasma/workspace/shell/autotests/mockserver/corecompositor.h b/plasma/workspace/shell/autotests/mockserver/corecompositor.h new file mode 100644 index 0000000000..d503a8a7fe --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/corecompositor.h @@ -0,0 +1,228 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MOCKCOMPOSITOR_CORECOMPOSITOR_H +#define MOCKCOMPOSITOR_CORECOMPOSITOR_H + +#include + +#include + +struct wl_resource; + +namespace MockCompositor +{ +class Global : public QObject +{ + Q_OBJECT +public: + virtual bool isClean() + { + return true; + } + virtual QString dirtyMessage() + { + return isClean() ? "clean" : "dirty"; + } +}; + +class CoreCompositor +{ +public: + explicit CoreCompositor(); + ~CoreCompositor(); + bool isClean(); + QString dirtyMessage(); + void dispatch(); + + template + auto exec(function_type func, arg_types &&...args) -> decltype(func()) + { + Lock lock(this); + return func(std::forward(args)...); + } + + // Unsafe section below, YOU are responsible that the compositor is locked or + // this is run through the mutex() method! + + void add(Global *global); + void remove(Global *global); + + /*! + * \brief Constructs and adds a new global with the given parameters + * + * Convenience function. i.e. + * + * compositor->add(new MyGlobal(compositor, version); + * + * can be written as: + * + * compositor->add(version); + * + * Returns the new global + */ + template + global_type *add(arg_types &&...args) + { + warnIfNotLockedByThread(Q_FUNC_INFO); + auto *global = new global_type(this, std::forward(args)...); + m_globals.append(global); + return global; + } + + /*! + * \brief Removes all globals of the given type + * + * Convenience function + */ + template + void removeAll() + { + const auto globals = getAll(); + for (auto global : globals) + remove(global); + } + + /*! + * \brief Returns a global with the given type, if any + */ + template + global_type *get() + { + warnIfNotLockedByThread(Q_FUNC_INFO); + for (auto *global : qAsConst(m_globals)) { + if (auto *casted = qobject_cast(global)) + return casted; + } + return nullptr; + } + + /*! + * \brief Returns the nth global with the given type, if any + */ + template + global_type *get(int index) + { + warnIfNotLockedByThread(Q_FUNC_INFO); + for (auto *global : qAsConst(m_globals)) { + if (auto *casted = qobject_cast(global)) { + if (index--) + continue; + return casted; + } + } + return nullptr; + } + + /*! + * \brief Returns all globals with the given type, if any + */ + template + QVector getAll() + { + warnIfNotLockedByThread(Q_FUNC_INFO); + QVector matching; + for (auto *global : qAsConst(m_globals)) { + if (auto *casted = qobject_cast(global)) + matching.append(casted); + } + return matching; + } + + uint nextSerial(); + wl_client *client(int index = 0); + void warnIfNotLockedByThread(const char *caller = "warnIfNotLockedbyThread"); + +public: + // Only use this carefully from the test thread (i.e. lock first) + wl_display *m_display = nullptr; +protected: + class Lock + { + public: + explicit Lock(CoreCompositor *compositor) + : m_compositor(compositor) + , m_threadId(std::this_thread::get_id()) + { + // Can't use a QMutexLocker here, as it's not movable + compositor->m_mutex.lock(); + Q_ASSERT(compositor->m_lock == nullptr); + compositor->m_lock = this; + } + ~Lock() + { + Q_ASSERT(m_compositor->m_lock == this); + m_compositor->m_lock = nullptr; + m_compositor->m_mutex.unlock(); + } + + // Move semantics + Lock(Lock &&) = default; + Lock &operator=(Lock &&) = default; + + // Disable copying + Lock(const Lock &) = delete; + Lock &operator=(const Lock &) = delete; + + bool isOwnedByCurrentThread() const + { + return m_threadId == std::this_thread::get_id(); + } + + private: + CoreCompositor *m_compositor = nullptr; + std::thread::id m_threadId; + }; + QByteArray m_socketName; + wl_event_loop *m_eventLoop = nullptr; + bool m_running = true; + QVector m_globals; + +private: + Lock *m_lock = nullptr; + QMutex m_mutex; + std::thread m_dispatchThread; +}; + +template +QByteArray toByteArray(container_type container) +{ + return QByteArray(reinterpret_cast(container.data()), sizeof(container[0]) * container.size()); +} + +template +return_type *fromResource(::wl_resource *resource) +{ + if (auto *r = return_type::Resource::fromResource(resource)) + return static_cast(r->object()); + return nullptr; +} + +} // namespace MockCompositor + +#endif // MOCKCOMPOSITOR_CORECOMPOSITOR_H diff --git a/plasma/workspace/shell/autotests/mockserver/coreprotocol.cpp b/plasma/workspace/shell/autotests/mockserver/coreprotocol.cpp new file mode 100644 index 0000000000..ac4c2cd934 --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/coreprotocol.cpp @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "coreprotocol.h" + +namespace MockCompositor +{ + +void Output::sendGeometry() +{ + const auto resources = resourceMap().values(); + for (auto r : resources) + sendGeometry(r); +} + +void Output::sendGeometry(Resource *resource) +{ + wl_output::send_geometry(resource->handle, + m_data.position.x(), + m_data.position.y(), + m_data.physicalSize.width(), + m_data.physicalSize.height(), + m_data.subpixel, + m_data.make, + m_data.model, + m_data.transform); +} + +void Output::sendScale(int factor) +{ + m_data.scale = factor; + const auto resources = resourceMap().values(); + for (auto r : resources) + sendScale(r); +} + +void Output::sendScale(Resource *resource) +{ + wl_output::send_scale(resource->handle, m_data.scale); +} + +void Output::sendDone(wl_client *client) +{ + auto resources = resourceMap().values(client); + for (auto *r : resources) + wl_output::send_done(r->handle); +} + +void Output::sendDone() +{ + const auto resources = resourceMap().values(); + for (auto r : resources) + wl_output::send_done(r->handle); +} + +void Output::output_bind_resource(QtWaylandServer::wl_output::Resource *resource) +{ + sendGeometry(resource); + send_mode(resource->handle, mode_preferred | mode_current, m_data.mode.resolution.width(), m_data.mode.resolution.height(), m_data.mode.refreshRate); + sendScale(resource); + wl_output::send_done(resource->handle); +} + +} // namespace MockCompositor diff --git a/plasma/workspace/shell/autotests/mockserver/coreprotocol.h b/plasma/workspace/shell/autotests/mockserver/coreprotocol.h new file mode 100644 index 0000000000..76485bc7c4 --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/coreprotocol.h @@ -0,0 +1,117 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MOCKCOMPOSITOR_COREPROTOCOL_H +#define MOCKCOMPOSITOR_COREPROTOCOL_H + +#include "corecompositor.h" + +#include + +namespace MockCompositor +{ +class WlCompositor; +class Output; +class Pointer; +class Touch; +class Keyboard; +class CursorRole; +class ShmPool; +class ShmBuffer; +class DataDevice; + +struct OutputMode { + explicit OutputMode() = default; + explicit OutputMode(const QSize &resolution, int refreshRate = 60000) + : resolution(resolution) + , refreshRate(refreshRate) + { + } + QSize resolution = QSize(1920, 1080); + int refreshRate = 60000; // In mHz + // TODO: flags (they're currently hard-coded) + + // in mm + QSize physicalSizeForDpi(int dpi) + { + return (QSizeF(resolution) * 25.4 / dpi).toSize(); + } +}; + +struct OutputData { + using Subpixel = QtWaylandServer::wl_output::subpixel; + using Transform = QtWaylandServer::wl_output::transform; + explicit OutputData() = default; + + // for geometry event + QPoint position; + QSize physicalSize = QSize(0, 0); // means unknown physical size + QString make = "Make"; + QString model = "Model"; + Subpixel subpixel = Subpixel::subpixel_unknown; + Transform transform = Transform::transform_normal; + + int scale = 1; // for scale event + OutputMode mode; // for mode event +}; + +class Output : public Global, public QtWaylandServer::wl_output +{ + Q_OBJECT +public: + explicit Output(CoreCompositor *compositor, OutputData data = OutputData()) + : QtWaylandServer::wl_output(compositor->m_display, 2) + , m_data(std::move(data)) + { + } + + void send_geometry() = delete; + void sendGeometry(); + void sendGeometry(Resource *resource); // Sends to only one client + + void send_scale(int32_t factor) = delete; + void sendScale(int factor); + void sendScale(Resource *resource); // Sends current scale to only one client + + void sendDone(wl_client *client); + void sendDone(); + + int scale() const + { + return m_data.scale; + } + + OutputData m_data; + +protected: + void output_bind_resource(Resource *resource) override; +}; + +} // namespace MockCompositor + +#endif // MOCKCOMPOSITOR_COREPROTOCOL_H diff --git a/plasma/workspace/shell/autotests/mockserver/mockcompositor.cpp b/plasma/workspace/shell/autotests/mockserver/mockcompositor.cpp new file mode 100644 index 0000000000..5df653639b --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/mockcompositor.cpp @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mockcompositor.h" + +namespace MockCompositor +{ +DefaultCompositor::DefaultCompositor() +{ + { + Lock l(this); + + add(); + auto *output = add(); + auto *primaryOutput = add(); + output->m_data.physicalSize = output->m_data.mode.physicalSizeForDpi(96); + primaryOutput->setPrimaryOutputName("WL-1"); + } + Q_ASSERT(isClean()); +} + +} // namespace MockCompositor diff --git a/plasma/workspace/shell/autotests/mockserver/mockcompositor.h b/plasma/workspace/shell/autotests/mockserver/mockcompositor.h new file mode 100644 index 0000000000..eabb3bf82e --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/mockcompositor.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MOCKCOMPOSITOR_H +#define MOCKCOMPOSITOR_H + +#include "corecompositor.h" +#include "coreprotocol.h" +#include "primaryoutput.h" +#include "xdgoutputv1.h" + +#include + +namespace MockCompositor +{ +class DefaultCompositor : public CoreCompositor +{ +public: + explicit DefaultCompositor(); + // Convenience functions + Output *output(int i = 0) + { + return getAll().value(i, nullptr); + } + XdgOutputV1 *xdgOutput(Output *out) + { + return get()->getXdgOutput(out); + } + PrimaryOutputV1 *primaryOutput() + { + auto *primary = get(); + Q_ASSERT(primary); + return primary; + } +}; + +// addOutput(OutputData) +// setPrimary() + +} // namespace MockCompositor + +#define QCOMPOSITOR_VERIFY(expr) \ + QVERIFY(exec([&] { \ + return expr; \ + })) +#define QCOMPOSITOR_TRY_VERIFY(expr) \ + QTRY_VERIFY(exec([&] { \ + return expr; \ + })) +#define QCOMPOSITOR_COMPARE(expr, expr2) \ + QCOMPARE(exec([&] { \ + return expr; \ + }), \ + expr2) +#define QCOMPOSITOR_TRY_COMPARE(expr, expr2) \ + QTRY_COMPARE(exec([&] { \ + return expr; \ + }), \ + expr2) + +#define QCOMPOSITOR_TEST_MAIN(test) \ + int main(int argc, char **argv) \ + { \ + QTemporaryDir tmpRuntimeDir; \ + setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1); \ + setenv("XDG_CURRENT_DESKTOP", "qtwaylandtests", 1); \ + setenv("QT_QPA_PLATFORM", "wayland", 1); \ + test tc; \ + QGuiApplication app(argc, argv); \ + return QTest::qExec(&tc, argc, argv); \ + } + +#endif diff --git a/plasma/workspace/shell/autotests/mockserver/primaryoutput.cpp b/plasma/workspace/shell/autotests/mockserver/primaryoutput.cpp new file mode 100644 index 0000000000..a31cf4a9d2 --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/primaryoutput.cpp @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2022 Marco Martin +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "primaryoutput.h" + +namespace MockCompositor +{ +PrimaryOutputV1::PrimaryOutputV1(CoreCompositor *compositor, int version) + : QtWaylandServer::kde_primary_output_v1(compositor->m_display, version) +{ +} + +void PrimaryOutputV1::setPrimaryOutputName(const QString &primaryName) +{ + m_primaryName = primaryName; + const auto resources = resourceMap(); + for (auto *resource : resources) { + send_primary_output(resource->handle, primaryName); + } +} + +void PrimaryOutputV1::kde_primary_output_v1_bind_resource(Resource *resource) +{ + send_primary_output(resource->handle, m_primaryName); +} + +} // namespace MockCompositor diff --git a/plasma/workspace/shell/autotests/mockserver/primaryoutput.h b/plasma/workspace/shell/autotests/mockserver/primaryoutput.h new file mode 100644 index 0000000000..e858ad25cf --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/primaryoutput.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2022 Marco Martin +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MOCKCOMPOSITOR_PRIMARYOUTPUT_H +#define MOCKCOMPOSITOR_PRIMARYOUTPUT_H + +#include "coreprotocol.h" +#include + +#include + +namespace MockCompositor +{ +class PrimaryOutputV1 : public Global, public QtWaylandServer::kde_primary_output_v1 +{ + Q_OBJECT +public: + explicit PrimaryOutputV1(CoreCompositor *compositor, int version = 1); + + void setPrimaryOutputName(const QString &primaryName); + +protected: + void kde_primary_output_v1_bind_resource(Resource *resource) override; + +private: + QString m_primaryName; +}; + +} // namespace MockCompositor + +#endif // MOCKCOMPOSITOR_PRIMARYOUTPUT_H diff --git a/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.cpp b/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.cpp new file mode 100644 index 0000000000..de8434cffe --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.cpp @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "xdgoutputv1.h" + +namespace MockCompositor +{ +int XdgOutputV1::s_nextId = 1; + +void XdgOutputV1::sendLogicalSize(const QSize &size) +{ + m_logicalGeometry.setSize(size); + for (auto *resource : resourceMap()) + zxdg_output_v1::send_logical_size(resource->handle, size.width(), size.height()); +} + +void XdgOutputV1::addResource(wl_client *client, int id, int version) +{ + auto *resource = add(client, id, version)->handle; + zxdg_output_v1::send_logical_size(resource, m_logicalGeometry.width(), m_logicalGeometry.height()); + send_logical_position(resource, m_logicalGeometry.x(), m_logicalGeometry.y()); + if (version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) + send_name(resource, m_name); + if (version >= ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION) + send_description(resource, m_description); + + if (version < 3) // zxdg_output_v1.done has been deprecated + zxdg_output_v1::send_done(resource); + else { + m_output->sendDone(client); + } +} + +} // namespace MockCompositor diff --git a/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.h b/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.h new file mode 100644 index 0000000000..db292f4b05 --- /dev/null +++ b/plasma/workspace/shell/autotests/mockserver/xdgoutputv1.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MOCKCOMPOSITOR_XDGOUTPUTV1_H +#define MOCKCOMPOSITOR_XDGOUTPUTV1_H + +#include "coreprotocol.h" + +#include + +namespace MockCompositor +{ +class XdgOutputV1 : public QObject, public QtWaylandServer::zxdg_output_v1 +{ + Q_OBJECT +public: + explicit XdgOutputV1(Output *output) + : m_output(output) + , m_logicalGeometry(m_output->m_data.position, QSize(m_output->m_data.mode.resolution / m_output->m_data.scale)) + , m_name(QString("WL-%1").arg(s_nextId++)) + { + } + + void send_logical_size(int32_t width, int32_t height) = delete; + void sendLogicalSize(const QSize &size); + + void send_done() = delete; // zxdg_output_v1.done has been deprecated (in protocol version 3) + + void addResource(wl_client *client, int id, int version); + Output *m_output = nullptr; + QRect m_logicalGeometry; + QString m_name; + QString m_description = "This is an Xdg Output description"; + static int s_nextId; +}; + +class XdgOutputManagerV1 : public Global, public QtWaylandServer::zxdg_output_manager_v1 +{ + Q_OBJECT +public: + explicit XdgOutputManagerV1(CoreCompositor *compositor) + : QtWaylandServer::zxdg_output_manager_v1(compositor->m_display, 3) + { + } + QMap m_xdgOutputs; + XdgOutputV1 *getXdgOutput(Output *output) + { + if (auto *xdgOutput = m_xdgOutputs.value(output)) + return xdgOutput; + return m_xdgOutputs[output] = new XdgOutputV1(output); // TODO: free memory + } + +protected: + void zxdg_output_manager_v1_get_xdg_output(Resource *resource, uint32_t id, wl_resource *outputResource) override + { + auto *output = fromResource(outputResource); + auto *xdgOutput = getXdgOutput(output); + xdgOutput->addResource(resource->client(), id, resource->version()); + } +}; + +} // namespace MockCompositor + +#endif // MOCKCOMPOSITOR_XDGOUTPUTV1_H diff --git a/plasma/workspace/shell/autotests/screenpooltest.cpp b/plasma/workspace/shell/autotests/screenpooltest.cpp new file mode 100644 index 0000000000..74f1661684 --- /dev/null +++ b/plasma/workspace/shell/autotests/screenpooltest.cpp @@ -0,0 +1,416 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include + +#include +#include +#include +#include +#include +#include + +#include "../screenpool.h" +#include "mockcompositor.h" +#include "xdgoutputv1.h" + +using namespace MockCompositor; + +class ScreenPoolTest : public QObject, DefaultCompositor +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void testScreenInsertion(); + void testRedundantScreenInsertion(); + void testMoveOutOfRedundant(); + void testMoveInRedundant(); + void testPrimarySwap(); + void testPrimarySwapToRedundant(); + void testMoveRedundantToMakePrimary(); + void testMoveInRedundantToLosePrimary(); + void testSecondScreenRemoval(); + void testThirdScreenRemoval(); + void testLastScreenRemoval(); + void testFakeToRealScreen(); + +private: + ScreenPool *m_screenPool; +}; + +void ScreenPoolTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + qRegisterMetaType(); + + KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("ScreenConnectors")); + cg.deleteGroup(); + cg.sync(); + m_screenPool = new ScreenPool(KSharedConfig::openConfig(), this); + m_screenPool->load(); + + QTRY_COMPARE(QGuiApplication::screens().size(), 1); + QCOMPARE(m_screenPool->screens().size(), 1); + QCOMPARE(QGuiApplication::screens().first()->name(), QStringLiteral("WL-1")); + QCOMPARE(QGuiApplication::primaryScreen(), QGuiApplication::screens().first()); + QCOMPARE(QGuiApplication::primaryScreen(), m_screenPool->primaryScreen()); + QCOMPARE(m_screenPool->id(m_screenPool->primaryScreen()->name()), 0); + QCOMPARE(m_screenPool->connector(0), QStringLiteral("WL-1")); +} + +void ScreenPoolTest::cleanupTestCase() +{ + QCOMPOSITOR_COMPARE(getAll().size(), 1); // Only the default output should be left + QTRY_COMPARE(QGuiApplication::screens().size(), 1); + QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); + + KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("ScreenConnectors")); + cg.deleteGroup(); + cg.sync(); +} + +void ScreenPoolTest::testScreenInsertion() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + + // Add a new output + exec([=] { + OutputData data; + data.mode.resolution = {1920, 1080}; + data.position = {1920, 0}; + data.physicalSize = data.mode.physicalSizeForDpi(96); + // NOTE: assumes that when a screen is added it will already have the final geometry + add(data); + }); + + addedSpy.wait(); + QCOMPARE(QGuiApplication::screens().size(), 2); + QCOMPARE(m_screenPool->screens().size(), 2); + QCOMPARE(addedSpy.size(), 1); + + QScreen *newScreen = addedSpy.takeFirst().at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-2")); + QCOMPARE(newScreen->geometry(), QRect(1920, 0, 1920, 1080)); + // Check mapping + QCOMPARE(m_screenPool->id(newScreen->name()), 1); + QCOMPARE(m_screenPool->connector(1), QStringLiteral("WL-2")); +} + +void ScreenPoolTest::testRedundantScreenInsertion() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + QSignalSpy addedFromAppSpy(qGuiApp, SIGNAL(screenAdded(QScreen *))); + + // Add a new output + exec([=] { + OutputData data; + data.mode.resolution = {1280, 720}; + data.position = {1920, 0}; + data.physicalSize = data.mode.physicalSizeForDpi(96); + // NOTE: assumes that when a screen is added it will already have the final geometry + add(data); + }); + + addedFromAppSpy.wait(); + addedSpy.wait(250); + // only addedFromAppSpy will have registered something, nothing in addedSpy, + // on ScreenPool API POV is like this new screen doesn't exist, because is redundant to WL-2 + QCOMPARE(QGuiApplication::screens().size(), 3); + QCOMPARE(m_screenPool->screens().size(), 2); + QCOMPARE(addedFromAppSpy.size(), 1); + QCOMPARE(addedSpy.size(), 0); + + QScreen *newScreen = addedFromAppSpy.takeFirst().at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(newScreen->geometry(), QRect(1920, 0, 1280, 720)); + QVERIFY(!m_screenPool->screens().contains(newScreen)); + + QCOMPARE(m_screenPool->id(newScreen->name()), 2); + QCOMPARE(m_screenPool->connector(2), QStringLiteral("WL-3")); +} + +void ScreenPoolTest::testMoveOutOfRedundant() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + + exec([=] { + auto *out = output(2); + auto *xdgOut = xdgOutput(out); + out->m_data.mode.resolution = {1280, 2048}; + xdgOut->sendLogicalSize(QSize(1280, 2048)); + out->sendDone(); + }); + + addedSpy.wait(); + QCOMPARE(addedSpy.size(), 1); + QScreen *newScreen = addedSpy.takeFirst().at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(newScreen->geometry(), QRect(1920, 0, 1280, 2048)); + QVERIFY(m_screenPool->screens().contains(newScreen)); +} + +void ScreenPoolTest::testMoveInRedundant() +{ + QSignalSpy removedSpy(m_screenPool, SIGNAL(screenRemoved(QScreen *))); + + exec([=] { + auto *out = output(2); + auto *xdgOut = xdgOutput(out); + out->m_data.mode.resolution = {1280, 720}; + xdgOut->sendLogicalSize(QSize(1280, 720)); + out->sendDone(); + }); + + removedSpy.wait(); + QCOMPARE(removedSpy.size(), 1); + QScreen *oldScreen = removedSpy.takeFirst().at(0).value(); + QCOMPARE(oldScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(oldScreen->geometry(), QRect(1920, 0, 1280, 720)); + QVERIFY(!m_screenPool->screens().contains(oldScreen)); +} + +void ScreenPoolTest::testPrimarySwap() +{ + QSignalSpy primaryChangeSpy(m_screenPool, SIGNAL(primaryScreenChanged(QScreen *, QScreen *))); + + // Check ScreenPool mapping before switch + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-1")); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(QStringLiteral("WL-1")), 0); + QCOMPARE(m_screenPool->id(QStringLiteral("WL-2")), 1); + + // Set a primary screen + exec([=] { + primaryOutput()->setPrimaryOutputName("WL-2"); + }); + + primaryChangeSpy.wait(); + + QCOMPARE(primaryChangeSpy.size(), 1); + QScreen *oldPrimary = primaryChangeSpy[0].at(0).value(); + QScreen *newPrimary = primaryChangeSpy[0].at(1).value(); + QCOMPARE(oldPrimary->name(), QStringLiteral("WL-1")); + QCOMPARE(oldPrimary->geometry(), QRect(0, 0, 1920, 1080)); + QCOMPARE(newPrimary->name(), QStringLiteral("WL-2")); + QCOMPARE(newPrimary->geometry(), QRect(1920, 0, 1920, 1080)); + + // Check ScreenPool mapping + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-2")); + QCOMPARE(m_screenPool->primaryConnector(), newPrimary->name()); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(newPrimary->name()), 0); + QCOMPARE(m_screenPool->id(oldPrimary->name()), 1); +} + +void ScreenPoolTest::testPrimarySwapToRedundant() +{ + QSignalSpy primaryChangeSpy(m_screenPool, SIGNAL(primaryScreenChanged(QScreen *, QScreen *))); + + // Set a primary screen + exec([=] { + primaryOutput()->setPrimaryOutputName("WL-3"); + }); + + primaryChangeSpy.wait(250); + // Nothing will happen, is like WL3 doesn't exist + QCOMPARE(primaryChangeSpy.size(), 0); +} + +void ScreenPoolTest::testMoveRedundantToMakePrimary() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + QSignalSpy primaryChangeSpy(m_screenPool, SIGNAL(primaryScreenChanged(QScreen *, QScreen *))); + + exec([=] { + auto *out = output(2); + auto *xdgOut = xdgOutput(out); + out->m_data.mode.resolution = {1280, 2048}; + xdgOut->sendLogicalSize(QSize(1280, 2048)); + out->sendDone(); + }); + + // Having multiple spies, when the wait of one will exit both signals will already have been emitted + QTRY_COMPARE(addedSpy.size(), 1); + QTRY_COMPARE(primaryChangeSpy.size(), 1); + QScreen *newScreen = addedSpy.takeFirst().at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(newScreen->geometry(), QRect(1920, 0, 1280, 2048)); + QVERIFY(m_screenPool->screens().contains(newScreen)); + + // Test the new primary + QScreen *oldPrimary = primaryChangeSpy[0].at(0).value(); + QScreen *newPrimary = primaryChangeSpy[0].at(1).value(); + QCOMPARE(oldPrimary->name(), QStringLiteral("WL-2")); + QCOMPARE(oldPrimary->geometry(), QRect(1920, 0, 1920, 1080)); + QCOMPARE(newPrimary->name(), QStringLiteral("WL-3")); + QCOMPARE(newPrimary->geometry(), QRect(1920, 0, 1280, 2048)); + + // Check ScreenPool mapping + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-3")); + QCOMPARE(m_screenPool->primaryConnector(), newPrimary->name()); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(newPrimary->name()), 0); + QCOMPARE(m_screenPool->id(oldPrimary->name()), 2); +} + +void ScreenPoolTest::testMoveInRedundantToLosePrimary() +{ + QSignalSpy removedSpy(m_screenPool, SIGNAL(screenRemoved(QScreen *))); + QSignalSpy primaryChangeSpy(m_screenPool, SIGNAL(primaryScreenChanged(QScreen *, QScreen *))); + + exec([=] { + auto *out = output(2); + auto *xdgOut = xdgOutput(out); + xdgOut->sendLogicalSize(QSize(1280, 720)); + out->m_data.mode.resolution = {1280, 720}; + out->sendDone(); + }); + + QTRY_COMPARE(primaryChangeSpy.size(), 1); + // Test the new primary + QScreen *oldPrimary = primaryChangeSpy[0].at(0).value(); + QScreen *newPrimary = primaryChangeSpy[0].at(1).value(); + QCOMPARE(oldPrimary->name(), QStringLiteral("WL-3")); + QCOMPARE(oldPrimary->geometry(), QRect(1920, 0, 1280, 720)); + QCOMPARE(newPrimary->name(), QStringLiteral("WL-2")); + QCOMPARE(newPrimary->geometry(), QRect(1920, 0, 1920, 1080)); + + // Check ScreenPool mapping + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-2")); + QCOMPARE(m_screenPool->primaryConnector(), newPrimary->name()); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(newPrimary->name()), 0); + QCOMPARE(m_screenPool->id(oldPrimary->name()), 2); + + QTRY_COMPARE(removedSpy.size(), 1); + QScreen *oldScreen = removedSpy.takeFirst().at(0).value(); + QCOMPARE(oldScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(oldScreen->geometry(), QRect(1920, 0, 1280, 720)); + QVERIFY(!m_screenPool->screens().contains(oldScreen)); +} + +void ScreenPoolTest::testSecondScreenRemoval() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + QSignalSpy primaryChangeSpy(m_screenPool, SIGNAL(primaryScreenChanged(QScreen *, QScreen *))); + QSignalSpy removedSpy(m_screenPool, SIGNAL(screenRemoved(QScreen *))); + + // Check ScreenPool mapping before switch + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-2")); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(QStringLiteral("WL-2")), 0); + QCOMPARE(m_screenPool->id(QStringLiteral("WL-1")), 1); + + // Remove an output + exec([=] { + remove(output(1)); + }); + + // Removing the primary screen, will change a primaryChange signal beforehand + QTRY_COMPARE(primaryChangeSpy.size(), 1); + QCOMPARE(primaryChangeSpy.size(), 1); + QScreen *newPrimary = primaryChangeSpy[0].at(1).value(); + QCOMPARE(newPrimary->name(), QStringLiteral("WL-3")); + QCOMPARE(newPrimary->geometry(), QRect(1920, 0, 1280, 720)); + + // Check ScreenPool mapping + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-3")); + QCOMPARE(m_screenPool->primaryScreen()->name(), m_screenPool->primaryConnector()); + QCOMPARE(m_screenPool->id(newPrimary->name()), 0); + QCOMPARE(m_screenPool->id("WL-2"), 2); + + // NOTE: we can neither access the data of removedSpy nor oldPrimary because at this point will be dangling + QTRY_COMPARE(removedSpy.size(), 1); + QTRY_COMPARE(addedSpy.size(), 1); + + QCOMPARE(QGuiApplication::screens().size(), 2); + QCOMPARE(m_screenPool->screens().size(), 2); + QScreen *firstScreen = m_screenPool->screens().at(1); + QCOMPARE(firstScreen, newPrimary); + QCOMPARE(m_screenPool->primaryScreen(), newPrimary); + + // We'll get an added signal for the screen WL-3 that was previously redundant to WL-2 + QScreen *newScreen = addedSpy[0].at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-3")); + QCOMPARE(newScreen->geometry(), QRect(1920, 0, 1280, 720)); + QCOMPARE(m_screenPool->screens().at(1), newScreen); +} + +void ScreenPoolTest::testThirdScreenRemoval() +{ + QSignalSpy removedSpy(m_screenPool, SIGNAL(screenRemoved(QScreen *))); + + // Remove an output + exec([=] { + // NOTE: Assume the server will always do the right thing to change the primary screen before deleting one + primaryOutput()->setPrimaryOutputName("WL-1"); + + remove(output(1)); + }); + + // NOTE: we can neither access the data of removedSpy nor oldPrimary because at this point will be dangling + removedSpy.wait(); + QCOMPARE(QGuiApplication::screens().size(), 1); + QCOMPARE(m_screenPool->screens().size(), 1); + QScreen *lastScreen = m_screenPool->screens().first(); + QCOMPARE(lastScreen->name(), QStringLiteral("WL-1")); + QCOMPARE(lastScreen->geometry(), QRect(0, 0, 1920, 1080)); + QCOMPARE(m_screenPool->screens().first(), lastScreen); + // This shouldn't have changed after removing a non primary screen + QCOMPARE(m_screenPool->primaryConnector(), QStringLiteral("WL-1")); +} + +void ScreenPoolTest::testLastScreenRemoval() +{ + QSignalSpy removedSpy(m_screenPool, SIGNAL(screenRemoved(QScreen *))); + + // Remove an output + exec([=] { + remove(output(0)); + }); + + // NOTE: we can neither access the data of removedSpy nor oldPrimary because at this point will be dangling + removedSpy.wait(); + QCOMPARE(QGuiApplication::screens().size(), 1); + QCOMPARE(m_screenPool->screens().size(), 0); + QScreen *fakeScreen = QGuiApplication::screens().first(); + QCOMPARE(fakeScreen->name(), QString()); + QCOMPARE(fakeScreen->geometry(), QRect(0, 0, 0, 0)); +} + +void ScreenPoolTest::testFakeToRealScreen() +{ + QSignalSpy addedSpy(m_screenPool, SIGNAL(screenAdded(QScreen *))); + + // Add a new output + exec([=] { + OutputData data; + data.mode.resolution = {1920, 1080}; + data.position = {0, 0}; + data.physicalSize = data.mode.physicalSizeForDpi(96); + auto *out = add(data); + auto *xdgOut = xdgOutput(out); + xdgOut->m_name = QStringLiteral("WL-1"); + }); + + addedSpy.wait(); + QCOMPARE(QGuiApplication::screens().size(), 1); + QCOMPARE(m_screenPool->screens().size(), 1); + QCOMPARE(addedSpy.size(), 1); + + QScreen *newScreen = addedSpy.takeFirst().at(0).value(); + QCOMPARE(newScreen->name(), QStringLiteral("WL-1")); + QCOMPARE(newScreen->geometry(), QRect(0, 0, 1920, 1080)); + + QCOMPARE(m_screenPool->id(newScreen->name()), 0); +} + +QCOMPOSITOR_TEST_MAIN(ScreenPoolTest) + +#include "screenpooltest.moc" diff --git a/plasma/workspace/shell/config-ktexteditor.h.cmake b/plasma/workspace/shell/config-ktexteditor.h.cmake new file mode 100644 index 0000000000..224bfd76a7 --- /dev/null +++ b/plasma/workspace/shell/config-ktexteditor.h.cmake @@ -0,0 +1,2 @@ +#cmakedefine01 HAVE_KTEXTEDITOR + diff --git a/plasma/workspace/shell/config-plasma.h.cmake b/plasma/workspace/shell/config-plasma.h.cmake new file mode 100644 index 0000000000..89858d17de --- /dev/null +++ b/plasma/workspace/shell/config-plasma.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 HAVE_X11 diff --git a/plasma/workspace/shell/containmentconfigview.cpp b/plasma/workspace/shell/containmentconfigview.cpp new file mode 100644 index 0000000000..d0b93a417a --- /dev/null +++ b/plasma/workspace/shell/containmentconfigview.cpp @@ -0,0 +1,239 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "containmentconfigview.h" +#include "config-workspace.h" +#include "currentcontainmentactionsmodel.h" +#include "shellcorona.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class WallpaperConfigModel : public PlasmaQuick::ConfigModel +{ + Q_OBJECT +public: + WallpaperConfigModel(QObject *parent); +public Q_SLOTS: + void repopulate(); +}; + +//////////////////////////////ContainmentConfigView +ContainmentConfigView::ContainmentConfigView(Plasma::Containment *cont, QWindow *parent) + : ConfigView(cont, parent) + , m_containment(cont) +{ + qmlRegisterAnonymousType("QAbstractItemModel",1); + rootContext()->setContextProperty(QStringLiteral("configDialog"), this); + setCurrentWallpaper(cont->containment()->wallpaper()); + + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Wallpaper")); + pkg.setPath(m_containment->wallpaper()); + KConfigGroup cfg = m_containment->config(); + cfg = KConfigGroup(&cfg, "Wallpaper"); + + syncWallpaperObjects(); +} + +ContainmentConfigView::~ContainmentConfigView() +{ +} + +void ContainmentConfigView::init() +{ + setSource(m_containment->corona()->kPackage().fileUrl("containmentconfigurationui")); +} + +PlasmaQuick::ConfigModel *ContainmentConfigView::containmentActionConfigModel() +{ + if (!m_containmentActionConfigModel) { + m_containmentActionConfigModel = new PlasmaQuick::ConfigModel(this); + + const QVector actions = Plasma::PluginLoader::self()->listContainmentActionsMetaData(QString()); + + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Generic")); + + for (const KPluginMetaData &plugin : actions) { + pkg.setDefaultPackageRoot(QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral(PLASMA_RELATIVE_DATA_INSTALL_DIR "/containmentactions"), + QStandardPaths::LocateDirectory)); + m_containmentActionConfigModel->appendCategory(plugin.iconName(), + plugin.name(), + pkg.filePath("ui", QStringLiteral("config.qml")), + plugin.pluginId()); + } + } + return m_containmentActionConfigModel; +} + +QAbstractItemModel *ContainmentConfigView::currentContainmentActionsModel() +{ + if (!m_currentContainmentActionsModel) { + m_currentContainmentActionsModel = new CurrentContainmentActionsModel(m_containment, this); + } + return m_currentContainmentActionsModel; +} + +QString ContainmentConfigView::containmentPlugin() const +{ + return m_containment->pluginMetaData().pluginId(); +} + +void ContainmentConfigView::setContainmentPlugin(const QString &plugin) +{ + if (plugin.isEmpty() || containmentPlugin() == plugin) { + return; + } + + m_containment = static_cast(m_containment->corona())->setContainmentTypeForScreen(m_containment->screen(), plugin); + Q_EMIT containmentPluginChanged(); +} + +PlasmaQuick::ConfigModel *ContainmentConfigView::wallpaperConfigModel() +{ + if (!m_wallpaperConfigModel) { + m_wallpaperConfigModel = new WallpaperConfigModel(this); + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Wallpaper"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageInstalled"), + m_wallpaperConfigModel, + SLOT(repopulate())); + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Wallpaper"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageUpdated"), + m_wallpaperConfigModel, + SLOT(repopulate())); + QDBusConnection::sessionBus().connect(QString(), + QStringLiteral("/KPackage/Plasma/Wallpaper"), + QStringLiteral("org.kde.plasma.kpackage"), + QStringLiteral("packageUninstalled"), + m_wallpaperConfigModel, + SLOT(repopulate())); + } + return m_wallpaperConfigModel; +} + +PlasmaQuick::ConfigModel *ContainmentConfigView::containmentPluginsConfigModel() +{ + if (!m_containmentPluginsConfigModel) { + m_containmentPluginsConfigModel = new PlasmaQuick::ConfigModel(this); + + const QList actions = Plasma::PluginLoader::self()->listContainmentsMetaDataOfType(QStringLiteral("Desktop")); + for (const KPluginMetaData &plugin : actions) { + m_containmentPluginsConfigModel->appendCategory(plugin.iconName(), plugin.name(), QString(), plugin.pluginId()); + } + } + return m_containmentPluginsConfigModel; +} + +KDeclarative::ConfigPropertyMap *ContainmentConfigView::wallpaperConfiguration() const +{ + return m_currentWallpaperConfig; +} + +QString ContainmentConfigView::currentWallpaper() const +{ + return m_currentWallpaper; +} + +void ContainmentConfigView::setCurrentWallpaper(const QString &wallpaper) +{ + if (m_currentWallpaper == wallpaper) { + return; + } + + delete m_ownWallpaperConfig; + m_ownWallpaperConfig = nullptr; + + if (m_containment->wallpaper() == wallpaper) { + syncWallpaperObjects(); + } else { + // we have to construct an independent ConfigPropertyMap when we want to configure wallpapers that are not the current one + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Generic")); + pkg.setDefaultPackageRoot(QStringLiteral(PLASMA_RELATIVE_DATA_INSTALL_DIR "/wallpapers")); + pkg.setPath(wallpaper); + QFile file(pkg.filePath("config", QStringLiteral("main.xml"))); + KConfigGroup cfg = m_containment->config(); + cfg = KConfigGroup(&cfg, "Wallpaper"); + cfg = KConfigGroup(&cfg, wallpaper); + m_currentWallpaperConfig = m_ownWallpaperConfig = new KDeclarative::ConfigPropertyMap(new KConfigLoader(cfg, &file), this); + } + + m_currentWallpaper = wallpaper; + Q_EMIT currentWallpaperChanged(); + Q_EMIT wallpaperConfigurationChanged(); +} + +void ContainmentConfigView::applyWallpaper() +{ + m_containment->setWallpaper(m_currentWallpaper); + + syncWallpaperObjects(); + + if (m_currentWallpaperConfig && m_ownWallpaperConfig) { + for (const auto &key : m_ownWallpaperConfig->keys()) { + auto value = m_ownWallpaperConfig->value(key); + m_currentWallpaperConfig->insert(key, value); + m_currentWallpaperConfig->valueChanged(key, value); + } + } + + delete m_ownWallpaperConfig; + m_ownWallpaperConfig = nullptr; + + Q_EMIT wallpaperConfigurationChanged(); +} + +void ContainmentConfigView::syncWallpaperObjects() +{ + QObject *wallpaperGraphicsObject = m_containment->property("wallpaperGraphicsObject").value(); + + if (!wallpaperGraphicsObject) { + return; + } + rootContext()->setContextProperty(QStringLiteral("wallpaper"), wallpaperGraphicsObject); + + // FIXME: why m_wallpaperGraphicsObject->property("configuration").value() doesn't work? + m_currentWallpaperConfig = static_cast(wallpaperGraphicsObject->property("configuration").value()); +} + +WallpaperConfigModel::WallpaperConfigModel(QObject *parent) + : PlasmaQuick::ConfigModel(parent) +{ + repopulate(); +} + +void WallpaperConfigModel::repopulate() +{ + clear(); + for (const KPluginMetaData &m : KPackage::PackageLoader::self()->listPackages(QStringLiteral("Plasma/Wallpaper"))) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Wallpaper"), m.pluginId()); + if (!pkg.isValid()) { + continue; + } + appendCategory(pkg.metadata().iconName(), pkg.metadata().name(), pkg.fileUrl("ui", QStringLiteral("config.qml")).toString(), m.pluginId()); + } +} + +#include "containmentconfigview.moc" diff --git a/plasma/workspace/shell/containmentconfigview.h b/plasma/workspace/shell/containmentconfigview.h new file mode 100644 index 0000000000..bc2be1bc7c --- /dev/null +++ b/plasma/workspace/shell/containmentconfigview.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Plasma +{ +class Containment; +} + +class QAbstractItemModel; +class CurrentContainmentActionsModel; + +// TODO: out of the library? +class ContainmentConfigView : public PlasmaQuick::ConfigView +{ + Q_OBJECT + Q_PROPERTY(PlasmaQuick::ConfigModel *containmentActionConfigModel READ containmentActionConfigModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *currentContainmentActionsModel READ currentContainmentActionsModel CONSTANT) + Q_PROPERTY(PlasmaQuick::ConfigModel *wallpaperConfigModel READ wallpaperConfigModel CONSTANT) + Q_PROPERTY(PlasmaQuick::ConfigModel *containmentPluginsConfigModel READ containmentPluginsConfigModel CONSTANT) + Q_PROPERTY(KDeclarative::ConfigPropertyMap *wallpaperConfiguration READ wallpaperConfiguration NOTIFY wallpaperConfigurationChanged) + Q_PROPERTY(QString currentWallpaper READ currentWallpaper WRITE setCurrentWallpaper NOTIFY currentWallpaperChanged) + Q_PROPERTY(QString containmentPlugin READ containmentPlugin WRITE setContainmentPlugin NOTIFY containmentPluginChanged) + +public: + explicit ContainmentConfigView(Plasma::Containment *interface, QWindow *parent = nullptr); + ~ContainmentConfigView() override; + + void init() override; + + PlasmaQuick::ConfigModel *containmentActionConfigModel(); + QAbstractItemModel *currentContainmentActionsModel(); + PlasmaQuick::ConfigModel *wallpaperConfigModel(); + PlasmaQuick::ConfigModel *containmentPluginsConfigModel(); + QString currentWallpaper() const; + void setCurrentWallpaper(const QString &wallpaper); + KDeclarative::ConfigPropertyMap *wallpaperConfiguration() const; + QString containmentPlugin() const; + void setContainmentPlugin(const QString &plugin); + + Q_INVOKABLE void applyWallpaper(); + +Q_SIGNALS: + void currentWallpaperChanged(); + void wallpaperConfigurationChanged(); + void containmentPluginChanged(); + +protected: + void syncWallpaperObjects(); + +private: + Plasma::Containment *m_containment = nullptr; + PlasmaQuick::ConfigModel *m_wallpaperConfigModel = nullptr; + PlasmaQuick::ConfigModel *m_containmentActionConfigModel = nullptr; + PlasmaQuick::ConfigModel *m_containmentPluginsConfigModel = nullptr; + CurrentContainmentActionsModel *m_currentContainmentActionsModel = nullptr; + QString m_currentWallpaper; + KDeclarative::ConfigPropertyMap *m_currentWallpaperConfig = nullptr; + KDeclarative::ConfigPropertyMap *m_ownWallpaperConfig = nullptr; +}; diff --git a/plasma/workspace/shell/coronatesthelper.cpp b/plasma/workspace/shell/coronatesthelper.cpp new file mode 100644 index 0000000000..f7d5035ef7 --- /dev/null +++ b/plasma/workspace/shell/coronatesthelper.cpp @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "coronatesthelper.h" +#include "debug.h" + +#include + +using namespace Plasma; + +CoronaTestHelper::CoronaTestHelper(Corona *parent) + : QObject(parent) + , m_corona(parent) + , m_exitcode(0) +{ + connect(m_corona, &Corona::startupCompleted, this, &CoronaTestHelper::initialize); +} + +void CoronaTestHelper::processContainment(Plasma::Containment *containment) +{ + foreach (Plasma::Applet *applet, containment->applets()) { + processApplet(applet); + } + connect(containment, &Plasma::Containment::appletAdded, this, &CoronaTestHelper::processApplet); +} + +void CoronaTestHelper::processApplet(Plasma::Applet *applet) +{ + PlasmaQuick::AppletQuickItem *obj = applet->property("_plasma_graphicObject").value(); + if (applet->failedToLaunch()) { + qCWarning(PLASMASHELL) << "cannot test an applet with a launch error" << applet->launchErrorMessage(); + qGuiApp->exit(1); + return; + } + if (!obj) { + qCWarning(PLASMASHELL) << "cannot get AppletQuickItem for applet"; + qGuiApp->exit(1); + return; + } + + auto testObject = obj->testItem(); + if (!testObject) { + qCWarning(PLASMASHELL) << "no test for" << applet->title() << applet->kPackage().path(); + return; + } + integrateTest(testObject); +} + +void CoronaTestHelper::integrateTest(QObject *testObject) +{ + if (m_registeredTests.contains(testObject)) + return; + + const int signal = testObject->metaObject()->indexOfSignal("done()"); + if (signal < 0) { + qCWarning(PLASMASHELL) << "the test object should offer a 'done()' signal" << testObject; + return; + } + if (testObject->metaObject()->indexOfProperty("failed") < 0) { + qCWarning(PLASMASHELL) << "the test object should offer a 'bool failed' property" << testObject; + return; + } + + qCDebug(PLASMASHELL) << "Test registered" << testObject; + m_tests.insert(testObject); + m_registeredTests << testObject; + + connect(testObject, SIGNAL(done()), this, SLOT(testFinished())); +} + +void CoronaTestHelper::testFinished() +{ + QObject *testObject = sender(); + + const bool failed = testObject->property("failed").toBool(); + m_exitcode += failed; + m_tests.remove(testObject); + + qCWarning(PLASMASHELL) << "test finished" << testObject << failed << "remaining" << m_tests; + if (m_tests.isEmpty()) { + qGuiApp->exit(m_exitcode); + } +} + +void CoronaTestHelper::initialize() +{ + foreach (Plasma::Containment *containment, m_corona->containments()) { + processContainment(containment); + } + connect(m_corona, &Corona::containmentAdded, this, &CoronaTestHelper::processContainment); + + if (m_tests.isEmpty()) { + qCWarning(PLASMASHELL) << "no tests found for the corona" << QCoreApplication::instance()->arguments(); + qGuiApp->exit(); + return; + } +} diff --git a/plasma/workspace/shell/coronatesthelper.h b/plasma/workspace/shell/coronatesthelper.h new file mode 100644 index 0000000000..e20392b878 --- /dev/null +++ b/plasma/workspace/shell/coronatesthelper.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2016 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class CoronaTestHelper : public QObject +{ + Q_OBJECT +public: + explicit CoronaTestHelper(Plasma::Corona *parent); + + void processContainment(Plasma::Containment *containment); + void processApplet(Plasma::Applet *applet); + +private Q_SLOTS: + void testFinished(); + +private: + void initialize(); + void integrateTest(QObject *testObject); + + Plasma::Corona *m_corona; + QSet m_registeredTests; + QSet m_tests; + + int m_exitcode; +}; diff --git a/plasma/workspace/shell/currentcontainmentactionsmodel.cpp b/plasma/workspace/shell/currentcontainmentactionsmodel.cpp new file mode 100644 index 0000000000..8cb265ad5a --- /dev/null +++ b/plasma/workspace/shell/currentcontainmentactionsmodel.cpp @@ -0,0 +1,260 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "currentcontainmentactionsmodel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +CurrentContainmentActionsModel::CurrentContainmentActionsModel(Plasma::Containment *containment, QObject *parent) + : QStandardItemModel(parent) + , m_containment(containment) + , m_tempConfigParent(QString(), KConfig::SimpleConfig) +{ + m_baseCfg = KConfigGroup(m_containment->corona()->config(), "ActionPlugins"); + m_baseCfg = KConfigGroup(&m_baseCfg, QString::number(m_containment->containmentType())); + + QHash actions = containment->containmentActions(); + + QHashIterator i(actions); + while (i.hasNext()) { + i.next(); + + QStandardItem *item = new QStandardItem(); + item->setData(i.key(), ActionRole); + item->setData(i.value()->metadata().pluginId(), PluginNameRole); + + m_plugins[i.key()] = Plasma::PluginLoader::self()->loadContainmentActions(m_containment, i.value()->metadata().pluginId()); + m_plugins[i.key()]->setContainment(m_containment); + KConfigGroup cfg(&m_baseCfg, i.key()); + m_plugins[i.key()]->restore(cfg); + item->setData(m_plugins[i.key()]->metadata().rawData().value(QStringLiteral("X-Plasma-HasConfigurationInterface")).toBool(), + HasConfigurationInterfaceRole); + + appendRow(item); + } +} + +CurrentContainmentActionsModel::~CurrentContainmentActionsModel() +{ +} + +QHash CurrentContainmentActionsModel::roleNames() const +{ + return { + {ActionRole, "action"}, + {PluginNameRole, "pluginName"}, + {HasConfigurationInterfaceRole, "hasConfigurationInterface"}, + }; +} + +QString CurrentContainmentActionsModel::mouseEventString(int mouseButton, int modifiers) +{ + QMouseEvent *mouse = + new QMouseEvent(QEvent::MouseButtonRelease, QPoint(), (Qt::MouseButton)mouseButton, (Qt::MouseButton)mouseButton, (Qt::KeyboardModifiers)modifiers); + + const QString string = Plasma::ContainmentActions::eventToString(mouse); + + delete mouse; + + return string; +} + +QString CurrentContainmentActionsModel::wheelEventString(const QPointF &delta, int mouseButtons, int modifiers) +{ + QWheelEvent wheel(QPointF(), QPointF(), delta.toPoint(), {}, Qt::MouseButtons(mouseButtons), Qt::KeyboardModifiers(modifiers), Qt::NoScrollPhase, false); + + return Plasma::ContainmentActions::eventToString(&wheel); +} + +bool CurrentContainmentActionsModel::isTriggerUsed(const QString &trigger) +{ + return m_plugins.contains(trigger); +} + +bool CurrentContainmentActionsModel::append(const QString &action, const QString &plugin) +{ + if (m_plugins.contains(action)) { + return false; + } + + QStandardItem *item = new QStandardItem(); + item->setData(action, ActionRole); + item->setData(plugin, PluginNameRole); + + Plasma::ContainmentActions *actions = Plasma::PluginLoader::self()->loadContainmentActions(m_containment, plugin); + + if (!actions) { + return false; + } + + m_plugins[action] = actions; + m_plugins[action]->setContainment(m_containment); + // empty config: the new one will ne in default state + KConfigGroup tempConfig(&m_tempConfigParent, "test"); + m_plugins[action]->restore(tempConfig); + item->setData(m_plugins[action]->metadata().rawData().value(QStringLiteral("X-Plasma-HasConfigurationInterface")).toBool(), HasConfigurationInterfaceRole); + m_removedTriggers.removeAll(action); + + appendRow(item); + + Q_EMIT configurationChanged(); + return true; +} + +void CurrentContainmentActionsModel::update(int row, const QString &action, const QString &plugin) +{ + const QString oldPlugin = itemData(index(row, 0)).value(PluginNameRole).toString(); + const QString oldTrigger = itemData(index(row, 0)).value(ActionRole).toString(); + + if (oldTrigger == action && oldPlugin == plugin) { + return; + } + + QModelIndex idx = index(row, 0); + + if (idx.isValid()) { + setData(idx, action, ActionRole); + setData(idx, plugin, PluginNameRole); + + delete m_plugins[oldTrigger]; + m_plugins.remove(oldTrigger); + + if (oldPlugin != plugin) { + m_removedTriggers << oldTrigger; + } + + if (!m_plugins.contains(action) || oldPlugin != plugin) { + delete m_plugins[action]; + m_plugins[action] = Plasma::PluginLoader::self()->loadContainmentActions(m_containment, plugin); + m_plugins[action]->setContainment(m_containment); + // empty config: the new one will ne in default state + KConfigGroup tempConfig(&m_tempConfigParent, "test"); + m_plugins[action]->restore(tempConfig); + setData(idx, + m_plugins[action]->metadata().rawData().value(QStringLiteral("X-Plasma-HasConfigurationInterface")).toBool(), + HasConfigurationInterfaceRole); + } + + Q_EMIT configurationChanged(); + } +} + +void CurrentContainmentActionsModel::remove(int row) +{ + const QString action = itemData(index(row, 0)).value(ActionRole).toString(); + removeRows(row, 1); + + if (m_plugins.contains(action)) { + delete m_plugins[action]; + m_plugins.remove(action); + m_removedTriggers << action; + Q_EMIT configurationChanged(); + } +} + +void CurrentContainmentActionsModel::showConfiguration(int row, QQuickItem *ctx) +{ + const QString action = itemData(index(row, 0)).value(ActionRole).toString(); + + if (!m_plugins.contains(action)) { + return; + } + + QDialog *configDlg = new QDialog(); + configDlg->setAttribute(Qt::WA_DeleteOnClose); + QLayout *lay = new QVBoxLayout(configDlg); + configDlg->setLayout(lay); + if (ctx && ctx->window()) { + configDlg->setWindowModality(Qt::WindowModal); + configDlg->winId(); // so it creates the windowHandle(); + configDlg->windowHandle()->setTransientParent(ctx->window()); + } + + Plasma::ContainmentActions *pluginInstance = m_plugins[action]; + // put the config in the dialog + QWidget *w = pluginInstance->createConfigurationInterface(configDlg); + QString title; + if (w) { + lay->addWidget(w); + title = w->windowTitle(); + } + + configDlg->setWindowTitle(title.isEmpty() ? i18n("Configure Mouse Actions Plugin") : title); + // put buttons below + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, configDlg); + lay->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, configDlg, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, configDlg, &QDialog::reject); + + QObject::connect(configDlg, &QDialog::accepted, pluginInstance, [pluginInstance]() { + pluginInstance->configurationAccepted(); + }); + + connect(configDlg, &QDialog::accepted, this, &CurrentContainmentActionsModel::configurationChanged); + + connect(pluginInstance, &QObject::destroyed, configDlg, &QDialog::reject); + + configDlg->show(); +} + +void CurrentContainmentActionsModel::showAbout(int row, QQuickItem *ctx) +{ + const QString action = itemData(index(row, 0)).value(ActionRole).toString(); + + if (!m_plugins.contains(action)) { + return; + } + + KPluginMetaData info = m_plugins[action]->metadata(); + + auto aboutDialog = new KAboutPluginDialog(info, qobject_cast(parent())); + aboutDialog->setWindowIcon(QIcon::fromTheme(info.iconName())); + aboutDialog->setAttribute(Qt::WA_DeleteOnClose); + + if (ctx && ctx->window()) { + aboutDialog->setWindowModality(Qt::WindowModal); + aboutDialog->winId(); // so it creates the windowHandle(); + aboutDialog->windowHandle()->setTransientParent(ctx->window()); + } + aboutDialog->show(); +} + +void CurrentContainmentActionsModel::save() +{ + foreach (const QString &removedTrigger, m_removedTriggers) { + m_containment->setContainmentActions(removedTrigger, QString()); + } + m_removedTriggers.clear(); + + QHashIterator i(m_plugins); + while (i.hasNext()) { + i.next(); + + KConfigGroup cfg(&m_baseCfg, i.key()); + i.value()->save(cfg); + + m_containment->setContainmentActions(i.key(), i.value()->metadata().pluginId()); + } +} + +#include "moc_currentcontainmentactionsmodel.cpp" diff --git a/plasma/workspace/shell/currentcontainmentactionsmodel.h b/plasma/workspace/shell/currentcontainmentactionsmodel.h new file mode 100644 index 0000000000..d3f62457f2 --- /dev/null +++ b/plasma/workspace/shell/currentcontainmentactionsmodel.h @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +namespace Plasma +{ +class Containment; +class ContainmentActions; +} + +class QQuickItem; + +// This model load the data about available containment actions plugins, such as context menus that can be loaded on mouse click +// TODO: out of the library? +class CurrentContainmentActionsModel : public QStandardItemModel +{ + Q_OBJECT + +public: + enum Roles { + ActionRole = Qt::UserRole + 1, + PluginNameRole, + HasConfigurationInterfaceRole, + }; + + explicit CurrentContainmentActionsModel(Plasma::Containment *containment, QObject *parent = nullptr); + ~CurrentContainmentActionsModel() override; + + QHash roleNames() const override; + + Q_INVOKABLE bool isTriggerUsed(const QString &trigger); + Q_INVOKABLE QString mouseEventString(int mouseButtons, int modifiers); + Q_INVOKABLE QString wheelEventString(const QPointF &delta, int mouseButtons, int modifiers); + Q_INVOKABLE bool append(const QString &action, const QString &plugin); + Q_INVOKABLE void update(int row, const QString &action, const QString &plugin); + Q_INVOKABLE void remove(int row); + Q_INVOKABLE void showConfiguration(int row, QQuickItem *ctx = nullptr); + Q_INVOKABLE void showAbout(int row, QQuickItem *ctx = nullptr); + Q_INVOKABLE void save(); + +Q_SIGNALS: + void configurationChanged(); + +private: + Plasma::Containment *m_containment; + QHash m_plugins; + KConfigGroup m_baseCfg; + KConfigGroup m_tempConfig; + KConfig m_tempConfigParent; + QStringList m_removedTriggers; +}; diff --git a/plasma/workspace/shell/dbus/org.kde.PlasmaShell.xml b/plasma/workspace/shell/dbus/org.kde.PlasmaShell.xml new file mode 100644 index 0000000000..dd47aca359 --- /dev/null +++ b/plasma/workspace/shell/dbus/org.kde.PlasmaShell.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/shell/desktopview.cpp b/plasma/workspace/shell/desktopview.cpp new file mode 100644 index 0000000000..c76ad72782 --- /dev/null +++ b/plasma/workspace/shell/desktopview.cpp @@ -0,0 +1,354 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopview.h" +#include "containmentconfigview.h" +#include "krunner_interface.h" +#include "shellcorona.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +DesktopView::DesktopView(Plasma::Corona *corona, QScreen *targetScreen) + : PlasmaQuick::ContainmentView(corona, nullptr) + , m_windowType(Desktop) + , m_shellSurface(nullptr) +{ + QObject::setParent(corona); + + // Setting clear color to black makes the panel lose alpha channel on X11. This looks like + // a QtXCB bug, so set clear color only on Wayland to let the compositor optimize rendering. + if (KWindowSystem::isPlatformWayland()) { + setColor(Qt::black); + } + + if (targetScreen) { + setScreenToFollow(targetScreen); + } + + setFlags(Qt::Window | Qt::FramelessWindowHint); + setTitle(corona->kPackage().metadata().name()); + rootContext()->setContextProperty(QStringLiteral("desktop"), this); + setSource(corona->kPackage().fileUrl("views", QStringLiteral("Desktop.qml"))); + + connect(this, &QWindow::screenChanged, this, &DesktopView::adaptToScreen); + + QObject::connect(corona, &Plasma::Corona::kPackageChanged, this, &DesktopView::coronaPackageChanged); + + KActivities::Controller *m_activityController = new KActivities::Controller(this); + + QObject::connect(m_activityController, &KActivities::Controller::activityAdded, this, &DesktopView::candidateContainmentsChanged); + QObject::connect(m_activityController, &KActivities::Controller::activityRemoved, this, &DesktopView::candidateContainmentsChanged); +} + +DesktopView::~DesktopView() +{ +} + +void DesktopView::showEvent(QShowEvent *e) +{ + QQuickWindow::showEvent(e); + adaptToScreen(); +} + +void DesktopView::setScreenToFollow(QScreen *screen) +{ + Q_ASSERT(screen); + if (screen == m_screenToFollow) { + return; + } + + m_screenToFollow = screen; + setScreen(screen); + adaptToScreen(); +} + +QScreen *DesktopView::screenToFollow() const +{ + return m_screenToFollow; +} + +void DesktopView::adaptToScreen() +{ + ensureWindowType(); + + // This happens sometimes, when shutting down the process + if (!m_screenToFollow || m_oldScreen == m_screenToFollow) { + return; + } + + if (m_oldScreen) { + disconnect(m_oldScreen.data(), &QScreen::geometryChanged, this, &DesktopView::screenGeometryChanged); + } + + if (m_windowType == Desktop || m_windowType == WindowedDesktop) { + screenGeometryChanged(); + + connect(m_screenToFollow.data(), &QScreen::geometryChanged, this, &DesktopView::screenGeometryChanged, Qt::UniqueConnection); + } + + m_oldScreen = m_screenToFollow; +} + +DesktopView::WindowType DesktopView::windowType() const +{ + return m_windowType; +} + +void DesktopView::setWindowType(DesktopView::WindowType type) +{ + if (m_windowType == type) { + return; + } + + m_windowType = type; + + adaptToScreen(); + + Q_EMIT windowTypeChanged(); +} + +void DesktopView::ensureWindowType() +{ + // This happens sometimes, when shutting down the process + if (!screen()) { + return; + } + + if (m_windowType == Window) { + setFlags(Qt::Window); + KWindowSystem::setType(winId(), NET::Normal); + KWindowSystem::clearState(winId(), NET::FullScreen); + if (m_shellSurface) { + m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal); + m_shellSurface->setSkipTaskbar(false); + } + + } else if (m_windowType == Desktop) { + setFlags(Qt::Window | Qt::FramelessWindowHint); + KWindowSystem::setType(winId(), NET::Desktop); + KWindowSystem::setState(winId(), NET::KeepBelow); + if (m_shellSurface) { + m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Desktop); + m_shellSurface->setSkipTaskbar(true); + } + + } else if (m_windowType == WindowedDesktop) { + KWindowSystem::setType(winId(), NET::Normal); + KWindowSystem::clearState(winId(), NET::FullScreen); + setFlags(Qt::FramelessWindowHint | flags()); + if (m_shellSurface) { + m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal); + m_shellSurface->setSkipTaskbar(false); + } + + } else if (m_windowType == FullScreen) { + setFlags(Qt::Window); + KWindowSystem::setType(winId(), NET::Normal); + KWindowSystem::setState(winId(), NET::FullScreen); + if (m_shellSurface) { + m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal); + m_shellSurface->setSkipTaskbar(false); + } + } +} + +DesktopView::SessionType DesktopView::sessionType() const +{ + if (qobject_cast(corona())) { + return ShellSession; + } else { + return ApplicationSession; + } +} + +QVariantMap DesktopView::candidateContainmentsGraphicItems() const +{ + QVariantMap map; + if (!containment()) { + return map; + } + + for (auto cont : corona()->containmentsForScreen(containment()->screen())) { + map[cont->activity()] = cont->property("_plasma_graphicObject"); + } + return map; +} + +Q_INVOKABLE QString DesktopView::fileFromPackage(const QString &key, const QString &fileName) +{ + return corona()->kPackage().filePath(key.toUtf8(), fileName); +} + +bool DesktopView::event(QEvent *e) +{ + if (e->type() == QEvent::PlatformSurface) { + switch (static_cast(e)->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceCreated: + setupWaylandIntegration(); + ensureWindowType(); + break; + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + delete m_shellSurface; + m_shellSurface = nullptr; + break; + } + } else if (e->type() == QEvent::FocusOut) { + m_krunnerText.clear(); + } + + return PlasmaQuick::ContainmentView::event(e); +} + +bool DesktopView::handleKRunnerTextInput(QKeyEvent *e) +{ + // allow only Shift and GroupSwitch modifiers + if (e->modifiers() & ~Qt::ShiftModifier & ~Qt::GroupSwitchModifier) { + return false; + } + bool krunnerTextChanged = false; + const QString eventText = e->text(); + for (const QChar ch : eventText) { + if (!ch.isPrint()) { + continue; + } + if (ch.isSpace() && m_krunnerText.isEmpty()) { + continue; + } + m_krunnerText += ch; + krunnerTextChanged = true; + } + if (krunnerTextChanged) { + const QString interface(QStringLiteral("org.kde.krunner")); + if (!KAuthorized::authorize(QStringLiteral("run_command"))) { + return false; + } + org::kde::krunner::App krunner(interface, QStringLiteral("/App"), QDBusConnection::sessionBus()); + krunner.query(m_krunnerText); + return true; + } + return false; +} + +void DesktopView::keyPressEvent(QKeyEvent *e) +{ + ContainmentView::keyPressEvent(e); + + if (e->isAccepted()) { + return; + } + + if (e->key() == Qt::Key_Escape && KWindowSystem::showingDesktop()) { + KWindowSystem::setShowingDesktop(false); + e->accept(); + return; + } + + // When a key is pressed on desktop when nothing else is active forward the key to krunner + if (handleKRunnerTextInput(e)) { + e->accept(); + return; + } +} + +void DesktopView::showConfigurationInterface(Plasma::Applet *applet) +{ + if (m_configView) { + if (m_configView->applet() != applet) { + m_configView->hide(); + m_configView->deleteLater(); + } else { + m_configView->show(); + auto window = qobject_cast(m_configView); + if (window && QX11Info::isPlatformX11()) { + KStartupInfo::setNewStartupId(window, QX11Info::nextStartupId()); + } + m_configView->requestActivate(); + return; + } + } + + if (!applet || !applet->containment()) { + return; + } + + Plasma::Containment *cont = qobject_cast(applet); + + if (cont && cont->isContainment() && cont->containmentType() == Plasma::Types::DesktopContainment) { + m_configView = new ContainmentConfigView(cont); + // if we changed containment with the config open, relaunch the config dialog but for the new containment + // third arg is used to disconnect when the config closes + connect(this, &ContainmentView::containmentChanged, m_configView.data(), [this]() { + showConfigurationInterface(containment()); + }); + } else { + m_configView = new PlasmaQuick::ConfigView(applet); + } + m_configView->init(); + m_configView->setTransientParent(this); + m_configView->show(); + m_configView->requestActivate(); + + auto window = qobject_cast(m_configView); + if (window && QX11Info::isPlatformX11()) { + KStartupInfo::setNewStartupId(window, QX11Info::nextStartupId()); + } + m_configView->requestActivate(); +} + +void DesktopView::screenGeometryChanged() +{ + const QRect geo = m_screenToFollow->geometry(); + // qDebug() << "newGeometry" << this << geo << geometry(); + setGeometry(geo); + if (m_shellSurface) { + m_shellSurface->setPosition(geo.topLeft()); + } + Q_EMIT geometryChanged(); +} + +void DesktopView::coronaPackageChanged(const KPackage::Package &package) +{ + setContainment(nullptr); + setSource(package.fileUrl("views", QStringLiteral("Desktop.qml"))); +} + +void DesktopView::setupWaylandIntegration() +{ + if (m_shellSurface) { + // already setup + return; + } + if (ShellCorona *c = qobject_cast(corona())) { + using namespace KWayland::Client; + PlasmaShell *interface = c->waylandPlasmaShellInterface(); + if (!interface) { + return; + } + Surface *s = Surface::fromWindow(this); + if (!s) { + return; + } + m_shellSurface = interface->createSurface(s, this); + m_shellSurface->setPosition(m_screenToFollow->geometry().topLeft()); + } +} diff --git a/plasma/workspace/shell/desktopview.h b/plasma/workspace/shell/desktopview.h new file mode 100644 index 0000000000..bbe2a75a21 --- /dev/null +++ b/plasma/workspace/shell/desktopview.h @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class PlasmaShellSurface; +} +} + +class DesktopView : public PlasmaQuick::ContainmentView +{ + Q_OBJECT + + Q_PROPERTY(WindowType windowType READ windowType WRITE setWindowType NOTIFY windowTypeChanged) + + // What kind of plasma session we're in: are we in a full workspace, an application?... + Q_PROPERTY(SessionType sessionType READ sessionType CONSTANT) + + Q_PROPERTY(QVariantMap candidateContainments READ candidateContainmentsGraphicItems NOTIFY candidateContainmentsChanged) + +public: + enum WindowType { + Window, /** The window is a normal resizable window with titlebar and appears in the taskbar */ + FullScreen, /** The window is fullscreen and goes over all the other windows */ + Desktop, /** The window is the desktop layer, under everything else, doesn't appear in the taskbar */ + WindowedDesktop, /** full screen and borderless as Desktop, but can be brought in front and appears in the taskbar */ + }; + Q_ENUM(WindowType) + + enum SessionType { + ApplicationSession, /** our session is a normal application */ + ShellSession, /** We are running as the primary user interface of this machine */ + }; + Q_ENUM(SessionType) + + explicit DesktopView(Plasma::Corona *corona, QScreen *targetScreen = nullptr); + ~DesktopView() override; + + /*This is different from screen() as is always there, even if the window is + temporarily outside the screen or if is hidden: only plasmashell will ever + change this property, unlike QWindow::screen()*/ + void setScreenToFollow(QScreen *screen); + QScreen *screenToFollow() const; + + void adaptToScreen(); + void showEvent(QShowEvent *) override; + + WindowType windowType() const; + void setWindowType(WindowType type); + + SessionType sessionType() const; + + QVariantMap candidateContainmentsGraphicItems() const; + + Q_INVOKABLE QString fileFromPackage(const QString &key, const QString &fileName); + +protected: + bool event(QEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +protected Q_SLOTS: + /** + * It will be called when the configuration is requested + */ + void showConfigurationInterface(Plasma::Applet *applet) override; + +private Q_SLOTS: + void screenGeometryChanged(); + +Q_SIGNALS: + void stayBehindChanged(); + void windowTypeChanged(); + void candidateContainmentsChanged(); + void geometryChanged(); + +private: + void coronaPackageChanged(const KPackage::Package &package); + void ensureWindowType(); + void setupWaylandIntegration(); + bool handleKRunnerTextInput(QKeyEvent *e); + + QPointer m_configView; + QPointer m_oldScreen; + QPointer m_screenToFollow; + WindowType m_windowType; + KWayland::Client::PlasmaShellSurface *m_shellSurface; + QString m_krunnerText; +}; diff --git a/plasma/workspace/shell/futureutil.h b/plasma/workspace/shell/futureutil.h new file mode 100644 index 0000000000..73526354e6 --- /dev/null +++ b/plasma/workspace/shell/futureutil.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2016 Ivan Cukic + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +template +inline void awaitFuture(const QFuture &future) +{ + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } +} diff --git a/plasma/workspace/shell/main.cpp b/plasma/workspace/shell/main.cpp new file mode 100644 index 0000000000..b53645136b --- /dev/null +++ b/plasma/workspace/shell/main.cpp @@ -0,0 +1,242 @@ +/* + SPDX-FileCopyrightText: 2012 Marco Martin + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WITH_KUSERFEEDBACKCORE +#include "userfeedback.h" +#endif + +#include +#include +#include +#include + +#include "coronatesthelper.h" +#include "debug.h" +#include "shellcorona.h" +#include "softwarerendernotifier.h" +#include "standaloneappcorona.h" + +#include +#include + +static QLoggingCategory::CategoryFilter oldCategoryFilter; + +// Qt 5.15 introduces a new syntax for connections +// framework code can't port away due to needing Qt5.12 +// this filters out the warnings +// Remove this once we depend on Qt5.15 in frameworks +void filterConnectionSyntaxWarning(QLoggingCategory *category) +{ + if (qstrcmp(category->categoryName(), "qt.qml.connections") == 0) { + category->setEnabled(QtWarningMsg, false); + } else if (oldCategoryFilter) { + oldCategoryFilter(category); + } +} + +int main(int argc, char *argv[]) +{ +#if QT_CONFIG(qml_debug) + if (qEnvironmentVariableIsSet("PLASMA_ENABLE_QML_DEBUG")) { + QQmlDebuggingEnabler debugger; + } +#endif + // Plasma scales itself to font DPI + // on X, where we don't have compositor scaling, this generally works fine. + // also there are bugs on older Qt, especially when it comes to fractional scaling + // there's advantages to disabling, and (other than small context menu icons) few advantages in enabling + + // On wayland, it's different. Everything is simpler as all co-ordinates are in the same co-ordinate system + // we don't have fractional scaling on the client so don't hit most the remaining bugs and + // even if we don't use Qt scaling the compositor will try to scale us anyway so we have no choice + if (!qEnvironmentVariableIsSet("PLASMA_USE_QT_SCALING")) { + qunsetenv("QT_DEVICE_PIXEL_RATIO"); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + } else { + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + } + + QQuickWindow::setDefaultAlphaBuffer(true); + + oldCategoryFilter = QLoggingCategory::installFilter(filterConnectionSyntaxWarning); + + const bool qpaVariable = qEnvironmentVariableIsSet("QT_QPA_PLATFORM"); + KWorkSpace::detectPlatform(argc, argv); + QApplication app(argc, argv); + if (!qpaVariable) { + // don't leak the env variable to processes we start + qunsetenv("QT_QPA_PLATFORM"); + } + KLocalizedString::setApplicationDomain("plasmashell"); + + // The executable's path is added to the library/plugin paths. + // This does not make much sense for plasmashell. + app.removeLibraryPath(QCoreApplication::applicationDirPath()); + + KQuickAddons::QtQuickSettings::init(); + + KAboutData aboutData(QStringLiteral("plasmashell"), i18n("Plasma"), QStringLiteral(PROJECT_VERSION), i18n("Plasma Shell"), KAboutLicense::GPL); + + KAboutData::setApplicationData(aboutData); + + app.setQuitOnLastWindowClosed(false); + + bool replace = false; + + ShellCorona *corona; + { + QCommandLineParser cliOptions; + + QCommandLineOption dbgOption(QStringList() << QStringLiteral("d") << QStringLiteral("qmljsdebugger"), i18n("Enable QML Javascript debugger")); + + QCommandLineOption noRespawnOption(QStringList() << QStringLiteral("n") << QStringLiteral("no-respawn"), + i18n("Do not restart plasma-shell automatically after a crash")); + + QCommandLineOption shellPluginOption(QStringList() << QStringLiteral("p") << QStringLiteral("shell-plugin"), + i18n("Force loading the given shell plugin"), + QStringLiteral("plugin"), + ShellCorona::defaultShell()); + + QCommandLineOption standaloneOption(QStringList() << QStringLiteral("a") << QStringLiteral("standalone"), + i18n("Load plasmashell as a standalone application, needs the shell-plugin option to be specified")); + + QCommandLineOption replaceOption({QStringLiteral("replace")}, i18n("Replace an existing instance")); + + QCommandLineOption testOption(QStringList() << QStringLiteral("test"), + i18n("Enables test mode and specifies the layout javascript file to set up the testing environment"), + i18n("file"), + QStringLiteral("layout.js")); + +#ifdef WITH_KUSERFEEDBACKCORE + QCommandLineOption feedbackOption(QStringList() << QStringLiteral("feedback"), i18n("Lists the available options for user feedback")); +#endif + cliOptions.addOption(dbgOption); + cliOptions.addOption(noRespawnOption); + cliOptions.addOption(shellPluginOption); + cliOptions.addOption(standaloneOption); + cliOptions.addOption(testOption); + cliOptions.addOption(replaceOption); +#ifdef WITH_KUSERFEEDBACKCORE + cliOptions.addOption(feedbackOption); +#endif + + aboutData.setupCommandLine(&cliOptions); + cliOptions.process(app); + aboutData.processCommandLine(&cliOptions); + + QGuiApplication::setFallbackSessionManagementEnabled(false); + + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement); + + corona = new ShellCorona(&app); + corona->setShell(cliOptions.value(shellPluginOption)); + if (!corona->kPackage().isValid()) { + qCritical() << "starting invalid corona" << corona->shell(); + return 1; + } + +#ifdef WITH_KUSERFEEDBACKCORE + auto userFeedback = new UserFeedback(corona, &app); + if (cliOptions.isSet(feedbackOption)) { + QTextStream(stdout) << userFeedback->describeDataSources(); + return 0; + } +#endif + + QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, corona, &QObject::deleteLater); + + if (!cliOptions.isSet(noRespawnOption) && !cliOptions.isSet(testOption)) { + KCrash::setFlags(KCrash::AutoRestart); + } + + if (cliOptions.isSet(testOption)) { + const QUrl layoutUrl = QUrl::fromUserInput(cliOptions.value(testOption), {}, QUrl::AssumeLocalFile); + if (!layoutUrl.isLocalFile()) { + qCWarning(PLASMASHELL) << "ensure the layout file is local" << layoutUrl; + cliOptions.showHelp(1); + } + + QStandardPaths::setTestModeEnabled(true); + QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).removeRecursively(); + corona->setTestModeLayout(layoutUrl.toLocalFile()); + + qApp->setProperty("org.kde.KActivities.core.disableAutostart", true); + + new CoronaTestHelper(corona); + } + + if (cliOptions.isSet(standaloneOption)) { + if (cliOptions.isSet(shellPluginOption)) { + app.setApplicationName(QStringLiteral("plasmashell_") + cliOptions.value(shellPluginOption)); + app.setQuitOnLastWindowClosed(true); + + KDBusService service(KDBusService::Unique); + // This will not leak, because corona deletes itself on window close + new StandaloneAppCorona(cliOptions.value(shellPluginOption)); + return app.exec(); + } else { + cliOptions.showHelp(1); + } + } else { + // Tells libnotificationmanager that we're the only true application that may own notification and job progress services + qApp->setProperty("_plasma_dbus_master", true); + } + + QObject::connect(corona, &ShellCorona::glInitializationFailed, &app, [&app]() { + // scene graphs errors come from a thread + // even though we process them in the main thread, app.exit could still process these events + static bool s_multipleInvokations = false; + if (s_multipleInvokations) { + return; + } + s_multipleInvokations = true; + + qCritical("Open GL context could not be created"); + auto configGroup = KSharedConfig::openConfig()->group("QtQuickRendererSettings"); + if (configGroup.readEntry("SceneGraphBackend") != QLatin1String("software")) { + configGroup.writeEntry("SceneGraphBackend", "software", KConfigBase::Global | KConfigBase::Persistent); + configGroup.sync(); + QProcess::startDetached(QStringLiteral("plasmashell"), app.arguments()); + } else { + QCoreApplication::setAttribute(Qt::AA_ForceRasterWidgets); + QMessageBox::critical(nullptr, + i18n("Plasma Failed To Start"), + i18n("Plasma is unable to start as it could not correctly use OpenGL 2 or software fallback\nPlease check that your " + "graphic drivers are set up correctly.")); + } + app.exit(-1); + }); + replace = cliOptions.isSet(replaceOption); + } + + KDBusService service(KDBusService::Unique | KDBusService::StartupOption(replace ? KDBusService::Replace : 0)); + + corona->init(); + SoftwareRendererNotifier::notifyIfRelevant(); + + return app.exec(); +} diff --git a/plasma/workspace/shell/org.kde.plasmashell.desktop.cmake b/plasma/workspace/shell/org.kde.plasmashell.desktop.cmake new file mode 100644 index 0000000000..ce1abde890 --- /dev/null +++ b/plasma/workspace/shell/org.kde.plasmashell.desktop.cmake @@ -0,0 +1,70 @@ +[Desktop Entry] +Exec=@CMAKE_INSTALL_PREFIX@/bin/plasmashell +X-DBUS-StartupType=Unique +Name=Plasma Desktop Workspace +Name[ar]=مساحة عمل سطح مكتب بلازما +Name[az]=Plasma İş Masası Mühiti +Name[bs]=Radni prostor plazma radne površi +Name[ca]=Espai de treball de l'escriptori Plasma +Name[ca@valencia]=Espai de treball de l'escriptori Plasma +Name[cs]=Pracovní plocha Plasma +Name[da]=Plasma Desktop Workspace +Name[de]=Plasma-Arbeitsflächenbereich +Name[el]=Χώρος επιφάνειας εργασίας Plasma +Name[en_GB]=Plasma Desktop Workspace +Name[es]=Espacio de trabajo del escritorio Plasma +Name[et]=Plasma töölaua töötsoon +Name[eu]=Plasma mahaigainaren lanerako guneak +Name[fi]=Plasma-työpöytä +Name[fr]=Espace de travail Plasma +Name[ga]=Spás Oibre Deisce Plasma +Name[gl]=Espazo de traballo do escritorio Plasma +Name[he]=תחנת עבודה של שולחן העבודה Plasma +Name[hi]=प्लाज़्मा डेस्कटॉप कार्यक्षेत्र +Name[hr]=Plasma radno okruženje +Name[hsb]=Plasma-dźěłowy rum z dźěłowym powjerchom +Name[hu]=Plazma asztali munkaterület +Name[ia]=Spatio de labor de scriptorio de Plasma +Name[id]=Ruangkerja Desktop Plasma +Name[is]=Vinnurýmd Plasma skjáborðs +Name[it]=Spazio di lavoro del desktop di Plasma +Name[ja]=Plasma デスクトップワークスペース +Name[ko]=Plasma 데스크톱 작업 공간 +Name[lt]=Plasma darbalaukio darbo sritis +Name[ml]=പ്ലാസ്മ ഡെസ്ക്ടോപ്പ് പണിയിടം +Name[nb]=Arbeidsrom for Plasma skrivebord +Name[nds]=Plasma-Schriefdischarbeitrebeet +Name[nl]=Plasma Bureaublad Werkplek +Name[nn]=Arbeidsområde for Plasma-skrivebord +Name[pa]=ਪਲਾਜ਼ਮਾ ਡੈਸਕਟਾਪ ਵਰਕਸਪੇਸ +Name[pl]=Przestrzeń Pracy Pulpitu Plazmy +Name[pt]=Área de Trabalho do Plasma +Name[pt_BR]=Espaço de Trabalho Plasma +Name[ro]=Spațiu de lucru al biroului Plasma +Name[ru]=Рабочая среда Plasma +Name[sk]=Pracovná plocha Plasma +Name[sl]=Delovni prostor Plasma Desktop +Name[sr]=Плазма, радни простор површи +Name[sr@ijekavian]=Плазма, радни простор површи +Name[sr@ijekavianlatin]=Plasma, radni prostor površi +Name[sr@latin]=Plasma, radni prostor površi +Name[sv]=Plasma arbetsområde för skrivbordet +Name[ta]=பிளாஸ்மா பணிமேடை +Name[tg]=Фазои мизи кории Плазма +Name[tr]=Plasma Masaüstü Çalışma Alanı +Name[uk]=Робочий простір Плазми для комп’ютерів +Name[vi]=Không gian bàn làm việc Plasma +Name[x-test]=xxPlasma Desktop Workspacexx +Name[zh_CN]=Plasma 桌面工作空间 +Name[zh_TW]=Plasma 桌面工作空間 +Type=Application +X-KDE-StartupNotify=false +X-DBUS-ServiceName=org.kde.plasmashell +OnlyShowIn=KDE; +X-KDE-autostart-phase=0 +Icon=plasmashell +NoDisplay=true +X-systemd-skip=true + +X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate,zkde_screencast_unstable_v1,org_kde_plasma_activation_feedback +X-KDE-DBUS-Restricted-Interfaces=org.kde.kwin.Screenshot,org.kde.KWin.ScreenShot2 diff --git a/plasma/workspace/shell/osd.cpp b/plasma/workspace/shell/osd.cpp new file mode 100644 index 0000000000..869c98b19d --- /dev/null +++ b/plasma/workspace/shell/osd.cpp @@ -0,0 +1,256 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "osd.h" +#include "debug.h" +#include "shellcorona.h" + +#include +#include +#include +#include + +#include +#include +#include + +Osd::Osd(const KSharedConfig::Ptr &config, ShellCorona *corona) + : QObject(corona) + , m_osdUrl(corona->lookAndFeelPackage().fileUrl("osdmainscript")) + , m_osdConfigGroup(config, "OSD") +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/osdService"), + this, + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); +} + +Osd::~Osd() +{ +} + +void Osd::brightnessChanged(int percent) +{ + showProgress(QStringLiteral("video-display-brightness"), percent, 100); +} + +void Osd::keyboardBrightnessChanged(int percent) +{ + showProgress(QStringLiteral("input-keyboard-brightness"), percent, 100); +} + +void Osd::volumeChanged(int percent) +{ + volumeChanged(percent, 100); +} + +void Osd::volumeChanged(int percent, int maximumPercent) +{ + QString icon; + if (percent <= 0) { + icon = QStringLiteral("audio-volume-muted"); + showText(icon, i18nc("OSD informing that the system is muted, keep short", "Audio Muted")); + return; + } else if (percent <= 25) { + icon = QStringLiteral("audio-volume-low"); + } else if (percent <= 75) { + icon = QStringLiteral("audio-volume-medium"); + } else { + icon = QStringLiteral("audio-volume-high"); + } + + showProgress(icon, percent, maximumPercent); +} + +void Osd::microphoneVolumeChanged(int percent) +{ + QString icon; + if (percent <= 0) { + icon = QStringLiteral("microphone-sensitivity-muted"); + showText(icon, i18nc("OSD informing that the microphone is muted, keep short", "Microphone Muted")); + return; + } else if (percent <= 25) { + icon = QStringLiteral("microphone-sensitivity-low"); + } else if (percent <= 75) { + icon = QStringLiteral("microphone-sensitivity-medium"); + } else { + icon = QStringLiteral("microphone-sensitivity-high"); + } + + showProgress(icon, percent, 100); +} + +void Osd::mediaPlayerVolumeChanged(int percent, const QString &playerName, const QString &playerIconName) +{ + if (percent == 0) { + showText(playerIconName, i18nc("OSD informing that some media app is muted, eg. Amarok Muted", "%1 Muted", playerName)); + } else { + showProgress(playerIconName, percent, 100, playerName); + } +} + +void Osd::kbdLayoutChanged(const QString &layoutName) +{ + if (m_osdConfigGroup.readEntry("kbdLayoutChangedEnabled", true)) { + showText(QStringLiteral("keyboard-layout"), layoutName); + } +} + +void Osd::virtualDesktopChanged(const QString ¤tVirtualDesktopName) +{ + // FIXME: need a VD icon + showText(QString(), currentVirtualDesktopName); +} + +void Osd::touchpadEnabledChanged(bool touchpadEnabled) +{ + if (touchpadEnabled) { + showText(QStringLiteral("input-touchpad-on"), i18nc("touchpad was enabled, keep short", "Touchpad On")); + } else { + showText(QStringLiteral("input-touchpad-off"), i18nc("touchpad was disabled, keep short", "Touchpad Off")); + } +} + +void Osd::wifiEnabledChanged(bool wifiEnabled) +{ + if (wifiEnabled) { + showText(QStringLiteral("network-wireless-on"), i18nc("wireless lan was enabled, keep short", "Wifi On")); + } else { + showText(QStringLiteral("network-wireless-off"), i18nc("wireless lan was disabled, keep short", "Wifi Off")); + } +} + +void Osd::bluetoothEnabledChanged(bool bluetoothEnabled) +{ + if (bluetoothEnabled) { + showText(QStringLiteral("preferences-system-bluetooth"), i18nc("Bluetooth was enabled, keep short", "Bluetooth On")); + } else { + showText(QStringLiteral("preferences-system-bluetooth-inactive"), i18nc("Bluetooth was disabled, keep short", "Bluetooth Off")); + } +} + +void Osd::wwanEnabledChanged(bool wwanEnabled) +{ + if (wwanEnabled) { + showText(QStringLiteral("network-mobile-on"), i18nc("mobile internet was enabled, keep short", "Mobile Internet On")); + } else { + showText(QStringLiteral("network-mobile-off"), i18nc("mobile internet was disabled, keep short", "Mobile Internet Off")); + } +} + +void Osd::virtualKeyboardEnabledChanged(bool virtualKeyboardEnabled) +{ + if (virtualKeyboardEnabled) { + showText(QStringLiteral("input-keyboard-virtual-on"), + i18nc("on screen keyboard was enabled because physical keyboard got unplugged, keep short", "On-Screen Keyboard Activated")); + } else { + showText(QStringLiteral("input-keyboard-virtual-off"), + i18nc("on screen keyboard was disabled because physical keyboard was plugged in, keep short", "On-Screen Keyboard Deactivated")); + } +} + +bool Osd::init() +{ + if (!m_osdConfigGroup.readEntry("Enabled", true)) { + return false; + } + + if (m_osdObject && m_osdObject->rootObject()) { + return true; + } + + if (m_osdUrl.isEmpty()) { + return false; + } + + if (!m_osdObject) { + m_osdObject = new KDeclarative::QmlObjectSharedEngine(this); + } + + m_osdObject->setSource(m_osdUrl); + + if (m_osdObject->status() != QQmlComponent::Ready) { + qCWarning(PLASMASHELL) << "Failed to load OSD QML file" << m_osdUrl; + return false; + } + + m_timeout = m_osdObject->rootObject()->property("timeout").toInt(); + + if (!m_osdTimer) { + m_osdTimer = new QTimer(this); + m_osdTimer->setSingleShot(true); + connect(m_osdTimer, &QTimer::timeout, this, &Osd::hideOsd); + } + + return true; +} + +void Osd::showProgress(const QString &icon, const int percent, const int maximumPercent, const QString &additionalText) +{ + if (!init()) { + return; + } + + auto *rootObject = m_osdObject->rootObject(); + int value = qBound(0, percent, maximumPercent); + rootObject->setProperty("osdValue", value); + rootObject->setProperty("osdMaxValue", maximumPercent); + rootObject->setProperty("osdAdditionalText", additionalText); + rootObject->setProperty("showingProgress", true); + rootObject->setProperty("icon", icon); + + Q_EMIT osdProgress(icon, value, additionalText); + showOsd(); +} + +void Osd::showText(const QString &icon, const QString &text) +{ + if (!init()) { + return; + } + + auto *rootObject = m_osdObject->rootObject(); + + rootObject->setProperty("showingProgress", false); + rootObject->setProperty("osdValue", text); + rootObject->setProperty("icon", icon); + + Q_EMIT osdText(icon, text); + showOsd(); +} + +void Osd::showOsd() +{ + m_osdTimer->stop(); + + auto *rootObject = m_osdObject->rootObject(); + + // if our OSD understands animating the opacity, do it; + // otherwise just show it to not break existing lnf packages + if (rootObject->property("animateOpacity").isValid()) { + rootObject->setProperty("animateOpacity", false); + rootObject->setProperty("opacity", 1); + rootObject->setProperty("visible", true); + rootObject->setProperty("animateOpacity", true); + rootObject->setProperty("opacity", 0); + } else { + rootObject->setProperty("visible", true); + } + + m_osdTimer->start(m_timeout); +} + +void Osd::hideOsd() +{ + auto *rootObject = m_osdObject->rootObject(); + if (!rootObject) { + return; + } + + rootObject->setProperty("visible", false); + + // this is needed to prevent fading from "old" values when the OSD shows up + rootObject->setProperty("osdValue", 0); +} diff --git a/plasma/workspace/shell/osd.h b/plasma/workspace/shell/osd.h new file mode 100644 index 0000000000..31562c8016 --- /dev/null +++ b/plasma/workspace/shell/osd.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace KDeclarative +{ +class QmlObjectSharedEngine; +} +namespace Plasma +{ +} + +class QTimer; +class ShellCorona; + +class Osd : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.osdService") +public: + Osd(const KSharedConfig::Ptr &config, ShellCorona *corona); + ~Osd() override; + +public Q_SLOTS: + void brightnessChanged(int percent); + void keyboardBrightnessChanged(int percent); + void volumeChanged(int percent); + void volumeChanged(int percent, int maximumPercent); + void microphoneVolumeChanged(int percent); + void mediaPlayerVolumeChanged(int percent, const QString &playerName, const QString &playerIconName); + void kbdLayoutChanged(const QString &layoutName); + void virtualDesktopChanged(const QString ¤tVirtualDesktopName); + void touchpadEnabledChanged(bool touchpadEnabled); + void wifiEnabledChanged(bool wifiEnabled); + void bluetoothEnabledChanged(bool bluetoothEnabled); + void wwanEnabledChanged(bool wwanEnabled); + void virtualKeyboardEnabledChanged(bool virtualKeyboardEnabled); + void showText(const QString &icon, const QString &text); + +Q_SIGNALS: + void osdProgress(const QString &icon, const int percent, const QString &additionalText); + void osdText(const QString &icon, const QString &text); + +private Q_SLOTS: + void hideOsd(); + +private: + bool init(); + + void showProgress(const QString &icon, const int percent, const int maximumPercent, const QString &additionalText = QString()); + void showOsd(); + + QUrl m_osdUrl; + KDeclarative::QmlObjectSharedEngine *m_osdObject = nullptr; + QTimer *m_osdTimer = nullptr; + int m_timeout = 0; + + KConfigGroup m_osdConfigGroup; +}; diff --git a/plasma/workspace/shell/packageplugins/CMakeLists.txt b/plasma/workspace/shell/packageplugins/CMakeLists.txt new file mode 100644 index 0000000000..54b8c8934d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(layouttemplate) +add_subdirectory(lookandfeel) +add_subdirectory(shell) +add_subdirectory(qmlWallpaper) +add_subdirectory(wallpaperimages) diff --git a/plasma/workspace/shell/packageplugins/layouttemplate/CMakeLists.txt b/plasma/workspace/shell/packageplugins/layouttemplate/CMakeLists.txt new file mode 100644 index 0000000000..92aeec26d1 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/layouttemplate/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_package_layouttemplate\") + +kcoreaddons_add_plugin(plasma_packagestructure_layoutemplate SOURCES layouttemplate.cpp INSTALL_NAMESPACE "kpackage/packagestructure") + +target_link_libraries(plasma_packagestructure_layoutemplate + KF5::I18n + KF5::Package +) + +set_target_properties(plasma_packagestructure_layoutemplate PROPERTIES OUTPUT_NAME plasma_layouttemplate) diff --git a/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.cpp b/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.cpp new file mode 100644 index 0000000000..fa5ce0ae7a --- /dev/null +++ b/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.cpp @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Aaron Seigo + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "layouttemplate.h" + +#include + +void LayoutTemplatePackage::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("plasma/layout-templates/")); + package->addFileDefinition("mainscript", QStringLiteral("layout.js"), i18n("Main Script File")); + package->setRequired("mainscript", true); +} + +K_PLUGIN_CLASS_WITH_JSON(LayoutTemplatePackage, "plasma-packagestructure-layouttemplate.json") + +#include "layouttemplate.moc" diff --git a/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.h b/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.h new file mode 100644 index 0000000000..6ed2cf3459 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/layouttemplate/layouttemplate.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class LayoutTemplatePackage : public KPackage::PackageStructure +{ +public: + LayoutTemplatePackage(QObject *, const QVariantList &) + { + } + void initPackage(KPackage::Package *package) override; +}; diff --git a/plasma/workspace/shell/packageplugins/layouttemplate/plasma-packagestructure-layouttemplate.json b/plasma/workspace/shell/packageplugins/layouttemplate/plasma-packagestructure-layouttemplate.json new file mode 100644 index 0000000000..1ddbed621d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/layouttemplate/plasma-packagestructure-layouttemplate.json @@ -0,0 +1,96 @@ +{ + "KPackageStructure": "Plasma/LayoutTemplate", + "KPlugin": { + "Authors": [ + { + "Email": "notmart@gmail.com", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Name": "Layout Template", + "Name[ar]": "قالب التخطيط", + "Name[az]": "Şablon Qatları", + "Name[bs]": "Predložak rasporeda", + "Name[ca@valencia]": "Plantilla de disposició", + "Name[ca]": "Plantilla de disposició", + "Name[cs]": "Šablona rozložení", + "Name[da]": "Layout-skabelon", + "Name[de]": "Layout-Vorlage", + "Name[el]": "Πρότυπο διάταξης", + "Name[en_GB]": "Layout Template", + "Name[es]": "Plantilla de distribución", + "Name[et]": "Paigutusemall", + "Name[eu]": "Diseinu-txantiloia", + "Name[fi]": "Asettelumallipohja", + "Name[fr]": "Modèle de mise en page", + "Name[gl]": "Modelo de disposición", + "Name[he]": "תבנית פריסה", + "Name[hi]": "ख़ाका नमूना", + "Name[hu]": "Elrendezéssablon", + "Name[ia]": "Patrono de disposition (layout-template)", + "Name[id]": "Templat Tataletak", + "Name[is]": "Sniðmát framsetningar", + "Name[it]": "Modello di disposizione", + "Name[ja]": "レイアウトテンプレート", + "Name[ko]": "레이아웃 템플릿", + "Name[lt]": "Išdėstymo šablonas", + "Name[ml]": "വിന്യാസ ശിലാഫലകം", + "Name[nb]": "Utformingsmal", + "Name[nds]": "Anornen-Vörlaag", + "Name[nl]": "Indelingssjabloon", + "Name[nn]": "Utformingsmal", + "Name[pa]": "ਲੇਆਉਟ ਟੈਪਲੇਟ", + "Name[pl]": "Szablon układu", + "Name[pt]": "Modelo de Disposição", + "Name[pt_BR]": "Modelo de layout", + "Name[ro]": "Șablon de aranjament", + "Name[ru]": "Шаблон размещения", + "Name[sk]": "Šablóna rozloženia", + "Name[sl]": "Predloga razporeda", + "Name[sr@ijekavian]": "Шаблон распореда", + "Name[sr@ijekavianlatin]": "Šablon rasporeda", + "Name[sr@latin]": "Šablon rasporeda", + "Name[sr]": "Шаблон распореда", + "Name[sv]": "Layoutmall", + "Name[ta]": "தளவமைப்பு வார்ப்புரு", + "Name[tr]": "Yerleşim Şablonu", + "Name[uk]": "Шаблон компонування", + "Name[vi]": "Bản mẫu bố cục", + "Name[x-test]": "xxLayout Templatexx", + "Name[zh_CN]": "布局模板", + "Name[zh_TW]": "佈局樣本", + "Version": "1" + }, + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/shell/packageplugins/lookandfeel/CMakeLists.txt b/plasma/workspace/shell/packageplugins/lookandfeel/CMakeLists.txt new file mode 100644 index 0000000000..fc3e994874 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/lookandfeel/CMakeLists.txt @@ -0,0 +1,9 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_package_lookandfeel\") + +kcoreaddons_add_plugin(plasma_packagestructure_lookandfeel SOURCES lookandfeel.cpp INSTALL_NAMESPACE "kpackage/packagestructure") + +target_link_libraries(plasma_packagestructure_lookandfeel + KF5::I18n + KF5::Package +) +set_target_properties(plasma_packagestructure_lookandfeel PROPERTIES OUTPUT_NAME plasma_lookandfeel) diff --git a/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.cpp b/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.cpp new file mode 100644 index 0000000000..d50b3e96f0 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.cpp @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Aaron Seigo + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "lookandfeel.h" + +#include +#include + +#define DEFAULT_LOOKANDFEEL "org.kde.breeze.desktop" + +void LookAndFeelPackage::initPackage(KPackage::Package *package) +{ + // https://community.kde.org/Plasma/lookAndFeelPackage# + package->setDefaultPackageRoot(QStringLiteral("plasma/look-and-feel/")); + + // Defaults + package->addFileDefinition("defaults", QStringLiteral("defaults"), i18n("Default settings for theme, etc.")); + package->addDirectoryDefinition("plasmoidsetupscripts", QStringLiteral("plasmoidsetupscripts"), i18n("Script to tweak default configs of plasmoids")); + // Colors + package->addFileDefinition("colors", QStringLiteral("colors"), i18n("Color scheme to use for applications.")); + + // Directories + package->addDirectoryDefinition("previews", QStringLiteral("previews"), i18n("Preview Images")); + package->addFileDefinition("preview", QStringLiteral("previews/preview.png"), i18n("Preview for the whole style")); + package->addFileDefinition("fullscreenpreview", QStringLiteral("previews/fullscreenpreview.jpg"), i18n("Full size preview for the whole style")); + package->addFileDefinition("loginmanagerpreview", QStringLiteral("previews/loginmanager.png"), i18n("Preview for the Login Manager")); + package->addFileDefinition("lockscreenpreview", QStringLiteral("previews/lockscreen.png"), i18n("Preview for the Lock Screen")); + package->addFileDefinition("userswitcherpreview", QStringLiteral("previews/userswitcher.png"), i18n("Preview for the Userswitcher")); + package->addFileDefinition("desktopswitcherpreview", QStringLiteral("previews/desktopswitcher.png"), i18n("Preview for the Virtual Desktop Switcher")); + package->addFileDefinition("splashpreview", QStringLiteral("previews/splash.png"), i18n("Preview for Splash Screen")); + package->addFileDefinition("runcommandpreview", QStringLiteral("previews/runcommand.png"), i18n("Preview for KRunner")); + package->addFileDefinition("windowdecorationpreview", QStringLiteral("previews/windowdecoration.png"), i18n("Preview for the Window Decorations")); + package->addFileDefinition("windowswitcherpreview", QStringLiteral("previews/windowswitcher.png"), i18n("Preview for Window Switcher")); + + package->addDirectoryDefinition("loginmanager", QStringLiteral("loginmanager"), i18n("Login Manager")); + package->addFileDefinition("loginmanagermainscript", QStringLiteral("loginmanager/LoginManager.qml"), i18n("Main Script for Login Manager")); + + package->addDirectoryDefinition("logout", QStringLiteral("logout"), i18n("Logout Dialog")); + package->addFileDefinition("logoutmainscript", QStringLiteral("logout/Logout.qml"), i18n("Main Script for Logout Dialog")); + + package->addDirectoryDefinition("lockscreen", QStringLiteral("lockscreen"), i18n("Screenlocker")); + package->addFileDefinition("lockscreenmainscript", QStringLiteral("lockscreen/LockScreen.qml"), i18n("Main Script for Lock Screen")); + + package->addDirectoryDefinition("userswitcher", QStringLiteral("userswitcher"), i18n("UI for fast user switching")); + package->addFileDefinition("userswitchermainscript", QStringLiteral("userswitcher/UserSwitcher.qml"), i18n("Main Script for User Switcher")); + + package->addDirectoryDefinition("desktopswitcher", QStringLiteral("desktopswitcher"), i18n("Virtual Desktop Switcher")); + package->addFileDefinition("desktopswitchermainscript", + QStringLiteral("desktopswitcher/DesktopSwitcher.qml"), + i18n("Main Script for Virtual Desktop Switcher")); + + package->addDirectoryDefinition("osd", QStringLiteral("osd"), i18n("On-Screen Display Notifications")); + package->addFileDefinition("osdmainscript", QStringLiteral("osd/Osd.qml"), i18n("Main Script for On-Screen Display Notifications")); + + package->addDirectoryDefinition("splash", QStringLiteral("splash"), i18n("Splash Screen")); + package->addFileDefinition("splashmainscript", QStringLiteral("splash/Splash.qml"), i18n("Main Script for Splash Screen")); + + package->addDirectoryDefinition("runcommand", QStringLiteral("runcommand"), i18n("KRunner UI")); + package->addFileDefinition("runcommandmainscript", QStringLiteral("runcommand/RunCommand.qml"), i18n("Main Script KRunner")); + + package->addDirectoryDefinition("windowdecoration", QStringLiteral("windowdecoration"), i18n("Window Decoration")); + package->addFileDefinition("windowdecorationmainscript", + QStringLiteral("windowdecoration/WindowDecoration.qml"), + i18n("Main Script for Window Decoration")); + + package->addDirectoryDefinition("windowswitcher", QStringLiteral("windowswitcher"), i18n("Window Switcher")); + package->addFileDefinition("windowswitchermainscript", QStringLiteral("windowswitcher/WindowSwitcher.qml"), i18n("Main Script for Window Switcher")); + + package->addDirectoryDefinition("systemdialog", QStringLiteral("systemdialog"), i18n("System Dialog")); + package->addFileDefinition("systemdialogscript", QStringLiteral("systemdialog/SystemDialog.qml"), i18n("The system dialog")); + + package->addDirectoryDefinition("layouts", QStringLiteral("layouts"), i18n("Default layout scripts")); + + package->setPath(DEFAULT_LOOKANDFEEL); +} + +void LookAndFeelPackage::pathChanged(KPackage::Package *package) +{ + if (!package->metadata().isValid()) { + return; + } + + const QString pluginName = package->metadata().pluginId(); + + if (!pluginName.isEmpty() && pluginName != DEFAULT_LOOKANDFEEL) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), DEFAULT_LOOKANDFEEL); + package->setFallbackPackage(pkg); + } else if (package->fallbackPackage().isValid() && pluginName == DEFAULT_LOOKANDFEEL) { + package->setFallbackPackage(KPackage::Package()); + } +} + +K_PLUGIN_CLASS_WITH_JSON(LookAndFeelPackage, "plasma-packagestructure-lookandfeel.json") + +#include "lookandfeel.moc" diff --git a/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.h b/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.h new file mode 100644 index 0000000000..710f7f9745 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/lookandfeel/lookandfeel.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class LookAndFeelPackage : public KPackage::PackageStructure +{ +public: + LookAndFeelPackage(QObject *, const QVariantList &) + { + } + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; diff --git a/plasma/workspace/shell/packageplugins/lookandfeel/plasma-packagestructure-lookandfeel.json b/plasma/workspace/shell/packageplugins/lookandfeel/plasma-packagestructure-lookandfeel.json new file mode 100644 index 0000000000..1243581480 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/lookandfeel/plasma-packagestructure-lookandfeel.json @@ -0,0 +1,101 @@ +{ + "KPackageStructure": "Plasma/LookAndFeel", + "KPlugin": { + "Authors": [ + { + "Email": "notmart@gmail.com", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Name": "Look and Feel", + "Name[ar]": "المظهر و الأحساس", + "Name[ast]": "Aspeutu y estilu", + "Name[az]": "Plasma Xarici Görünüşü", + "Name[bs]": "Izgled i osjećaj", + "Name[ca@valencia]": "Aspecte i comportament", + "Name[ca]": "Aspecte i comportament", + "Name[cs]": "Vzhled a dojem", + "Name[da]": "Udseende og fremtoning", + "Name[de]": "Erscheinungsbild und Verhalten", + "Name[el]": "Εμφάνιση και αίσθηση", + "Name[en_GB]": "Look and Feel", + "Name[eo]": "Fasado", + "Name[es]": "Aspecto visual", + "Name[et]": "Välimus", + "Name[eu]": "Itxura eta Izaera", + "Name[fi]": "Ulkoasu ja tuntuma", + "Name[fr]": "Apparence", + "Name[gl]": "Aparencia e comportamento", + "Name[he]": "מראה ותחושה", + "Name[hi]": "रंगरूप और अनुभव", + "Name[hu]": "Megjelenés", + "Name[ia]": "Semblantia", + "Name[id]": "Look and Feel", + "Name[is]": "Útlit og viðmót", + "Name[it]": "Aspetto", + "Name[ja]": "外観", + "Name[kk]": "Сыртқы көрнісі", + "Name[ko]": "모습과 느낌", + "Name[lt]": "Išvaizda ir turinys", + "Name[lv]": "Izskats un izjūtas", + "Name[ml]": "കെട്ടും മട്ടും", + "Name[nb]": "Utseende og oppførsel", + "Name[nds]": "Utsehn un Bedenen", + "Name[nl]": "Uiterlijk en gedrag", + "Name[nn]": "Utsjånad og åtferd", + "Name[pa]": "ਦਿੱਖ ਅਤੇ ਮਹਿਸੂਸ", + "Name[pl]": "Zestaw wyglądu", + "Name[pt]": "Aparência e Comportamento", + "Name[pt_BR]": "Aparência", + "Name[ro]": "Aspect și comportament", + "Name[ru]": "Оформление", + "Name[sk]": "Vzhľad a dojem", + "Name[sl]": "Videz in občutek", + "Name[sr@ijekavian]": "Изглед и осећај", + "Name[sr@ijekavianlatin]": "Izgled i osećaj", + "Name[sr@latin]": "Izgled i osećaj", + "Name[sr]": "Изглед и осећај", + "Name[sv]": "Utseende och känsla", + "Name[ta]": "தோற்றம்", + "Name[tg]": "Намуди зоҳирӣ", + "Name[tr]": "Bakın ve Hissedin", + "Name[uk]": "Вигляд і поведінка", + "Name[vi]": "Nhìn và Cảm", + "Name[x-test]": "xxLook and Feelxx", + "Name[zh_CN]": "界面外观", + "Name[zh_TW]": "外觀與感覺", + "Version": "1" + }, + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/shell/packageplugins/qmlWallpaper/CMakeLists.txt b/plasma/workspace/shell/packageplugins/qmlWallpaper/CMakeLists.txt new file mode 100644 index 0000000000..b166a812ba --- /dev/null +++ b/plasma/workspace/shell/packageplugins/qmlWallpaper/CMakeLists.txt @@ -0,0 +1,11 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_package_wallpaper\") + +kcoreaddons_add_plugin(plasma_packagestructure_wallpaper SOURCES wallpaper.cpp INSTALL_NAMESPACE "kpackage/packagestructure") + +target_link_libraries(plasma_packagestructure_wallpaper + KF5::Declarative + KF5::I18n + KF5::Package +) + +set_target_properties(plasma_packagestructure_wallpaper PROPERTIES OUTPUT_NAME plasma_wallpaper) diff --git a/plasma/workspace/shell/packageplugins/qmlWallpaper/plasma-packagestructure-wallpaper.json b/plasma/workspace/shell/packageplugins/qmlWallpaper/plasma-packagestructure-wallpaper.json new file mode 100644 index 0000000000..3b304f4e3e --- /dev/null +++ b/plasma/workspace/shell/packageplugins/qmlWallpaper/plasma-packagestructure-wallpaper.json @@ -0,0 +1,99 @@ +{ + "KPackageStructure": "Plasma/Wallpaper", + "KPlugin": { + "Authors": [ + { + "Email": "notmart@gmail.com", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Name": "Wallpaper", + "Name[ar]": "الخلفية", + "Name[ast]": "Fondu de pantalla", + "Name[az]": "Divar Kağızı", + "Name[bs]": "Pozadina", + "Name[ca@valencia]": "Fons de pantalla", + "Name[ca]": "Fons de pantalla", + "Name[cs]": "Tapeta", + "Name[da]": "Baggrundsbillede", + "Name[de]": "Hintergrundbild", + "Name[el]": "Ταπετσαρία", + "Name[en_GB]": "Wallpaper", + "Name[es]": "Fondo del escritorio", + "Name[et]": "Taustapilt", + "Name[eu]": "Horma-papera", + "Name[fi]": "Tausta", + "Name[fr]": "Fond d'écran", + "Name[gl]": "Fondo de escritorio", + "Name[he]": "טפט", + "Name[hi]": "वाॅलपेपर", + "Name[hsb]": "Pozadk dźěłoweho powjercha", + "Name[hu]": "Háttérkép", + "Name[ia]": "Tapete de papiro", + "Name[id]": "Wallpaper", + "Name[is]": "Bakgrunnsmynd", + "Name[it]": "Sfondo", + "Name[ja]": "壁紙", + "Name[ko]": "배경 그림", + "Name[lt]": "Darbalaukio fonas", + "Name[lv]": "Tapete", + "Name[ml]": "പശ്ചാത്തലചിത്രം", + "Name[nb]": "Tapet", + "Name[nds]": "Achtergrundbild", + "Name[nl]": "Achtergrondafbeelding", + "Name[nn]": "Bakgrunnsbilete", + "Name[pa]": "ਵਾਲਪੇਪਰ", + "Name[pl]": "Tapeta", + "Name[pt]": "Papel de Parede", + "Name[pt_BR]": "Papel de parede", + "Name[ro]": "Tapet", + "Name[ru]": "Обои", + "Name[sk]": "Tapeta", + "Name[sl]": "Slika ozadja", + "Name[sr@ijekavian]": "Тапет", + "Name[sr@ijekavianlatin]": "Tapet", + "Name[sr@latin]": "Tapet", + "Name[sr]": "Тапет", + "Name[sv]": "Skrivbordsunderlägg", + "Name[ta]": "பின்னணிப் படம்", + "Name[tr]": "Duvar Kağıdı", + "Name[uk]": "Зображення тла", + "Name[vi]": "Phông nền", + "Name[x-test]": "xxWallpaperxx", + "Name[zh_CN]": "壁纸", + "Name[zh_TW]": "桌布", + "Version": "1" + }, + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.cpp b/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.cpp new file mode 100644 index 0000000000..ff562b9d5d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2007-2009 Aaron Seigo + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "wallpaper.h" + +#include +#include + +void QmlWallpaperPackage::initPackage(KPackage::Package *package) +{ + package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"), i18n("Main Script File")); + package->setRequired("mainscript", true); + + QStringList platform = KDeclarative::KDeclarative::runtimePlatform(); + if (!platform.isEmpty()) { + QMutableStringListIterator it(platform); + while (it.hasNext()) { + it.next(); + it.setValue("platformcontents/" + it.value()); + } + + platform.append(QStringLiteral("contents")); + package->setContentsPrefixPaths(platform); + } + + package->setDefaultPackageRoot(QStringLiteral("plasma/wallpapers/")); + + package->addDirectoryDefinition("images", QStringLiteral("images"), i18n("Images")); + package->addDirectoryDefinition("theme", QStringLiteral("theme"), i18n("Themed Images")); + QStringList mimetypes; + mimetypes << QStringLiteral("image/svg+xml") << QStringLiteral("image/png") << QStringLiteral("image/jpeg"); + package->setMimeTypes("images", mimetypes); + package->setMimeTypes("theme", mimetypes); + + package->addDirectoryDefinition("config", QStringLiteral("config"), i18n("Configuration Definitions")); + mimetypes.clear(); + mimetypes << QStringLiteral("text/xml"); + package->setMimeTypes("config", mimetypes); + + package->addDirectoryDefinition("ui", QStringLiteral("ui"), i18n("User Interface")); + + package->addDirectoryDefinition("data", QStringLiteral("data"), i18n("Data Files")); + + package->addDirectoryDefinition("scripts", QStringLiteral("code"), i18n("Executable Scripts")); + mimetypes.clear(); + mimetypes << QStringLiteral("text/plain"); + package->setMimeTypes("scripts", mimetypes); + + package->addDirectoryDefinition("translations", QStringLiteral("locale"), i18n("Translations")); +} + +K_PLUGIN_CLASS_WITH_JSON(QmlWallpaperPackage, "plasma-packagestructure-wallpaper.json") + +#include "wallpaper.moc" diff --git a/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.h b/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.h new file mode 100644 index 0000000000..b94c93245b --- /dev/null +++ b/plasma/workspace/shell/packageplugins/qmlWallpaper/wallpaper.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2013 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class QmlWallpaperPackage : public KPackage::PackageStructure +{ +public: + QmlWallpaperPackage(QObject *, const QVariantList &) + { + } + void initPackage(KPackage::Package *package) override; +}; diff --git a/plasma/workspace/shell/packageplugins/shell/CMakeLists.txt b/plasma/workspace/shell/packageplugins/shell/CMakeLists.txt new file mode 100644 index 0000000000..853a70e034 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/shell/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_package_plasmashell\") + +kcoreaddons_add_plugin(plasma_packagestructure_plasmashell SOURCES shellpackage.cpp INSTALL_NAMESPACE "kpackage/packagestructure") + +target_link_libraries(plasma_packagestructure_plasmashell + KF5::I18n + KF5::Package +) + +set_target_properties(plasma_packagestructure_plasmashell PROPERTIES OUTPUT_NAME plasma_shell) diff --git a/plasma/workspace/shell/packageplugins/shell/Messages.sh b/plasma/workspace/shell/packageplugins/shell/Messages.sh new file mode 100644 index 0000000000..44d07a4c64 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/shell/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/plasma_package_plasmashell.pot diff --git a/plasma/workspace/shell/packageplugins/shell/plasma-packagestructure-plasma-shell.json b/plasma/workspace/shell/packageplugins/shell/plasma-packagestructure-plasma-shell.json new file mode 100644 index 0000000000..9452d6038d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/shell/plasma-packagestructure-plasma-shell.json @@ -0,0 +1,97 @@ +{ + "KPackageStructure": "Plasma/Shell", + "KPlugin": { + "Authors": [ + { + "Email": "notmart@gmail.com", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Name": "Plasma Shell", + "Name[ar]": "طرفية بلازما", + "Name[ast]": "Shell de Plasma", + "Name[az]": "Plasma üz qabığı", + "Name[bs]": "Plazma školjka", + "Name[ca@valencia]": "Intèrpret d'ordres de Plasma", + "Name[ca]": "Intèrpret d'ordres del Plasma", + "Name[cs]": "Shell Plasmy", + "Name[da]": "Plasma-skal", + "Name[de]": "Plasma-Umgebung", + "Name[el]": "Κέλυφος Plasma", + "Name[en_GB]": "Plasma Shell", + "Name[es]": "Intérprete de Plasma", + "Name[et]": "Plasma shell", + "Name[eu]": "Plasma-ingurunea", + "Name[fi]": "Plasma-käyttöliittymä", + "Name[fr]": "Environnement Plasma", + "Name[gl]": "Shell de Plasma", + "Name[hi]": "प्लाज़्मा शेल", + "Name[hu]": "Plasma felület", + "Name[ia]": "Plasma Shell (Shell de Plasma)", + "Name[id]": "Shell Plasma", + "Name[is]": "Plasma skel", + "Name[it]": "Shell di Plasma", + "Name[ja]": "Plasma シェル", + "Name[ko]": "Plasma 셸", + "Name[lt]": "Plasma apvalkalas", + "Name[lv]": "Plasma čaula", + "Name[ml]": "പ്ലാസ്മ ഷെൽ", + "Name[nb]": "Plasma-skall", + "Name[nds]": "Plasma-Konsool", + "Name[nl]": "Plasma Shell", + "Name[nn]": "Plasma-skal", + "Name[pa]": "ਪਲਾਜ਼ਮਾ ਸ਼ੈੱਲ", + "Name[pl]": "Powłoka Plazmy", + "Name[pt]": "Consola do Plasma", + "Name[pt_BR]": "Plasma Shell", + "Name[ro]": "Învelișul Plasma", + "Name[ru]": "Оболочка Plasma", + "Name[sk]": "Plasma Shell", + "Name[sl]": "Lupina za Plasmo", + "Name[sr@ijekavian]": "Плазма шкољка", + "Name[sr@ijekavianlatin]": "Plasma školjka", + "Name[sr@latin]": "Plasma školjka", + "Name[sr]": "Плазма шкољка", + "Name[sv]": "Plasma-skal", + "Name[ta]": "பிளாஸ்மா நிரல்", + "Name[tr]": "Plasma Kabuğu", + "Name[uk]": "Оболонка Плазми", + "Name[vi]": "Hệ vỏ Plasma", + "Name[x-test]": "xxPlasma Shellxx", + "Name[zh_CN]": "Plasma 外壳", + "Name[zh_TW]": "Plasma Shell", + "Version": "1" + }, + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/shell/packageplugins/shell/shellpackage.cpp b/plasma/workspace/shell/packageplugins/shell/shellpackage.cpp new file mode 100644 index 0000000000..6db303066d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/shell/shellpackage.cpp @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "shellpackage.h" +#include +#include + +#include +#include +#include + +#define DEFAULT_SHELL "org.kde.plasma.desktop" + +ShellPackage::ShellPackage(QObject *, const QVariantList &) +{ +} + +void ShellPackage::initPackage(KPackage::Package *package) +{ + package->setDefaultPackageRoot(QStringLiteral("plasma/shells/")); + + // Directories + package->addDirectoryDefinition("applet", QStringLiteral("applet"), i18n("Applets furniture")); + package->addDirectoryDefinition("configuration", QStringLiteral("configuration"), i18n("Applets furniture")); + package->addDirectoryDefinition("explorer", QStringLiteral("explorer"), i18n("Explorer UI for adding widgets")); + package->addDirectoryDefinition("views", QStringLiteral("views"), i18n("User interface for the views that will show containments")); + + package->setMimeTypes("applet", QStringList() << QStringLiteral("text/x-qml")); + package->setMimeTypes("configuration", QStringList() << QStringLiteral("text/x-qml")); + package->setMimeTypes("views", QStringList() << QStringLiteral("text/x-qml")); + + // Files + // Default layout + package->addFileDefinition("defaultlayout", QStringLiteral("layout.js"), i18n("Default layout file")); + package->addFileDefinition("defaults", QStringLiteral("defaults"), i18n("Default plugins for containments, containmentActions, etc.")); + package->setMimeTypes("defaultlayout", QStringList() << QStringLiteral("application/javascript")); + package->setMimeTypes("defaults", QStringList() << QStringLiteral("text/plain")); + + // Applet furniture + package->addFileDefinition("appleterror", QStringLiteral("applet/AppletError.qml"), i18n("Error message shown when an applet fails to load")); + package->addFileDefinition("compactapplet", QStringLiteral("applet/CompactApplet.qml"), i18n("QML component that shows an applet in a popup")); + package->addFileDefinition( + "defaultcompactrepresentation", + QStringLiteral("applet/DefaultCompactRepresentation.qml"), + i18n("Compact representation of an applet when collapsed in a popup, for instance as an icon. Applets can override this component.")); + + // Configuration + package->addFileDefinition("appletconfigurationui", + QStringLiteral("configuration/AppletConfiguration.qml"), + i18n("QML component for the configuration dialog for applets")); + package->addFileDefinition("containmentconfigurationui", + QStringLiteral("configuration/ContainmentConfiguration.qml"), + i18n("QML component for the configuration dialog for containments")); + package->addFileDefinition("panelconfigurationui", QStringLiteral("configuration/PanelConfiguration.qml"), i18n("Panel configuration UI")); + package->addFileDefinition("appletalternativesui", + QStringLiteral("explorer/AppletAlternatives.qml"), + i18n("QML component for choosing an alternate applet")); + package->addFileDefinition("containmentmanagementui", + QStringLiteral("configuration/ShellContainmentConfiguration.qml"), + i18n("QML component for the configuration dialog of containments")); + + // Widget explorer + package->addFileDefinition("widgetexplorer", QStringLiteral("explorer/WidgetExplorer.qml"), i18n("Widgets explorer UI")); + + package->addFileDefinition("interactiveconsole", + QStringLiteral("InteractiveConsole.qml"), + i18n("A UI for writing, loading and running desktop scripts in the current live session")); +} + +void ShellPackage::pathChanged(KPackage::Package *package) +{ + if (!package->metadata().isValid()) { + return; + } + + const QString pluginName = package->metadata().pluginId(); + if (!pluginName.isEmpty() && pluginName != DEFAULT_SHELL) { + const QString fallback = package->metadata().value("X-Plasma-FallbackPackage", QStringLiteral(DEFAULT_SHELL)); + + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell"), fallback); + package->setFallbackPackage(pkg); + } else if (package->fallbackPackage().isValid() && pluginName == DEFAULT_SHELL) { + package->setFallbackPackage(KPackage::Package()); + } +} + +K_PLUGIN_CLASS_WITH_JSON(ShellPackage, "plasma-packagestructure-plasma-shell.json") + +#include "shellpackage.moc" diff --git a/plasma/workspace/shell/packageplugins/shell/shellpackage.h b/plasma/workspace/shell/packageplugins/shell/shellpackage.h new file mode 100644 index 0000000000..8b3ba49425 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/shell/shellpackage.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ShellPackage : public KPackage::PackageStructure +{ +public: + ShellPackage(QObject *parent, const QVariantList &list); + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; diff --git a/plasma/workspace/shell/packageplugins/wallpaperimages/CMakeLists.txt b/plasma/workspace/shell/packageplugins/wallpaperimages/CMakeLists.txt new file mode 100644 index 0000000000..ec4e1b9e5f --- /dev/null +++ b/plasma/workspace/shell/packageplugins/wallpaperimages/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_package_wallpaperimages\") + +kcoreaddons_add_plugin(plasma_packagestructure_wallpaperimages SOURCES wallpaperpackage.cpp INSTALL_NAMESPACE "kpackage/packagestructure") + +target_link_libraries(plasma_packagestructure_wallpaperimages + KF5::I18n + KF5::Package +) + +set_target_properties(plasma_packagestructure_wallpaperimages PROPERTIES OUTPUT_NAME wallpaper_images) diff --git a/plasma/workspace/shell/packageplugins/wallpaperimages/plasma-packagestructure-wallpaperimages.json b/plasma/workspace/shell/packageplugins/wallpaperimages/plasma-packagestructure-wallpaperimages.json new file mode 100644 index 0000000000..998dbf241d --- /dev/null +++ b/plasma/workspace/shell/packageplugins/wallpaperimages/plasma-packagestructure-wallpaperimages.json @@ -0,0 +1,95 @@ +{ + "KPackageStructure": "Wallpaper/Images", + "KPlugin": { + "Authors": [ + { + "Email": "aseigo@kde.org", + "Name": "Aaron Seigo", + "Name[ar]": "Aaron Seigo", + "Name[az]": "Aaron Seigo", + "Name[ca]": "Aaron Seigo", + "Name[cs]": "Aaron Seigo", + "Name[de]": "Aaron Seigo", + "Name[en_GB]": "Aaron Seigo", + "Name[es]": "Aaron Seigo", + "Name[eu]": "Aaron Seigo", + "Name[fi]": "Aaron Seigo", + "Name[fr]": "Aaron Seigo", + "Name[hu]": "Aaron Seigo", + "Name[ia]": "Aaron Seigo", + "Name[it]": "Aaron Seigo", + "Name[ko]": "Aaron Seigo", + "Name[lt]": "Aaron Seigo", + "Name[nl]": "Aaron Seigo", + "Name[nn]": "Aaron Seigo", + "Name[pl]": "Aaron Seigo", + "Name[pt_BR]": "Aaron Seigo", + "Name[ro]": "Aaron Seigo", + "Name[ru]": "Aaron Seigo", + "Name[sk]": "Aaron Seigo", + "Name[sl]": "Aaron Seigo", + "Name[sv]": "Aaron Seigo", + "Name[tr]": "Aaron Seigo", + "Name[uk]": "Aaron Seigo", + "Name[vi]": "Aaron Seigo", + "Name[x-test]": "xxAaron Seigoxx", + "Name[zh_CN]": "Aaron Seigo" + } + ], + "Name": "Wallpaper Images", + "Name[ar]": "صور خلفيات", + "Name[az]": "Divar Kağızı şəkilləri", + "Name[bs]": "Pozadinske slike", + "Name[ca@valencia]": "Imatges de fons de pantalla", + "Name[ca]": "Imatges de fons de pantalla", + "Name[cs]": "Tapety", + "Name[da]": "Baggrundsbilleder", + "Name[de]": "Hintergrundbilder", + "Name[el]": "Εικόνες ταπετσαρίας", + "Name[en_GB]": "Wallpaper Images", + "Name[es]": "Imágenes de fondo del escritorio", + "Name[et]": "Taustapildid", + "Name[eu]": "Horma-papereko irudiak", + "Name[fi]": "Taustakuvat", + "Name[fr]": "Images de fond d'écran", + "Name[gl]": "Fondos de escritorio", + "Name[he]": "תמונת רקע", + "Name[hi]": "वाॅलपेपर छवियाँ", + "Name[hsb]": "Wobrazy w pozadku dźěłoweho powjercha", + "Name[hu]": "Háttérképek", + "Name[ia]": "Images de tapete de papiro", + "Name[id]": "Citra Wallpaper", + "Name[is]": "Bakgrunnsmyndir", + "Name[it]": "Immagini di sfondo", + "Name[ja]": "壁紙画像", + "Name[ko]": "배경 그림", + "Name[lt]": "Darbalaukio fono paveikslai", + "Name[lv]": "Tapešu attēli", + "Name[ml]": "പശ്ചാത്തലചിത്രങ്ങൾ", + "Name[nb]": "Tapetbilder", + "Name[nl]": "Achtergrondafbeeldingen", + "Name[nn]": "Bakgrunnsbilete", + "Name[pa]": "ਵਾਲਪੇਪਰ ਚਿੱਤਰ", + "Name[pl]": "Obrazy tapet", + "Name[pt]": "Imagens do Papel de Parede", + "Name[pt_BR]": "Papéis de parede", + "Name[ro]": "Imagini de tapet", + "Name[ru]": "Изображения для обоев", + "Name[sk]": "Obrázky tapety", + "Name[sl]": "Slike ozadja", + "Name[sr@ijekavian]": "Тапетне слике", + "Name[sr@ijekavianlatin]": "Tapetne slike", + "Name[sr@latin]": "Tapetne slike", + "Name[sr]": "Тапетне слике", + "Name[sv]": "Skrivbordsunderlägg", + "Name[ta]": "பின்னணிக்கான படங்கள்", + "Name[tr]": "Duvar Kağıdı Resimleri", + "Name[uk]": "Зображення шпалер", + "Name[vi]": "Ảnh phông nền", + "Name[x-test]": "xxWallpaper Imagesxx", + "Name[zh_CN]": "壁纸图像", + "Name[zh_TW]": "桌布影像", + "Version": "1" + }, + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.cpp b/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.cpp new file mode 100644 index 0000000000..3242d126ff --- /dev/null +++ b/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "wallpaperpackage.h" + +#include +#include + +#include + +WallpaperPackage::WallpaperPackage(QObject *parent, const QVariantList &args) + : KPackage::PackageStructure(parent, args) +{ +} + +void WallpaperPackage::initPackage(KPackage::Package *package) +{ + package->addDirectoryDefinition("images", QStringLiteral("images/"), i18n("Images")); + + QStringList mimetypes; + mimetypes << QStringLiteral("image/svg") << QStringLiteral("image/png") << QStringLiteral("image/jpeg") << QStringLiteral("image/jpg"); + package->setMimeTypes("images", mimetypes); + + package->setRequired("images", true); + package->addFileDefinition("screenshot", QStringLiteral("screenshot.png"), i18n("Screenshot")); + package->setAllowExternalPaths(true); +} + +void WallpaperPackage::pathChanged(KPackage::Package *package) +{ + static bool guard = false; + + if (guard) { + return; + } + + guard = true; + QString ppath = package->path(); + if (ppath.endsWith('/')) { + ppath.chop(1); + if (!QFile::exists(ppath)) { + ppath = package->path(); + } + } + + QFileInfo info(ppath); + const bool isFullPackage = info.isDir(); + package->removeDefinition("preferred"); + package->setRequired("images", isFullPackage); + + if (isFullPackage) { + package->setContentsPrefixPaths(QStringList() << QStringLiteral("contents/")); + } else { + package->addFileDefinition("screenshot", info.fileName(), i18n("Preview")); + package->addFileDefinition("preferred", info.fileName(), QString()); + package->setContentsPrefixPaths(QStringList()); + package->setPath(info.path()); + } + + guard = false; +} + +K_PLUGIN_CLASS_WITH_JSON(WallpaperPackage, "plasma-packagestructure-wallpaperimages.json") + +#include "wallpaperpackage.moc" diff --git a/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.h b/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.h new file mode 100644 index 0000000000..789b557d70 --- /dev/null +++ b/plasma/workspace/shell/packageplugins/wallpaperimages/wallpaperpackage.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class WallpaperPackage : public KPackage::PackageStructure +{ + Q_OBJECT + +public: + explicit WallpaperPackage(QObject *parent = nullptr, const QVariantList &args = QVariantList()); + + void initPackage(KPackage::Package *package) override; + void pathChanged(KPackage::Package *package) override; +}; diff --git a/plasma/workspace/shell/panelconfigview.cpp b/plasma/workspace/shell/panelconfigview.cpp new file mode 100644 index 0000000000..34ff82a4a3 --- /dev/null +++ b/plasma/workspace/shell/panelconfigview.cpp @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "panelconfigview.h" +#include "panelshadows_p.h" +#include "shellcorona.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +//////////////////////////////PanelConfigView +PanelConfigView::PanelConfigView(Plasma::Containment *containment, PanelView *panelView, QWindow *parent) + : ConfigView(containment, parent) + , m_containment(containment) + , m_panelView(panelView) +{ + connect(panelView, &QObject::destroyed, this, &QObject::deleteLater); + + setScreen(panelView->screen()); + + connect(panelView, &QWindow::screenChanged, &m_screenSyncTimer, QOverload<>::of(&QTimer::start)); + m_screenSyncTimer.setSingleShot(true); + m_screenSyncTimer.setInterval(150); + connect(&m_screenSyncTimer, &QTimer::timeout, [=]() { + setScreen(panelView->screen()); + KWindowSystem::setType(winId(), NET::Dock); + KWindowSystem::setState(winId(), NET::KeepAbove); + syncGeometry(); + syncLocation(); + }); + + KWindowSystem::setType(winId(), NET::Dock); + KWindowSystem::setState(winId(), NET::KeepAbove); + KWindowSystem::forceActiveWindow(winId()); + + updateBlurBehindAndContrast(); + connect(&m_theme, &Plasma::Theme::themeChanged, this, &PanelConfigView::updateBlurBehindAndContrast); + + rootContext()->setContextProperty(QStringLiteral("panel"), panelView); + rootContext()->setContextProperty(QStringLiteral("configDialog"), this); + connect(containment, &Plasma::Containment::formFactorChanged, this, &PanelConfigView::syncGeometry); + connect(containment, &Plasma::Containment::locationChanged, this, &PanelConfigView::syncLocation); +} + +PanelConfigView::~PanelConfigView() +{ +} + +void PanelConfigView::init() +{ + setSource(m_containment->corona()->kPackage().fileUrl("panelconfigurationui")); + syncGeometry(); + syncLocation(); +} + +void PanelConfigView::updateBlurBehindAndContrast() +{ + KWindowEffects::enableBlurBehind(this, m_theme.blurBehindEnabled()); + KWindowEffects::enableBackgroundContrast(this, + m_theme.backgroundContrastEnabled(), + m_theme.backgroundContrast(), + m_theme.backgroundIntensity(), + m_theme.backgroundSaturation()); +} + +void PanelConfigView::showAddWidgetDialog() +{ + QAction *addWidgetAction = m_containment->actions()->action(QStringLiteral("add widgets")); + if (addWidgetAction) { + addWidgetAction->trigger(); + } +} + +void PanelConfigView::addPanelSpacer() +{ + ShellCorona *c = qobject_cast(m_containment->corona()); + if (!c) { + return; + } + // Add a spacer at the end *except* if there is exactly one spacer already + // this to trigger the panel centering mode of the spacer in a slightly more discoverable way + c->evaluateScript(QStringLiteral("panel = panelById(") + QString::number(m_containment->id()) + + QStringLiteral(");" + "var spacers = panel.widgets(\"org.kde.plasma.panelspacer\");" + "if (spacers.length === 1) {" + " panel.addWidget(\"org.kde.plasma.panelspacer\", 0,0,1,1);" + "} else {" + " panel.addWidget(\"org.kde.plasma.panelspacer\");" + "}")); +} + +void PanelConfigView::syncGeometry() +{ + if (!m_containment || !rootObject()) { + return; + } + + if (m_containment->formFactor() == Plasma::Types::Vertical) { + QSize s(rootObject()->implicitWidth(), screen()->size().height()); + resize(s); + setMinimumSize(s); + setMaximumSize(s); + + if (m_containment->location() == Plasma::Types::LeftEdge) { + setPosition(m_panelView->geometry().right(), screen()->geometry().top()); + } else if (m_containment->location() == Plasma::Types::RightEdge) { + setPosition(m_panelView->geometry().left() - width(), screen()->geometry().top()); + } + + } else { + QSize s(screen()->size().width(), rootObject()->implicitHeight()); + resize(s); + setMinimumSize(s); + setMaximumSize(s); + + if (m_containment->location() == Plasma::Types::TopEdge) { + setPosition(screen()->geometry().left(), m_panelView->geometry().bottom()); + } else if (m_containment->location() == Plasma::Types::BottomEdge) { + setPosition(screen()->geometry().left(), m_panelView->geometry().top() - height()); + } + } +} + +void PanelConfigView::syncLocation() +{ + if (!m_containment) { + return; + } + + KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge; + Plasma::FrameSvg::EnabledBorders enabledBorders = Plasma::FrameSvg::AllBorders; + + switch (m_containment->location()) { + case Plasma::Types::TopEdge: + slideLocation = KWindowEffects::TopEdge; + enabledBorders = Plasma::FrameSvg::BottomBorder; + break; + case Plasma::Types::RightEdge: + slideLocation = KWindowEffects::RightEdge; + enabledBorders = Plasma::FrameSvg::LeftBorder; + break; + case Plasma::Types::BottomEdge: + slideLocation = KWindowEffects::BottomEdge; + enabledBorders = Plasma::FrameSvg::TopBorder; + break; + case Plasma::Types::LeftEdge: + slideLocation = KWindowEffects::LeftEdge; + enabledBorders = Plasma::FrameSvg::RightBorder; + break; + default: + break; + } + + KWindowEffects::slideWindow(this, slideLocation, -1); + + if (m_enabledBorders != enabledBorders) { + m_enabledBorders = enabledBorders; + + PanelShadows::self()->setEnabledBorders(this, enabledBorders); + + Q_EMIT enabledBordersChanged(); + } +} + +void PanelConfigView::showEvent(QShowEvent *ev) +{ + QQuickWindow::showEvent(ev); + + KWindowSystem::setType(winId(), NET::Dock); + setFlags(Qt::WindowFlags((flags() | Qt::FramelessWindowHint) & (~Qt::WindowDoesNotAcceptFocus)) + | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint); + KWindowSystem::setState(winId(), NET::KeepAbove); + KWindowSystem::forceActiveWindow(winId()); + updateBlurBehindAndContrast(); + syncGeometry(); + syncLocation(); + + // this because due to Qt xcb implementation the actual flags gets set only after a while after the window is actually visible + m_screenSyncTimer.start(); + + if (m_containment) { + m_containment->setUserConfiguring(true); + } + + PanelShadows::self()->addWindow(this, m_enabledBorders); +} + +void PanelConfigView::hideEvent(QHideEvent *ev) +{ + QQuickWindow::hideEvent(ev); + + if (m_containment) { + m_containment->setUserConfiguring(false); + } + deleteLater(); +} + +void PanelConfigView::focusOutEvent(QFocusEvent *ev) +{ + const QWindow *focusWindow = QGuiApplication::focusWindow(); + + if (focusWindow && ((focusWindow->flags().testFlag(Qt::Popup)) || focusWindow->objectName() == QLatin1String("QMenuClassWindow"))) { + return; + } + Q_UNUSED(ev) + close(); +} + +void PanelConfigView::moveEvent(QMoveEvent *ev) +{ + if (m_shellSurface) { + m_shellSurface->setPosition(ev->pos()); + } +} + +bool PanelConfigView::event(QEvent *e) +{ + if (e->type() == QEvent::PlatformSurface) { + switch (static_cast(e)->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceCreated: + KWindowSystem::setState(winId(), NET::SkipTaskbar | NET::SkipPager); + + if (m_shellSurface) { + break; + } + if (ShellCorona *c = qobject_cast(m_containment->corona())) { + using namespace KWayland::Client; + PlasmaShell *interface = c->waylandPlasmaShellInterface(); + if (!interface) { + break; + } + Surface *s = Surface::fromWindow(this); + if (!s) { + break; + } + m_shellSurface = interface->createSurface(s, this); + m_shellSurface->setPanelTakesFocus(true); + } + break; + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + delete m_shellSurface; + m_shellSurface = nullptr; + PanelShadows::self()->removeWindow(this); + break; + } + } + + return PlasmaQuick::ConfigView::event(e); +} + +void PanelConfigView::setVisibilityMode(PanelView::VisibilityMode mode) +{ + m_panelView->setVisibilityMode(mode); + Q_EMIT visibilityModeChanged(); +} + +PanelView::VisibilityMode PanelConfigView::visibilityMode() const +{ + return m_panelView->visibilityMode(); +} + +void PanelConfigView::setOpacityMode(PanelView::OpacityMode mode) +{ + m_panelView->setOpacityMode(mode); + Q_EMIT opacityModeChanged(); +} + +PanelView::OpacityMode PanelConfigView::opacityMode() const +{ + return m_panelView->opacityMode(); +} + +Plasma::FrameSvg::EnabledBorders PanelConfigView::enabledBorders() const +{ + return m_enabledBorders; +} + +#include "moc_panelconfigview.cpp" diff --git a/plasma/workspace/shell/panelconfigview.h b/plasma/workspace/shell/panelconfigview.h new file mode 100644 index 0000000000..3e8a695a06 --- /dev/null +++ b/plasma/workspace/shell/panelconfigview.h @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "panelview.h" + +#include +#include +#include +#include +#include +#include + +class PanelView; + +namespace Plasma +{ +class Containment; +} + +namespace KWayland +{ +namespace Client +{ +class PlasmaShellSurface; +} +} + +class PanelConfigView : public PlasmaQuick::ConfigView +{ + Q_OBJECT + Q_PROPERTY(PanelView::VisibilityMode visibilityMode READ visibilityMode WRITE setVisibilityMode NOTIFY visibilityModeChanged) + Q_PROPERTY(PanelView::OpacityMode opacityMode READ opacityMode WRITE setOpacityMode NOTIFY opacityModeChanged) + Q_PROPERTY(Plasma::FrameSvg::EnabledBorders enabledBorders READ enabledBorders NOTIFY enabledBordersChanged) + +public: + PanelConfigView(Plasma::Containment *interface, PanelView *panelView, QWindow *parent = nullptr); + ~PanelConfigView() override; + + void init() override; + + PanelView::VisibilityMode visibilityMode() const; + void setVisibilityMode(PanelView::VisibilityMode mode); + + PanelView::OpacityMode opacityMode() const; + void setOpacityMode(PanelView::OpacityMode mode); + + Plasma::FrameSvg::EnabledBorders enabledBorders() const; + +protected: + void showEvent(QShowEvent *ev) override; + void hideEvent(QHideEvent *ev) override; + void focusOutEvent(QFocusEvent *ev) override; + void moveEvent(QMoveEvent *ev) override; + bool event(QEvent *e) override; + +public Q_SLOTS: + void showAddWidgetDialog(); + void addPanelSpacer(); + +protected Q_SLOTS: + void syncGeometry(); + void syncLocation(); + +private Q_SLOTS: + void updateBlurBehindAndContrast(); + +Q_SIGNALS: + void visibilityModeChanged(); + void opacityModeChanged(); + void enabledBordersChanged(); + +private: + Plasma::Containment *m_containment; + QPointer m_panelView; + Plasma::FrameSvg::EnabledBorders m_enabledBorders = Plasma::FrameSvg::AllBorders; + Plasma::Theme m_theme; + QTimer m_screenSyncTimer; + QPointer m_shellSurface; +}; diff --git a/plasma/workspace/shell/panelshadows.cpp b/plasma/workspace/shell/panelshadows.cpp new file mode 100644 index 0000000000..5cec44786a --- /dev/null +++ b/plasma/workspace/shell/panelshadows.cpp @@ -0,0 +1,288 @@ +/* + SPDX-FileCopyrightText: 2011 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "debug.h" +#include "panelshadows_p.h" + +#include + +class PanelShadows::Private +{ +public: + Private(PanelShadows *shadows) + : q(shadows) + { + } + + ~Private() + { + } + + void clearTiles(); + void setupTiles(); + void initTile(const QString &element); + void updateShadow(QWindow *window, Plasma::FrameSvg::EnabledBorders); + void clearShadow(QWindow *window); + void updateShadows(); + bool hasShadows() const; + + PanelShadows *q; + + QHash m_windows; + QHash m_shadows; + QVector m_tiles; +}; + +class PanelShadowsSingleton +{ +public: + PanelShadowsSingleton() + { + } + + PanelShadows self; +}; + +Q_GLOBAL_STATIC(PanelShadowsSingleton, privatePanelShadowsSelf) + +PanelShadows::PanelShadows(QObject *parent, const QString &prefix) + : Plasma::Svg(parent) + , d(new Private(this)) +{ + setImagePath(prefix); + connect(this, &Plasma::Svg::repaintNeeded, this, [this]() { + d->updateShadows(); + }); +} + +PanelShadows::~PanelShadows() +{ + delete d; +} + +PanelShadows *PanelShadows::self() +{ + return &privatePanelShadowsSelf->self; +} + +void PanelShadows::addWindow(QWindow *window, Plasma::FrameSvg::EnabledBorders enabledBorders) +{ + if (!window) { + return; + } + + d->m_windows[window] = enabledBorders; + d->updateShadow(window, enabledBorders); + connect(window, &QObject::destroyed, this, [this, window]() { + d->m_windows.remove(window); + d->clearShadow(window); + if (d->m_windows.isEmpty()) { + d->clearTiles(); + } + }); +} + +void PanelShadows::removeWindow(QWindow *window) +{ + if (!d->m_windows.contains(window)) { + return; + } + + d->m_windows.remove(window); + disconnect(window, nullptr, this, nullptr); + d->clearShadow(window); + + if (d->m_windows.isEmpty()) { + d->clearTiles(); + } +} + +void PanelShadows::setEnabledBorders(QWindow *window, Plasma::FrameSvg::EnabledBorders enabledBorders) +{ + if (!window || !d->m_windows.contains(window)) { + return; + } + + d->m_windows[window] = enabledBorders; + d->updateShadow(window, enabledBorders); +} + +void PanelShadows::Private::updateShadows() +{ + const bool hadShadowsBefore = !m_tiles.isEmpty(); + + // has shadows now? + if (hasShadows()) { + if (hadShadowsBefore) { + clearTiles(); + } + for (auto i = m_windows.constBegin(); i != m_windows.constEnd(); ++i) { + updateShadow(i.key(), i.value()); + } + } else { + if (hadShadowsBefore) { + for (auto i = m_windows.constBegin(); i != m_windows.constEnd(); ++i) { + clearShadow(i.key()); + } + clearTiles(); + } + } +} + +void PanelShadows::Private::initTile(const QString &element) +{ + const QImage image = q->pixmap(element).toImage(); + + KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create(); + tile->setImage(image); + + m_tiles << tile; +} + +void PanelShadows::Private::setupTiles() +{ + clearTiles(); + + initTile(QStringLiteral("shadow-top")); + initTile(QStringLiteral("shadow-topright")); + initTile(QStringLiteral("shadow-right")); + initTile(QStringLiteral("shadow-bottomright")); + initTile(QStringLiteral("shadow-bottom")); + initTile(QStringLiteral("shadow-bottomleft")); + initTile(QStringLiteral("shadow-left")); + initTile(QStringLiteral("shadow-topleft")); +} + +void PanelShadows::Private::clearTiles() +{ + m_tiles.clear(); +} + +void PanelShadows::Private::updateShadow(QWindow *window, Plasma::FrameSvg::EnabledBorders enabledBorders) +{ + if (!hasShadows()) { + return; + } + + if (m_tiles.isEmpty()) { + setupTiles(); + } + + KWindowShadow *&shadow = m_shadows[window]; + + if (!shadow) { + shadow = new KWindowShadow(q); + } + + if (shadow->isCreated()) { + shadow->destroy(); + } + + if (enabledBorders & Plasma::FrameSvg::TopBorder) { + shadow->setTopTile(m_tiles.at(0)); + } else { + shadow->setTopTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::TopBorder && enabledBorders & Plasma::FrameSvg::RightBorder) { + shadow->setTopRightTile(m_tiles.at(1)); + } else { + shadow->setTopRightTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::RightBorder) { + shadow->setRightTile(m_tiles.at(2)); + } else { + shadow->setRightTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::BottomBorder && enabledBorders & Plasma::FrameSvg::RightBorder) { + shadow->setBottomRightTile(m_tiles.at(3)); + } else { + shadow->setBottomRightTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::BottomBorder) { + shadow->setBottomTile(m_tiles.at(4)); + } else { + shadow->setBottomTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::BottomBorder && enabledBorders & Plasma::FrameSvg::LeftBorder) { + shadow->setBottomLeftTile(m_tiles.at(5)); + } else { + shadow->setBottomLeftTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::LeftBorder) { + shadow->setLeftTile(m_tiles.at(6)); + } else { + shadow->setLeftTile(nullptr); + } + + if (enabledBorders & Plasma::FrameSvg::TopBorder && enabledBorders & Plasma::FrameSvg::LeftBorder) { + shadow->setTopLeftTile(m_tiles.at(7)); + } else { + shadow->setTopLeftTile(nullptr); + } + + QMargins padding; + + if (enabledBorders & Plasma::FrameSvg::TopBorder) { + const QSize marginHint = q->elementSize(QStringLiteral("shadow-hint-top-margin")); + if (marginHint.isValid()) { + padding.setTop(marginHint.height()); + } else { + padding.setTop(m_tiles[0]->image().height()); + } + } + + if (enabledBorders & Plasma::FrameSvg::RightBorder) { + const QSize marginHint = q->elementSize(QStringLiteral("shadow-hint-right-margin")); + if (marginHint.isValid()) { + padding.setRight(marginHint.width()); + } else { + padding.setRight(m_tiles[2]->image().width()); + } + } + + if (enabledBorders & Plasma::FrameSvg::BottomBorder) { + const QSize marginHint = q->elementSize(QStringLiteral("shadow-hint-bottom-margin")); + if (marginHint.isValid()) { + padding.setBottom(marginHint.height()); + } else { + padding.setBottom(m_tiles[4]->image().height()); + } + } + + if (enabledBorders & Plasma::FrameSvg::LeftBorder) { + const QSize marginHint = q->elementSize(QStringLiteral("shadow-hint-left-margin")); + if (marginHint.isValid()) { + padding.setLeft(marginHint.width()); + } else { + padding.setLeft(m_tiles[6]->image().width()); + } + } + + shadow->setPadding(padding); + shadow->setWindow(window); + + if (!shadow->create()) { + qCWarning(PLASMASHELL) << "Couldn't create KWindowShadow for" << window; + } +} + +void PanelShadows::Private::clearShadow(QWindow *window) +{ + delete m_shadows.take(window); +} + +bool PanelShadows::Private::hasShadows() const +{ + return q->hasElement(QStringLiteral("shadow-left")); +} + +#include "moc_panelshadows_p.cpp" diff --git a/plasma/workspace/shell/panelshadows_p.h b/plasma/workspace/shell/panelshadows_p.h new file mode 100644 index 0000000000..6ecb56c719 --- /dev/null +++ b/plasma/workspace/shell/panelshadows_p.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2011 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "plasma/framesvg.h" +#include "plasma/svg.h" + +class PanelShadows : public Plasma::Svg +{ + Q_OBJECT + +public: + explicit PanelShadows(QObject *parent = nullptr, const QString &prefix = QStringLiteral("widgets/panel-background")); + ~PanelShadows() override; + + static PanelShadows *self(); + + void addWindow(QWindow *window, Plasma::FrameSvg::EnabledBorders enabledBorders = Plasma::FrameSvg::AllBorders); + void removeWindow(QWindow *window); + + void setEnabledBorders(QWindow *window, Plasma::FrameSvg::EnabledBorders enabledBorders = Plasma::FrameSvg::AllBorders); + +private: + class Private; + Private *const d; +}; diff --git a/plasma/workspace/shell/panelview.cpp b/plasma/workspace/shell/panelview.cpp new file mode 100644 index 0000000000..f74263de0b --- /dev/null +++ b/plasma/workspace/shell/panelview.cpp @@ -0,0 +1,1447 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "debug.h" +#include "panelconfigview.h" +#include "panelshadows_p.h" +#include "panelview.h" +#include "screenpool.h" +#include "shellcorona.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#if HAVE_X11 +#include +#include +#include +#include +#endif + +static const int MINSIZE = 10; + +PanelView::PanelView(ShellCorona *corona, QScreen *targetScreen, QWindow *parent) + : PlasmaQuick::ContainmentView(corona, parent) + , m_offset(0) + , m_maxLength(0) + , m_minLength(0) + , m_contentLength(0) + , m_distance(0) + , m_thickness(30) + , m_initCompleted(false) + , m_alignment(Qt::AlignLeft) + , m_corona(corona) + , m_visibilityMode(NormalPanel) + , m_opacityMode(Adaptive) + , m_backgroundHints(Plasma::Types::StandardBackground) + , m_shellSurface(nullptr) +{ + if (targetScreen) { + setPosition(targetScreen->geometry().center()); + setScreenToFollow(targetScreen); + setScreen(targetScreen); + } + setResizeMode(QuickViewSharedEngine::SizeRootObjectToView); + setClearBeforeRendering(true); + setColor(QColor(Qt::transparent)); + setFlags(Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus); + updateAdaptiveOpacityEnabled(); + + connect(&m_theme, &Plasma::Theme::themeChanged, this, &PanelView::updateMask); + connect(&m_theme, &Plasma::Theme::themeChanged, this, &PanelView::updateAdaptiveOpacityEnabled); + connect(this, &PanelView::backgroundHintsChanged, this, &PanelView::updateMask); + connect(this, &PanelView::backgroundHintsChanged, this, &PanelView::updateEnabledBorders); + // TODO: add finished/componentComplete signal to QuickViewSharedEngine, + // so we exactly know when rootobject is available + connect(this, &QuickViewSharedEngine::statusChanged, this, &PanelView::handleQmlStatusChange); + + m_positionPaneltimer.setSingleShot(true); + m_positionPaneltimer.setInterval(150); + connect(&m_positionPaneltimer, &QTimer::timeout, this, &PanelView::restore); + + m_unhideTimer.setSingleShot(true); + m_unhideTimer.setInterval(500); + connect(&m_unhideTimer, &QTimer::timeout, this, &PanelView::restoreAutoHide); + + m_lastScreen = targetScreen; + connect(this, &PanelView::locationChanged, &m_positionPaneltimer, qOverload<>(&QTimer::start)); + connect(this, &PanelView::containmentChanged, this, &PanelView::refreshContainment); + + if (!m_corona->kPackage().isValid()) { + qCWarning(PLASMASHELL) << "Invalid home screen package"; + } + + m_strutsTimer.setSingleShot(true); + connect(&m_strutsTimer, &QTimer::timeout, this, &PanelView::updateStruts); + + // Register enums + qmlRegisterUncreatableMetaObject(PanelView::staticMetaObject, "org.kde.plasma.shell.panel", 0, 1, "Global", QStringLiteral("Error: only enums")); + + qmlRegisterAnonymousType("", 1); + rootContext()->setContextProperty(QStringLiteral("panel"), this); + setSource(m_corona->kPackage().fileUrl("views", QStringLiteral("Panel.qml"))); + updatePadding(); +} + +PanelView::~PanelView() +{ + if (containment()) { + m_corona->requestApplicationConfigSync(); + } +} + +KConfigGroup PanelView::panelConfig(ShellCorona *corona, Plasma::Containment *containment, QScreen *screen) +{ + if (!containment || !screen) { + return KConfigGroup(); + } + KConfigGroup views(corona->applicationConfig(), "PlasmaViews"); + views = KConfigGroup(&views, QStringLiteral("Panel %1").arg(containment->id())); + + if (containment->formFactor() == Plasma::Types::Vertical) { + return KConfigGroup(&views, QStringLiteral("Vertical") + QString::number(screen->size().height())); + // treat everything else as horizontal + } else { + return KConfigGroup(&views, QStringLiteral("Horizontal") + QString::number(screen->size().width())); + } +} + +KConfigGroup PanelView::panelConfigDefaults(ShellCorona *corona, Plasma::Containment *containment, QScreen *screen) +{ + if (!containment || !screen) { + return KConfigGroup(); + } + + KConfigGroup views(corona->applicationConfig(), "PlasmaViews"); + views = KConfigGroup(&views, QStringLiteral("Panel %1").arg(containment->id())); + + return KConfigGroup(&views, QStringLiteral("Defaults")); +} + +int PanelView::readConfigValueWithFallBack(const QString &key, int defaultValue) +{ + int value = config().readEntry(key, configDefaults().readEntry(key, defaultValue)); + return value; +} + +KConfigGroup PanelView::config() const +{ + return panelConfig(m_corona, containment(), m_screenToFollow); +} + +KConfigGroup PanelView::configDefaults() const +{ + return panelConfigDefaults(m_corona, containment(), m_screenToFollow); +} + +Q_INVOKABLE QString PanelView::fileFromPackage(const QString &key, const QString &fileName) +{ + return corona()->kPackage().filePath(key.toUtf8(), fileName); +} + +void PanelView::maximize() +{ + int length; + if (containment()->formFactor() == Plasma::Types::Vertical) { + length = m_screenToFollow->size().height(); + } else { + length = m_screenToFollow->size().width(); + } + setOffset(0); + setMinimumLength(length); + setMaximumLength(length); +} + +Qt::Alignment PanelView::alignment() const +{ + return m_alignment; +} + +void PanelView::setAlignment(Qt::Alignment alignment) +{ + if (m_alignment == alignment) { + return; + } + + m_alignment = alignment; + // alignment is not resolution dependent, doesn't save to Defaults + config().parent().writeEntry("alignment", (int)m_alignment); + Q_EMIT alignmentChanged(); + positionPanel(); +} + +int PanelView::offset() const +{ + return m_offset; +} + +void PanelView::setOffset(int offset) +{ + if (m_offset == offset) { + return; + } + + if (formFactor() == Plasma::Types::Vertical) { + if (offset + m_maxLength > m_screenToFollow->size().height()) { + setMaximumLength(-m_offset + m_screenToFollow->size().height()); + } + } else { + if (offset + m_maxLength > m_screenToFollow->size().width()) { + setMaximumLength(-m_offset + m_screenToFollow->size().width()); + } + } + + m_offset = offset; + config().writeEntry("offset", m_offset); + configDefaults().writeEntry("offset", m_offset); + positionPanel(); + Q_EMIT offsetChanged(); + m_corona->requestApplicationConfigSync(); + Q_EMIT m_corona->availableScreenRegionChanged(); +} + +int PanelView::thickness() const +{ + return m_thickness; +} + +void PanelView::setThickness(int value) +{ + if (value == thickness()) { + return; + } + + m_thickness = value; + Q_EMIT thicknessChanged(); + + config().writeEntry("thickness", value); + configDefaults().writeEntry("thickness", value); + m_corona->requestApplicationConfigSync(); + resizePanel(); +} + +int PanelView::length() const +{ + return qMax(1, m_contentLength); +} + +void PanelView::setLength(int value) +{ + if (value == m_contentLength) { + return; + } + + m_contentLength = value; + + resizePanel(); +} + +int PanelView::maximumLength() const +{ + return m_maxLength; +} + +void PanelView::setMaximumLength(int length) +{ + if (length == m_maxLength) { + return; + } + + if (m_minLength > length) { + setMinimumLength(length); + } + + config().writeEntry("maxLength", length); + configDefaults().writeEntry("maxLength", length); + m_maxLength = length; + Q_EMIT maximumLengthChanged(); + m_corona->requestApplicationConfigSync(); + + resizePanel(); +} + +int PanelView::minimumLength() const +{ + return m_minLength; +} + +void PanelView::setMinimumLength(int length) +{ + if (length == m_minLength) { + return; + } + + if (m_maxLength < length) { + setMaximumLength(length); + } + + config().writeEntry("minLength", length); + configDefaults().writeEntry("minLength", length); + m_minLength = length; + Q_EMIT minimumLengthChanged(); + m_corona->requestApplicationConfigSync(); + + resizePanel(); +} + +int PanelView::distance() const +{ + return m_distance; +} + +void PanelView::setDistance(int dist) +{ + if (m_distance == dist) { + return; + } + + m_distance = dist; + Q_EMIT distanceChanged(); + positionPanel(); +} + +Plasma::Types::BackgroundHints PanelView::backgroundHints() const +{ + return m_backgroundHints; +} + +void PanelView::setBackgroundHints(Plasma::Types::BackgroundHints hint) +{ + if (m_backgroundHints == hint) { + return; + } + + m_backgroundHints = hint; + + Q_EMIT backgroundHintsChanged(); +} + +Plasma::FrameSvg::EnabledBorders PanelView::enabledBorders() const +{ + return m_enabledBorders; +} + +void PanelView::setVisibilityMode(PanelView::VisibilityMode mode) +{ + if (m_visibilityMode == mode) { + return; + } + + m_visibilityMode = mode; + + disconnect(containment(), &Plasma::Applet::activated, this, &PanelView::showTemporarily); + if (edgeActivated()) { + connect(containment(), &Plasma::Applet::activated, this, &PanelView::showTemporarily); + } + + if (config().isValid() && config().parent().isValid()) { + // panelVisibility is not resolution dependent, don't write to Defaults + config().parent().writeEntry("panelVisibility", (int)mode); + m_corona->requestApplicationConfigSync(); + } + + visibilityModeToWayland(); + updateStruts(); + + Q_EMIT visibilityModeChanged(); + + restoreAutoHide(); +} + +void PanelView::visibilityModeToWayland() +{ + if (!m_shellSurface) { + return; + } + KWayland::Client::PlasmaShellSurface::PanelBehavior behavior; + switch (m_visibilityMode) { + case NormalPanel: + behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::AlwaysVisible; + break; + case AutoHide: + behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::AutoHide; + break; + case LetWindowsCover: + behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsCanCover; + break; + case WindowsGoBelow: + behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsGoBelow; + break; + default: + Q_UNREACHABLE(); + return; + } + m_shellSurface->setPanelBehavior(behavior); +} + +PanelView::VisibilityMode PanelView::visibilityMode() const +{ + return m_visibilityMode; +} + +PanelView::OpacityMode PanelView::opacityMode() const +{ + if (!m_theme.adaptiveTransparencyEnabled()) { + return PanelView::Translucent; + } + return m_opacityMode; +} + +bool PanelView::adaptiveOpacityEnabled() +{ + return m_theme.adaptiveTransparencyEnabled(); +} + +void PanelView::setOpacityMode(PanelView::OpacityMode mode) +{ + if (m_opacityMode != mode) { + m_opacityMode = mode; + if (config().isValid() && config().parent().isValid()) { + config().parent().writeEntry("panelOpacity", (int)mode); + m_corona->requestApplicationConfigSync(); + } + Q_EMIT opacityModeChanged(); + } +} + +void PanelView::updateAdaptiveOpacityEnabled() +{ + Q_EMIT opacityModeChanged(); + Q_EMIT adaptiveOpacityEnabledChanged(); +} + +void PanelView::positionPanel() +{ + if (!containment()) { + return; + } + + if (!m_initCompleted) { + return; + } + + KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge; + + switch (containment()->location()) { + case Plasma::Types::TopEdge: + containment()->setFormFactor(Plasma::Types::Horizontal); + slideLocation = KWindowEffects::TopEdge; + break; + + case Plasma::Types::LeftEdge: + containment()->setFormFactor(Plasma::Types::Vertical); + slideLocation = KWindowEffects::LeftEdge; + break; + + case Plasma::Types::RightEdge: + containment()->setFormFactor(Plasma::Types::Vertical); + slideLocation = KWindowEffects::RightEdge; + break; + + case Plasma::Types::BottomEdge: + default: + containment()->setFormFactor(Plasma::Types::Horizontal); + slideLocation = KWindowEffects::BottomEdge; + break; + } + const QPoint pos = geometryByDistance(m_distance).topLeft(); + setPosition(pos); + + if (m_shellSurface) { + m_shellSurface->setPosition(pos); + } + + KWindowEffects::slideWindow(this, slideLocation, -1); +} + +QRect PanelView::geometryByDistance(int distance) const +{ + QScreen *s = m_screenToFollow; + QPoint position; + const QRect screenGeometry = s->geometry(); + + switch (containment()->location()) { + case Plasma::Types::TopEdge: + switch (m_alignment) { + case Qt::AlignCenter: + position = QPoint(QPoint(screenGeometry.center().x(), screenGeometry.top()) + QPoint(m_offset - width() / 2, distance)); + break; + case Qt::AlignRight: + position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y()) - QPoint(m_offset + width(), distance)); + break; + case Qt::AlignLeft: + default: + position = QPoint(screenGeometry.topLeft() + QPoint(m_offset, distance)); + } + break; + + case Plasma::Types::LeftEdge: + switch (m_alignment) { + case Qt::AlignCenter: + position = QPoint(QPoint(screenGeometry.left(), screenGeometry.center().y()) + QPoint(distance, m_offset - height() / 2)); + break; + case Qt::AlignRight: + position = QPoint(QPoint(screenGeometry.left(), screenGeometry.y() + screenGeometry.height()) - QPoint(distance, m_offset + height())); + break; + case Qt::AlignLeft: + default: + position = QPoint(screenGeometry.topLeft() + QPoint(distance, m_offset)); + } + break; + + case Plasma::Types::RightEdge: + switch (m_alignment) { + case Qt::AlignCenter: + // Never use rect.right(); for historical reasons it returns left() + width() - 1; see https://doc.qt.io/qt-5/qrect.html#right + position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.center().y()) - QPoint(thickness() + distance, 0) + + QPoint(0, m_offset - height() / 2)); + break; + case Qt::AlignRight: + position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y() + screenGeometry.height()) + - QPoint(thickness() + distance, 0) - QPoint(0, m_offset + height())); + break; + case Qt::AlignLeft: + default: + position = + QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y()) - QPoint(thickness() + distance, 0) + QPoint(0, m_offset)); + } + break; + + case Plasma::Types::BottomEdge: + default: + switch (m_alignment) { + case Qt::AlignCenter: + position = QPoint(QPoint(screenGeometry.center().x(), screenGeometry.bottom() - thickness() - distance) + QPoint(m_offset - width() / 2, 1)); + break; + case Qt::AlignRight: + position = QPoint(screenGeometry.bottomRight() - QPoint(0, thickness() + distance) - QPoint(m_offset + width(), -1)); + break; + case Qt::AlignLeft: + default: + position = QPoint(screenGeometry.bottomLeft() - QPoint(0, thickness() + distance) + QPoint(m_offset, 1)); + } + } + QRect ret = formFactor() == Plasma::Types::Vertical ? QRect(position, QSize(thickness(), height())) : QRect(position, QSize(width(), thickness())); + ret = ret.intersected(screenGeometry); + return ret; +} + +void PanelView::resizePanel() +{ + if (!m_initCompleted) { + return; + } + + // On Wayland when a screen is disconnected and the panel is migrating to a newscreen + // it can happen a moment where the qscreen gets destroyed before it gets reassigned + // to the new screen + if (!m_screenToFollow) { + return; + } + + QSize targetSize; + QSize targetMinSize; + QSize targetMaxSize; + + if (formFactor() == Plasma::Types::Vertical) { + const int minSize = qMax(MINSIZE, m_minLength); + const int maxSize = qMin(m_maxLength, m_screenToFollow->size().height() - m_offset); + targetMinSize = QSize(thickness(), minSize); + targetMaxSize = QSize(thickness(), maxSize); + targetSize = QSize(thickness(), qBound(minSize, m_contentLength, maxSize)); + } else { + const int minSize = qMax(MINSIZE, m_minLength); + const int maxSize = qMin(m_maxLength, m_screenToFollow->size().width() - m_offset); + targetMinSize = QSize(minSize, thickness()); + targetMaxSize = QSize(maxSize, thickness()); + targetSize = QSize(qBound(minSize, m_contentLength, maxSize), thickness()); + } + if (minimumSize() != targetMinSize) { + setMinimumSize(targetMinSize); + } + if (maximumSize() != targetMaxSize) { + setMaximumSize(targetMaxSize); + } + if (size() != targetSize) { + resize(targetSize); + } + + // position will be updated implicitly from resizeEvent +} + +void PanelView::restore() +{ + KConfigGroup panelConfig = config(); + if (!panelConfig.isValid()) { + return; + } + + // All the defaults are based on whatever are the current values + // so won't be weirdly reset after screen resolution change + + // alignment is not resolution dependent + // but if fails read it from the resolution dependent one as + // the place for this config key is changed in Plasma 5.9 + // Do NOT use readConfigValueWithFallBack + setAlignment((Qt::Alignment)panelConfig.parent().readEntry("alignment", panelConfig.readEntry("alignment", m_alignment))); + + // All the other values are read from screen independent values, + // but fallback on the screen independent section, as is the only place + // is safe to directly write during plasma startup, as there can be + // resolution changes + m_offset = readConfigValueWithFallBack("offset", m_offset); + if (m_alignment != Qt::AlignCenter) { + m_offset = qMax(0, m_offset); + } + + setThickness(readConfigValueWithFallBack("thickness", m_thickness)); + + const QSize screenSize = m_screenToFollow->size(); + setMinimumSize(QSize(-1, -1)); + // FIXME: an invalid size doesn't work with QWindows + setMaximumSize(screenSize); + + const int side = containment()->formFactor() == Plasma::Types::Vertical ? screenSize.height() : screenSize.width(); + const int maxSize = side - m_offset; + m_maxLength = qBound(MINSIZE, readConfigValueWithFallBack("maxLength", side), maxSize); + m_minLength = qBound(MINSIZE, readConfigValueWithFallBack("minLength", side), maxSize); + + // panelVisibility is not resolution dependent + // but if fails read it from the resolution dependent one as + // the place for this config key is changed in Plasma 5.9 + // Do NOT use readConfigValueWithFallBack + setVisibilityMode((VisibilityMode)panelConfig.parent().readEntry("panelVisibility", panelConfig.readEntry("panelVisibility", (int)NormalPanel))); + setOpacityMode((OpacityMode)config().parent().readEntry("panelOpacity", + configDefaults().parent().readEntry("panelOpacity", PanelView::OpacityMode::Adaptive))); + m_initCompleted = true; + resizePanel(); + positionPanel(); + + Q_EMIT maximumLengthChanged(); + Q_EMIT minimumLengthChanged(); + Q_EMIT offsetChanged(); + Q_EMIT alignmentChanged(); + + //::restore might have been called directly before the timer fires + // at which point we don't still need the timer + m_positionPaneltimer.stop(); +} + +void PanelView::showConfigurationInterface(Plasma::Applet *applet) +{ + if (!applet || !applet->containment()) { + return; + } + + Plasma::Containment *cont = qobject_cast(applet); + + const bool isPanelConfig = (cont && cont == containment() && cont->isContainment()); + + if (m_panelConfigView) { + if (m_panelConfigView->applet() == applet) { + if (isPanelConfig) { // toggles panel controller, whereas applet config is always brought to front + if (m_panelConfigView->isVisible()) { + m_panelConfigView->hide(); + } else { + m_panelConfigView->show(); + } + return; + } + + m_panelConfigView->show(); + m_panelConfigView->requestActivate(); + return; + } + + m_panelConfigView->hide(); + m_panelConfigView->deleteLater(); + } + + if (isPanelConfig) { + m_panelConfigView = new PanelConfigView(cont, this); + } else { + m_panelConfigView = new PlasmaQuick::ConfigView(applet); + } + + m_panelConfigView->init(); + m_panelConfigView->show(); + m_panelConfigView->requestActivate(); + + if (isPanelConfig) { + KWindowSystem::setState(m_panelConfigView->winId(), NET::SkipTaskbar | NET::SkipPager); + } +} + +void PanelView::restoreAutoHide() +{ + bool autoHide = true; + disconnect(m_transientWindowVisibleWatcher); + + if (!edgeActivated()) { + autoHide = false; + } else if (m_containsMouse) { + autoHide = false; + } else if (containment() && containment()->isUserConfiguring()) { + autoHide = false; + } else if (containment() && containment()->status() >= Plasma::Types::NeedsAttentionStatus && containment()->status() != Plasma::Types::HiddenStatus) { + autoHide = false; + } else { + for (QWindow *window : qApp->topLevelWindows()) { + if (window->transientParent() == this && window->isVisible()) { + m_transientWindowVisibleWatcher = connect(window, &QWindow::visibleChanged, this, &PanelView::restoreAutoHide); + autoHide = false; + break; // if multiple are open, we will re-evaluate this expression after the first closes + } + } + } + + setAutoHideEnabled(autoHide); +} + +void PanelView::setAutoHideEnabled(bool enabled) +{ +#if HAVE_X11 + if (KWindowSystem::isPlatformX11()) { + xcb_connection_t *c = QX11Info::connection(); + + const QByteArray effectName = QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"); + xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(c, false, effectName.length(), effectName.constData()); + + QScopedPointer atom(xcb_intern_atom_reply(c, atomCookie, nullptr)); + + if (!atom) { + return; + } + + if (!enabled) { + xcb_delete_property(c, winId(), atom->atom); + return; + } + + KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge; + uint32_t value = 0; + + switch (location()) { + case Plasma::Types::TopEdge: + value = 0; + slideLocation = KWindowEffects::TopEdge; + break; + case Plasma::Types::RightEdge: + value = 1; + slideLocation = KWindowEffects::RightEdge; + break; + case Plasma::Types::BottomEdge: + value = 2; + slideLocation = KWindowEffects::BottomEdge; + break; + case Plasma::Types::LeftEdge: + value = 3; + slideLocation = KWindowEffects::LeftEdge; + break; + case Plasma::Types::Floating: + default: + value = 4; + break; + } + + int hideType = 0; + if (m_visibilityMode == LetWindowsCover) { + hideType = 1; + } + value |= hideType << 8; + + xcb_change_property(c, XCB_PROP_MODE_REPLACE, winId(), atom->atom, XCB_ATOM_CARDINAL, 32, 1, &value); + KWindowEffects::slideWindow(this, slideLocation, -1); + } +#endif + if (m_shellSurface && (m_visibilityMode == PanelView::AutoHide || m_visibilityMode == PanelView::LetWindowsCover)) { + if (enabled) { + m_shellSurface->requestHideAutoHidingPanel(); + } else { + if (m_visibilityMode == PanelView::AutoHide) + m_shellSurface->requestShowAutoHidingPanel(); + } + } +} + +void PanelView::resizeEvent(QResizeEvent *ev) +{ + updateEnabledBorders(); + // don't setGeometry() to make really sure we aren't doing a resize loop + const QPoint pos = geometryByDistance(m_distance).topLeft(); + setPosition(pos); + if (m_shellSurface) { + m_shellSurface->setPosition(pos); + } + m_strutsTimer.start(STRUTSTIMERDELAY); + Q_EMIT m_corona->availableScreenRegionChanged(); + + PlasmaQuick::ContainmentView::resizeEvent(ev); +} + +void PanelView::moveEvent(QMoveEvent *ev) +{ + updateEnabledBorders(); + m_strutsTimer.start(STRUTSTIMERDELAY); + PlasmaQuick::ContainmentView::moveEvent(ev); + if (!m_screenToFollow->geometry().contains(geometry())) { + positionPanel(); + } +} + +void PanelView::keyPressEvent(QKeyEvent *event) +{ + PlasmaQuick::ContainmentView::keyPressEvent(event); + if (event->isAccepted()) { + return; + } + + // Catch arrows keyPress to have same behavior as tab/backtab + if ((event->key() == Qt::Key_Right && qApp->layoutDirection() == Qt::LeftToRight) + || (event->key() == Qt::Key_Left && qApp->layoutDirection() != Qt::LeftToRight) || event->key() == Qt::Key_Down) { + event->accept(); + QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier); + qApp->sendEvent((QObject *)this, (QEvent *)&tabEvent); + return; + } else if ((event->key() == Qt::Key_Right && qApp->layoutDirection() != Qt::LeftToRight) + || (event->key() == Qt::Key_Left && qApp->layoutDirection() == Qt::LeftToRight) || event->key() == Qt::Key_Up) { + event->accept(); + QKeyEvent backtabEvent(QEvent::KeyPress, Qt::Key_Backtab, Qt::NoModifier); + qApp->sendEvent((QObject *)this, (QEvent *)&backtabEvent); + return; + } +} + +void PanelView::integrateScreen() +{ + updateMask(); + KWindowSystem::setOnAllDesktops(winId(), true); + KWindowSystem::setType(winId(), NET::Dock); +#if HAVE_X11 + QXcbWindowFunctions::setWmWindowType(this, QXcbWindowFunctions::Dock); +#endif + if (m_shellSurface) { + m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); + m_shellSurface->setSkipTaskbar(true); + } + setVisibilityMode(m_visibilityMode); + + if (containment()) { + containment()->reactToScreenChange(); + } +} + +void PanelView::showEvent(QShowEvent *event) +{ + PlasmaQuick::ContainmentView::showEvent(event); + + integrateScreen(); +} + +void PanelView::setScreenToFollow(QScreen *screen) +{ + if (screen == m_screenToFollow) { + return; + } + + if (!screen) { + return; + } + + if (!m_screenToFollow.isNull()) { + // disconnect from old screen + disconnect(m_screenToFollow, &QScreen::virtualGeometryChanged, this, &PanelView::updateStruts); + disconnect(m_screenToFollow, &QScreen::geometryChanged, this, &PanelView::restore); + } + + connect(screen, &QScreen::virtualGeometryChanged, this, &PanelView::updateStruts, Qt::UniqueConnection); + connect(screen, &QScreen::geometryChanged, this, &PanelView::restore, Qt::UniqueConnection); + + /*connect(screen, &QObject::destroyed, this, [this]() { + if (PanelView::screen()) { + m_screenToFollow = PanelView::screen(); + adaptToScreen(); + } + });*/ + + m_screenToFollow = screen; + + setScreen(screen); + adaptToScreen(); +} + +QScreen *PanelView::screenToFollow() const +{ + return m_screenToFollow; +} + +void PanelView::adaptToScreen() +{ + Q_EMIT screenToFollowChanged(m_screenToFollow); + m_lastScreen = m_screenToFollow; + + if (!m_screenToFollow) { + return; + } + + integrateScreen(); + showTemporarily(); + m_positionPaneltimer.start(); +} + +bool PanelView::event(QEvent *e) +{ + switch (e->type()) { + case QEvent::Enter: + m_containsMouse = true; + if (edgeActivated()) { + m_unhideTimer.stop(); + } + break; + + case QEvent::Leave: + m_containsMouse = false; + if (edgeActivated()) { + m_unhideTimer.start(); + } + break; + + /*Fitt's law: if the containment has margins, and the mouse cursor clicked + * on the mouse edge, forward the click in the containment boundaries + */ + + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(e); + + // first, don't mess with position if the cursor is actually outside the view: + // somebody is doing a click and drag that must not break when the cursor i outside + if (geometry().contains(QCursor::pos(screenToFollow()))) { + if (!containmentContainsPosition(me->windowPos()) && !m_fakeEventPending) { + QMouseEvent me2(me->type(), + positionAdjustedForContainment(me->windowPos()), + positionAdjustedForContainment(me->windowPos()), + positionAdjustedForContainment(me->windowPos()) + position(), + me->button(), + me->buttons(), + me->modifiers()); + + m_fakeEventPending = true; + QCoreApplication::sendEvent(this, &me2); + m_fakeEventPending = false; + return true; + } + } else { + // default handling if current mouse position is outside the panel + return ContainmentView::event(e); + } + break; + } + + case QEvent::Wheel: { + QWheelEvent *we = static_cast(e); + + if (!containmentContainsPosition(we->position()) && !m_fakeEventPending) { + QWheelEvent we2(positionAdjustedForContainment(we->position()), + positionAdjustedForContainment(we->position()) + position(), + we->pixelDelta(), + we->angleDelta(), + we->buttons(), + we->modifiers(), + we->phase(), + we->inverted()); + + m_fakeEventPending = true; + QCoreApplication::sendEvent(this, &we2); + m_fakeEventPending = false; + return true; + } + break; + } + + case QEvent::DragEnter: { + QDragEnterEvent *de = static_cast(e); + if (!containmentContainsPosition(de->pos()) && !m_fakeEventPending) { + QDragEnterEvent de2(positionAdjustedForContainment(de->pos()).toPoint(), + de->possibleActions(), + de->mimeData(), + de->mouseButtons(), + de->keyboardModifiers()); + + m_fakeEventPending = true; + QCoreApplication::sendEvent(this, &de2); + m_fakeEventPending = false; + return true; + } + break; + } + // DragLeave just works + case QEvent::DragLeave: + break; + case QEvent::DragMove: { + QDragMoveEvent *de = static_cast(e); + if (!containmentContainsPosition(de->pos()) && !m_fakeEventPending) { + QDragMoveEvent de2(positionAdjustedForContainment(de->pos()).toPoint(), + de->possibleActions(), + de->mimeData(), + de->mouseButtons(), + de->keyboardModifiers()); + + m_fakeEventPending = true; + QCoreApplication::sendEvent(this, &de2); + m_fakeEventPending = false; + return true; + } + break; + } + case QEvent::Drop: { + QDropEvent *de = static_cast(e); + if (!containmentContainsPosition(de->pos()) && !m_fakeEventPending) { + QDropEvent de2(positionAdjustedForContainment(de->pos()).toPoint(), + de->possibleActions(), + de->mimeData(), + de->mouseButtons(), + de->keyboardModifiers()); + + m_fakeEventPending = true; + QCoreApplication::sendEvent(this, &de2); + m_fakeEventPending = false; + return true; + } + break; + } + + case QEvent::Hide: { + if (m_panelConfigView && m_panelConfigView->isVisible()) { + m_panelConfigView->hide(); + } + m_containsMouse = false; + break; + } + case QEvent::PlatformSurface: + switch (static_cast(e)->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceCreated: + setupWaylandIntegration(); + PanelShadows::self()->addWindow(this, enabledBorders()); + break; + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + delete m_shellSurface; + m_shellSurface = nullptr; + PanelShadows::self()->removeWindow(this); + break; + } + break; + default: + break; + } + + return ContainmentView::event(e); +} + +bool PanelView::containmentContainsPosition(const QPointF &point) const +{ + QQuickItem *containmentItem = containment()->property("_plasma_graphicObject").value(); + + if (!containmentItem) { + return false; + } + + return QRectF(containmentItem->mapToScene(QPoint(m_leftPadding, m_topPadding)), + QSizeF(containmentItem->width() - m_leftPadding - m_rightPadding, containmentItem->height() - m_topPadding - m_bottomPadding)) + .contains(point); +} + +QPointF PanelView::positionAdjustedForContainment(const QPointF &point) const +{ + QQuickItem *containmentItem = containment()->property("_plasma_graphicObject").value(); + + if (!containmentItem) { + return point; + } + + QRectF containmentRect(containmentItem->mapToScene(QPoint(0, 0)), QSizeF(containmentItem->width(), containmentItem->height())); + + // We are removing 1 to the e.g. containmentRect.right() - m_rightPadding because the last pixel would otherwise + // the first one in the margin, and thus the mouse event would be discarded. Instead, the first pixel given by + // containmentRect.left() + m_leftPadding the first one *not* in the margin, so it work. + return QPointF(qBound(containmentRect.left() + m_leftPadding, point.x(), containmentRect.right() - m_rightPadding - 1), + qBound(containmentRect.top() + m_topPadding, point.y(), containmentRect.bottom() - m_bottomPadding - 1)); +} + +void PanelView::updateMask() +{ + if (m_backgroundHints == Plasma::Types::NoBackground) { + KWindowEffects::enableBlurBehind(this, false); + KWindowEffects::enableBackgroundContrast(this, false); + setMask(QRegion()); + } else { + QRegion mask; + + QQuickItem *rootObject = this->rootObject(); + if (rootObject) { + const QVariant maskProperty = rootObject->property("panelMask"); + if (static_cast(maskProperty.type()) == QMetaType::QRegion) { + mask = maskProperty.value(); + } + } + + KWindowEffects::enableBlurBehind(this, m_theme.blurBehindEnabled(), mask); + KWindowEffects::enableBackgroundContrast(this, + m_theme.backgroundContrastEnabled(), + m_theme.backgroundContrast(), + m_theme.backgroundIntensity(), + m_theme.backgroundSaturation(), + mask); + + if (KWindowSystem::compositingActive()) { + setMask(QRegion()); + } else { + setMask(mask); + } + } +} + +bool PanelView::canSetStrut() const +{ +#if HAVE_X11 + if (!KWindowSystem::isPlatformX11()) { + return true; + } + // read the wm name, need to do this every time which means a roundtrip unfortunately + // but WM might have changed + NETRootInfo rootInfo(QX11Info::connection(), NET::Supported | NET::SupportingWMCheck); + if (qstricmp(rootInfo.wmName(), "KWin") == 0) { + // KWin since 5.7 can handle this fine, so only exclude for other window managers + return true; + } + + const QRect thisScreen = screen()->geometry(); + const int numScreens = corona()->numScreens(); + if (numScreens < 2) { + return true; + } + + // Extended struts against a screen edge near to another screen are really harmful, so windows maximized under the panel is a lesser pain + // TODO: force "windows can cover" in those cases? + const auto screenIds = m_corona->screenIds(); + for (int id : screenIds) { + if (id == containment()->screen()) { + continue; + } + + const QRect otherScreen = corona()->screenGeometry(id); + if (!otherScreen.isValid()) { + continue; + } + + switch (location()) { + case Plasma::Types::TopEdge: + if (otherScreen.bottom() <= thisScreen.top()) { + return false; + } + break; + case Plasma::Types::BottomEdge: + if (otherScreen.top() >= thisScreen.bottom()) { + return false; + } + break; + case Plasma::Types::RightEdge: + if (otherScreen.left() >= thisScreen.right()) { + return false; + } + break; + case Plasma::Types::LeftEdge: + if (otherScreen.right() <= thisScreen.left()) { + return false; + } + break; + default: + return false; + } + } + return true; +#else + return true; +#endif +} + +void PanelView::updateStruts() +{ + if (!containment() || containment()->isUserConfiguring() || !m_screenToFollow) { + return; + } + + NETExtendedStrut strut; + + if (m_visibilityMode == NormalPanel) { + const QRect thisScreen = m_screenToFollow->geometry(); + // QScreen::virtualGeometry() is very unreliable (Qt 5.5) + const QRect wholeScreen = QRect(QPoint(0, 0), m_screenToFollow->virtualSize()); + + if (!canSetStrut()) { + KWindowSystem::setExtendedStrut(winId(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + return; + } + // extended struts are to the combined screen geoms, not the single screen + int leftOffset = thisScreen.x(); + int rightOffset = wholeScreen.right() - thisScreen.right(); + int bottomOffset = wholeScreen.bottom() - thisScreen.bottom(); + // qDebug() << "screen l/r/b/t offsets are:" << leftOffset << rightOffset << bottomOffset << topOffset << location(); + int topOffset = thisScreen.top(); + + switch (location()) { + case Plasma::Types::TopEdge: + strut.top_width = thickness() + topOffset; + strut.top_start = x(); + strut.top_end = x() + width() - 1; + // qDebug() << "setting top edge to" << strut.top_width << strut.top_start << strut.top_end; + break; + + case Plasma::Types::BottomEdge: + strut.bottom_width = thickness() + bottomOffset; + strut.bottom_start = x(); + strut.bottom_end = x() + width() - 1; + // qDebug() << "setting bottom edge to" << strut.bottom_width << strut.bottom_start << strut.bottom_end; + break; + + case Plasma::Types::RightEdge: + strut.right_width = thickness() + rightOffset; + strut.right_start = y(); + strut.right_end = y() + height() - 1; + // qDebug() << "setting right edge to" << strut.right_width << strut.right_start << strut.right_end; + break; + + case Plasma::Types::LeftEdge: + strut.left_width = thickness() + leftOffset; + strut.left_start = y(); + strut.left_end = y() + height() - 1; + // qDebug() << "setting left edge to" << strut.left_width << strut.left_start << strut.left_end; + break; + + default: + // qDebug() << "where are we?"; + break; + } + } + + KWindowSystem::setExtendedStrut(winId(), + strut.left_width, + strut.left_start, + strut.left_end, + strut.right_width, + strut.right_start, + strut.right_end, + strut.top_width, + strut.top_start, + strut.top_end, + strut.bottom_width, + strut.bottom_start, + strut.bottom_end); +} + +void PanelView::refreshContainment() +{ + restore(); + connect(containment(), &Plasma::Containment::userConfiguringChanged, this, [this](bool configuring) { + if (configuring) { + showTemporarily(); + } else { + m_unhideTimer.start(); + updateStruts(); + } + }); + + connect(containment(), &Plasma::Applet::statusChanged, this, &PanelView::refreshStatus); + connect(containment(), &Plasma::Applet::appletDeleted, this, [this] { + // containment()->destroyed() is true only when the user deleted it + // so the config is to be thrown away, not during shutdown + if (containment()->destroyed()) { + KConfigGroup views(m_corona->applicationConfig(), "PlasmaViews"); + for (auto grp : views.groupList()) { + if (grp.contains(QRegularExpression(QStringLiteral("Panel %1$").arg(QString::number(containment()->id()))))) { + qDebug() << "Panel" << containment()->id() << "removed by user"; + views.deleteGroup(grp); + } + views.sync(); + } + } + }); +} + +void PanelView::handleQmlStatusChange(QQmlComponent::Status status) +{ + if (status != QQmlComponent::Ready) { + return; + } + + QQuickItem *rootObject = this->rootObject(); + if (rootObject) { + disconnect(this, &QuickViewSharedEngine::statusChanged, this, &PanelView::handleQmlStatusChange); + + updatePadding(); + int paddingSignal = rootObject->metaObject()->indexOfSignal(SIGNAL(bottomPaddingChanged())); + if (paddingSignal >= 0) { + connect(rootObject, SIGNAL(bottomPaddingChanged()), this, SLOT(updatePadding())); + connect(rootObject, SIGNAL(topPaddingChanged()), this, SLOT(updatePadding())); + connect(rootObject, SIGNAL(rightPaddingChanged()), this, SLOT(updatePadding())); + connect(rootObject, SIGNAL(leftPaddingChanged()), this, SLOT(updatePadding())); + } + + const QVariant maskProperty = rootObject->property("panelMask"); + if (static_cast(maskProperty.type()) == QMetaType::QRegion) { + connect(rootObject, SIGNAL(panelMaskChanged()), this, SLOT(updateMask())); + updateMask(); + } + } +} + +void PanelView::refreshStatus(Plasma::Types::ItemStatus status) +{ + if (status == Plasma::Types::NeedsAttentionStatus) { + showTemporarily(); + setFlags(flags() | Qt::WindowDoesNotAcceptFocus); + if (m_shellSurface) { + m_shellSurface->setPanelTakesFocus(false); + } + } else if (status == Plasma::Types::AcceptingInputStatus) { + setFlags(flags() & ~Qt::WindowDoesNotAcceptFocus); + KWindowSystem::forceActiveWindow(winId()); + if (m_shellSurface) { + m_shellSurface->setPanelTakesFocus(true); + } + } else { + restoreAutoHide(); + setFlags(flags() | Qt::WindowDoesNotAcceptFocus); + if (m_shellSurface) { + m_shellSurface->setPanelTakesFocus(false); + } + } +} + +void PanelView::showTemporarily() +{ + setAutoHideEnabled(false); + + QTimer *t = new QTimer(this); + t->setSingleShot(true); + t->setInterval(3000); + connect(t, &QTimer::timeout, this, &PanelView::restoreAutoHide); + connect(t, &QTimer::timeout, t, &QObject::deleteLater); + t->start(); +} + +void PanelView::screenDestroyed(QObject *) +{ + // NOTE: this is overriding the screen destroyed slot, we need to do this because + // otherwise Qt goes mental and starts moving our panels. See: + // https://codereview.qt-project.org/#/c/88351/ + // if(screen == this->m_screenToFollow) { + // DO NOTHING, panels are moved by ::readaptToScreen + // } +} + +void PanelView::setupWaylandIntegration() +{ + if (m_shellSurface) { + // already setup + return; + } + if (ShellCorona *c = qobject_cast(corona())) { + using namespace KWayland::Client; + PlasmaShell *interface = c->waylandPlasmaShellInterface(); + if (!interface) { + return; + } + Surface *s = Surface::fromWindow(this); + if (!s) { + return; + } + m_shellSurface = interface->createSurface(s, this); + } +} + +bool PanelView::edgeActivated() const +{ + return m_visibilityMode == PanelView::AutoHide || m_visibilityMode == LetWindowsCover; +} + +void PanelView::updateEnabledBorders() +{ + Plasma::FrameSvg::EnabledBorders borders = Plasma::FrameSvg::AllBorders; + + if (m_backgroundHints == Plasma::Types::NoBackground) { + borders = Plasma::FrameSvg::NoBorder; + } else { + switch (location()) { + case Plasma::Types::TopEdge: + borders &= ~Plasma::FrameSvg::TopBorder; + break; + case Plasma::Types::LeftEdge: + borders &= ~Plasma::FrameSvg::LeftBorder; + break; + case Plasma::Types::RightEdge: + borders &= ~Plasma::FrameSvg::RightBorder; + break; + case Plasma::Types::BottomEdge: + borders &= ~Plasma::FrameSvg::BottomBorder; + break; + default: + break; + } + + if (m_screenToFollow) { + if (x() <= m_screenToFollow->geometry().x()) { + borders &= ~Plasma::FrameSvg::LeftBorder; + } + if (x() + width() >= m_screenToFollow->geometry().x() + m_screenToFollow->geometry().width()) { + borders &= ~Plasma::FrameSvg::RightBorder; + } + if (y() <= m_screenToFollow->geometry().y()) { + borders &= ~Plasma::FrameSvg::TopBorder; + } + if (y() + height() >= m_screenToFollow->geometry().y() + m_screenToFollow->geometry().height()) { + borders &= ~Plasma::FrameSvg::BottomBorder; + } + } + } + + if (m_enabledBorders != borders) { + PanelShadows::self()->setEnabledBorders(this, borders); + m_enabledBorders = borders; + Q_EMIT enabledBordersChanged(); + } +} + +void PanelView::updatePadding() +{ + if (!rootObject()) + return; + m_leftPadding = rootObject()->property("leftPadding").toInt(); + m_rightPadding = rootObject()->property("rightPadding").toInt(); + m_topPadding = rootObject()->property("topPadding").toInt(); + m_bottomPadding = rootObject()->property("bottomPadding").toInt(); +} + +#include "moc_panelview.cpp" diff --git a/plasma/workspace/shell/panelview.h b/plasma/workspace/shell/panelview.h new file mode 100644 index 0000000000..f552a200d1 --- /dev/null +++ b/plasma/workspace/shell/panelview.h @@ -0,0 +1,266 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +class ShellCorona; + +namespace KWayland +{ +namespace Client +{ +class PlasmaShellSurface; +} +} + +class PanelView : public PlasmaQuick::ContainmentView + +{ + Q_OBJECT + /** + * Alignment of the panel: when not fullsize it can be aligned at left, + * right or center of the screen (left and right work as top/bottom + * too for vertical panels) + */ + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged) + + /** + * how much the panel is moved from the left/right/center anchor point + */ + Q_PROPERTY(int offset READ offset WRITE setOffset NOTIFY offsetChanged) + + /** + * height of horizontal panels, width of vertical panels + */ + Q_PROPERTY(int thickness READ thickness WRITE setThickness NOTIFY thicknessChanged) + + /** + * width of horizontal panels, height of vertical panels + */ + Q_PROPERTY(int length READ length WRITE setLength NOTIFY lengthChanged) + + /** + * if the panel resizes itself, never resize more than that + */ + Q_PROPERTY(int maximumLength READ maximumLength WRITE setMaximumLength NOTIFY maximumLengthChanged) + + /** + * if the panel resizes itself, never resize less than that + */ + Q_PROPERTY(int minimumLength READ minimumLength WRITE setMinimumLength NOTIFY minimumLengthChanged) + + /** + * how much the panel is distant for the screen edge: used by the panel controller to drag it around + */ + Q_PROPERTY(int distance READ distance WRITE setDistance NOTIFY distanceChanged) + + /** + * support NoBackground in order to disable blur/contrast effects and remove + * the panel shadows + * @since 5.9 + */ + Q_PROPERTY(Plasma::Types::BackgroundHints backgroundHints WRITE setBackgroundHints READ backgroundHints NOTIFY backgroundHintsChanged) + + /** + * The borders that should have a shadow + * @since 5.7 + */ + Q_PROPERTY(Plasma::FrameSvg::EnabledBorders enabledBorders READ enabledBorders NOTIFY enabledBordersChanged) + + /** + * information about the screen in which the panel is in + */ + Q_PROPERTY(QScreen *screenToFollow READ screenToFollow WRITE setScreenToFollow NOTIFY screenToFollowChanged) + + /** + * how the panel behaves, visible, autohide etc. + */ + Q_PROPERTY(VisibilityMode visibilityMode READ visibilityMode WRITE setVisibilityMode NOTIFY visibilityModeChanged) + + /** + * Property that determines how a panel's opacity behaves. + * + * @see OpacityMode + */ + Q_PROPERTY(OpacityMode opacityMode READ opacityMode WRITE setOpacityMode NOTIFY opacityModeChanged) + + /** + * Property that determines whether adaptive opacity is used. + */ + Q_PROPERTY(bool adaptiveOpacityEnabled READ adaptiveOpacityEnabled NOTIFY adaptiveOpacityEnabledChanged) + +public: + enum VisibilityMode { + NormalPanel = 0, /** default, always visible panel, the windowmanager reserves a places for it */ + AutoHide, /**the panel will be shownn only if the mouse cursor is on screen edges */ + LetWindowsCover, /** always visible, windows will go over the panel, no area reserved */ + WindowsGoBelow, /** always visible, windows will go under the panel, no area reserved */ + }; + Q_ENUM(VisibilityMode) + + /** Enumeration of possible opacity modes. */ + enum OpacityMode { + Adaptive = 0, /** The panel will change opacity depending on the presence of a maximized window */ + Opaque, /** The panel will always be opaque */ + Translucent /** The panel will always be translucent */ + }; + Q_ENUM(OpacityMode) + + explicit PanelView(ShellCorona *corona, QScreen *targetScreen = nullptr, QWindow *parent = nullptr); + ~PanelView() override; + + KConfigGroup config() const override; + KConfigGroup configDefaults() const; + + Q_INVOKABLE QString fileFromPackage(const QString &key, const QString &fileName); + Q_INVOKABLE void maximize(); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment alignment); + + int offset() const; + void setOffset(int offset); + + int thickness() const; + void setThickness(int thickness); + + int length() const; + void setLength(int value); + + int maximumLength() const; + void setMaximumLength(int length); + + int minimumLength() const; + void setMinimumLength(int length); + + int distance() const; + void setDistance(int dist); + + Plasma::Types::BackgroundHints backgroundHints() const; + void setBackgroundHints(Plasma::Types::BackgroundHints hint); + + Plasma::FrameSvg::EnabledBorders enabledBorders() const; + + VisibilityMode visibilityMode() const; + void setVisibilityMode(PanelView::VisibilityMode mode); + + PanelView::OpacityMode opacityMode() const; + bool adaptiveOpacityEnabled(); + void setOpacityMode(PanelView::OpacityMode mode); + void updateAdaptiveOpacityEnabled(); + + /** + * @returns the geometry of the panel given a distance + */ + QRect geometryByDistance(int distance) const; + + /* Both Shared with script/panel.cpp */ + static KConfigGroup panelConfig(ShellCorona *corona, Plasma::Containment *containment, QScreen *screen); + static KConfigGroup panelConfigDefaults(ShellCorona *corona, Plasma::Containment *containment, QScreen *screen); + + void updateStruts(); + + /*This is different from screen() as is always there, even if the window is + temporarily outside the screen or if is hidden: only plasmashell will ever + change this property, unlike QWindow::screen()*/ + void setScreenToFollow(QScreen *screen); + QScreen *screenToFollow() const; + +protected: + void resizeEvent(QResizeEvent *ev) override; + void showEvent(QShowEvent *event) override; + void moveEvent(QMoveEvent *ev) override; + void keyPressEvent(QKeyEvent *event) override; + bool event(QEvent *e) override; + +Q_SIGNALS: + void alignmentChanged(); + void offsetChanged(); + void screenGeometryChanged(); + void thicknessChanged(); + void lengthChanged(); + void maximumLengthChanged(); + void minimumLengthChanged(); + void distanceChanged(); + void backgroundHintsChanged(); + void enabledBordersChanged(); + + // QWindow does not have a property for screen. Adding this property requires re-implementing the signal + void screenToFollowChanged(QScreen *screen); + void visibilityModeChanged(); + void opacityModeChanged(); + void adaptiveOpacityEnabledChanged(); + +protected Q_SLOTS: + /** + * It will be called when the configuration is requested + */ + void showConfigurationInterface(Plasma::Applet *applet) override; + +private Q_SLOTS: + void positionPanel(); + void restore(); + void setAutoHideEnabled(bool autoHideEnabled); + void showTemporarily(); + void refreshContainment(); + void refreshStatus(Plasma::Types::ItemStatus); + void restoreAutoHide(); + void screenDestroyed(QObject *screen); + void adaptToScreen(); + void handleQmlStatusChange(QQmlComponent::Status status); + void updateMask(); + void updateEnabledBorders(); + void updatePadding(); + +private: + int readConfigValueWithFallBack(const QString &key, int defaultValue); + void resizePanel(); + void integrateScreen(); + bool containmentContainsPosition(const QPointF &point) const; + QPointF positionAdjustedForContainment(const QPointF &point) const; + void setupWaylandIntegration(); + void visibilityModeToWayland(); + bool edgeActivated() const; + bool canSetStrut() const; + + int m_offset; + int m_maxLength; + int m_minLength; + int m_contentLength; + int m_distance; + int m_thickness; + int m_bottomPadding; + int m_topPadding; + int m_leftPadding; + int m_rightPadding; + bool m_initCompleted; + bool m_containsMouse = false; + bool m_fakeEventPending = false; + Qt::Alignment m_alignment; + QPointer m_panelConfigView; + ShellCorona *m_corona; + QTimer m_strutsTimer; + VisibilityMode m_visibilityMode; + OpacityMode m_opacityMode; + Plasma::Theme m_theme; + QTimer m_positionPaneltimer; + QTimer m_unhideTimer; + Plasma::Types::BackgroundHints m_backgroundHints; + Plasma::FrameSvg::EnabledBorders m_enabledBorders = Plasma::FrameSvg::AllBorders; + KWayland::Client::PlasmaShellSurface *m_shellSurface; + QPointer m_lastScreen; + QPointer m_screenToFollow; + QMetaObject::Connection m_transientWindowVisibleWatcher; + + static const int STRUTSTIMERDELAY = 200; +}; diff --git a/plasma/workspace/shell/plasma-plasmashell.service.in b/plasma/workspace/shell/plasma-plasmashell.service.in new file mode 100644 index 0000000000..c95e89a54d --- /dev/null +++ b/plasma/workspace/shell/plasma-plasmashell.service.in @@ -0,0 +1,15 @@ +[Unit] +Description=KDE Plasma Workspace +After=plasma-ksmserver.service plasma-kcminit.service +PartOf=graphical-session.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/plasmashell --no-respawn +Restart=on-failure +Type=dbus +BusName=org.kde.plasmashell +Slice=session.slice +TimeoutSec=40sec + +[Install] +WantedBy=plasma-core.target diff --git a/plasma/workspace/shell/primaryoutputwatcher.cpp b/plasma/workspace/shell/primaryoutputwatcher.cpp new file mode 100644 index 0000000000..0b13e25758 --- /dev/null +++ b/plasma/workspace/shell/primaryoutputwatcher.cpp @@ -0,0 +1,159 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "primaryoutputwatcher.h" + +#include "debug.h" +#include +#include +#include + +#include "qwayland-kde-primary-output-v1.h" +#include +#include + +#include +#if HAVE_X11 +#include //Used only in x11 case +#include +#include +#include +#include +#endif + +class WaylandPrimaryOutput : public QObject, public QtWayland::kde_primary_output_v1 +{ + Q_OBJECT +public: + WaylandPrimaryOutput(struct ::wl_registry *registry, int id, int version, QObject *parent) + : QObject(parent) + , QtWayland::kde_primary_output_v1(registry, id, version) + { + } + + void kde_primary_output_v1_primary_output(const QString &outputName) override + { + Q_EMIT primaryOutputChanged(outputName); + } + +Q_SIGNALS: + void primaryOutputChanged(const QString &outputName); +}; + +PrimaryOutputWatcher::PrimaryOutputWatcher(QObject *parent) + : QObject(parent) +{ +#if HAVE_X11 + if (KWindowSystem::isPlatformX11()) { + m_primaryOutputName = qGuiApp->primaryScreen()->name(); + qGuiApp->installNativeEventFilter(this); + const xcb_query_extension_reply_t *reply = xcb_get_extension_data(QX11Info::connection(), &xcb_randr_id); + m_xrandrExtensionOffset = reply->first_event; + setPrimaryOutputName(qGuiApp->primaryScreen()->name()); + connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, [this](QScreen *newPrimary) { + setPrimaryOutputName(newPrimary->name()); + }); + } +#endif + if (KWindowSystem::isPlatformWayland()) { + setupRegistry(); + } +} + +void PrimaryOutputWatcher::setPrimaryOutputName(const QString &newOutputName) +{ + if (newOutputName != m_primaryOutputName) { + const QString oldOutputName = m_primaryOutputName; + m_primaryOutputName = newOutputName; + Q_EMIT primaryOutputNameChanged(oldOutputName, newOutputName); + } +} + +void PrimaryOutputWatcher::setupRegistry() +{ + auto m_connection = KWayland::Client::ConnectionThread::fromApplication(this); + if (!m_connection) { + return; + } + + // Asking for primaryOutputName() before this happened, will return qGuiApp->primaryScreen()->name() anyways, so set it so the primaryOutputNameChange will + // have parameters that are coherent + m_primaryOutputName = qGuiApp->primaryScreen()->name(); + m_registry = new KWayland::Client::Registry(this); + connect(m_registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this](const QByteArray &interface, quint32 name, quint32 version) { + if (interface == WaylandPrimaryOutput::interface()->name) { + auto m_outputManagement = new WaylandPrimaryOutput(m_registry->registry(), name, version, this); + connect(m_outputManagement, &WaylandPrimaryOutput::primaryOutputChanged, this, [this](const QString &outputName) { + m_primaryOutputWayland = outputName; + // Only set the outputName when there's a QScreen attached to it + if (screenForName(outputName)) { + setPrimaryOutputName(outputName); + } + }); + } + }); + + // In case the outputName was received before Qt reported the screen + connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *screen) { + if (screen->name() == m_primaryOutputWayland) { + setPrimaryOutputName(m_primaryOutputWayland); + } + }); + + m_registry->create(m_connection); + m_registry->setup(); +} + +bool PrimaryOutputWatcher::nativeEventFilter(const QByteArray &eventType, void *message, long int *result) +{ + Q_UNUSED(result); +#if HAVE_X11 + // a particular edge case: when we switch the only enabled screen + // we don't have any signal about it, the primary screen changes but we have the same old QScreen* getting recycled + // see https://bugs.kde.org/show_bug.cgi?id=373880 + // if this slot will be invoked many times, their//second time on will do nothing as name and primaryOutputName will be the same by then + if (eventType[0] != 'x') { + return false; + } + + xcb_generic_event_t *ev = static_cast(message); + + const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); + + if (responseType == m_xrandrExtensionOffset + XCB_RANDR_SCREEN_CHANGE_NOTIFY) { + QTimer::singleShot(0, this, [this]() { + setPrimaryOutputName(qGuiApp->primaryScreen()->name()); + }); + } +#endif + return false; +} + +QScreen *PrimaryOutputWatcher::screenForName(const QString &outputName) const +{ + const auto screens = qGuiApp->screens(); + for (auto screen : screens) { + if (screen->name() == outputName) { + return screen; + } + } + return nullptr; +} + +QScreen *PrimaryOutputWatcher::primaryScreen() const +{ + auto screen = screenForName(m_primaryOutputName); + if (!screen) { +#ifdef PLASMASHELL + qCWarning(PLASMASHELL) << "could not find primary screen" << m_primaryOutputName; +#endif + return qGuiApp->primaryScreen(); + } + return screen; +} + +#include "primaryoutputwatcher.moc" diff --git a/plasma/workspace/shell/primaryoutputwatcher.h b/plasma/workspace/shell/primaryoutputwatcher.h new file mode 100644 index 0000000000..bbfffce57f --- /dev/null +++ b/plasma/workspace/shell/primaryoutputwatcher.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef PRIMARYOUTPUTWATCHER_H +#define PRIMARYOUTPUTWATCHER_H + +#include +#include + +namespace KWayland +{ +namespace Client +{ +class Registry; +class ConnectionThread; +} +} + +class QScreen; + +class PrimaryOutputWatcher : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT +public: + PrimaryOutputWatcher(QObject *parent); + QScreen *primaryScreen() const; + QScreen *screenForName(const QString &outputName) const; + +Q_SIGNALS: + void primaryOutputNameChanged(const QString &oldOutputName, const QString &newOutputName); + +protected: + friend class WaylandOutputDevice; + void setPrimaryOutputName(const QString &outputName); + +private: + void setupRegistry(); + bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override; + + // All + QString m_primaryOutputName; + + // Wayland + KWayland::Client::Registry *m_registry = nullptr; + QString m_primaryOutputWayland; + + // Xrandr + int m_xrandrExtensionOffset; +}; + +#endif // PRIMARYOUTPUTWATCHER_H diff --git a/plasma/workspace/shell/screenpool.cpp b/plasma/workspace/shell/screenpool.cpp new file mode 100644 index 0000000000..40734c8e85 --- /dev/null +++ b/plasma/workspace/shell/screenpool.cpp @@ -0,0 +1,559 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screenpool.h" +#include "primaryoutputwatcher.h" +#include "screenpool-debug.h" + +#include +#include +#include +#include + +#ifndef NDEBUG +#define CHECK_SCREEN_INVARIANTS screenInvariants(); +#else +#define CHECK_SCREEN_INVARIANTS +#endif + +ScreenPool::ScreenPool(const KSharedConfig::Ptr &config, QObject *parent) + : QObject(parent) + , m_configGroup(KConfigGroup(config, QStringLiteral("ScreenConnectors"))) + , m_primaryWatcher(new PrimaryOutputWatcher(this)) +{ + connect(qGuiApp, &QGuiApplication::screenAdded, this, &ScreenPool::handleScreenAdded); + connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ScreenPool::handleScreenRemoved); + connect(m_primaryWatcher, &PrimaryOutputWatcher::primaryOutputNameChanged, this, &ScreenPool::handlePrimaryOutputNameChanged); + + m_reconsiderOutputsTimer.setSingleShot(true); + m_reconsiderOutputsTimer.setInterval(250); + connect(&m_reconsiderOutputsTimer, &QTimer::timeout, this, &ScreenPool::reconsiderOutputs); + + m_configSaveTimer.setSingleShot(true); + connect(&m_configSaveTimer, &QTimer::timeout, this, [this]() { + m_configGroup.sync(); + }); +} + +void ScreenPool::load() +{ + QScreen *primary = m_primaryWatcher->primaryScreen(); + m_primaryConnector = QString(); + m_connectorForId.clear(); + m_idForConnector.clear(); + + if (primary) { + m_primaryConnector = primary->name(); + if (!m_primaryConnector.isEmpty()) { + m_connectorForId[0] = m_primaryConnector; + m_idForConnector[m_primaryConnector] = 0; + } + } + + // restore the known ids to connector mappings + const auto keys = m_configGroup.keyList(); + for (const QString &key : keys) { + QString connector = m_configGroup.readEntry(key, QString()); + const int currentId = key.toInt(); + + if (!key.isEmpty() && !connector.isEmpty() && !m_connectorForId.contains(currentId) && !m_idForConnector.contains(connector)) { + m_connectorForId[currentId] = connector; + m_idForConnector[connector] = currentId; + } else if (m_idForConnector.value(connector) != currentId) { + m_configGroup.deleteEntry(key); + } + } + + // Populate allthe screen based on what's connected at startup + for (QScreen *screen : qGuiApp->screens()) { + // On some devices QGuiApp::screenAdded is always emitted for some screens at startup so at this point that screen would already be managed + if (!m_allSortedScreens.contains(screen)) { + handleScreenAdded(screen); + } else if (!m_idForConnector.contains(screen->name())) { + insertScreenMapping(firstAvailableId(), screen->name()); + } + } + CHECK_SCREEN_INVARIANTS +} + +ScreenPool::~ScreenPool() +{ + m_configGroup.sync(); +} + +QString ScreenPool::primaryConnector() const +{ + return m_primaryConnector; +} + +void ScreenPool::setPrimaryConnector(const QString &primary) +{ + if (m_primaryConnector == primary) { + return; + } + + int oldIdForPrimary = m_idForConnector.value(primary, -1); + if (oldIdForPrimary == -1) { + // move old primary to new free id + oldIdForPrimary = firstAvailableId(); + } + + m_idForConnector[primary] = 0; + m_connectorForId[0] = primary; + m_idForConnector[m_primaryConnector] = oldIdForPrimary; + m_connectorForId[oldIdForPrimary] = m_primaryConnector; + m_primaryConnector = primary; + save(); +} + +void ScreenPool::save() +{ + QMap::const_iterator i; + for (i = m_connectorForId.constBegin(); i != m_connectorForId.constEnd(); ++i) { + m_configGroup.writeEntry(QString::number(i.key()), i.value()); + } + // write to disck every 30 seconds at most + m_configSaveTimer.start(30000); +} + +void ScreenPool::insertScreenMapping(int id, const QString &connector) +{ + Q_ASSERT(!m_connectorForId.contains(id) || m_connectorForId.value(id) == connector); + Q_ASSERT(!m_idForConnector.contains(connector) || m_idForConnector.value(connector) == id); + + if (id == 0) { + m_primaryConnector = connector; + } + + m_connectorForId[id] = connector; + m_idForConnector[connector] = id; + save(); +} + +int ScreenPool::id(const QString &connector) const +{ + return m_idForConnector.value(connector, -1); +} + +QString ScreenPool::connector(int id) const +{ + Q_ASSERT(m_connectorForId.contains(id)); + + return m_connectorForId.value(id); +} + +int ScreenPool::firstAvailableId() const +{ + int i = 0; + // find the first integer not stored in m_connectorForId + // m_connectorForId is the only map, so the ids are sorted + foreach (int existingId, m_connectorForId.keys()) { + if (i != existingId) { + return i; + } + ++i; + } + + return i; +} + +QList ScreenPool::knownIds() const +{ + return m_connectorForId.keys(); +} + +QList ScreenPool::screens() const +{ + return m_availableScreens; +} + +QScreen *ScreenPool::primaryScreen() const +{ + QScreen *primary = m_primaryWatcher->primaryScreen(); + if (m_redundantScreens.contains(primary)) { + return m_redundantScreens[primary]; + } else { + return primary; + } +} + +QScreen *ScreenPool::screenForId(int id) const +{ + if (!m_connectorForId.contains(id)) { + return nullptr; + } + + // TODO: do QScreen bookeeping completely in screenpool, cache also available QScreens + const QString name = m_connectorForId.value(id); + for (QScreen *screen : m_availableScreens) { + if (screen->name() == name) { + return screen; + } + } + return nullptr; +} + +QScreen *ScreenPool::screenForConnector(const QString &connector) +{ + for (QScreen *screen : m_availableScreens) { + if (screen->name() == connector) { + return screen; + } + } + return nullptr; +} + +bool ScreenPool::noRealOutputsConnected() const +{ + if (qApp->screens().count() > 1) { + return false; + } + + return isOutputFake(m_primaryWatcher->primaryScreen()); +} + +bool ScreenPool::isOutputFake(QScreen *screen) const +{ + Q_ASSERT(screen); + // On X11 the output named :0.0 is fake (the geometry is usually valid and whatever the geometry + // of the last connected screen was), on wayland the fake output has no name and no geometry + const bool fake = screen->name() == QStringLiteral(":0.0") || screen->geometry().isEmpty() || screen->name().isEmpty(); + // If there is a fake output we can only have one screen left (the fake one) + // Q_ASSERT(!fake || fake == (qGuiApp->screens().count() == 1)); + return fake; +} + +QScreen *ScreenPool::outputRedundantTo(QScreen *screen) const +{ + Q_ASSERT(screen); + // Manage separatedly fake screens + if (isOutputFake(screen)) { + return nullptr; + } + const QRect thisGeometry = screen->geometry(); + + const int thisId = id(screen->name()); + + // FIXME: QScreen doesn't have any idea of "this qscreen is clone of this other one + // so this ultra inefficient heuristic has to stay until we have a slightly better api + // logic is: + // a screen is redundant if: + //* its geometry is contained in another one + //* if their resolutions are different, the "biggest" one wins + //* if they have the same geometry, the one with the lowest id wins (arbitrary, but gives reproducible behavior and makes the primary screen win) + for (QScreen *s : m_allSortedScreens) { + // don't compare with itself + if (screen == s) { + continue; + } + + const QRect otherGeometry = s->geometry(); + + if (otherGeometry.isNull()) { + continue; + } + + const int otherId = id(s->name()); + + if (otherGeometry.contains(thisGeometry, false) + && ( // since at this point contains is true, if either + // measure of othergeometry is bigger, has a bigger area + otherGeometry.width() > thisGeometry.width() || otherGeometry.height() > thisGeometry.height() || + // ids not -1 are considered in descending order of importance + //-1 means that is a screen not known yet, just arrived and + // not yet in screenpool: this happens for screens that + // are hotplugged and weren't known. it does NOT happen + // at first startup, as screenpool populates on load with all screens connected at the moment before the rest of the shell starts up + (thisId == -1 && otherId != -1) || (thisId > otherId && otherId != -1))) { + return s; + } + } + + return nullptr; +} + +void ScreenPool::reconsiderOutputs() +{ + QScreen *oldPrimaryScreen = primaryScreen(); + for (QScreen *screen : m_allSortedScreens) { + if (m_redundantScreens.contains(screen)) { + if (QScreen *toScreen = outputRedundantTo(screen)) { + // Insert again, redndantTo may have changed + m_fakeScreens.remove(screen); + m_redundantScreens.insert(screen, toScreen); + } else { + qCDebug(SCREENPOOL) << "not redundant anymore" << screen << (isOutputFake(screen) ? "but is a fake screen" : ""); + Q_ASSERT(!m_availableScreens.contains(screen)); + m_redundantScreens.remove(screen); + if (isOutputFake(screen)) { + m_fakeScreens.insert(screen); + } else { + m_fakeScreens.remove(screen); + m_availableScreens.append(screen); + if (!m_idForConnector.contains(screen->name())) { + insertScreenMapping(firstAvailableId(), screen->name()); + } + Q_EMIT screenAdded(screen); + QScreen *newPrimaryScreen = primaryScreen(); + if (newPrimaryScreen != oldPrimaryScreen) { + // Primary screen was redundant, not anymore + setPrimaryConnector(newPrimaryScreen->name()); + Q_EMIT primaryScreenChanged(oldPrimaryScreen, newPrimaryScreen); + } + } + } + } else if (QScreen *toScreen = outputRedundantTo(screen)) { + qCDebug(SCREENPOOL) << "new redundant screen" << screen << "with primary screen" << m_primaryWatcher->primaryScreen(); + + m_fakeScreens.remove(screen); + m_redundantScreens.insert(screen, toScreen); + if (!m_idForConnector.contains(screen->name())) { + insertScreenMapping(firstAvailableId(), screen->name()); + } + if (m_availableScreens.contains(screen)) { + QScreen *newPrimaryScreen = primaryScreen(); + if (newPrimaryScreen != oldPrimaryScreen) { + // Primary screen became redundant + setPrimaryConnector(newPrimaryScreen->name()); + Q_EMIT primaryScreenChanged(oldPrimaryScreen, newPrimaryScreen); + } + m_availableScreens.removeAll(screen); + Q_EMIT screenRemoved(screen); + } + } else if (isOutputFake(screen)) { + // NOTE: order of operations is important + qCDebug(SCREENPOOL) << "new fake screen" << screen; + m_redundantScreens.remove(screen); + m_fakeScreens.insert(screen); + if (m_availableScreens.contains(screen)) { + QScreen *newPrimaryScreen = primaryScreen(); + if (newPrimaryScreen != oldPrimaryScreen) { + // Primary screen became fake + setPrimaryConnector(newPrimaryScreen->name()); + Q_EMIT primaryScreenChanged(oldPrimaryScreen, newPrimaryScreen); + } + m_availableScreens.removeAll(screen); + Q_EMIT screenRemoved(screen); + } + } else if (m_fakeScreens.contains(screen)) { + Q_ASSERT(!m_availableScreens.contains(screen)); + m_fakeScreens.remove(screen); + m_availableScreens.append(screen); + if (!m_idForConnector.contains(screen->name())) { + insertScreenMapping(firstAvailableId(), screen->name()); + } + Q_EMIT screenAdded(screen); + QScreen *newPrimaryScreen = primaryScreen(); + if (newPrimaryScreen != oldPrimaryScreen) { + // Primary screen was redundant, not anymore + setPrimaryConnector(newPrimaryScreen->name()); + Q_EMIT primaryScreenChanged(oldPrimaryScreen, newPrimaryScreen); + } + } else { + qCDebug(SCREENPOOL) << "fine screen" << screen; + } + } + + // updateStruts(); + + CHECK_SCREEN_INVARIANTS +} + +void ScreenPool::insertSortedScreen(QScreen *screen) +{ + if (m_allSortedScreens.contains(screen)) { + // This should happen only when a fake screen isn't anymore + return; + } + auto before = std::find_if(m_allSortedScreens.begin(), m_allSortedScreens.end(), [this, screen](QScreen *otherScreen) { + return (screen->geometry().width() > otherScreen->geometry().width() && screen->geometry().height() > otherScreen->geometry().height()) + || id(screen->name()) < id(otherScreen->name()); + }); + m_allSortedScreens.insert(before, screen); +} + +void ScreenPool::handleScreenAdded(QScreen *screen) +{ + qCDebug(SCREENPOOL) << "handleScreenAdded" << screen << screen->geometry(); + connect( + screen, + &QScreen::geometryChanged, + this, + [this, screen]() { + m_allSortedScreens.removeAll(screen); + insertSortedScreen(screen); + m_reconsiderOutputsTimer.start(); + }, + Qt::UniqueConnection); + insertSortedScreen(screen); + + if (isOutputFake(screen)) { + m_fakeScreens.insert(screen); + return; + } else if (!m_idForConnector.contains(screen->name())) { + insertScreenMapping(firstAvailableId(), screen->name()); + } + + if (QScreen *toScreen = outputRedundantTo(screen)) { + m_redundantScreens.insert(screen, toScreen); + return; + } + + if (m_fakeScreens.contains(screen)) { + qCDebug(SCREENPOOL) << "not fake anymore" << screen; + m_fakeScreens.remove(screen); + } + + m_reconsiderOutputsTimer.start(); + Q_ASSERT(!m_availableScreens.contains(screen)); + m_availableScreens.append(screen); + Q_EMIT screenAdded(screen); +} + +void ScreenPool::handleScreenRemoved(QScreen *screen) +{ + qCDebug(SCREENPOOL) << "handleScreenRemoved" << screen; + m_allSortedScreens.removeAll(screen); + if (m_redundantScreens.contains(screen)) { + Q_ASSERT(!m_fakeScreens.contains(screen)); + Q_ASSERT(!m_availableScreens.contains(screen)); + m_redundantScreens.remove(screen); + } else if (m_fakeScreens.contains(screen)) { + Q_ASSERT(!m_redundantScreens.contains(screen)); + Q_ASSERT(!m_availableScreens.contains(screen)); + m_fakeScreens.remove(screen); + } else if (isOutputFake(screen)) { + // This happens when an output is recicled because it was the last one and became fake + Q_ASSERT(m_availableScreens.contains(screen)); + Q_ASSERT(!m_redundantScreens.contains(screen)); + Q_ASSERT(!m_fakeScreens.contains(screen)); + Q_ASSERT(m_allSortedScreens.isEmpty()); + m_allSortedScreens.append(screen); + m_availableScreens.removeAll(screen); + m_fakeScreens.insert(screen); + } else { + Q_ASSERT(m_availableScreens.contains(screen)); + Q_ASSERT(!m_redundantScreens.contains(screen)); + Q_ASSERT(!m_fakeScreens.contains(screen)); + m_availableScreens.removeAll(screen); + reconsiderOutputs(); + Q_EMIT screenRemoved(screen); + } + CHECK_SCREEN_INVARIANTS +} + +void ScreenPool::handlePrimaryOutputNameChanged(const QString &oldOutputName, const QString &newOutputName) +{ + // when the appearance of a new primary screen *moves* + // the position of the now secondary, the two screens will appear overlapped for an instant, and a spurious output redundant would happen here if checked + // immediately + m_reconsiderOutputsTimer.start(); + + QScreen *oldPrimary = screenForConnector(oldOutputName); + QScreen *newPrimary = m_primaryWatcher->primaryScreen(); + // First check if the data arrived is correct, then set the new peimary considering redundants + Q_ASSERT(newPrimary && newPrimary->name() == newOutputName); + newPrimary = primaryScreen(); + + // This happens when a screen that was primary because the real primary was redundant becomes the real primary + if (m_primaryConnector == newPrimary->name()) { + return; + } + + if (!newPrimary || newPrimary == oldPrimary || newPrimary->geometry().isNull()) { + return; + } + + // On X11 we get fake screens as primary + + // Special case: we are in "no connectors" mode, there is only a (recycled) QScreen instance which is not attached to any output. Treat this as a screen + // removed This happens only on X, wayland doesn't seem to be getting fake screens + if (noRealOutputsConnected()) { + qCDebug(SCREENPOOL) << "EMITTING SCREEN REMOVED" << newPrimary; + handleScreenRemoved(newPrimary); + return; + // On X11, the output named :0.0 is fake + } else if (oldOutputName == ":0.0" || oldOutputName.isEmpty()) { + setPrimaryConnector(newOutputName); + // NOTE: when we go from 0 to 1 screen connected, screens can be renamed in those two followinf cases + // * last output connected/disconnected -> we go between the fake screen and the single output, renamed + // * external screen connected to a closed lid laptop, disconnecting the qscreen instance will be recycled from external output to internal + // In the latter case m_availableScreens will aready contain newPrimary + // We'll go here also once at startup, for which we don't need to do anything besides setting internally the primary conector name + handleScreenAdded(newPrimary); + return; + } else { + Q_ASSERT(newPrimary); + qCDebug(SCREENPOOL) << "PRIMARY CHANGED" << oldPrimary << "-->" << newPrimary; + setPrimaryConnector(newOutputName); + Q_EMIT primaryScreenChanged(oldPrimary, newPrimary); + } +} + +void ScreenPool::screenInvariants() +{ + // Is the primary connector in sync with the actual primaryScreen? The only way it can get out of sync with primaryConnector() is the single fake screen/no + // real outputs scenario + Q_ASSERT(noRealOutputsConnected() || primaryScreen()->name() == primaryConnector()); + // Is the primary screen available? TODO: it can be redundant + // Q_ASSERT(m_availableScreens.contains(primaryScreen())); + + // QScreen bookeeping integrity + auto allScreens = qGuiApp->screens(); + // Do we actually track every screen? + + Q_ASSERT((m_availableScreens.count() + m_redundantScreens.count() + m_fakeScreens.count()) == allScreens.count()); + Q_ASSERT(allScreens.count() == m_allSortedScreens.count()); + + // At most one fake output + Q_ASSERT(m_fakeScreens.count() <= 1); + if (m_fakeScreens.count() == 1) { + // If we have a fake output we can't have anything else + Q_ASSERT(m_availableScreens.count() == 0); + Q_ASSERT(m_redundantScreens.count() == 0); + } else { + for (QScreen *screen : allScreens) { + if (m_availableScreens.contains(screen)) { + // If available can't be redundant + Q_ASSERT(!m_redundantScreens.contains(screen)); + } else if (m_redundantScreens.contains(screen)) { + // If redundant can't be available + Q_ASSERT(!m_availableScreens.contains(screen)); + } else { + // We can't have a screen unaccounted for + Q_ASSERT(false); + } + // Is every screen mapped to an id? + Q_ASSERT(m_idForConnector.contains(screen->name())); + // Are the two maps symmetrical? + Q_ASSERT(connector(id(screen->name())) == screen->name()); + } + } + for (QScreen *screen : m_redundantScreens.keys()) { + Q_ASSERT(outputRedundantTo(screen) != nullptr); + } +} + +QDebug operator<<(QDebug debug, const ScreenPool *pool) +{ + debug << pool->metaObject()->className() << '(' << static_cast(pool) << ") Internal state:\n"; + debug << "Connector Mapping:\n"; + auto it = pool->m_idForConnector.constBegin(); + while (it != pool->m_idForConnector.constEnd()) { + debug << it.key() << "\t-->\t" << it.value() << '\n'; + it++; + } + debug << "Platform primary screen:\t" << pool->m_primaryWatcher->primaryScreen() << '\n'; + debug << "Actual primary screen:\t" << pool->primaryScreen() << '\n'; + debug << "Available screens:\t" << pool->m_availableScreens << '\n'; + debug << "\"Fake\" screens:\t" << pool->m_fakeScreens << '\n'; + debug << "Redundant screens covered by other ones:\t" << pool->m_redundantScreens << '\n'; + debug << "All screens, ordered by size:\t" << pool->m_allSortedScreens << '\n'; + debug << "All screen that QGuiApplication knows:\t" << qGuiApp->screens() << '\n'; + return debug; +} + +#include "moc_screenpool.cpp" diff --git a/plasma/workspace/shell/screenpool.h b/plasma/workspace/shell/screenpool.h new file mode 100644 index 0000000000..3615e7f048 --- /dev/null +++ b/plasma/workspace/shell/screenpool.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +class QScreen; +class PrimaryOutputWatcher; + +class ScreenPool : public QObject +{ + Q_OBJECT + +public: + explicit ScreenPool(const KSharedConfig::Ptr &config, QObject *parent = nullptr); + void load(); + ~ScreenPool() override; + + QString primaryConnector() const; + + int id(const QString &connector) const; + + QString connector(int id) const; + + // all ids that are known, included screens not enabled at the moment + QList knownIds() const; + + // QScreen API + QList screens() const; + QScreen *primaryScreen() const; + QScreen *screenForId(int id) const; + QScreen *screenForConnector(const QString &connector); + + bool noRealOutputsConnected() const; + +Q_SIGNALS: + void screenAdded(QScreen *screen); + void screenRemoved(QScreen *screen); + void primaryScreenChanged(QScreen *oldPrimary, QScreen *newPrimary); + +private: + void save(); + void setPrimaryConnector(const QString &primary); + void insertScreenMapping(int id, const QString &connector); + int firstAvailableId() const; + + QScreen *outputRedundantTo(QScreen *screen) const; + void reconsiderOutputs(); + bool isOutputFake(QScreen *screen) const; + + void insertSortedScreen(QScreen *screen); + void handleScreenAdded(QScreen *screen); + void handleScreenRemoved(QScreen *screen); + void handlePrimaryOutputNameChanged(const QString &oldOutputName, const QString &newOutputName); + + void screenInvariants(); + + KConfigGroup m_configGroup; + QString m_primaryConnector; + // order is important + QMap m_connectorForId; + QHash m_idForConnector; + + // List correspondent to qGuiApp->screens(), but sorted first by size then by Id, + // determines the screen importance while figuring out the reduntant ones + QList m_allSortedScreens; + // m_availableScreens + m_redundantOutputs + m_fakeOutputs == qGuiApp->screens() + QList m_availableScreens; // Those are all the screen that are available to Corona + QHash m_redundantScreens; + QSet m_fakeScreens; + + QTimer m_reconsiderOutputsTimer; + QTimer m_configSaveTimer; + PrimaryOutputWatcher *const m_primaryWatcher; + friend QDebug operator<<(QDebug d, const ScreenPool *pool); +}; + +QDebug operator<<(QDebug d, const ScreenPool *pool); diff --git a/plasma/workspace/shell/scripting/appinterface.cpp b/plasma/workspace/shell/scripting/appinterface.cpp new file mode 100644 index 0000000000..81d00f6ab5 --- /dev/null +++ b/plasma/workspace/shell/scripting/appinterface.cpp @@ -0,0 +1,215 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "appinterface.h" +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +#if HAVE_X11 +#include +#include +#endif + +#include "debug.h" +#include "scriptengine.h" + +namespace WorkspaceScripting +{ +AppInterface::AppInterface(ScriptEngine *env) + : QObject(env) + , m_env(env) +{ + m_theme = new Plasma::Theme(this); +} + +int AppInterface::screenCount() const +{ + return m_env->corona()->numScreens(); +} + +QJSValue AppInterface::screenGeometry(int screen) const +{ + QRectF rect = m_env->corona()->screenGeometry(screen); + QJSValueList args({QJSValue(rect.x()), QJSValue(rect.y()), QJSValue(rect.width()), QJSValue(rect.height())}); + return m_env->globalObject().property("QRectF").callAsConstructor(args); +} + +QList AppInterface::activityIds() const +{ + // FIXME: the ints could overflow since Applet::id() returns a uint, + // however QScript deals with QList very, very poorly + QList containments; + + foreach (Plasma::Containment *c, m_env->corona()->containments()) { + if (!ScriptEngine::isPanel(c)) { + containments.append(c->id()); + } + } + + return containments; +} + +QList AppInterface::panelIds() const +{ + // FIXME: the ints could overflow since Applet::id() returns a uint, + // however QScript deals with QList very, very poorly + QList panels; + + foreach (Plasma::Containment *c, m_env->corona()->containments()) { + // qDebug() << "checking" << (QObject*)c << isPanel(c); + if (ScriptEngine::isPanel(c)) { + panels.append(c->id()); + } + } + + return panels; +} + +QString AppInterface::applicationVersion() const +{ + return qApp->applicationVersion(); +} + +QString AppInterface::platformVersion() const +{ + return QString(); // KDE::versionString(); +} + +int AppInterface::scriptingVersion() const +{ + return PLASMA_DESKTOP_SCRIPTING_VERSION; +} + +QString AppInterface::theme() const +{ + return m_theme->themeName(); +} + +void AppInterface::setTheme(const QString &name) +{ + m_theme->setThemeName(name); +} + +QString AppInterface::locale() const +{ + return QLocale::system().name(); +} + +QString AppInterface::language() const +{ + return QLocale::system().languageToString(QLocale::system().language()); +} + +QString AppInterface::languageId() const +{ + return QLocale::system().bcp47Name().section(QLatin1Char('-'), 0, 0); +} + +bool AppInterface::multihead() const +{ + return false; +} + +int AppInterface::multiheadScreen() const +{ + return 0; +} + +void AppInterface::lockCorona(bool locked) +{ + m_env->corona()->setImmutability(locked ? Plasma::Types::UserImmutable : Plasma::Types::Mutable); +} + +bool AppInterface::coronaLocked() const +{ + return m_env->corona()->immutability() != Plasma::Types::Mutable; +} + +void AppInterface::sleep(int ms) +{ + Q_UNUSED(ms) + // TODO KF6 remove + + // Sleep was implemented with a nested event loop which would cause nested + // event processing from QML and offer a wide array of sources for + // segfaulting. There isn't really a need to sleep from the scripting API, + // so it was turned no-op. + qCWarning(PLASMASHELL) << "Plasma scripting sleep() is deprecated and does nothing!"; +} + +bool AppInterface::hasBattery() const +{ + QList batteryDevices = Solid::Device::listFromType(Solid::DeviceInterface::Battery); + + for (auto device : batteryDevices) { + Solid::Battery *battery = device.as(); + // check for _both_ primary and power supply status + // apparently some devices misreport as "primary", and we don't + // want to trigger on just having UPC connected to a desktop box + if (battery && battery->type() == Solid::Battery::PrimaryBattery && battery->isPowerSupply()) { + return true; + } + } + + return false; +} + +QStringList AppInterface::knownWidgetTypes() const +{ + if (m_knownWidgets.isEmpty()) { + QStringList widgets; + const QList plugins = Plasma::PluginLoader::self()->listAppletMetaData(QString()); + + for (const auto &plugin : plugins) { + widgets.append(plugin.pluginId()); + } + + const_cast(this)->m_knownWidgets = widgets; + } + + return m_knownWidgets; +} + +QStringList AppInterface::knownActivityTypes() const +{ + return knownContainmentTypes(QStringLiteral("Desktop")); +} + +QStringList AppInterface::knownPanelTypes() const +{ + return knownContainmentTypes(QStringLiteral("Panel")); +} + +QStringList AppInterface::knownContainmentTypes(const QString &type) const +{ + QStringList containments; + const QList plugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(type); + + containments.reserve(plugins.count()); + for (const KPluginMetaData &plugin : plugins) { + containments.append(plugin.pluginId()); + } + + return containments; +} + +} diff --git a/plasma/workspace/shell/scripting/appinterface.h b/plasma/workspace/shell/scripting/appinterface.h new file mode 100644 index 0000000000..1fd6e89f56 --- /dev/null +++ b/plasma/workspace/shell/scripting/appinterface.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +namespace Plasma +{ +class Containment; +class Corona; +class Theme; +} // namespace Plasma + +namespace WorkspaceScripting +{ +class ScriptEngine; + +class AppInterface : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool locked READ coronaLocked WRITE lockCorona) + Q_PROPERTY(bool hasBattery READ hasBattery) + Q_PROPERTY(int screenCount READ screenCount) + Q_PROPERTY(QList activityIds READ activityIds) + Q_PROPERTY(QList panelIds READ panelIds) + Q_PROPERTY(QStringList knownPanelTypes READ knownPanelTypes) + Q_PROPERTY(QStringList knownActivityTypes READ knownActivityTypes) + Q_PROPERTY(QStringList knownWidgetTypes READ knownWidgetTypes) + Q_PROPERTY(QString theme READ theme WRITE setTheme) + Q_PROPERTY(QString applicationVersion READ applicationVersion) + Q_PROPERTY(QString platformVersion READ platformVersion) + Q_PROPERTY(int scriptingVersion READ scriptingVersion) + Q_PROPERTY(bool multihead READ multihead) + Q_PROPERTY(bool multiheadScreen READ multihead) + Q_PROPERTY(QString locale READ locale) + Q_PROPERTY(QString language READ language) + Q_PROPERTY(QString languageId READ languageId) + +public: + explicit AppInterface(ScriptEngine *env); + + bool hasBattery() const; + int screenCount() const; + QList activityIds() const; + QList panelIds() const; + + QStringList knownWidgetTypes() const; + QStringList knownActivityTypes() const; + QStringList knownPanelTypes() const; + QStringList knownContainmentTypes(const QString &type) const; + + QString applicationVersion() const; + QString platformVersion() const; + int scriptingVersion() const; + + QString theme() const; + void setTheme(const QString &name); + + QString locale() const; + QString language() const; + QString languageId() const; + + bool multihead() const; + int multiheadScreen() const; + + bool coronaLocked() const; + +public Q_SLOTS: + QJSValue screenGeometry(int screen) const; + void lockCorona(bool locked); + void sleep(int ms); + +Q_SIGNALS: + void print(const QString &string); + +private: + ScriptEngine *m_env; + QStringList m_knownWidgets; + Plasma::Theme *m_theme; +}; + +} diff --git a/plasma/workspace/shell/scripting/applet.cpp b/plasma/workspace/shell/scripting/applet.cpp new file mode 100644 index 0000000000..4865389985 --- /dev/null +++ b/plasma/workspace/shell/scripting/applet.cpp @@ -0,0 +1,266 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "applet.h" +#include "scriptengine.h" + +#include + +#include +#include +#include + +#include +#include +#include + +namespace WorkspaceScripting +{ +class Applet::Private +{ +public: + Private() + : configDirty(false) + , inWallpaperConfig(false) + { + } + + QPointer engine = nullptr; + KConfigGroup configGroup; + QStringList configGroupPath; + KConfigGroup globalConfigGroup; + QStringList globalConfigGroupPath; + bool configDirty : 1; + bool inWallpaperConfig : 1; +}; + +Applet::Applet(ScriptEngine *parent) + : QObject(parent) + , d(new Applet::Private) +{ + d->engine = parent; +} + +Applet::~Applet() +{ + delete d; +} + +void Applet::setCurrentConfigGroup(const QStringList &groupNames) +{ + Plasma::Applet *app = applet(); + if (!app) { + d->configGroup = KConfigGroup(); + d->configGroupPath.clear(); + return; + } + + d->configGroup = app->config(); + d->configGroupPath = groupNames; + + for (const QString &groupName : groupNames) { + d->configGroup = KConfigGroup(&d->configGroup, groupName); + } + + d->inWallpaperConfig = !groupNames.isEmpty() && groupNames.first() == QLatin1String("Wallpaper"); +} + +QStringList Applet::currentConfigGroup() const +{ + return d->configGroupPath; +} + +QStringList Applet::configKeys() const +{ + if (d->configGroup.isValid()) { + return d->configGroup.keyList(); + } + + return QStringList(); +} + +QStringList Applet::configGroups() const +{ + if (d->configGroup.isValid()) { + return d->configGroup.groupList(); + } + + return QStringList(); +} + +QVariant Applet::readConfig(const QString &key, const QJSValue &def) const +{ + if (d->configGroup.isValid()) { + return d->configGroup.readEntry(key, def.toVariant()); + } else { + return QVariant(); + } +} + +void Applet::writeConfig(const QString &key, const QJSValue &value) +{ + if (d->configGroup.isValid()) { + if (d->inWallpaperConfig) { + // hacky, but only way to make the wallpaper react immediately + QObject *wallpaperGraphicsObject = applet()->property("wallpaperGraphicsObject").value(); + if (wallpaperGraphicsObject) { + KDeclarative::ConfigPropertyMap *config = + static_cast(wallpaperGraphicsObject->property("configuration").value()); + config->setProperty(key.toLatin1(), value.toVariant()); + } + } else if (applet()->configScheme()) { + // check if it can be written in the applets' configScheme + KConfigSkeletonItem *item = applet()->configScheme()->findItemByName(key); + if (item) { + item->setProperty(value.toVariant()); + applet()->configScheme()->blockSignals(true); + applet()->configScheme()->save(); + // why read? read will update KConfigSkeletonItem::mLoadedValue, + // allowing a write operation to be performed next time + applet()->configScheme()->read(); + applet()->configScheme()->blockSignals(false); + Q_EMIT applet()->configScheme()->configChanged(); + } + } + + d->configGroup.writeEntry(key, value.toVariant()); + d->configDirty = true; + } +} + +void Applet::setCurrentGlobalConfigGroup(const QStringList &groupNames) +{ + Plasma::Applet *app = applet(); + if (!app) { + d->globalConfigGroup = KConfigGroup(); + d->globalConfigGroupPath.clear(); + return; + } + + d->globalConfigGroup = app->globalConfig(); + d->globalConfigGroupPath = groupNames; + + for (const QString &groupName : groupNames) { + d->globalConfigGroup = KConfigGroup(&d->globalConfigGroup, groupName); + } +} + +QStringList Applet::currentGlobalConfigGroup() const +{ + return d->globalConfigGroupPath; +} + +QStringList Applet::globalConfigKeys() const +{ + if (d->globalConfigGroup.isValid()) { + return d->globalConfigGroup.keyList(); + } + + return QStringList(); +} + +QStringList Applet::globalConfigGroups() const +{ + if (d->globalConfigGroup.isValid()) { + return d->globalConfigGroup.groupList(); + } + + return QStringList(); +} + +QVariant Applet::readGlobalConfig(const QString &key, const QJSValue &def) const +{ + if (d->globalConfigGroup.isValid()) { + return d->globalConfigGroup.readEntry(key, def.toVariant()); + } else { + return QVariant(); + } +} + +void Applet::writeGlobalConfig(const QString &key, const QJSValue &value) +{ + if (d->globalConfigGroup.isValid()) { + d->globalConfigGroup.writeEntry(key, value.toVariant()); + d->configDirty = true; + } +} + +void Applet::reloadConfigIfNeeded() +{ + if (d->configDirty) { + reloadConfig(); + } +} + +void Applet::reloadConfig() +{ + Plasma::Applet *app = applet(); + if (app) { + KConfigGroup cg = app->config(); + + if (!app->isContainment()) { + app->restore(cg); + } + + app->configChanged(); + + if (app->containment() && app->containment()->corona()) { + app->containment()->corona()->requestConfigSync(); + } + + d->configDirty = false; + } +} + +QString Applet::version() const +{ + Plasma::Applet *app = applet(); + if (!app) { + return QString(); + } + + return app->pluginMetaData().version(); +} + +void Applet::setLocked(bool locked) +{ + Plasma::Applet *app = applet(); + if (!app) { + return; + } + + app->setImmutability(locked ? Plasma::Types::UserImmutable : Plasma::Types::Mutable); + KConfigGroup cg = app->config(); + if (!app->isContainment()) { + cg = cg.parent(); + } + + if (cg.isValid()) { + cg.writeEntry("immutability", (int)app->immutability()); + } +} + +bool Applet::locked() const +{ + Plasma::Applet *app = applet(); + if (!app) { + return Plasma::Types::Mutable; + } + + return app->immutability() != Plasma::Types::Mutable; +} + +Plasma::Applet *Applet::applet() const +{ + return nullptr; +} + +ScriptEngine *Applet::engine() const +{ + return d->engine; +} + +} diff --git a/plasma/workspace/shell/scripting/applet.h b/plasma/workspace/shell/scripting/applet.h new file mode 100644 index 0000000000..282c4b8cda --- /dev/null +++ b/plasma/workspace/shell/scripting/applet.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2010 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +namespace Plasma +{ +class Applet; +} // namespace Plasma + +namespace WorkspaceScripting +{ +class ScriptEngine; + +class Applet : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList currentConfigGroup WRITE setCurrentConfigGroup READ currentConfigGroup) + +public: + explicit Applet(ScriptEngine *parent); + ~Applet() override; + + QStringList configKeys() const; + QStringList configGroups() const; + + void setCurrentConfigGroup(const QStringList &groupNames); + QStringList currentConfigGroup() const; + + QStringList globalConfigKeys() const; + QStringList globalConfigGroups() const; + + void setCurrentGlobalConfigGroup(const QStringList &groupNames); + QStringList currentGlobalConfigGroup() const; + + QString version() const; + + void setLocked(bool locked); + bool locked() const; + + virtual Plasma::Applet *applet() const; + + ScriptEngine *engine() const; + +protected: + void reloadConfigIfNeeded(); + +public Q_SLOTS: + virtual QVariant readConfig(const QString &key, const QJSValue &def = QString()) const; + virtual void writeConfig(const QString &key, const QJSValue &value); + virtual QVariant readGlobalConfig(const QString &key, const QJSValue &def = QString()) const; + virtual void writeGlobalConfig(const QString &key, const QJSValue &value); + virtual void reloadConfig(); + +private: + class Private; + Private *const d; +}; + +} diff --git a/plasma/workspace/shell/scripting/configgroup.cpp b/plasma/workspace/shell/scripting/configgroup.cpp new file mode 100644 index 0000000000..2f445a20bf --- /dev/null +++ b/plasma/workspace/shell/scripting/configgroup.cpp @@ -0,0 +1,207 @@ +/* + SPDX-FileCopyrightText: 2011-2012 Sebastian Kügler + SPDX-FileCopyrightText: 2013 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "configgroup.h" +#include "debug.h" + +#include +#include +#include +#include +#include + +class ConfigGroupPrivate +{ +public: + ConfigGroupPrivate(ConfigGroup *q) + : q(q) + , config(nullptr) + , configGroup(nullptr) + { + } + + ~ConfigGroupPrivate() + { + delete configGroup; + } + + ConfigGroup *q; + KSharedConfigPtr config; + KConfigGroup *configGroup; + QString file; + QTimer *synchTimer; + QString group; +}; + +ConfigGroup::ConfigGroup(QObject *parent) + : QObject(parent) + , d(new ConfigGroupPrivate(this)) +{ + // Delay and compress everything within 5 seconds into one sync + d->synchTimer = new QTimer(this); + d->synchTimer->setSingleShot(true); + d->synchTimer->setInterval(1500); + connect(d->synchTimer, &QTimer::timeout, this, &ConfigGroup::sync); +} + +ConfigGroup::~ConfigGroup() +{ + if (d->synchTimer->isActive()) { + // qDebug() << "SYNC......"; + d->synchTimer->stop(); + sync(); + } + + delete d; +} + +KConfigGroup *ConfigGroup::configGroup() +{ + return d->configGroup; +} + +QString ConfigGroup::file() const +{ + return d->file; +} + +void ConfigGroup::setFile(const QString &filename) +{ + if (d->file == filename) { + return; + } + d->file = filename; + d->config = nullptr; + readConfigFile(); + Q_EMIT fileChanged(); +} + +KSharedConfigPtr ConfigGroup::config() const +{ + return d->config; +} + +void ConfigGroup::setConfig(KSharedConfigPtr config) +{ + if (d->config == config) { + return; + } + + d->config = config; + + if (d->config) { + d->file = config->name(); + } else { + d->file.clear(); + } + + readConfigFile(); +} + +QString ConfigGroup::group() const +{ + return d->group; +} + +void ConfigGroup::setGroup(const QString &groupname) +{ + if (d->group == groupname) { + return; + } + d->group = groupname; + readConfigFile(); + Q_EMIT groupChanged(); + Q_EMIT keyListChanged(); +} + +QStringList ConfigGroup::keyList() const +{ + if (!d->configGroup) { + return QStringList(); + } + return d->configGroup->keyList(); +} + +QStringList ConfigGroup::groupList() const +{ + if (!d->configGroup) { + return QStringList(); + } + return d->configGroup->groupList(); +} + +bool ConfigGroup::readConfigFile() +{ + // Find parent ConfigGroup + ConfigGroup *parentGroup = nullptr; + QObject *current = parent(); + while (current) { + parentGroup = qobject_cast(current); + if (parentGroup) { + break; + } + current = current->parent(); + } + + delete d->configGroup; + d->configGroup = nullptr; + + if (parentGroup) { + d->configGroup = new KConfigGroup(parentGroup->configGroup(), d->group); + return true; + } else { + if (!d->config) { + if (d->file.isEmpty()) { + qCWarning(PLASMASHELL) << "Could not find KConfig Parent: specify a file or parent to another ConfigGroup"; + return false; + } + + d->config = KSharedConfig::openConfig(d->file); + } + + d->configGroup = new KConfigGroup(d->config, d->group); + return true; + } +} + +// Bound methods and slots + +bool ConfigGroup::writeEntry(const QString &key, const QJSValue &value) +{ + if (!d->configGroup) { + return false; + } + + d->configGroup->writeEntry(key, value.toVariant()); + d->synchTimer->start(); + return true; +} + +QVariant ConfigGroup::readEntry(const QString &key) +{ + if (!d->configGroup) { + return QVariant(); + } + const QVariant value = d->configGroup->readEntry(key, QVariant("")); + // qDebug() << " reading setting: " << key << value; + return value; +} + +void ConfigGroup::deleteEntry(const QString &key) +{ + if (d->configGroup) { + d->configGroup->deleteEntry(key); + } +} + +void ConfigGroup::sync() +{ + if (d->configGroup) { + // qDebug() << "synching config..."; + d->configGroup->sync(); + } +} diff --git a/plasma/workspace/shell/scripting/configgroup.h b/plasma/workspace/shell/scripting/configgroup.h new file mode 100644 index 0000000000..6b34e9c0eb --- /dev/null +++ b/plasma/workspace/shell/scripting/configgroup.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2011-2012 Sebastian Kügler + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +class KConfigGroup; +class ConfigGroupPrivate; + +class ConfigGroup : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString file READ file WRITE setFile NOTIFY fileChanged) + Q_PROPERTY(QString group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(QStringList keyList READ keyList NOTIFY keyListChanged) + Q_PROPERTY(QStringList groupList READ groupList NOTIFY groupListChanged) + +public: + explicit ConfigGroup(QObject *parent = nullptr); + ~ConfigGroup() override; + + KConfigGroup *configGroup(); + + KSharedConfigPtr config() const; + void setConfig(KSharedConfigPtr config); + QString file() const; + void setFile(const QString &filename); + QString group() const; + void setGroup(const QString &groupname); + QStringList keyList() const; + QStringList groupList() const; + + Q_INVOKABLE QVariant readEntry(const QString &key); + Q_INVOKABLE bool writeEntry(const QString &key, const QJSValue &value); + Q_INVOKABLE void deleteEntry(const QString &key); + +Q_SIGNALS: + void fileChanged(); + void groupChanged(); + void keyListChanged(); + void groupListChanged(); + +private: + ConfigGroupPrivate *d; + + bool readConfigFile(); + +private Q_SLOTS: + void sync(); +}; diff --git a/plasma/workspace/shell/scripting/containment.cpp b/plasma/workspace/shell/scripting/containment.cpp new file mode 100644 index 0000000000..d3738d6a36 --- /dev/null +++ b/plasma/workspace/shell/scripting/containment.cpp @@ -0,0 +1,276 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "containment.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "scriptengine.h" +#include "shellcorona.h" +#include "widget.h" + +namespace WorkspaceScripting +{ +class Containment::Private +{ +public: + QPointer containment; + ShellCorona *corona; + QString oldWallpaperPlugin; + QString wallpaperPlugin; + QString oldWallpaperMode; + QString wallpaperMode; + + QString type; + QString plugin; +}; + +Containment::Containment(Plasma::Containment *containment, ScriptEngine *engine) + : Applet(engine) + , d(new Containment::Private) +{ + d->containment = containment; + d->corona = qobject_cast(containment->corona()); + + setCurrentConfigGroup(QStringList()); + setCurrentGlobalConfigGroup(QStringList()); + if (containment) { + d->oldWallpaperPlugin = d->wallpaperPlugin = containment->wallpaper(); + } +} + +Containment::~Containment() +{ + if (d->containment) { + if (d->oldWallpaperPlugin != d->wallpaperPlugin || d->oldWallpaperMode != d->wallpaperMode) { + d->containment->setWallpaper(d->wallpaperPlugin); + } + } + + reloadConfigIfNeeded(); + + delete d; +} + +ShellCorona *Containment::corona() const +{ + return d->corona; +} + +int Containment::screen() const +{ + if (!d->containment) { + return -1; + } + + return d->containment->screen(); +} + +QString Containment::wallpaperPlugin() const +{ + return d->wallpaperPlugin; +} + +void Containment::setWallpaperPlugin(const QString &wallpaperPlugin) +{ + d->wallpaperPlugin = wallpaperPlugin; +} + +QString Containment::wallpaperMode() const +{ + return d->wallpaperMode; +} + +void Containment::setWallpaperMode(const QString &wallpaperMode) +{ + d->wallpaperMode = wallpaperMode; +} + +QString Containment::formFactor() const +{ + if (!d->containment) { + return QStringLiteral("Planar"); + } + + switch (d->containment->formFactor()) { + case Plasma::Types::Planar: + return QStringLiteral("planar"); + case Plasma::Types::MediaCenter: + return QStringLiteral("mediacenter"); + case Plasma::Types::Horizontal: + return QStringLiteral("horizontal"); + case Plasma::Types::Vertical: + return QStringLiteral("vertical"); + case Plasma::Types::Application: + return QStringLiteral("application"); + } + + return QStringLiteral("Planar"); +} + +QList Containment::widgetIds() const +{ + // FIXME: the ints could overflow since Applet::id() returns a uint, + // however QScript deals with QList very, very poory + QList w; + + if (d->containment) { + foreach (const Plasma::Applet *applet, d->containment->applets()) { + w.append(applet->id()); + } + } + + return w; +} + +QJSValue Containment::widgetById(const QJSValue ¶mId) const +{ + if (!paramId.isNumber()) { + return engine()->newError(i18n("widgetById requires an id")); + } + + const uint id = paramId.toInt(); + + if (d->containment) { + foreach (Plasma::Applet *w, d->containment->applets()) { + if (w->id() == id) { + return engine()->wrap(w); + } + } + } + + return QJSValue(); +} + +QJSValue Containment::addWidget(const QJSValue &v, qreal x, qreal y, qreal w, qreal h, const QVariantList &args) +{ + if (!v.isString() && !v.isQObject()) { + return engine()->newError(i18n("addWidget requires a name of a widget or a widget object")); + } + + if (!d->containment) { + return QJSValue(); + } + + QRectF geometry(x, y, w, h); + + Plasma::Applet *applet = nullptr; + if (v.isString()) { + // A position has been supplied: search for the containment's graphics object + QQuickItem *containmentItem = nullptr; + + if (geometry.x() >= 0 && geometry.y() >= 0) { + containmentItem = d->containment->property("_plasma_graphicObject").value(); + + if (containmentItem) { + QMetaObject::invokeMethod(containmentItem, + "createApplet", + Qt::DirectConnection, + Q_RETURN_ARG(Plasma::Applet *, applet), + Q_ARG(QString, v.toString()), + Q_ARG(QVariantList, args), + Q_ARG(QRectF, geometry)); + } + if (applet) { + return engine()->wrap(applet); + } + return engine()->newError(i18n("Could not create the %1 widget!", v.toString())); + } + + // Case in which either: + // * a geometry wasn't provided + // * containmentItem wasn't found + applet = d->containment->createApplet(v.toString(), args); + + if (applet) { + return engine()->wrap(applet); + } + + return engine()->newError(i18n("Could not create the %1 widget!", v.toString())); + } else if (Widget *widget = qobject_cast(v.toQObject())) { + applet = widget->applet(); + d->containment->addApplet(applet); + return v; + } + + return QJSValue(); +} + +QJSValue Containment::widgets(const QString &widgetType) const +{ + if (!d->containment) { + return QJSValue(); + } + + QJSValue widgets = engine()->newArray(); + int count = 0; + + foreach (Plasma::Applet *widget, d->containment->applets()) { + if (widgetType.isEmpty() || widget->pluginMetaData().pluginId() == widgetType) { + widgets.setProperty(count, engine()->wrap(widget)); + ++count; + } + } + + widgets.setProperty(QStringLiteral("length"), count); + return widgets; +} + +uint Containment::id() const +{ + if (!d->containment) { + return 0; + } + + return d->containment->id(); +} + +QString Containment::type() const +{ + if (!d->containment) { + return QString(); + } + + return d->containment->pluginMetaData().pluginId(); +} + +void Containment::remove() +{ + if (d->containment) { + d->containment->destroy(); + } +} + +void Containment::showConfigurationInterface() +{ + if (d->containment) { + QAction *configAction = d->containment->actions()->action(QStringLiteral("configure")); + if (configAction && configAction->isEnabled()) { + configAction->trigger(); + } + } +} + +Plasma::Applet *Containment::applet() const +{ + return d->containment; +} + +Plasma::Containment *Containment::containment() const +{ + return d->containment; +} + +} diff --git a/plasma/workspace/shell/scripting/containment.h b/plasma/workspace/shell/scripting/containment.h new file mode 100644 index 0000000000..6c9d99ccd3 --- /dev/null +++ b/plasma/workspace/shell/scripting/containment.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "applet.h" + +namespace Plasma +{ +class Containment; +} // namespace Plasma + +class ShellCorona; + +namespace WorkspaceScripting +{ +class ScriptEngine; + +class Containment : public Applet +{ + Q_OBJECT + /// FIXME: add NOTIFY + Q_PROPERTY(QString version READ version) + Q_PROPERTY(QStringList configKeys READ configKeys) + Q_PROPERTY(QStringList configGroups READ configGroups) + Q_PROPERTY(QStringList globalConfigKeys READ globalConfigKeys) + Q_PROPERTY(QStringList globalConfigGroups READ globalConfigGroups) + Q_PROPERTY(QStringList currentConfigGroup WRITE setCurrentConfigGroup READ currentConfigGroup) + Q_PROPERTY(QString wallpaperPlugin READ wallpaperPlugin WRITE setWallpaperPlugin) + Q_PROPERTY(QString wallpaperMode READ wallpaperMode WRITE setWallpaperMode) + Q_PROPERTY(bool locked READ locked WRITE setLocked) + Q_PROPERTY(QString type READ type) + Q_PROPERTY(QString formFactor READ formFactor) + Q_PROPERTY(QList widgetIds READ widgetIds) + Q_PROPERTY(int screen READ screen) + Q_PROPERTY(int id READ id) + +public: + explicit Containment(Plasma::Containment *containment, ScriptEngine *parent); + ~Containment() override; + + uint id() const; + QString type() const; + QString formFactor() const; + QList widgetIds() const; + + int screen() const; + + Plasma::Applet *applet() const override; + Plasma::Containment *containment() const; + + QString wallpaperPlugin() const; + void setWallpaperPlugin(const QString &wallpaperPlugin); + QString wallpaperMode() const; + void setWallpaperMode(const QString &wallpaperMode); + + Q_INVOKABLE QJSValue widgetById(const QJSValue ¶mId = QJSValue()) const; + Q_INVOKABLE QJSValue + addWidget(const QJSValue &v = QJSValue(), qreal x = -1, qreal y = -1, qreal w = -1, qreal h = -1, const QVariantList &args = QVariantList()); + Q_INVOKABLE QJSValue widgets(const QString &widgetType = QString()) const; + +public Q_SLOTS: + void remove(); + void showConfigurationInterface(); + +protected: + ShellCorona *corona() const; + +private: + class Private; + Private *const d; +}; + +} diff --git a/plasma/workspace/shell/scripting/panel.cpp b/plasma/workspace/shell/scripting/panel.cpp new file mode 100644 index 0000000000..308f887439 --- /dev/null +++ b/plasma/workspace/shell/scripting/panel.cpp @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "panel.h" + +#include +#include + +#include +#include +#include + +#include "panelview.h" +#include "scriptengine.h" +#include "shellcorona.h" +#include "widget.h" + +namespace WorkspaceScripting +{ +Panel::Panel(Plasma::Containment *containment, ScriptEngine *engine) + : Containment(containment, engine) +{ +} + +Panel::~Panel() +{ +} + +QString Panel::location() const +{ + Plasma::Containment *c = containment(); + if (!c) { + return "floating"; + } + + switch (c->location()) { + case Plasma::Types::Floating: + return "floating"; + case Plasma::Types::Desktop: + return "desktop"; + case Plasma::Types::FullScreen: + return "fullscreen"; + case Plasma::Types::TopEdge: + return "top"; + case Plasma::Types::BottomEdge: + return "bottom"; + case Plasma::Types::LeftEdge: + return "left"; + case Plasma::Types::RightEdge: + return "right"; + } + + return "floating"; +} + +void Panel::setLocation(const QString &locationString) +{ + Plasma::Containment *c = containment(); + if (!c) { + return; + } + + const QString lower = locationString.toLower(); + Plasma::Types::Location loc = Plasma::Types::Floating; + Plasma::Types::FormFactor ff = Plasma::Types::Planar; + if (lower == "desktop") { + loc = Plasma::Types::Desktop; + } else if (lower == "fullscreen") { + loc = Plasma::Types::FullScreen; + } else if (lower == "top") { + loc = Plasma::Types::TopEdge; + ff = Plasma::Types::Horizontal; + } else if (lower == "bottom") { + loc = Plasma::Types::BottomEdge; + ff = Plasma::Types::Horizontal; + } else if (lower == "left") { + loc = Plasma::Types::LeftEdge; + ff = Plasma::Types::Vertical; + } else if (lower == "right") { + loc = Plasma::Types::RightEdge; + ff = Plasma::Types::Vertical; + } + + c->setLocation(loc); + c->setFormFactor(ff); +} + +PanelView *Panel::panel() const +{ + Plasma::Containment *c = containment(); + if (!c || !corona()) { + return nullptr; + } + + return corona()->panelView(c); +} + +// NOTE: this is used *only* for alignment and visibility +KConfigGroup Panel::panelConfig() const +{ + int screenNum = qMax(screen(), 0); // if we don't have a screen (-1) we'll be put on screen 0 + + if (QGuiApplication::screens().size() < screenNum) { + return KConfigGroup(); + } + QScreen *s = QGuiApplication::screens().at(screenNum); + return PanelView::panelConfig(corona(), containment(), s); +} + +// NOTE: when we don't have a view we should write only to the defaults group as we don't know yet during startup if we are on the "final" screen resolution yet +KConfigGroup Panel::panelConfigDefaults() const +{ + int screenNum = qMax(screen(), 0); // if we don't have a screen (-1) we'll be put on screen 0 + + if (QGuiApplication::screens().size() < screenNum) { + return KConfigGroup(); + } + QScreen *s = QGuiApplication::screens().at(screenNum); + return PanelView::panelConfigDefaults(corona(), containment(), s); +} + +// NOTE: Alignment is the only one that reads and writes directly from panelconfig() +QString Panel::alignment() const +{ + int alignment; + if (panel()) { + alignment = panel()->alignment(); + } else { + alignment = panelConfig().readEntry("alignment", 0); + } + + switch (alignment) { + case Qt::AlignRight: + return "right"; + case Qt::AlignCenter: + return "center"; + default: + return "left"; + } + + return "left"; +} + +// NOTE: Alignment is the only one that reads and writes directly from panelconfig() +void Panel::setAlignment(const QString &alignment) +{ + int a = Qt::AlignLeft; + if (alignment.compare("right", Qt::CaseInsensitive) == 0) { + a = Qt::AlignRight; + } else if (alignment.compare("center", Qt::CaseInsensitive) == 0) { + a = Qt::AlignCenter; + } + + // Always prefer the view, if available + if (panel()) { + panel()->setAlignment(Qt::Alignment(a)); + } else { + panelConfig().writeEntry("alignment", a); + } +} + +// From now on only panelConfigDefaults should be used +int Panel::offset() const +{ + if (panel()) { + return panel()->offset(); + } else { + return panelConfigDefaults().readEntry("offset", 0); + } +} + +void Panel::setOffset(int pixels) +{ + panelConfigDefaults().writeEntry("offset", pixels); + if (panel()) { + panel()->setOffset(pixels); + } else { + panelConfigDefaults().readEntry("offset", pixels); + } +} + +int Panel::length() const +{ + if (panel()) { + return panel()->length(); + } else { + return panelConfigDefaults().readEntry("length", 0); + } +} + +void Panel::setLength(int pixels) +{ + if (panel()) { + panel()->setLength(pixels); + } else { + panelConfigDefaults().writeEntry("length", pixels); + } +} + +int Panel::minimumLength() const +{ + if (panel()) { + return panel()->minimumLength(); + } else { + return panelConfigDefaults().readEntry("minLength", 0); + } +} + +void Panel::setMinimumLength(int pixels) +{ + if (panel()) { + panel()->setMinimumLength(pixels); + } else { + panelConfigDefaults().writeEntry("minLength", pixels); + } +} + +int Panel::maximumLength() const +{ + if (panel()) { + return panel()->maximumLength(); + } else { + return panelConfigDefaults().readEntry("maxLength", 0); + } +} + +void Panel::setMaximumLength(int pixels) +{ + if (panel()) { + panel()->setMaximumLength(pixels); + } else { + panelConfigDefaults().writeEntry("maxLength", pixels); + } +} + +int Panel::height() const +{ + if (panel()) { + return panel()->thickness(); + } else { + return panelConfigDefaults().readEntry("thickness", 0); + } +} + +void Panel::setHeight(int height) +{ + if (panel()) { + panel()->setThickness(height); + } else { + panelConfigDefaults().writeEntry("thickness", height); + } +} + +QString Panel::hiding() const +{ + int visibility; + if (panel()) { + visibility = panel()->visibilityMode(); + } else { + visibility = panelConfig().readEntry("panelVisibility", 0); + } + + switch (visibility) { + case PanelView::NormalPanel: + return "none"; + case PanelView::AutoHide: + return "autohide"; + case PanelView::LetWindowsCover: + return "windowscover"; + case PanelView::WindowsGoBelow: + return "windowsbelow"; + } + return "none"; +} + +void Panel::setHiding(const QString &mode) +{ + PanelView::VisibilityMode visibilityMode = PanelView::NormalPanel; + if (mode.compare("autohide", Qt::CaseInsensitive) == 0) { + visibilityMode = PanelView::AutoHide; + } else if (mode.compare("windowscover", Qt::CaseInsensitive) == 0) { + visibilityMode = PanelView::LetWindowsCover; + } else if (mode.compare("windowsbelow", Qt::CaseInsensitive) == 0) { + visibilityMode = PanelView::WindowsGoBelow; + } + + if (panel()) { + panel()->setVisibilityMode(visibilityMode); + } else { + panelConfig().writeEntry("panelVisibility", (int)visibilityMode); + } +} + +} diff --git a/plasma/workspace/shell/scripting/panel.h b/plasma/workspace/shell/scripting/panel.h new file mode 100644 index 0000000000..5d8b2c4c02 --- /dev/null +++ b/plasma/workspace/shell/scripting/panel.h @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "containment.h" + +class PanelView; +class ShellCorona; + +namespace WorkspaceScripting +{ +class Panel : public Containment +{ + Q_OBJECT + Q_PROPERTY(QStringList configKeys READ configKeys) + Q_PROPERTY(QStringList configGroups READ configGroups) + Q_PROPERTY(QStringList currentConfigGroup WRITE setCurrentConfigGroup READ currentConfigGroup) + Q_PROPERTY(QStringList globalConfigKeys READ globalConfigKeys) + Q_PROPERTY(QStringList globalConfigGroups READ globalConfigGroups) + + Q_PROPERTY(QString version READ version) + Q_PROPERTY(QString type READ type) + Q_PROPERTY(QString formFactor READ formFactor) + Q_PROPERTY(QList widgetIds READ widgetIds) + Q_PROPERTY(int screen READ screen) + Q_PROPERTY(QString location READ location WRITE setLocation) + Q_PROPERTY(int id READ id) + + Q_PROPERTY(bool locked READ locked WRITE setLocked) + + // panel properties + Q_PROPERTY(QString alignment READ alignment WRITE setAlignment) + Q_PROPERTY(int offset READ offset WRITE setOffset) + Q_PROPERTY(int length READ length WRITE setLength) + Q_PROPERTY(int minimumLength READ minimumLength WRITE setMinimumLength) + Q_PROPERTY(int maximumLength READ maximumLength WRITE setMaximumLength) + Q_PROPERTY(int height READ height WRITE setHeight) + Q_PROPERTY(QString hiding READ hiding WRITE setHiding) + +public: + explicit Panel(Plasma::Containment *containment, ScriptEngine *parent); + ~Panel() override; + + QString location() const; + void setLocation(const QString &location); + + QString alignment() const; + void setAlignment(const QString &alignment); + + int offset() const; + void setOffset(int pixels); + + int length() const; + void setLength(int pixels); + + int minimumLength() const; + void setMinimumLength(int pixels); + + int maximumLength() const; + void setMaximumLength(int pixels); + + int height() const; + void setHeight(int height); + + QString hiding() const; + void setHiding(const QString &mode); + +public Q_SLOTS: + void remove() + { + Containment::remove(); + } + void showConfigurationInterface() + { + Containment::showConfigurationInterface(); + } + +private: + PanelView *panel() const; + KConfigGroup panelConfig() const; + KConfigGroup panelConfigDefaults() const; + + ShellCorona *m_corona; +}; + +} diff --git a/plasma/workspace/shell/scripting/plasma-layouttemplate.desktop b/plasma/workspace/shell/scripting/plasma-layouttemplate.desktop new file mode 100644 index 0000000000..0c5c9e18c2 --- /dev/null +++ b/plasma/workspace/shell/scripting/plasma-layouttemplate.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=ServiceType +X-KDE-ServiceType=Plasma/LayoutTemplate + +[PropertyDef::X-Plasma-Shell] +Type=QString + +[PropertyDef::X-Plasma-ContainmentCategories] +Type=QStringList diff --git a/plasma/workspace/shell/scripting/scriptengine.cpp b/plasma/workspace/shell/scripting/scriptengine.cpp new file mode 100644 index 0000000000..a18e4f332f --- /dev/null +++ b/plasma/workspace/shell/scripting/scriptengine.cpp @@ -0,0 +1,424 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "scriptengine.h" +#include "debug.h" +#include "scriptengine_v1.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../screenpool.h" +#include "../standaloneappcorona.h" +#include "appinterface.h" +#include "configgroup.h" +#include "containment.h" +#include "panel.h" +#include "widget.h" + +namespace WorkspaceScripting +{ +ScriptEngine::ScriptEngine(Plasma::Corona *corona, QObject *parent) + : QJSEngine(parent) + , m_corona(corona) +{ + Q_ASSERT(m_corona); + m_appInterface = new AppInterface(this); + connect(m_appInterface, &AppInterface::print, this, &ScriptEngine::print); + m_scriptSelf = globalObject(); + m_globalScriptEngineObject = new ScriptEngine::V1(this); + m_localizedContext = new KLocalizedContext(this); + setupEngine(); +} + +ScriptEngine::~ScriptEngine() +{ +} + +QString ScriptEngine::errorString() const +{ + return m_errorString; +} + +QJSValue ScriptEngine::wrap(Plasma::Applet *w) +{ + Widget *wrapper = new Widget(w, this); + return newQObject(wrapper); +} + +QJSValue ScriptEngine::wrap(Plasma::Containment *c) +{ + Containment *wrapper = isPanel(c) ? new Panel(c, this) : new Containment(c, this); + return newQObject(wrapper); +} + +int ScriptEngine::defaultPanelScreen() const +{ + return 1; +} + +QJSValue ScriptEngine::newError(const QString &message) +{ + return evaluate(QStringLiteral("new Error('%1');").arg(message)); +} + +QString ScriptEngine::onlyExec(const QString &commandLine) +{ + if (commandLine.isEmpty()) { + return commandLine; + } + + return KShell::splitArgs(commandLine, KShell::TildeExpand).first(); +} + +void ScriptEngine::setupEngine() +{ + QJSValue globalScriptEngineObject = newQObject(m_globalScriptEngineObject); + QJSValue localizedContext = newQObject(m_localizedContext); + QJSValue appInterface = newQObject(m_appInterface); + + // AppInterface stuff + // FIXME: this line doesn't have much effect for now, if QTBUG-68397 gets fixed, + // all the connects to rewrite the properties won't be necessary anymore + // globalObject().setPrototype(appInterface); + // FIXME: remove __AppInterface if QTBUG-68397 gets solved + // as workaround we build manually a js object with getters and setters + m_scriptSelf.setProperty(QStringLiteral("__AppInterface"), appInterface); + QJSValue res = evaluate( + "__proto__ = {\ + get locked() {return __AppInterface.locked;},\ + get hasBattery() {return __AppInterface.hasBattery;},\ + get screenCount() {return __AppInterface.screenCount;},\ + get activityIds() {return __AppInterface.activityIds;},\ + get panelIds() {return __AppInterface.panelIds;},\ + get knownPanelTypes() {return __AppInterface.knownPanelTypes;},\ + get knownActivityTypes() {return __AppInterface.knownActivityTypes;},\ + get knownWidgetTypes() {return __AppInterface.knownWidgetTypes;},\ + get theme() {return __AppInterface.theme;},\ + set theme(name) {__AppInterface.theme = name;},\ + get applicationVersion() {return __AppInterface.applicationVersion;},\ + get platformVersion() {return __AppInterface.platformVersion;},\ + get scriptingVersion() {return __AppInterface.scriptingVersion;},\ + get multihead() {return __AppInterface.multihead;},\ + get multiheadScreen() {return __AppInterface.multihead;},\ + get locale() {return __AppInterface.locale;},\ + get language() {return __AppInterface.language;},\ + get languageId() {return __AppInterface.languageId;},\ + }"); + Q_ASSERT(!res.isError()); + // methods from AppInterface + m_scriptSelf.setProperty(QStringLiteral("screenGeometry"), appInterface.property("screenGeometry")); + m_scriptSelf.setProperty(QStringLiteral("lockCorona"), appInterface.property("lockCorona")); + m_scriptSelf.setProperty(QStringLiteral("sleep"), appInterface.property("sleep")); + m_scriptSelf.setProperty(QStringLiteral("print"), appInterface.property("print")); + + m_scriptSelf.setProperty(QStringLiteral("getApiVersion"), globalScriptEngineObject.property("getApiVersion")); + + // Constructors: prefer them js based as they make the c++ code of panel et al way simpler without hacks to get the engine + m_scriptSelf.setProperty(QStringLiteral("__newPanel"), globalScriptEngineObject.property("newPanel")); + m_scriptSelf.setProperty(QStringLiteral("__newConfigFile"), globalScriptEngineObject.property("configFile")); + // definitions of qrectf properties from documentation + // only properties/functions which were already binded are. + // TODO KF6: just a plain QRectF binding + res = evaluate( + "function QRectF(x,y,w,h) {\ + return {x: x, y: y, width: w, height: h,\ + get left() {return this.x},\ + get top() {return this.y},\ + get right() {return this.x + this.width},\ + get bottom() {return this.y + this.height},\ + get empty() {return this.width <= 0 || this.height <= 0},\ + get null() {return this.width == 0 || this.height == 0},\ + get valid() {return !this.empty},\ + adjust: function(dx1, dy1, dx2, dy2) {\ + this.x += dx1; this.y += dy1;\ + this.width = this.width - dx1 + dx2;\ + this.height = this.height - dy1 + dy2;},\ + adjusted: function(dx1, dy1, dx2, dy2) {\ + return new QRectF(this.x + dx1, this.y + dy1,\ + this.width - dx1 + dx2,\ + this.height - dy1 + dy2)},\ + translate: function(dx, dy) {this.x += dx; this.y += dy;},\ + setCoords: function(x1, y1, x2, y2) {\ + this.x = x1; this.y = y1;\ + this.width = x2 - x1;\ + this.height = y2 - y1;},\ + setRect: function(x1, y1, w1, h1) {\ + this.x = x1; this.y = y1;\ + this.width = w1; this.height = h1;},\ + contains: function(x1, y1) { return x1 >= this.x && x1 <= this.x + this.width && y1 >= this.y && y1 <= this.y + this.height},\ + moveBottom: function(bottom1) {this.y = bottom1 - this.height;},\ + moveLeft: function(left1) {this.x = left1;},\ + moveRight: function(right1) {this.x = right1 - this.width;},\ + moveTop: function(top1) {this.y = top1;},\ + moveTo: function(x1, y1) {this.x = x1; this.y = y1;}\ + }};\ + function ConfigFile(config, group){return __newConfigFile(config, group)};\ + function Panel(plugin){return __newPanel(plugin)};"); + Q_ASSERT(!res.isError()); + + m_scriptSelf.setProperty(QStringLiteral("createActivity"), globalScriptEngineObject.property("createActivity")); + m_scriptSelf.setProperty(QStringLiteral("setCurrentActivity"), globalScriptEngineObject.property("setCurrentActivity")); + m_scriptSelf.setProperty(QStringLiteral("currentActivity"), globalScriptEngineObject.property("currentActivity")); + m_scriptSelf.setProperty(QStringLiteral("activities"), globalScriptEngineObject.property("activities")); + m_scriptSelf.setProperty(QStringLiteral("activityName"), globalScriptEngineObject.property("activityName")); + m_scriptSelf.setProperty(QStringLiteral("setActivityName"), globalScriptEngineObject.property("setActivityName")); + m_scriptSelf.setProperty(QStringLiteral("loadSerializedLayout"), globalScriptEngineObject.property("loadSerializedLayout")); + m_scriptSelf.setProperty(QStringLiteral("desktopsForActivity"), globalScriptEngineObject.property("desktopsForActivity")); + m_scriptSelf.setProperty(QStringLiteral("desktops"), globalScriptEngineObject.property("desktops")); + m_scriptSelf.setProperty(QStringLiteral("desktopById"), globalScriptEngineObject.property("desktopById")); + m_scriptSelf.setProperty(QStringLiteral("desktopForScreen"), globalScriptEngineObject.property("desktopForScreen")); + m_scriptSelf.setProperty(QStringLiteral("screenForConnector"), globalScriptEngineObject.property("screenForConnector")); + m_scriptSelf.setProperty(QStringLiteral("panelById"), globalScriptEngineObject.property("panelById")); + m_scriptSelf.setProperty(QStringLiteral("panels"), globalScriptEngineObject.property("panels")); + m_scriptSelf.setProperty(QStringLiteral("fileExists"), globalScriptEngineObject.property("fileExists")); + m_scriptSelf.setProperty(QStringLiteral("loadTemplate"), globalScriptEngineObject.property("loadTemplate")); + m_scriptSelf.setProperty(QStringLiteral("applicationExists"), globalScriptEngineObject.property("applicationExists")); + m_scriptSelf.setProperty(QStringLiteral("defaultApplication"), globalScriptEngineObject.property("defaultApplication")); + m_scriptSelf.setProperty(QStringLiteral("userDataPath"), globalScriptEngineObject.property("userDataPath")); + m_scriptSelf.setProperty(QStringLiteral("applicationPath"), globalScriptEngineObject.property("applicationPath")); + m_scriptSelf.setProperty(QStringLiteral("knownWallpaperPlugins"), globalScriptEngineObject.property("knownWallpaperPlugins")); + m_scriptSelf.setProperty(QStringLiteral("gridUnit"), globalScriptEngineObject.property("gridUnit")); + m_scriptSelf.setProperty(QStringLiteral("setImmutability"), globalScriptEngineObject.property("setImmutability")); + m_scriptSelf.setProperty(QStringLiteral("immutability"), globalScriptEngineObject.property("immutability")); + + // i18n + m_scriptSelf.setProperty(QStringLiteral("i18n"), localizedContext.property("i18n")); + m_scriptSelf.setProperty(QStringLiteral("i18nc"), localizedContext.property("i18nc")); + m_scriptSelf.setProperty(QStringLiteral("i18np"), localizedContext.property("i18np")); + m_scriptSelf.setProperty(QStringLiteral("i18ncp"), localizedContext.property("i18ncp")); + m_scriptSelf.setProperty(QStringLiteral("i18nd"), localizedContext.property("i18nd")); + m_scriptSelf.setProperty(QStringLiteral("i18ndc"), localizedContext.property("i18ndc")); + m_scriptSelf.setProperty(QStringLiteral("i18ndp"), localizedContext.property("i18ndp")); + m_scriptSelf.setProperty(QStringLiteral("i18ndcp"), localizedContext.property("i18ndcp")); + + m_scriptSelf.setProperty(QStringLiteral("xi18n"), localizedContext.property("xi18n")); + m_scriptSelf.setProperty(QStringLiteral("xi18nc"), localizedContext.property("xi18nc")); + m_scriptSelf.setProperty(QStringLiteral("xi18np"), localizedContext.property("xi18np")); + m_scriptSelf.setProperty(QStringLiteral("xi18ncp"), localizedContext.property("xi18ncp")); + m_scriptSelf.setProperty(QStringLiteral("xi18nd"), localizedContext.property("xi18nd")); + m_scriptSelf.setProperty(QStringLiteral("xi18ndc"), localizedContext.property("xi18ndc")); + m_scriptSelf.setProperty(QStringLiteral("xi18ndp"), localizedContext.property("xi18ndp")); + m_scriptSelf.setProperty(QStringLiteral("xi18ndcp"), localizedContext.property("xi18ndcp")); +} + +bool ScriptEngine::isPanel(const Plasma::Containment *c) +{ + if (!c) { + return false; + } + + return c->containmentType() == Plasma::Types::PanelContainment || c->containmentType() == Plasma::Types::CustomPanelContainment; +} + +Plasma::Corona *ScriptEngine::corona() const +{ + return m_corona; +} + +bool ScriptEngine::evaluateScript(const QString &script, const QString &path) +{ + m_errorString = QString(); + + QJSValue result = evaluate(script, path); + if (result.isError()) { + QString error = i18n("Error: %1 at line %2\n\nBacktrace:\n%3", + result.toString(), + result.property("lineNumber").toInt(), + result.property("stack").toVariant().value().join(QLatin1String("\n "))); + Q_EMIT printError(error); + Q_EMIT exception(result); + m_errorString = error; + return false; + } + + return true; +} + +void ScriptEngine::exception(const QJSValue &value) +{ + Q_EMIT printError(value.toVariant().toString()); +} + +QStringList ScriptEngine::pendingUpdateScripts(Plasma::Corona *corona) +{ + if (!corona->kPackage().isValid()) { + qCWarning(PLASMASHELL) << "Warning: corona package invalid"; + return QStringList(); + } + + const QString appName = corona->kPackage().metadata().pluginId(); + QStringList scripts; + + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, + "plasma/shells/" + appName + QStringLiteral("/contents/updates"), + QStandardPaths::LocateDirectory); + for (const QString &dir : dirs) { + QDirIterator it(dir, QStringList() << QStringLiteral("*.js")); + while (it.hasNext()) { + scripts.append(it.next()); + } + } + QStringList scriptPaths; + + if (scripts.isEmpty()) { + return scriptPaths; + } + + KConfigGroup cg(KSharedConfig::openConfig(), "Updates"); + QStringList performed = cg.readEntry("performed", QStringList()); + const QString localXdgDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + + foreach (const QString &script, scripts) { + if (performed.contains(script)) { + continue; + } + + if (script.startsWith(localXdgDir)) { + continue; + } + + scriptPaths.append(script); + performed.append(script); + } + + cg.writeEntry("performed", performed); + KSharedConfig::openConfig()->sync(); + return scriptPaths; +} + +QStringList ScriptEngine::availableActivities() const +{ + ShellCorona *sc = qobject_cast(m_corona); + StandaloneAppCorona *ac = qobject_cast(m_corona); + if (sc) { + return sc->availableActivities(); + } else if (ac) { + return ac->availableActivities(); + } + + return QStringList(); +} + +QList ScriptEngine::desktopsForActivity(const QString &id) +{ + QList result; + + // confirm this activity actually exists + bool found = false; + for (const QString &act : availableActivities()) { + if (act == id) { + found = true; + break; + } + } + + if (!found) { + return result; + } + + foreach (Plasma::Containment *c, m_corona->containments()) { + if (c->activity() == id && !isPanel(c)) { + result << new Containment(c, this); + } + } + + if (result.count() == 0) { + // we have no desktops for this activity, so lets make them now + // this can happen when the activity already exists but has never been activated + // with the current shell package and layout.js is run to set up the shell for the + // first time + ShellCorona *sc = qobject_cast(m_corona); + StandaloneAppCorona *ac = qobject_cast(m_corona); + if (sc) { + foreach (int i, sc->screenIds()) { + result << new Containment(sc->createContainmentForActivity(id, i), this); + } + } else if (ac) { + const int numScreens = m_corona->numScreens(); + for (int i = 0; i < numScreens; ++i) { + result << new Containment(ac->createContainmentForActivity(id, i), this); + } + } + } + + return result; +} + +Plasma::Containment *ScriptEngine::createContainment(const QString &type, const QString &plugin) +{ + bool exists = false; + const QList list = Plasma::PluginLoader::listContainmentsMetaDataOfType(type); + foreach (const KPluginMetaData &pluginInfo, list) { + if (pluginInfo.pluginId() == plugin) { + exists = true; + break; + } + } + + if (!exists) { + return nullptr; + } + + Plasma::Containment *c = nullptr; + if (type == QLatin1String("Panel")) { + ShellCorona *sc = qobject_cast(m_corona); + StandaloneAppCorona *ac = qobject_cast(m_corona); + if (sc) { + c = sc->addPanel(plugin); + } else if (ac) { + c = ac->addPanel(plugin); + } + } else { + c = m_corona->createContainment(plugin); + } + + if (c) { + if (type == QLatin1String("Panel")) { + // some defaults + c->setFormFactor(Plasma::Types::Horizontal); + c->setLocation(Plasma::Types::TopEdge); + // we have to force lastScreen of the newly created containment, + // or it won't have a screen yet at that point, breaking JS code + // that relies on it + // NOTE: if we'll allow setting a panel screen from JS, it will have to use the following lines as well + KConfigGroup cg = c->config(); + cg.writeEntry(QStringLiteral("lastScreen"), 0); + c->restore(cg); + } + c->updateConstraints(Plasma::Types::AllConstraints | Plasma::Types::StartupCompletedConstraint); + c->flushPendingConstraintsEvents(); + } + + return c; +} + +Containment *ScriptEngine::createContainmentWrapper(const QString &type, const QString &plugin) +{ + Plasma::Containment *c = createContainment(type, plugin); + return isPanel(c) ? new Panel(c, this) : new Containment(c, this); +} + +} // namespace WorkspaceScripting diff --git a/plasma/workspace/shell/scripting/scriptengine.h b/plasma/workspace/shell/scripting/scriptengine.h new file mode 100644 index 0000000000..1cfaa9afb2 --- /dev/null +++ b/plasma/workspace/shell/scripting/scriptengine.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +#include + +#include "../shellcorona.h" + +namespace Plasma +{ +class Applet; +class Containment; +} // namespace Plasma + +class KLocalizedContext; + +namespace WorkspaceScripting +{ +class AppInterface; +class Containment; +class V1; + +class ScriptEngine : public QJSEngine +{ + Q_OBJECT + +public: + explicit ScriptEngine(Plasma::Corona *corona, QObject *parent = nullptr); + ~ScriptEngine() override; + + QString errorString() const; + + static QStringList pendingUpdateScripts(Plasma::Corona *corona); + + Plasma::Corona *corona() const; + QJSValue wrap(Plasma::Applet *w); + QJSValue wrap(Plasma::Containment *c); + virtual int defaultPanelScreen() const; + QJSValue newError(const QString &message); + + static bool isPanel(const Plasma::Containment *c); + + Plasma::Containment *createContainment(const QString &type, const QString &plugin); + +public Q_SLOTS: + bool evaluateScript(const QString &script, const QString &path = QString()); + +Q_SIGNALS: + void print(const QString &string); + void printError(const QString &string); + +private: + void setupEngine(); + static QString onlyExec(const QString &commandLine); + + // Script API versions + class V1; + + // helpers + QStringList availableActivities() const; + QList desktopsForActivity(const QString &id); + Containment *createContainmentWrapper(const QString &type, const QString &plugin); + +private Q_SLOTS: + void exception(const QJSValue &value); + +private: + Plasma::Corona *m_corona; + ScriptEngine::V1 *m_globalScriptEngineObject; + KLocalizedContext *m_localizedContext; + AppInterface *m_appInterface; + QJSValue m_scriptSelf; + QString m_errorString; +}; + +static const int PLASMA_DESKTOP_SCRIPTING_VERSION = 20; +} diff --git a/plasma/workspace/shell/scripting/scriptengine_v1.cpp b/plasma/workspace/shell/scripting/scriptengine_v1.cpp new file mode 100644 index 0000000000..f7f5aec01b --- /dev/null +++ b/plasma/workspace/shell/scripting/scriptengine_v1.cpp @@ -0,0 +1,856 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "scriptengine_v1.h" +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// KIO +//#include // no camelcase include + +#include +#include +#include +#include +#include +#include + +#include "../screenpool.h" +#include "../standaloneappcorona.h" +#include "appinterface.h" +#include "configgroup.h" +#include "containment.h" +#include "panel.h" +#include "widget.h" + +namespace +{ +template +inline void awaitFuture(const QFuture &future) +{ + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } +} + +class ScriptArray_forEach_Helper +{ +public: + ScriptArray_forEach_Helper(const QJSValue &array) + : array(array) + { + } + + // operator + is commonly used for these things + // to avoid having the lambda inside the parenthesis + template + void operator+(Function function) const + { + if (!array.isArray()) + return; + + int length = array.property("length").toInt(); + for (int i = 0; i < length; ++i) { + function(array.property(i)); + } + } + +private: + const QJSValue &array; +}; + +#define SCRIPT_ARRAY_FOREACH(Variable, Array) ScriptArray_forEach_Helper(Array) + [&](const QJSValue &Variable) + +class ScriptObject_forEach_Helper +{ +public: + ScriptObject_forEach_Helper(const QJSValue &object) + : object(object) + { + } + + // operator + is commonly used for these things + // to avoid having the lambda inside the parenthesis + template + void operator+(Function function) const + { + QJSValueIterator it(object); + while (it.hasNext()) { + it.next(); + function(it.name(), it.value()); + } + } + +private: + const QJSValue &object; +}; + +#define SCRIPT_OBJECT_FOREACH(Key, Value, Array) ScriptObject_forEach_Helper(Array) + [&](const QString &Key, const QJSValue &Value) + +// Case insensitive comparison of two strings +template +inline bool matches(const QString &object, const StringType &string) +{ + return object.compare(string, Qt::CaseInsensitive) == 0; +} +} + +namespace WorkspaceScripting +{ +ScriptEngine::V1::V1(ScriptEngine *parent) + : QObject(parent) + , m_engine(parent) +{ +} + +ScriptEngine::V1::~V1() +{ +} + +QJSValue ScriptEngine::V1::getApiVersion(const QJSValue ¶m) +{ + if (param.toInt() != 1) { + return m_engine->newError(i18n("maximum api version supported is 1")); + } + return m_engine->newQObject(this); +} + +int ScriptEngine::V1::gridUnit() const +{ + int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); + if (gridUnit % 2 != 0) { + gridUnit++; + } + + return gridUnit; +} + +QJSValue ScriptEngine::V1::desktopById(const QJSValue ¶m) const +{ + // this needs to work also for string of numberls, like "20" + if (param.isUndefined()) { + return m_engine->newError(i18n("desktopById required an id")); + } + + const quint32 id = param.toInt(); + + foreach (Plasma::Containment *c, m_engine->m_corona->containments()) { + if (c->id() == id && !isPanel(c)) { + return m_engine->wrap(c); + } + } + + return QJSValue(); +} + +QJSValue ScriptEngine::V1::desktopsForActivity(const QJSValue &actId) const +{ + if (!actId.isString()) { + return m_engine->newError(i18n("desktopsForActivity requires an id")); + } + + QJSValue containments = m_engine->newArray(); + int count = 0; + + const QString id = actId.toString(); + + const auto result = m_engine->desktopsForActivity(id); + + for (Containment *c : result) { + containments.setProperty(count, m_engine->newQObject(c)); + ++count; + } + + containments.setProperty(QStringLiteral("length"), count); + return containments; +} + +QJSValue ScriptEngine::V1::desktopForScreen(const QJSValue ¶m) const +{ + // this needs to work also for string of numberls, like "20" + if (param.isUndefined()) { + return m_engine->newError(i18n("activityForScreen requires a screen id")); + } + + const uint screen = param.toInt(); + const auto containments = m_engine->m_corona->containmentsForScreen(screen); + return m_engine->wrap(containments.empty() ? nullptr : containments[0]); +} + +QJSValue ScriptEngine::V1::screenForConnector(const QJSValue ¶m) const +{ + // this needs to work also for string of numerals, like "20" + if (param.isUndefined()) { + return m_engine->newError(i18n("screenForConnector requires a connector name")); + } + + const QString connector = param.toString(); + ShellCorona *sc = qobject_cast(m_engine->m_corona); + if (sc) { + return m_engine->toScriptValue(sc->screenPool()->id(connector)); + } + return m_engine->toScriptValue(-1); +} + +QJSValue ScriptEngine::V1::createActivity(const QJSValue &nameParam, const QString &pluginParam) +{ + if (!nameParam.isString()) { + return m_engine->newError(i18n("createActivity required the activity name")); + } + + QString plugin = pluginParam; + const QString name = nameParam.toString(); + + KActivities::Controller controller; + + // This is not the nicest way to do this, but createActivity + // is a synchronous API :/ + QFuture futureId = controller.addActivity(name); + awaitFuture(futureId); + + QString id = futureId.result(); + + qDebug() << "Setting default Containment plugin:" << plugin; + + ShellCorona *sc = qobject_cast(m_engine->m_corona); + StandaloneAppCorona *ac = qobject_cast(m_engine->m_corona); + if (sc) { + if (plugin.isEmpty() || plugin == QLatin1String("undefined")) { + plugin = sc->defaultContainmentPlugin(); + } + sc->insertActivity(id, plugin); + } else if (ac) { + if (plugin.isEmpty() || plugin == QLatin1String("undefined")) { + KConfigGroup shellCfg = KConfigGroup(KSharedConfig::openConfig(m_engine->m_corona->kPackage().filePath("defaults")), "Desktop"); + plugin = shellCfg.readEntry("Containment", "org.kde.desktopcontainment"); + } + ac->insertActivity(id, plugin); + } + + return m_engine->toScriptValue(id); +} + +QJSValue ScriptEngine::V1::setCurrentActivity(const QJSValue ¶m) +{ + if (!param.isString()) { + return m_engine->newError(i18n("setCurrentActivity required the activity id")); + } + + const QString id = param.toString(); + + KActivities::Controller controller; + + QFuture task = controller.setCurrentActivity(id); + awaitFuture(task); + + return task.result(); +} + +QJSValue ScriptEngine::V1::setActivityName(const QJSValue &idParam, const QJSValue &nameParam) +{ + if (!idParam.isString() || !nameParam.isString()) { + return m_engine->newError(i18n("setActivityName required the activity id and name")); + } + + const QString id = idParam.toString(); + const QString name = nameParam.toString(); + + KActivities::Controller controller; + + QFuture task = controller.setActivityName(id, name); + awaitFuture(task); + return QJSValue(); +} + +QJSValue ScriptEngine::V1::activityName(const QJSValue &idParam) const +{ + if (!idParam.isString()) { + return m_engine->newError(i18n("activityName required the activity id")); + } + + const QString id = idParam.toString(); + + KActivities::Info info(id); + + return QJSValue(info.name()); +} + +QString ScriptEngine::V1::currentActivity() const +{ + KActivities::Consumer consumer; + return consumer.currentActivity(); +} + +QJSValue ScriptEngine::V1::activities() const +{ + QJSValue acts = m_engine->newArray(); + int count = 0; + + const auto result = m_engine->availableActivities(); + + for (const auto &a : result) { + acts.setProperty(count, a); + ++count; + } + acts.setProperty(QStringLiteral("length"), count); + + return acts; +} + +// Utility function to process configs and config groups +template +void loadSerializedConfigs(Object *object, const QJSValue &configs) +{ + SCRIPT_OBJECT_FOREACH(escapedGroup, config, configs) + { + // If the config group is set, pass it on to the containment + QStringList groups = escapedGroup.split('/', Qt::SkipEmptyParts); + for (QString &group : groups) { + group = QUrl::fromPercentEncoding(group.toUtf8()); + } + qDebug() << "Config group" << groups; + object->setCurrentConfigGroup(groups); + + // Read other properties and set the configuration + SCRIPT_OBJECT_FOREACH(key, value, config) + { + object->writeConfig(key, value); + }; + }; +} + +QJSValue ScriptEngine::V1::loadSerializedLayout(const QJSValue &data) +{ + if (!data.isObject()) { + return m_engine->newError(i18n("loadSerializedLayout requires the JSON object to deserialize from")); + } + + if (data.property("serializationFormatVersion").toInt() != 1) { + return m_engine->newError(i18n("loadSerializedLayout: invalid version of the serialized object")); + } + + const auto desktops = m_engine->desktopsForActivity(KActivities::Consumer().currentActivity()); + Q_ASSERT_X(desktops.size() != 0, "V1::loadSerializedLayout", "We need desktops"); + + // qDebug() << "DESKTOP DESERIALIZATION: Loading desktops..."; + + int count = 0; + SCRIPT_ARRAY_FOREACH(desktopData, data.property("desktops")) + { + // If the template has more desktops than we do, ignore them + if (count >= desktops.size()) + return; + + auto desktop = desktops[count]; + // qDebug() << "DESKTOP DESERIALIZATION: var cont = desktopsArray[...]; " << count << " -> " << desktop; + + // Setting the wallpaper plugin because it is special + desktop->setWallpaperPlugin(desktopData.property("wallpaperPlugin").toString()); + // qDebug() << "DESKTOP DESERIALIZATION: cont->setWallpaperPlugin(...) " << desktop->wallpaperPlugin(); + + // Now, lets go through the configs + loadSerializedConfigs(desktop, desktopData.property("config")); + + // After the config, we want to load the applets + SCRIPT_ARRAY_FOREACH(appletData, desktopData.property("applets")) + { + // qDebug() << "DESKTOP DESERIALIZATION: Applet: " << appletData.toString(); + + auto appletObject = desktop->addWidget(appletData.property("plugin"), + appletData.property("geometry.x").toInt() * gridUnit(), + appletData.property("geometry.y").toInt() * gridUnit(), + appletData.property("geometry.width").toInt() * gridUnit(), + appletData.property("geometry.height").toInt() * gridUnit()); + + if (auto applet = qobject_cast(appletObject.toQObject())) { + // Now, lets go through the configs for the applet + loadSerializedConfigs(applet, appletData.property("config")); + } + }; + + count++; + }; + + // qDebug() << "PANEL DESERIALIZATION: Loading panels..."; + + SCRIPT_ARRAY_FOREACH(panelData, data.property("panels")) + { + const auto panel = qobject_cast(m_engine->createContainmentWrapper(QStringLiteral("Panel"), QStringLiteral("org.kde.panel"))); + + Q_ASSERT(panel); + + // Basic panel setup + panel->setLocation(panelData.property("location").toString()); + panel->setHeight(panelData.property("height").toNumber() * gridUnit()); + panel->setMaximumLength(panelData.property("maximumLength").toNumber() * gridUnit()); + panel->setMinimumLength(panelData.property("minimumLength").toNumber() * gridUnit()); + panel->setOffset(panelData.property("offset").toNumber() * gridUnit()); + panel->setAlignment(panelData.property("alignment").toString()); + panel->setHiding(panelData.property("hiding").toString()); + + // Loading the config for the panel + loadSerializedConfigs(panel, panelData.property("config")); + + // Now dealing with the applets + SCRIPT_ARRAY_FOREACH(appletData, panelData.property("applets")) + { + // qDebug() << "PANEL DESERIALIZATION: Applet: " << appletData.toString(); + + auto appletObject = panel->addWidget(appletData.property("plugin")); + // qDebug() << "PANEL DESERIALIZATION: addWidget" + // << appletData.property("plugin").toString() + // ; + + if (auto applet = qobject_cast(appletObject.toQObject())) { + // Now, lets go through the configs for the applet + loadSerializedConfigs(applet, appletData.property("config")); + } + }; + }; + + return QJSValue(); +} + +QJSValue ScriptEngine::V1::newPanel(const QString &plugin) +{ + return createContainment(QStringLiteral("Panel"), QStringLiteral("org.kde.panel"), plugin); +} + +QJSValue ScriptEngine::V1::panelById(const QJSValue &idParam) const +{ + // this needs to work also for string of numberls, like "20" + if (idParam.isUndefined()) { + return m_engine->newError(i18n("panelById requires an id")); + } + + const quint32 id = idParam.toInt(); + + foreach (Plasma::Containment *c, m_engine->m_corona->containments()) { + if (c->id() == id && isPanel(c)) { + return m_engine->wrap(c); + } + } + + return QJSValue(); +} + +QJSValue ScriptEngine::V1::desktops() const +{ + QJSValue containments = m_engine->newArray(); + int count = 0; + + const auto result = m_engine->m_corona->containments(); + + for (const auto c : result) { + // make really sure we get actual desktops, so check for a non empty + // activity id + if (!isPanel(c) && !c->activity().isEmpty()) { + containments.setProperty(count, m_engine->wrap(c)); + ++count; + } + } + + containments.setProperty(QStringLiteral("length"), count); + return containments; +} + +QJSValue ScriptEngine::V1::panels() const +{ + QJSValue panels = m_engine->newArray(); + int count = 0; + + const auto result = m_engine->m_corona->containments(); + + for (const auto c : result) { + if (isPanel(c)) { + panels.setProperty(count, m_engine->wrap(c)); + ++count; + } + } + panels.setProperty(QStringLiteral("length"), count); + + return panels; +} + +bool ScriptEngine::V1::fileExists(const QString &path) const +{ + if (path.isEmpty()) { + return false; + } + + QFile f(KShell::tildeExpand(path)); + return f.exists(); +} + +bool ScriptEngine::V1::loadTemplate(const QString &layout) +{ + if (layout.isEmpty() || layout.contains(QLatin1Char('\''))) { + // qDebug() << "layout is empty"; + return false; + } + + auto filter = [&layout](const KPluginMetaData &md) -> bool { + return md.pluginId() == layout && md.value(QStringLiteral("X-Plasma-ContainmentCategories"), QStringList()).contains(QLatin1String("panel")); + }; + QList offers = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); + + if (offers.isEmpty()) { + // qDebug() << "offers fail" << constraint; + return false; + } + + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate")); + KPluginMetaData pluginData(offers.first()); + + QString path; + { + ShellCorona *sc = qobject_cast(m_engine->m_corona); + if (sc) { + const QString overridePackagePath = sc->lookAndFeelPackage().path() + QLatin1String("contents/layouts/") + pluginData.pluginId(); + + path = overridePackagePath + QStringLiteral("/metadata.json"); + if (QFile::exists(path)) { + package.setPath(overridePackagePath); + } + + path = overridePackagePath + QStringLiteral("/metadata.desktop"); + if (QFile::exists(path)) { + package.setPath(overridePackagePath); + } + } + } + + if (!package.isValid()) { + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, package.defaultPackageRoot() + pluginData.pluginId() + "/metadata.json"); + if (path.isEmpty()) { + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, package.defaultPackageRoot() + pluginData.pluginId() + "/metadata.desktop"); + } + if (path.isEmpty()) { + // qDebug() << "script path is empty"; + return false; + } + + package.setPath(pluginData.pluginId()); + } + + const QString scriptFile = package.filePath("mainscript"); + if (scriptFile.isEmpty()) { + // qDebug() << "scriptfile is empty"; + return false; + } + + QFile file(scriptFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(PLASMASHELL) << "Unable to load script file:" << path; + return false; + } + + QString script = file.readAll(); + if (script.isEmpty()) { + // qDebug() << "script is empty"; + return false; + } + + ScriptEngine *engine = new ScriptEngine(m_engine->corona(), this); + engine->globalObject().setProperty(QStringLiteral("templateName"), pluginData.name()); + engine->globalObject().setProperty(QStringLiteral("templateComment"), pluginData.description()); + + engine->evaluateScript(script, path); + + engine->deleteLater(); + return true; +} + +bool ScriptEngine::V1::applicationExists(const QString &application) const +{ + if (application.isEmpty()) { + return false; + } + + // first, check for it in $PATH + if (!QStandardPaths::findExecutable(application).isEmpty()) { + return true; + } + + if (KService::serviceByStorageId(application)) { + return true; + } + + if (application.contains(QLatin1Char('\''))) { + // apostrophes just screw up the trader lookups below, so check for it + return false; + } + + // next, consult ksycoca for an app by that name + if (!KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("Name =~ '%1'").arg(application)).isEmpty()) { + return true; + } + + // next, consult ksycoca for an app by that generic name + if (!KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("GenericName =~ '%1'").arg(application)).isEmpty()) { + return true; + } + + return false; +} + +QJSValue ScriptEngine::V1::defaultApplication(const QString &application, bool storageId) const +{ + if (application.isEmpty()) { + return false; + } + + // FIXME: there are some pretty horrible hacks below, in the sense that they + // assume a very + // specific implementation system. there is much room for improvement here. + // see + // kdebase-runtime/kcontrol/componentchooser/ for all the gory details ;) + if (matches(application, QLatin1String("mailer"))) { + // KEMailSettings settings; + + // in KToolInvocation, the default is kmail; but let's be friendlier :) + // QString command = settings.getSetting(KEMailSettings::ClientProgram); + QString command; + if (command.isEmpty()) { + if (KService::Ptr kontact = KService::serviceByStorageId(QStringLiteral("kontact"))) { + return storageId ? kontact->storageId() : onlyExec(kontact->exec()); + } else if (KService::Ptr kmail = KService::serviceByStorageId(QStringLiteral("kmail"))) { + return storageId ? kmail->storageId() : onlyExec(kmail->exec()); + } + } + + if (!command.isEmpty()) { + if (false) { + KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); + const QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole")); + command = preferredTerminal + QLatin1String(" -e ") + command; + } + + return command; + } + + } else if (matches(application, QLatin1String("browser"))) { + KConfigGroup config(KSharedConfig::openConfig(), "General"); + QString browserApp = config.readPathEntry("BrowserApplication", QString()); + if (browserApp.isEmpty()) { + const KService::Ptr htmlApp = KApplicationTrader::preferredService(QStringLiteral("text/html")); + if (htmlApp) { + browserApp = storageId ? htmlApp->storageId() : htmlApp->exec(); + } + } else if (browserApp.startsWith('!')) { + browserApp.remove(0, 1); + } + + return onlyExec(browserApp); + + } else if (matches(application, QLatin1String("terminal"))) { + KConfigGroup confGroup(KSharedConfig::openConfig(), "General"); + return onlyExec(confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"))); + + } else if (matches(application, QLatin1String("filemanager"))) { + KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("inode/directory")); + if (service) { + return storageId ? service->storageId() : onlyExec(service->exec()); + } + + } else if (matches(application, QLatin1String("windowmanager"))) { + KConfig cfg(QStringLiteral("ksmserverrc"), KConfig::NoGlobals); + KConfigGroup confGroup(&cfg, "General"); + return onlyExec(confGroup.readEntry("windowManager", QStringLiteral("kwin"))); + + } else if (KService::Ptr service = KApplicationTrader::preferredService(application)) { + return storageId ? service->storageId() : onlyExec(service->exec()); + + } + + return false; +} + +QJSValue ScriptEngine::V1::applicationPath(const QString &application) const +{ + if (application.isEmpty()) { + return false; + } + + // first, check for it in $PATH + const QString path = QStandardPaths::findExecutable(application); + if (!path.isEmpty()) { + return path; + } + + if (KService::Ptr service = KService::serviceByStorageId(application)) { + return QStandardPaths::locate(QStandardPaths::ApplicationsLocation, service->entryPath()); + } + + if (application.contains(QLatin1Char('\''))) { + // apostrophes just screw up the trader lookups below, so check for it + return QString(); + } + + // next, consult ksycoca for an app by that name + KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("Name =~ '%1'").arg(application)); + if (offers.isEmpty()) { + // next, consult ksycoca for an app by that generic name + offers = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("GenericName =~ '%1'").arg(application)); + } + + if (!offers.isEmpty()) { + KService::Ptr offer = offers.first(); + return QStandardPaths::locate(QStandardPaths::ApplicationsLocation, offer->entryPath()); + } + + return QString(); +} + +QJSValue ScriptEngine::V1::userDataPath(const QString &type, const QString &path) const +{ + if (type.isEmpty()) { + return QDir::homePath(); + } + + QStandardPaths::StandardLocation location = QStandardPaths::GenericDataLocation; + if (matches(type, QLatin1String("desktop"))) { + location = QStandardPaths::DesktopLocation; + + } else if (matches(type, QLatin1String("documents"))) { + location = QStandardPaths::DocumentsLocation; + + } else if (matches(type, QLatin1String("music"))) { + location = QStandardPaths::MusicLocation; + + } else if (matches(type, QLatin1String("video"))) { + location = QStandardPaths::MoviesLocation; + + } else if (matches(type, QLatin1String("downloads"))) { + location = QStandardPaths::DownloadLocation; + + } else if (matches(type, QLatin1String("pictures"))) { + location = QStandardPaths::PicturesLocation; + + } else if (matches(type, QLatin1String("config"))) { + location = QStandardPaths::GenericConfigLocation; + } + + if (!path.isEmpty()) { + QString loc = QStandardPaths::writableLocation(location); + loc.append(QDir::separator()); + loc.append(path); + return loc; + } + + const QStringList &locations = QStandardPaths::standardLocations(location); + return locations.count() ? locations.first() : QString(); +} + +QJSValue ScriptEngine::V1::knownWallpaperPlugins(const QString &formFactor) const +{ + QString constraint; + if (!formFactor.isEmpty()) { + constraint.append("[X-Plasma-FormFactors] ~~ '").append(formFactor).append("'"); + } + + const QList wallpapers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("Plasma/Wallpaper"), QString()); + QJSValue rv = m_engine->newArray(wallpapers.size()); + for (auto wp : wallpapers) { + rv.setProperty(wp.name(), m_engine->newArray(0)); + } + + return rv; +} + +QJSValue ScriptEngine::V1::configFile(const QJSValue &config, const QString &group) +{ + ConfigGroup *file = nullptr; + + if (!config.isUndefined()) { + if (config.isString()) { + file = new ConfigGroup; + + const Plasma::Corona *corona = m_engine->corona(); + const QString &fileName = config.toString(); + + if (fileName == corona->config()->name()) { + file->setConfig(corona->config()); + } else { + file->setFile(fileName); + } + + if (!group.isEmpty()) { + file->setGroup(group); + } + + } else if (ConfigGroup *parent = qobject_cast(config.toQObject())) { + file = new ConfigGroup(parent); + + if (!group.isEmpty()) { + file->setGroup(group); + } + } + + } else { + file = new ConfigGroup; + } + + QJSValue v = m_engine->newQObject(file); + return v; +} + +void ScriptEngine::V1::setImmutability(const QString &immutability) +{ + if (immutability.isEmpty()) { + return; + } + + if (immutability == QLatin1String("systemImmutable")) { + m_engine->corona()->setImmutability(Plasma::Types::SystemImmutable); + } else if (immutability == QLatin1String("userImmutable")) { + m_engine->corona()->setImmutability(Plasma::Types::UserImmutable); + } else { + m_engine->corona()->setImmutability(Plasma::Types::Mutable); + } + + return; +} + +QString ScriptEngine::V1::immutability() const +{ + switch (m_engine->corona()->immutability()) { + case Plasma::Types::SystemImmutable: + return QLatin1String("systemImmutable"); + case Plasma::Types::UserImmutable: + return QLatin1String("userImmutable"); + default: + return QLatin1String("mutable"); + } +} + +QJSValue ScriptEngine::V1::createContainment(const QString &type, const QString &defaultPlugin, const QString &plugin) +{ + const QString actualPlugin = plugin.isEmpty() ? defaultPlugin : plugin; + + auto result = m_engine->createContainmentWrapper(type, actualPlugin); + + if (!result) { + return m_engine->newError(i18n("Could not find a plugin for %1 named %2.", type, actualPlugin)); + } + + return m_engine->newQObject(result); +} + +} // namespace WorkspaceScripting diff --git a/plasma/workspace/shell/scripting/scriptengine_v1.h b/plasma/workspace/shell/scripting/scriptengine_v1.h new file mode 100644 index 0000000000..4b917a1eef --- /dev/null +++ b/plasma/workspace/shell/scripting/scriptengine_v1.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +#include + +#include "../shellcorona.h" +#include "scriptengine.h" + +namespace WorkspaceScripting +{ +class ScriptEngine::V1 : public QObject +{ + Q_OBJECT + Q_PROPERTY(int gridUnit READ gridUnit CONSTANT) + +public: + V1(ScriptEngine *parent); + ~V1(); + int gridUnit() const; + + Q_INVOKABLE QJSValue getApiVersion(const QJSValue ¶m); + Q_INVOKABLE QJSValue desktopById(const QJSValue &id = QJSValue()) const; + Q_INVOKABLE QJSValue desktopsForActivity(const QJSValue &id = QJSValue()) const; + Q_INVOKABLE QJSValue desktopForScreen(const QJSValue &screen = QJSValue()) const; + Q_INVOKABLE QJSValue screenForConnector(const QJSValue ¶m = QJSValue()) const; + Q_INVOKABLE QJSValue createActivity(const QJSValue &nameParam = QJSValue(), const QString &plugin = QString()); + Q_INVOKABLE QJSValue setCurrentActivity(const QJSValue &id = QJSValue()); + Q_INVOKABLE QJSValue setActivityName(const QJSValue &idParam = QJSValue(), const QJSValue &nameParam = QJSValue()); + Q_INVOKABLE QJSValue activityName(const QJSValue &idParam = QJSValue()) const; + Q_INVOKABLE QString currentActivity() const; + Q_INVOKABLE QJSValue activities() const; + Q_INVOKABLE QJSValue loadSerializedLayout(const QJSValue &data = QJSValue()); + Q_INVOKABLE QJSValue panelById(const QJSValue &idParam = QJSValue()) const; + Q_INVOKABLE QJSValue desktops() const; + Q_INVOKABLE QJSValue panels() const; + Q_INVOKABLE bool fileExists(const QString &path = QString()) const; + Q_INVOKABLE bool loadTemplate(const QString &layout = QString()); + Q_INVOKABLE bool applicationExists(const QString &application = QString()) const; + Q_INVOKABLE QJSValue defaultApplication(const QString &application = QString(), bool storageId = false) const; + Q_INVOKABLE QJSValue applicationPath(const QString &application = QString()) const; + Q_INVOKABLE QJSValue userDataPath(const QString &type = QString(), const QString &path = QString()) const; + Q_INVOKABLE QJSValue knownWallpaperPlugins(const QString &formFactor = QString()) const; + + Q_INVOKABLE void setImmutability(const QString &immutability = QString()); + Q_INVOKABLE QString immutability() const; + Q_INVOKABLE QJSValue createContainment(const QString &type, const QString &defautPlugin, const QString &plugin = QString()); + + // for ctors + Q_INVOKABLE QJSValue newPanel(const QString &plugin = QStringLiteral("org.kde.panel")); + Q_INVOKABLE QJSValue configFile(const QJSValue &config = QJSValue(), const QString &group = QString()); + +Q_SIGNALS: + void print(const QJSValue ¶m); + +private: + ScriptEngine *m_engine; +}; + +} diff --git a/plasma/workspace/shell/scripting/widget.cpp b/plasma/workspace/shell/scripting/widget.cpp new file mode 100644 index 0000000000..5ff3a5a739 --- /dev/null +++ b/plasma/workspace/shell/scripting/widget.cpp @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "widget.h" +#include "scriptengine.h" + +#include +#include +#include + +#include +#include +#include + +namespace WorkspaceScripting +{ +class Widget::Private +{ +public: + Private() + { + } + + QPointer applet; +}; + +Widget::Widget(Plasma::Applet *applet, ScriptEngine *parent) + : Applet(parent) + , d(new Widget::Private) +{ + d->applet = applet; + setCurrentConfigGroup(QStringList()); + setCurrentGlobalConfigGroup(QStringList()); +} + +Widget::~Widget() +{ + reloadConfigIfNeeded(); + delete d; +} + +uint Widget::id() const +{ + if (d->applet) { + return d->applet->id(); + } + + return 0; +} + +QString Widget::type() const +{ + if (d->applet) { + return d->applet->pluginMetaData().pluginId(); + } + + return QString(); +} + +void Widget::remove() +{ + if (d->applet) { + d->applet->destroy(); + d->applet.clear(); + } +} + +void Widget::setGlobalShortcut(const QString &shortcut) +{ + if (d->applet) { + d->applet->setGlobalShortcut(QKeySequence(shortcut)); + } +} + +QString Widget::globalShorcut() const +{ + if (d->applet) { + return d->applet->globalShortcut().toString(); + } + + return QString(); +} + +Plasma::Applet *Widget::applet() const +{ + return d->applet; +} + +int Widget::index() const +{ + if (!d->applet) { + return -1; + } + + Plasma::Containment *c = d->applet->containment(); + if (!c) { + return -1; + } + + /*QGraphicsLayout *layout = c->layout(); + if (!layout) { + return - 1; + } + + for (int i = 0; i < layout->count(); ++i) { + if (layout->itemAt(i) == applet) { + return i; + } + }*/ + + return -1; +} + +void Widget::setIndex(int index) +{ + Q_UNUSED(index) + /* + if (!d->applet) { + return; + } + + Plasma::Containment *c = d->applet->containment(); + if (!c) { + return; + } + //FIXME: this is hackish. would be nice to define this for gridlayouts too + QGraphicsLinearLayout *layout = dynamic_cast(c->layout()); + if (!layout) { + return; + } + + layout->insertItem(index, applet);*/ +} + +QJSValue Widget::geometry() const +{ + QQuickItem *appletItem = d->applet->property("_plasma_graphicObject").value(); + + if (appletItem) { + QJSValue rect = engine()->newObject(); + const QPointF pos = appletItem->mapToScene(QPointF(0, 0)); + rect.setProperty(QStringLiteral("x"), pos.x()); + rect.setProperty(QStringLiteral("y"), pos.y()); + rect.setProperty(QStringLiteral("width"), appletItem->width()); + rect.setProperty(QStringLiteral("height"), appletItem->height()); + return rect; + } + + return QJSValue(); +} + +void Widget::setGeometry(const QJSValue &geometry) +{ + Q_UNUSED(geometry) + /*if (d->applet) { + d->applet->setGeometry(geometry); + KConfigGroup cg = d->applet->config().parent(); + if (cg.isValid()) { + cg.writeEntry("geometry", geometry); + } + }*/ +} + +void Widget::showConfigurationInterface() +{ + /* if (d->applet) { + d->applet->showConfigurationInterface(); + }*/ +} + +QString Widget::userBackgroundHints() const +{ + QMetaEnum hintEnum = QMetaEnum::fromType(); + return hintEnum.valueToKey(applet()->userBackgroundHints()); +} + +void Widget::setUserBackgroundHints(QString hint) +{ + QMetaEnum hintEnum = QMetaEnum::fromType(); + bool ok; + int value = hintEnum.keyToValue(hint.toUtf8().constData(), &ok); + if (ok) { + applet()->setUserBackgroundHints(Plasma::Types::BackgroundHints(value)); + } +} + +} diff --git a/plasma/workspace/shell/scripting/widget.h b/plasma/workspace/shell/scripting/widget.h new file mode 100644 index 0000000000..dd64d24eb6 --- /dev/null +++ b/plasma/workspace/shell/scripting/widget.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2009 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "applet.h" + +namespace Plasma +{ +class Applet; +} // namespace Plasma + +namespace WorkspaceScripting +{ +class Widget : public Applet +{ + Q_OBJECT + Q_PROPERTY(QString type READ type) + Q_PROPERTY(QString version READ version) + Q_PROPERTY(int id READ id) + Q_PROPERTY(QStringList configKeys READ configKeys) + Q_PROPERTY(QStringList configGroups READ configGroups) + Q_PROPERTY(QStringList globalConfigKeys READ globalConfigKeys) + Q_PROPERTY(QStringList globalConfigGroups READ globalConfigGroups) + Q_PROPERTY(int index WRITE setIndex READ index) + // We pass our js based QRect wrapper instead of a simple QRectF + Q_PROPERTY(QJSValue geometry WRITE setGeometry READ geometry) + Q_PROPERTY(QStringList currentConfigGroup WRITE setCurrentConfigGroup READ currentConfigGroup) + Q_PROPERTY(QString globalShortcut WRITE setGlobalShortcut READ globalShorcut) + Q_PROPERTY(bool locked READ locked WRITE setLocked) + Q_PROPERTY(QString userBackgroundHints WRITE setUserBackgroundHints READ userBackgroundHints) + +public: + explicit Widget(Plasma::Applet *applet, ScriptEngine *parent = nullptr); + ~Widget() override; + + uint id() const; + QString type() const; + + int index() const; + void setIndex(int index); + + QJSValue geometry() const; + void setGeometry(const QJSValue &geometry); + + void setGlobalShortcut(const QString &shortcut); + QString globalShorcut() const; + + QString userBackgroundHints() const; + void setUserBackgroundHints(QString hint); + + Plasma::Applet *applet() const override; + +public Q_SLOTS: + void remove(); + void showConfigurationInterface(); + +private: + class Private; + Private *const d; +}; + +} diff --git a/plasma/workspace/shell/shellcontainmentconfig.cpp b/plasma/workspace/shell/shellcontainmentconfig.cpp new file mode 100644 index 0000000000..a9e9c7cb7c --- /dev/null +++ b/plasma/workspace/shell/shellcontainmentconfig.cpp @@ -0,0 +1,483 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "shellcontainmentconfig.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "shellcorona.h" +#include "screenpool.h" +#include "panelview.h" + + +ScreenPoolModel::ScreenPoolModel(ShellCorona *corona, QObject *parent) + : QAbstractListModel(parent) + , m_corona(corona) +{ + m_reloadTimer = new QTimer(this); + m_reloadTimer->setSingleShot(true); + m_reloadTimer->setInterval(200); + + connect(m_reloadTimer, &QTimer::timeout, this, &ScreenPoolModel::load); + + connect(m_corona, &Plasma::Corona::screenAdded, m_reloadTimer, static_cast(&QTimer::start)); + connect(m_corona, &Plasma::Corona::screenRemoved, m_reloadTimer, static_cast(&QTimer::start)); +} + +ScreenPoolModel::~ScreenPoolModel() = default; + +QVariant ScreenPoolModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_screens.size())) { + return QVariant(); + } + const Data &d = m_screens.at(index.row()); + switch (role) { + case ScreenIdRole: + return d.id; + case ScreenNameRole: + return d.name; + case ContainmentsRole: { + auto *cont = m_containments.at(index.row()); + return QVariant::fromValue(cont); + } + case PrimaryRole: + return d.primary; + case EnabledRole: + return d.enabled; + } + return QVariant(); +} + +int ScreenPoolModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_screens.size(); +} + +QHash ScreenPoolModel::roleNames() const +{ + QHash roles({{ScreenIdRole, QByteArrayLiteral("screenId")}, + {ScreenNameRole, QByteArrayLiteral("screenName")}, + {ContainmentsRole, QByteArrayLiteral("containments")}, + {EnabledRole, QByteArrayLiteral("isEnabled")}, + {PrimaryRole, QByteArrayLiteral("isPrimary")}}); + return roles; +} + +void ScreenPoolModel::load() +{ + beginResetModel(); + m_screens.clear(); + qDeleteAll(m_containments); + m_containments.clear(); + + QSet unknownScreenIds; + for (auto *cont : m_corona->containments()) { + unknownScreenIds.insert(cont->lastScreen()); + } + for (auto &knownId : m_corona->screenPool()->knownIds()) { + Data d; + unknownScreenIds.remove(knownId); + d.id = knownId; + d.name = m_corona->screenPool()->connector(knownId); + d.primary = knownId == 0; + d.enabled = false; + // TODO: add a m_corona->screenPool()->screenForId() method instead of this loop, but needs the whole primary screen tracking be moved from shellcorona + // to screenpool + for (QScreen *screen : qGuiApp->screens()) { + if (screen->name() == d.name) { + d.enabled = true; + break; + } + } + + auto *conts = new ShellContainmentModel(m_corona, knownId, this); + conts->load(); + + // Exclude screens which don't have any containemnt assigned + if (conts->rowCount() > 0) { + m_containments.push_back(conts); + m_screens.push_back(d); + } else { + delete conts; + } + } + + QList sortedIds = unknownScreenIds.values(); + std::sort(sortedIds.begin(), sortedIds.end()); + int i = 1; + for (int id : sortedIds) { + Data d; + d.id = id; + d.name = i18n("Unknown %1", i); + d.primary = id == 0; + d.enabled = false; + + auto *conts = new ShellContainmentModel(m_corona, id, this); + conts->load(); + m_containments.push_back(conts); + m_screens.push_back(d); + i++; + } + endResetModel(); +} + +// --- + +ShellContainmentModel::ShellContainmentModel(ShellCorona *corona, int screenId, ScreenPoolModel *parent) + : QAbstractListModel(parent) + , m_screenId(screenId) + , m_corona(corona) + , m_screenPoolModel(parent) + , m_activityConsumer(new KActivities::Consumer(this)) +{ + m_reloadTimer = new QTimer(this); + m_reloadTimer->setSingleShot(true); + m_reloadTimer->setInterval(200); + + connect(m_reloadTimer, &QTimer::timeout, this, &ShellContainmentModel::load); + + connect(m_corona, &ShellCorona::startupCompleted, this, &ShellContainmentModel::load); + + connect(m_corona, &Plasma::Corona::containmentAdded, m_reloadTimer, static_cast(&QTimer::start)); + connect(m_corona, &Plasma::Corona::screenOwnerChanged, m_reloadTimer, static_cast(&QTimer::start)); + + connect(m_corona, &ShellCorona::containmentPreviewReady, this, [this](Plasma::Containment *containment, const QString &path) { + int i = 0; + for (auto &d : m_containments) { + if (d.containment == containment) { + d.image = path; + emit dataChanged(index(i, 0), index(i, 0)); + break; + } + ++i; + } + }); +} + +ShellContainmentModel::~ShellContainmentModel() = default; + +QVariant ShellContainmentModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_containments.size())) { + return QVariant(); + } + const Data &d = m_containments.at(index.row()); + switch (role) { + case Qt::DisplayRole: + return d.name; + case ContainmentIdRole: + return d.id; + case NameRole: + return d.name; + case ScreenRole: + return d.screen; + case EdgeRole: + return ShellContainmentModel::plasmaLocationToString(d.edge); + case EdgePositionRole: + return qMax(0, m_edgeCount.value(d.screen).value(d.edge).indexOf(d.id)); + case PanelCountAtRightRole: + return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::RightEdge).count()); + case PanelCountAtTopRole: + return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::TopEdge).count()); + case PanelCountAtLeftRole: + return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::LeftEdge).count()); + case PanelCountAtBottomRole: + return qMax(0, m_edgeCount.value(d.screen).value(Plasma::Types::BottomEdge).count()); + case ActivityRole: + { + const auto *activityInfo = m_activitiesInfos.value(d.activity); + if (activityInfo) { + return activityInfo->name(); + } + break; + } + case IsActiveRole: + return d.isActive; + case ImageSourceRole: + return d.image; + case DestroyedRole: + return d.containment->destroyed(); + } + return QVariant(); +} + +int ShellContainmentModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_containments.size(); +} + +QHash ShellContainmentModel::roleNames() const +{ + QHash roles({{ContainmentIdRole, QByteArrayLiteral("containmentId")}, + {NameRole, QByteArrayLiteral("name")}, + {ScreenRole, QByteArrayLiteral("screen")}, + {EdgeRole, QByteArrayLiteral("edge")}, + {EdgePositionRole, QByteArrayLiteral("edgePosition")}, + {PanelCountAtRightRole, QByteArrayLiteral("panelCountAtRight")}, + {PanelCountAtTopRole, QByteArrayLiteral("panelCountAtTop")}, + {PanelCountAtLeftRole, QByteArrayLiteral("panelCountAtLeft")}, + {PanelCountAtBottomRole, QByteArrayLiteral("panelCountAtBottom")}, + {ActivityRole, QByteArrayLiteral("activity")}, + {IsActiveRole, QByteArrayLiteral("active")}, + {ImageSourceRole, QByteArrayLiteral("imageSource")}, + {DestroyedRole, QByteArrayLiteral("isDestroyed")}}); + return roles; +} + +ScreenPoolModel *ShellContainmentModel::screenPoolModel() const +{ + return m_screenPoolModel; +} + +void ShellContainmentModel::remove(int contId) +{ + if (contId < 0) { + return; + } + + auto *cont = containmentById(contId); + if (cont) { + disconnect(cont, nullptr, this, nullptr); + // Don't call destroy directly, so we can have the undo action notification + auto *destroyAction = cont->actions()->action("remove"); + if (destroyAction) { + destroyAction->trigger(); + } + } + load(); +} + +void ShellContainmentModel::moveContainementToScreen(unsigned int contId, int newScreen) +{ + if (contId == 0 || newScreen < 0) { + return; + } + + auto containmentIt = std::find_if(m_containments.begin(), m_containments.end(), [contId](Data &d) { + return d.id == contId; + }); + if (containmentIt == m_containments.end()) { + return; + } + if (containmentIt->screen == newScreen) { + return; + } + + auto *cont = containmentById(contId); + if (cont == nullptr) { + return; + } + + // If it's a panel, only move that one + if (cont->containmentType() == Plasma::Types::PanelContainment || cont->containmentType() == Plasma::Types::CustomPanelContainment) { + m_corona->setScreenForContainment(cont, newScreen); + } else { + // If it's a desktop, for now move all desktops for all activities + const int oldScreen = cont->screen() >= 0 ? cont->screen() : cont->lastScreen(); + m_corona->swapDesktopScreens(oldScreen, newScreen); + } +} + +bool ShellContainmentModel::findContainment(unsigned int containmentId) const +{ + return m_containments.cend() != std::find_if(m_containments.cbegin(), m_containments.cend(), [containmentId](const Data &d) { + return d.id == containmentId; + }); +} + +void ShellContainmentModel::load() +{ + beginResetModel(); + + for (auto &d : m_containments) { + disconnect(d.containment, nullptr, this, nullptr); + } + m_containments.clear(); + m_edgeCount.clear(); + + for (const auto *cont : m_corona->containments()) { + // Skip the systray + if (qobject_cast(cont->parent())) { + continue; + } + // Only allow current activity for now (panels always go in) + if (cont->containmentType() != Plasma::Types::PanelContainment && cont->containmentType() != Plasma::Types::CustomPanelContainment + && cont->activity() != m_activityConsumer->currentActivity()) { + continue; + } + if (!m_edgeCount.contains(cont->lastScreen())) { + m_edgeCount[cont->lastScreen()] = QHash>(); + m_edgeCount[cont->lastScreen()][cont->location()] = QList(); + } + m_edgeCount[cont->lastScreen()][cont->location()].append(cont->id()); + m_corona->grabContainmentPreview(const_cast(cont)); + Data d; + d.id = cont->id(); + d.name = cont->title() + " (" + ShellContainmentModel::containmentTypeToString(cont->containmentType()) + ")"; + d.screen = cont->lastScreen(); + d.edge = cont->location(); + d.activity = cont->activity(); + d.isActive = cont->screen() != -1; + d.containment = cont; + d.image = containmentPreview(const_cast(cont)); + + if (cont->lastScreen() == m_screenId || (cont->lastScreen() == -1 && cont->screen() == m_screenId)) { + m_containments.push_back(d); + connect(cont, &QObject::destroyed, this, &ShellContainmentModel::load); + connect(cont, &Plasma::Containment::destroyedChanged, this, &ShellContainmentModel::load); + connect(cont, &Plasma::Containment::locationChanged, this, &ShellContainmentModel::load); + } + } + endResetModel(); +} + +void ShellContainmentModel::loadActivitiesInfos() +{ + beginResetModel(); + for (const auto &cont : m_containments) { + const auto activitId = cont.activity; + if (activitId.isEmpty()) { + continue; + } + auto *activityInfo = new KActivities::Info(cont.activity, this); + if (activityInfo) { + if (!m_activitiesInfos.value(cont.activity)) { + m_activitiesInfos[cont.activity] = activityInfo; + } + } + } + endResetModel(); +} + +QString ShellContainmentModel::plasmaLocationToString(Plasma::Types::Location location) +{ + switch (location) { + case Plasma::Types::Floating: + return QStringLiteral("floating"); + case Plasma::Types::Desktop: + return QStringLiteral("desktop"); + case Plasma::Types::FullScreen: + return QStringLiteral("Full Screen"); + case Plasma::Types::TopEdge: + return QStringLiteral("top"); + case Plasma::Types::BottomEdge: + return QStringLiteral("bottom"); + case Plasma::Types::LeftEdge: + return QStringLiteral("left"); + case Plasma::Types::RightEdge: + return QStringLiteral("right"); + default: + return QString("unknown"); + } +} + +QString ShellContainmentModel::containmentTypeToString(Plasma::Types::ContainmentType containmentType) +{ + switch (containmentType) { + case Plasma::Types::DesktopContainment: /**< A desktop containment */ + return QStringLiteral("Desktop"); + case Plasma::Types::PanelContainment: /**< A desktop panel */ + return QStringLiteral("Panel"); + case Plasma::Types::CustomContainment: /**< A containment that is neither a desktop nor a panel + but something application specific */ + return QStringLiteral("Custom"); + case Plasma::Types::CustomPanelContainment: /**< A customized desktop panel */ + return QStringLiteral("Custom Desktop"); + case Plasma::Types::CustomEmbeddedContainment: /**< A customized containment embedded in another applet */ + return QStringLiteral("Embedded"); + default: + return QStringLiteral("Unknown"); + } +} + +Plasma::Containment *ShellContainmentModel::containmentById(unsigned int id) +{ + for (auto *cont : m_corona->containments()) { + if (cont->id() == id) { + return cont; + } + } + return nullptr; +} + +QString ShellContainmentModel::containmentPreview(Plasma::Containment *containment) +{ + QString savedThumbnail = m_corona->containmentPreviewPath(containment); + + if (!savedThumbnail.isEmpty()) { + return savedThumbnail; + } + + m_corona->grabContainmentPreview(containment); + + // If not found, try to understand the configured wallpaper for the containment, assuming is using the Image plugin + KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_corona->shell() + QLatin1String("-appletsrc"), KConfig::SimpleConfig); + KConfigGroup containmentsGroup(conf, "Containments"); + KConfigGroup config = containmentsGroup.group(QString::number(containment->id())); + auto wallpaperPlugin = config.readEntry("wallpaperplugin"); + auto wallpaperConfig = config.group("Wallpaper").group(wallpaperPlugin).group("General"); + + if (wallpaperConfig.hasKey("Image")) { + // Trying for the wallpaper + auto wallpaper = wallpaperConfig.readEntry("Image", QString()); + if (!wallpaper.isEmpty()) { + return wallpaper; + } + } + if (wallpaperConfig.hasKey("Color")) { + auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0)); + return backgroundColor.name(); + } + + return QString(); +} + +// --- + +ShellContainmentConfig::ShellContainmentConfig(ShellCorona *corona, QWindow *parent) + : QQmlApplicationEngine(parent) + , m_corona(corona) + , m_model(nullptr) +{ +} + +ShellContainmentConfig::~ShellContainmentConfig() = default; + +void ShellContainmentConfig::init() +{ + m_model = new ScreenPoolModel(m_corona, this); + m_model->load(); + + auto *localizedContext = new KLocalizedContext(this); + localizedContext->setTranslationDomain(QStringLiteral("plasma_shell_") + m_corona->shell()); + + rootContext()->setContextObject(localizedContext); + rootContext()->setContextProperty(QStringLiteral("ShellContainmentModel"), m_model); + load(m_corona->kPackage().fileUrl("containmentmanagementui")); + + if (!rootObjects().isEmpty()) { + auto *obj = qobject_cast(rootObjects().first()); + connect(obj, &QWindow::visibleChanged, this, [this, obj]() { + deleteLater(); + }); + } +} diff --git a/plasma/workspace/shell/shellcontainmentconfig.h b/plasma/workspace/shell/shellcontainmentconfig.h new file mode 100644 index 0000000000..184bb419e8 --- /dev/null +++ b/plasma/workspace/shell/shellcontainmentconfig.h @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef SHELLCONTAINMENTCONFIG_H +#define SHELLCONTAINMENTCONFIG_H + +#include +#include +#include +#include +#include + +#include +#include + +namespace KActivities { +class Consumer; +class Info; +} + +class ShellCorona; +class ShellContainmentModel; + +class ScreenPoolModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum ScreenPoolModelRoles { ScreenIdRole = Qt::UserRole + 1, ScreenNameRole, ContainmentsRole, PrimaryRole, EnabledRole }; + +public: + explicit ScreenPoolModel(ShellCorona *corona, QObject *parent = nullptr); + ~ScreenPoolModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +public Q_SLOTS: + void load(); + +private: + ShellCorona *m_corona; + struct Data { + int id; + QString name; + bool primary; + bool enabled; + }; + QTimer *m_reloadTimer = nullptr; + QVector m_screens; + QVector m_containments; +}; + +class ShellContainmentModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(ScreenPoolModel *screenPoolModel READ screenPoolModel CONSTANT) + +public: + enum ShellContainmentModelRoles { + ContainmentIdRole = Qt::UserRole + 1, + NameRole, + ScreenRole, + EdgeRole, + EdgePositionRole, + PanelCountAtRightRole, + PanelCountAtTopRole, + PanelCountAtLeftRole, + PanelCountAtBottomRole, + ActivityRole, + IsActiveRole, + ImageSourceRole, + DestroyedRole + }; + +public: + explicit ShellContainmentModel(ShellCorona *corona, int screenId, ScreenPoolModel *parent = nullptr); + ~ShellContainmentModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + ScreenPoolModel *screenPoolModel() const; + + Q_INVOKABLE void remove(int contId); + Q_INVOKABLE void moveContainementToScreen(unsigned int contId, int newScreen); + + bool findContainment(unsigned int containmentId) const; + + void loadActivitiesInfos(); + +public Q_SLOTS: + void load(); + +private: + static QString plasmaLocationToString(const Plasma::Types::Location location); + static QString containmentTypeToString(const Plasma::Types::ContainmentType containmentType); + + Plasma::Containment *containmentById(unsigned int id); + QString containmentPreview(Plasma::Containment *containment); + +private: + int m_screenId = -1; + ShellCorona *m_corona; + struct Data { + unsigned int id; + QString name; + int screen; + Plasma::Types::Location edge; + QString activity; + bool changed = false; + bool isActive = true; + QString image; + const Plasma::Containment *containment; + }; + QTimer *m_reloadTimer = nullptr; + QVector m_containments; + ScreenPoolModel *m_screenPoolModel; + QHash m_activitiesInfos; + KActivities::Consumer *m_activityConsumer; + QHash>> m_edgeCount; +}; + +class ShellContainmentConfig : public QQmlApplicationEngine +{ + Q_OBJECT + +public: + ShellContainmentConfig(ShellCorona *corona, QWindow *parent = nullptr); + ~ShellContainmentConfig() override; + + void init(); + +private: + ShellCorona *m_corona; + ScreenPoolModel *m_model; +}; + +#endif // SHELLCONTAINMENTCONFIG_H diff --git a/plasma/workspace/shell/shellcorona.cpp b/plasma/workspace/shell/shellcorona.cpp new file mode 100644 index 0000000000..7dddd314a6 --- /dev/null +++ b/plasma/workspace/shell/shellcorona.cpp @@ -0,0 +1,2313 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2013 Ivan Cukic + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "shellcorona.h" +#include "debug.h" +#include "strutmanager.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "config-ktexteditor.h" // HAVE_KTEXTEDITOR + +#include "alternativeshelper.h" +#include "desktopview.h" +#include "osd.h" +#include "panelview.h" +#include "screenpool.h" +#include "scripting/scriptengine.h" +#include "shellcontainmentconfig.h" + +#include "debug.h" +#include "futureutil.h" +#include "plasmashelladaptor.h" + +#ifndef NDEBUG +#define CHECK_SCREEN_INVARIANTS screenInvariants(); +#else +#define CHECK_SCREEN_INVARIANTS +#endif + +#if HAVE_X11 +#include +#include +#include +#endif + +static const int s_configSyncDelay = 10000; // 10 seconds + +ShellCorona::ShellCorona(QObject *parent) + : Plasma::Corona(parent) + , m_config(KSharedConfig::openConfig(QStringLiteral("plasmarc"))) + , m_screenPool(new ScreenPool(KSharedConfig::openConfig(), this)) + , m_activityController(new KActivities::Controller(this)) + , m_addPanelAction(nullptr) + , m_addPanelsMenu(nullptr) + , m_waylandPlasmaShell(nullptr) + , m_closingDown(false) + , m_strutManager(new StrutManager(this)) + , m_shellContainmentConfig(nullptr) +{ + setupWaylandIntegration(); + qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Desktop", QStringLiteral("It is not possible to create objects of type Desktop")); + qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Panel", QStringLiteral("It is not possible to create objects of type Panel")); + + KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); + const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); + m_lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), packageName); +} + +void ShellCorona::init() +{ + connect(this, &Plasma::Corona::containmentCreated, this, [this](Plasma::Containment *c) { + executeSetupPlasmoidScript(c, c); + }); + + connect(this, &Plasma::Corona::availableScreenRectChanged, this, &Plasma::Corona::availableScreenRegionChanged); + + m_appConfigSyncTimer.setSingleShot(true); + m_appConfigSyncTimer.setInterval(s_configSyncDelay); + connect(&m_appConfigSyncTimer, &QTimer::timeout, this, &ShellCorona::syncAppConfig); + // we want our application config with screen mapping to always be in sync with the applets one, so a crash at any time will still + // leave containments pointing to the correct screens + connect(this, &Corona::configSynced, this, &ShellCorona::syncAppConfig); + + m_waitingPanelsTimer.setSingleShot(true); + m_waitingPanelsTimer.setInterval(250); + connect(&m_waitingPanelsTimer, &QTimer::timeout, this, &ShellCorona::createWaitingPanels); + +#ifndef NDEBUG + m_invariantsTimer.setSingleShot(true); + m_invariantsTimer.setInterval(250); + connect(&m_invariantsTimer, &QTimer::timeout, this, &ShellCorona::screenInvariants); +#endif + + m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(kPackage().filePath("defaults")), "Desktop"); + m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); + m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, QStringLiteral("org.kde.plasma.desktop")); + + new PlasmaShellAdaptor(this); + + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/PlasmaShell"), this); + + // Look for theme config in plasmarc, if it isn't configured, take the theme from the + // LookAndFeel package, if either is set, change the default theme + + connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { + // saveLayout is a slot but arguments not compatible + m_closingDown = true; + saveLayout(); + }); + + connect(this, &ShellCorona::containmentAdded, this, &ShellCorona::handleContainmentAdded); + + QAction *dashboardAction = actions()->addAction(QStringLiteral("show dashboard")); + QObject::connect(dashboardAction, &QAction::triggered, this, &ShellCorona::setDashboardShown); + dashboardAction->setText(i18n("Show Desktop")); + connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, dashboardAction, [dashboardAction](bool showing) { + dashboardAction->setText(showing ? i18n("Hide Desktop") : i18n("Show Desktop")); + dashboardAction->setChecked(showing); + }); + + dashboardAction->setAutoRepeat(true); + dashboardAction->setCheckable(true); + dashboardAction->setIcon(QIcon::fromTheme(QStringLiteral("dashboard-show"))); + dashboardAction->setData(Plasma::Types::ControlAction); + KGlobalAccel::self()->setGlobalShortcut(dashboardAction, Qt::CTRL | Qt::Key_F12); + + checkAddPanelAction(); + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &ShellCorona::checkAddPanelAction); + + // Activity stuff + QAction *activityAction = actions()->addAction(QStringLiteral("manage activities")); + connect(activityAction, &QAction::triggered, this, &ShellCorona::toggleActivityManager); + activityAction->setText(i18n("Show Activity Switcher")); + activityAction->setIcon(QIcon::fromTheme(QStringLiteral("activities"))); + activityAction->setData(Plasma::Types::ConfigureAction); + activityAction->setShortcut(QKeySequence(QStringLiteral("alt+d, alt+a"))); + activityAction->setShortcutContext(Qt::ApplicationShortcut); + + KGlobalAccel::self()->setGlobalShortcut(activityAction, Qt::META | Qt::Key_Q); + + QAction *stopActivityAction = actions()->addAction(QStringLiteral("stop current activity")); + QObject::connect(stopActivityAction, &QAction::triggered, this, &ShellCorona::stopCurrentActivity); + + stopActivityAction->setText(i18n("Stop Current Activity")); + stopActivityAction->setData(Plasma::Types::ControlAction); + stopActivityAction->setVisible(false); + + KGlobalAccel::self()->setGlobalShortcut(stopActivityAction, Qt::META | Qt::Key_S); + + QAction *previousActivityAction = actions()->addAction(QStringLiteral("switch to previous activity")); + connect(previousActivityAction, &QAction::triggered, this, &ShellCorona::previousActivity); + previousActivityAction->setText(i18n("Switch to Previous Activity")); + previousActivityAction->setData(Plasma::Types::ConfigureAction); + previousActivityAction->setShortcutContext(Qt::ApplicationShortcut); + + KGlobalAccel::self()->setGlobalShortcut(previousActivityAction, QKeySequence()); + + QAction *nextActivityAction = actions()->addAction(QStringLiteral("switch to next activity")); + connect(nextActivityAction, &QAction::triggered, this, &ShellCorona::nextActivity); + nextActivityAction->setText(i18n("Switch to Next Activity")); + nextActivityAction->setData(Plasma::Types::ConfigureAction); + nextActivityAction->setShortcutContext(Qt::ApplicationShortcut); + + KGlobalAccel::self()->setGlobalShortcut(nextActivityAction, QKeySequence()); + + connect(m_activityController, &KActivities::Controller::currentActivityChanged, this, &ShellCorona::currentActivityChanged); + connect(m_activityController, &KActivities::Controller::activityAdded, this, &ShellCorona::activityAdded); + connect(m_activityController, &KActivities::Controller::activityRemoved, this, &ShellCorona::activityRemoved); + + KActionCollection *taskbarActions = new KActionCollection(this); + for (int i = 0; i < 10; ++i) { + const int entryNumber = i + 1; + const Qt::Key key = static_cast(Qt::Key_0 + (entryNumber % 10)); + + QAction *action = taskbarActions->addAction(QStringLiteral("activate task manager entry %1").arg(QString::number(entryNumber))); + action->setText(i18n("Activate Task Manager Entry %1", entryNumber)); + KGlobalAccel::setGlobalShortcut(action, QKeySequence(Qt::META + key)); + connect(action, &QAction::triggered, this, [this, i] { + activateTaskManagerEntry(i); + }); + } + + new Osd(m_config, this); + + // catch when plasmarc changes, so we e.g. enable/disable the OSd + m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_config->name(); + KDirWatch::self()->addFile(m_configPath); + connect(KDirWatch::self(), &KDirWatch::dirty, this, &ShellCorona::configurationChanged); + connect(KDirWatch::self(), &KDirWatch::created, this, &ShellCorona::configurationChanged); + + connect(qApp, &QGuiApplication::focusWindowChanged, this, [this](QWindow *focusWindow) { + if (!focusWindow) { + setEditMode(false); + } + }); + connect(this, &ShellCorona::editModeChanged, this, [this](bool edit) { + setDashboardShown(edit); + }); + + QAction *manageContainmentsAction = actions()->addAction(QStringLiteral("manage-containments")); + manageContainmentsAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-effect-fadedesktop"))); + manageContainmentsAction->setText(i18n("Manage Desktops And Panels...")); + connect(manageContainmentsAction, &QAction::triggered, this, [this]() { + if (m_shellContainmentConfig == nullptr) { + m_shellContainmentConfig = new ShellContainmentConfig(this); + m_shellContainmentConfig->init(); + } + // Swapping desktop views around causes problems with the show desktop effect + setEditMode(false); + }); + auto updateManageContainmentsVisiblility = [this, manageContainmentsAction]() { + QSet allScreenIds; + for (auto *cont : containments()) { + allScreenIds.insert(cont->lastScreen()); + } + manageContainmentsAction->setVisible(allScreenIds.count() > 1); + }; + connect(this, &ShellCorona::containmentAdded, this, updateManageContainmentsVisiblility); + connect(this, &ShellCorona::screenRemoved, this, updateManageContainmentsVisiblility); + updateManageContainmentsVisiblility(); + + QAction *cyclePanelFocusAction = actions()->addAction(QStringLiteral("cycle-panels")); + cyclePanelFocusAction->setText(i18n("Move keyboard focus between panels")); + KGlobalAccel::self()->setGlobalShortcut(cyclePanelFocusAction, Qt::META | Qt::ALT | Qt::Key_P); + connect(cyclePanelFocusAction, &QAction::triggered, this, [this]() { + if (m_panelViews.isEmpty()) { + return; + } + PanelView *activePanel = qobject_cast(qGuiApp->focusWindow()); + Plasma::Containment *containmentToActivate = nullptr; + if (activePanel) { + auto it = m_panelViews.constFind(activePanel->containment()); + it++; + if (it != m_panelViews.constEnd()) { + containmentToActivate = it.value()->containment(); + } + } + if (!containmentToActivate) { + containmentToActivate = m_panelViews.values().first()->containment(); + } + emit containmentToActivate->activated(); + }); +} + +ShellCorona::~ShellCorona() +{ + while (!containments().isEmpty()) { + // Deleting a containment will remove it from the list due to QObject::destroyed connect in Corona + // Deleting a containment in turn also kills any panel views + delete containments().constFirst(); + } +} + +KPackage::Package ShellCorona::lookAndFeelPackage() +{ + return m_lookAndFeelPackage; +} + +void ShellCorona::setShell(const QString &shell) +{ + if (m_shell == shell) { + return; + } + + m_shell = shell; + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell")); + package.setPath(shell); + package.setAllowExternalPaths(true); + setKPackage(package); + m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Desktop"); + m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); + m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, shell); + + const QString themeGroupKey = QStringLiteral("Theme"); + const QString themeNameKey = QStringLiteral("name"); + + QString themeName; + + KConfigGroup plasmarc(m_config, themeGroupKey); + themeName = plasmarc.readEntry(themeNameKey, themeName); + + if (themeName.isEmpty()) { + KConfigGroup shellCfg = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Theme"); + themeName = shellCfg.readEntry(themeNameKey, "default"); + } + + if (!themeName.isEmpty()) { + Plasma::Theme *t = new Plasma::Theme(this); + t->setThemeName(themeName); + } + + // FIXME: this would change the runtime platform to a fixed one if available + // but a different way to load platform specific components is needed beforehand + // because if we import and use two different components plugin, the second time + // the import is called it will fail + /* KConfigGroup cg(KSharedConfig::openConfig(package.filePath("defaults")), "General"); + KDeclarative::KDeclarative::setRuntimePlatform(cg.readEntry("DefaultRuntimePlatform", QStringList()));*/ + + unload(); + + /* + * we want to make an initial load once we have the initial screen config and we have loaded the activities _IF_ KAMD is running + * it is valid for KAMD to not be running. + * + * Potentially 2 async jobs + * + * here we connect for status changes from KAMD, and fetch the first config from kscreen. + * load() will check that we have a kscreen config, and m_activityController->serviceStatus() is not loading (i.e not unknown) + * + * It might seem that we only need this connection if the activityConsumer is currently in state Unknown, however + * there is an issue where m_activityController will start the kactivitymanagerd, as KAMD is starting the serviceStatus will be "not running" + * Whilst we are loading the kscreen config, the event loop runs and we might find KAMD has started. + * m_activityController will change from "not running" to unknown, and might still be unknown when the kscreen fetching is complete. + * + * if that happens we want to continue monitoring for state changes, and only finally load when it is up. + * + * See https://bugs.kde.org/show_bug.cgi?id=342431 be careful about changing + * + * The unique connection makes sure we don't reload plasma if KAMD ever crashes and reloads, the signal is disconnected in the body of load + */ + + connect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load, Qt::UniqueConnection); + + load(); +} + +QJsonObject dumpconfigGroupJS(const KConfigGroup &rootGroup) +{ + QJsonObject result; + + QStringList hierarchy; + QStringList escapedHierarchy; + QList groups{rootGroup}; + QSet visitedNodes; + + const QSet forbiddenKeys{QStringLiteral("activityId"), + QStringLiteral("ItemsGeometries"), + QStringLiteral("AppletOrder"), + QStringLiteral("SystrayContainmentId"), + QStringLiteral("location"), + QStringLiteral("plugin")}; + + auto groupID = [&escapedHierarchy]() { + return '/' + escapedHierarchy.join('/'); + }; + + // Perform a depth-first tree traversal for config groups + while (!groups.isEmpty()) { + KConfigGroup cg = groups.last(); + + KConfigGroup parentCg = cg; + // FIXME: name is not enough + + hierarchy.clear(); + escapedHierarchy.clear(); + while (parentCg.isValid() && parentCg.name() != rootGroup.name()) { + const auto name = parentCg.name(); + hierarchy.prepend(name); + escapedHierarchy.prepend(QString::fromUtf8(QUrl::toPercentEncoding(name.toUtf8()))); + parentCg = parentCg.parent(); + } + + visitedNodes.insert(groupID()); + groups.pop_back(); + + QJsonObject configGroupJson; + + if (!cg.keyList().isEmpty()) { + // TODO: this is conditional if applet or containment + + const auto map = cg.entryMap(); + auto i = map.cbegin(); + for (; i != map.cend(); ++i) { + // some blacklisted keys we don't want to save + if (!forbiddenKeys.contains(i.key())) { + configGroupJson.insert(i.key(), i.value()); + } + } + } + + const auto groupList = cg.groupList(); + for (const QString &groupName : groupList) { + if (groupName == QLatin1String("Applets") || visitedNodes.contains(groupID() + '/' + groupName)) { + continue; + } + groups << KConfigGroup(&cg, groupName); + } + + if (!configGroupJson.isEmpty()) { + result.insert(groupID(), configGroupJson); + } + } + + return result; +} + +QByteArray ShellCorona::dumpCurrentLayoutJS() const +{ + QJsonObject root; + root.insert("serializationFormatVersion", "1"); + + // same gridUnit calculation as ScriptEngine + int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); + if (gridUnit % 2 != 0) { + gridUnit++; + } + + auto isPanel = [](Plasma::Containment *cont) { + return (cont->formFactor() == Plasma::Types::Horizontal || cont->formFactor() == Plasma::Types::Vertical) + && (cont->location() == Plasma::Types::TopEdge || cont->location() == Plasma::Types::BottomEdge || cont->location() == Plasma::Types::LeftEdge + || cont->location() == Plasma::Types::RightEdge) + && cont->pluginMetaData().pluginId() != QLatin1String("org.kde.plasma.private.systemtray"); + }; + + auto isDesktop = [](Plasma::Containment *cont) { + return !cont->activity().isEmpty(); + }; + + const auto containments = ShellCorona::containments(); + + // Collecting panels + + QJsonArray panelsJsonArray; + + for (Plasma::Containment *cont : containments) { + if (!isPanel(cont)) { + continue; + } + + QJsonObject panelJson; + + const PanelView *view = m_panelViews.value(cont); + const auto location = cont->location(); + + panelJson.insert("location", + location == Plasma::Types::TopEdge ? "top" + : location == Plasma::Types::LeftEdge ? "left" + : location == Plasma::Types::RightEdge ? "right" + : /* Plasma::Types::BottomEdge */ "bottom"); + + const qreal height = + // If we do not have a panel, fallback to 4 units + !view ? 4 : (qreal)view->thickness() / gridUnit; + + panelJson.insert("height", height); + if (view) { + const auto alignment = view->alignment(); + panelJson.insert("maximumLength", (qreal)view->maximumLength() / gridUnit); + panelJson.insert("minimumLength", (qreal)view->minimumLength() / gridUnit); + panelJson.insert("offset", (qreal)view->offset() / gridUnit); + panelJson.insert("alignment", alignment == Qt::AlignRight ? "right" : alignment == Qt::AlignCenter ? "center" : "left"); + switch (view->visibilityMode()) { + case PanelView::AutoHide: + panelJson.insert("hiding", "autohide"); + break; + case PanelView::LetWindowsCover: + panelJson.insert("hiding", "windowscover"); + break; + case PanelView::WindowsGoBelow: + panelJson.insert("hiding", "windowsbelow"); + break; + case PanelView::NormalPanel: + default: + panelJson.insert("hiding", "normal"); + break; + } + } + + // Saving the config keys + const KConfigGroup contConfig = cont->config(); + + panelJson.insert("config", dumpconfigGroupJS(contConfig)); + + // Generate the applets array + QJsonArray appletsJsonArray; + + // Try to parse the encoded applets order + const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); + const QStringList appletsOrderStrings = genericConf.readEntry(QStringLiteral("AppletOrder"), QString()).split(QChar(';')); + + // Consider the applet order to be valid only if there are as many entries as applets() + if (appletsOrderStrings.length() == cont->applets().length()) { + for (const QString &appletId : appletsOrderStrings) { + KConfigGroup appletConfig(&contConfig, QStringLiteral("Applets")); + appletConfig = KConfigGroup(&appletConfig, appletId); + + const QString pluginName = appletConfig.readEntry(QStringLiteral("plugin"), QString()); + + if (pluginName.isEmpty()) { + continue; + } + + QJsonObject appletJson; + + appletJson.insert("plugin", pluginName); + appletJson.insert("config", dumpconfigGroupJS(appletConfig)); + + appletsJsonArray << appletJson; + } + + } else { + const auto applets = cont->applets(); + for (Plasma::Applet *applet : applets) { + QJsonObject appletJson; + + KConfigGroup appletConfig = applet->config(); + + appletJson.insert("plugin", applet->pluginMetaData().pluginId()); + appletJson.insert("config", dumpconfigGroupJS(appletConfig)); + + appletsJsonArray << appletJson; + } + } + + panelJson.insert("applets", appletsJsonArray); + + panelsJsonArray << panelJson; + } + + root.insert("panels", panelsJsonArray); + + // Now we are collecting desktops + + QJsonArray desktopsJson; + + const auto currentActivity = m_activityController->currentActivity(); + + for (Plasma::Containment *cont : containments) { + if (!isDesktop(cont) || cont->activity() != currentActivity) { + continue; + } + + QJsonObject desktopJson; + + desktopJson.insert("wallpaperPlugin", cont->wallpaper()); + + // Get the config for the containment + KConfigGroup contConfig = cont->config(); + desktopJson.insert("config", dumpconfigGroupJS(contConfig)); + + // Try to parse the item geometries + const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); + const QStringList appletsGeomStrings = genericConf.readEntry(QStringLiteral("ItemsGeometries"), QString()).split(QChar(';')); + + QHash appletGeometries; + for (const QString &encoded : appletsGeomStrings) { + const QStringList keyValue = encoded.split(QChar(':')); + if (keyValue.length() != 2) { + continue; + } + + const QStringList rectPieces = keyValue.last().split(QChar(',')); + if (rectPieces.length() != 5) { + continue; + } + + QRect rect(rectPieces[0].toInt(), rectPieces[1].toInt(), rectPieces[2].toInt(), rectPieces[3].toInt()); + + appletGeometries[keyValue.first()] = rect; + } + + QJsonArray appletsJsonArray; + + const auto applets = cont->applets(); + for (Plasma::Applet *applet : applets) { + const QRect geometry = appletGeometries.value(QStringLiteral("Applet-") % QString::number(applet->id())); + + QJsonObject appletJson; + + appletJson.insert("title", applet->title()); + appletJson.insert("plugin", applet->pluginMetaData().pluginId()); + + appletJson.insert("geometry.x", geometry.x() / gridUnit); + appletJson.insert("geometry.y", geometry.y() / gridUnit); + appletJson.insert("geometry.width", geometry.width() / gridUnit); + appletJson.insert("geometry.height", geometry.height() / gridUnit); + + KConfigGroup appletConfig = applet->config(); + appletJson.insert("config", dumpconfigGroupJS(appletConfig)); + + appletsJsonArray << appletJson; + } + + desktopJson.insert("applets", appletsJsonArray); + desktopsJson << desktopJson; + } + + root.insert("desktops", desktopsJson); + + QJsonDocument json; + json.setObject(root); + + return + "var plasma = getApiVersion(1);\n\n" + "var layout = " + json.toJson() + ";\n\n" + "plasma.loadSerializedLayout(layout);\n"; +} + +void ShellCorona::loadLookAndFeelDefaultLayout(const QString &packageName) +{ + KPackage::Package newPack = m_lookAndFeelPackage; + newPack.setPath(packageName); + + if (!newPack.isValid()) { + return; + } + + KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_shell + QLatin1String("-appletsrc"), KConfig::SimpleConfig); + + m_lookAndFeelPackage.setPath(packageName); + + // get rid of old config + const QStringList groupList = conf->groupList(); + for (const QString &group : groupList) { + conf->deleteGroup(group); + } + conf->sync(); + unload(); + // Put load in queue of the event loop to wait for the whole set of containments to have been deleteLater(), as some like FolderView operate on singletons + // which can cause inconsistent states + QTimer::singleShot(0, this, &ShellCorona::load); +} + +QString ShellCorona::shell() const +{ + return m_shell; +} + +void ShellCorona::load() +{ + if (m_shell.isEmpty()) { + return; + } + + auto activityStatus = m_activityController->serviceStatus(); + if (activityStatus != KActivities::Controller::Running && !qApp->property("org.kde.KActivities.core.disableAutostart").toBool()) { + if (activityStatus == KActivities::Controller::NotRunning) { + qWarning("Aborting shell load: The activity manager daemon (kactivitymanagerd) is not running."); + qWarning( + "If this Plasma has been installed into a custom prefix, verify that its D-Bus services dir is known to the system for the daemon to be " + "activatable."); + } + return; + } + + disconnect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load); + + m_screenPool->load(); + + // TODO: a kconf_update script is needed + QString configFileName(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc")); + + loadLayout(configFileName); + + checkActivities(); + + if (containments().isEmpty()) { + // Seems like we never really get to this point since loadLayout already + // (virtually) calls loadDefaultLayout if it does not load anything + // from the config file. Maybe if the config file is not empty, + // but still does not have any containments + loadDefaultLayout(); + processUpdateScripts(); + } else { + processUpdateScripts(); + const auto containments = this->containments(); + for (Plasma::Containment *containment : containments) { + if (containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment) { + // Don't give a view to containments that don't want one (negative lastscreen) + //(this is pretty mucha special case for the systray) + // also, make sure we don't have a view already. + // this will be true for first startup as the view has already been created at the new Panel JS call + if (!m_waitingPanels.contains(containment) && containment->lastScreen() >= 0 && !m_panelViews.contains(containment)) { + m_waitingPanels << containment; + } + // historically CustomContainments are treated as desktops + } else if (containment->containmentType() == Plasma::Types::DesktopContainment + || containment->containmentType() == Plasma::Types::CustomContainment) { + // FIXME ideally fix this, or at least document the crap out of it + int screen = containment->lastScreen(); + if (screen < 0) { + screen = 0; + qCWarning(PLASMASHELL) << "last screen is < 0 so putting containment on screen " << screen; + } + insertContainment(containment->activity(), screen, containment); + } + } + } + + // NOTE: this is needed in case loadLayout() did *not* call loadDefaultLayout() + // it needs to be after of loadLayout() as it would always create new + // containments on each startup otherwise + const auto screens = m_screenPool->screens(); + for (QScreen *screen : screens) { + // the containments may have been created already by the startup script + // check their existence in order to not have duplicated desktopviews + if (!m_desktopViewForScreen.contains(screen)) { + addOutput(screen); + } + } + connect(m_screenPool, &ScreenPool::screenAdded, this, &ShellCorona::addOutput, Qt::UniqueConnection); + connect(m_screenPool, &ScreenPool::screenRemoved, this, &ShellCorona::handleScreenRemoved, Qt::UniqueConnection); + connect(m_screenPool, &ScreenPool::primaryScreenChanged, this, &ShellCorona::primaryScreenChanged, Qt::UniqueConnection); + + if (!m_waitingPanels.isEmpty()) { + m_waitingPanelsTimer.start(); + } + + if (config()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma/plasmashell/unlockedDesktop"))) { + setImmutability(Plasma::Types::SystemImmutable); + } else { + KConfigGroup coronaConfig(config(), "General"); + setImmutability((Plasma::Types::ImmutabilityType)coronaConfig.readEntry("immutability", static_cast(Plasma::Types::Mutable))); + } +} + +void ShellCorona::primaryScreenChanged(QScreen *oldPrimary, QScreen *newPrimary) +{ + // when the appearance of a new primary screen *moves* + // the position of the now secondary, the two screens will appear overlapped for an instant, and a spurious output redundant would happen here if checked + // immediately +#ifndef NDEBUG + m_invariantsTimer.start(); +#endif + // swap order in m_desktopViewForScreen + if (m_desktopViewForScreen.contains(oldPrimary) && m_desktopViewForScreen.contains(newPrimary)) { + DesktopView *primaryDesktop = m_desktopViewForScreen.value(oldPrimary); + DesktopView *oldDesktopOfPrimary = m_desktopViewForScreen.value(newPrimary); + primaryDesktop->setScreenToFollow(newPrimary); + m_desktopViewForScreen[newPrimary] = primaryDesktop; + primaryDesktop->show(); + oldDesktopOfPrimary->setScreenToFollow(oldPrimary); + m_desktopViewForScreen[oldPrimary] = oldDesktopOfPrimary; + oldDesktopOfPrimary->show(); + } + + for (PanelView *panel : qAsConst(m_panelViews)) { + if (panel->screen() == oldPrimary) { + panel->setScreenToFollow(newPrimary); + } else if (panel->screen() == newPrimary) { + panel->setScreenToFollow(oldPrimary); + } + } + + // can't do the screen invariant here as reconsideroutputs wasn't executed yet + // CHECK_SCREEN_INVARIANTS +} + +#ifndef NDEBUG +void ShellCorona::screenInvariants() const +{ + if (m_screenPool->noRealOutputsConnected()) { + Q_ASSERT(m_desktopViewForScreen.isEmpty()); + Q_ASSERT(m_panelViews.isEmpty()); + return; + } + const QList screenKeys = m_desktopViewForScreen.keys(); + + Q_ASSERT(screenKeys.count() <= m_screenPool->screens().count()); + + QSet screens; + for (const QScreen *screenKey : screenKeys) { + const int id = m_screenPool->id(screenKey->name()); + const DesktopView *view = m_desktopViewForScreen.value(screenKey); + QScreen *screen = view->screenToFollow(); + Q_ASSERT(screenKey == screen); + Q_ASSERT(!screens.contains(screen)); + // commented out because a different part of the code-base is responsible for this + // and sometimes is not yet called here. + // Q_ASSERT(!view->fillScreen() || view->geometry() == screen->geometry()); + Q_ASSERT(view->containment()); + + Q_ASSERT(view->containment()->screen() == id || view->containment()->screen() == -1); + Q_ASSERT(view->containment()->lastScreen() == id || view->containment()->lastScreen() == -1); + Q_ASSERT(view->isVisible()); + + foreach (const PanelView *panel, panelsForScreen(screen)) { + Q_ASSERT(panel->containment()); + Q_ASSERT(panel->containment()->screen() == id || panel->containment()->screen() == -1); + // If any kscreen related activities occurred + // during startup, the panel wouldn't be visible yet, and this would assert + if (panel->containment()->isUiReady()) { + Q_ASSERT(panel->isVisible()); + } + } + + screens.insert(screen); + } + + if (m_desktopViewForScreen.isEmpty()) { + qWarning() << "no screens!!"; + } +} +#endif + +void ShellCorona::showAlternativesForApplet(Plasma::Applet *applet) +{ + const QUrl alternativesQML = kPackage().fileUrl("appletalternativesui"); + if (alternativesQML.isEmpty()) { + return; + } + + auto *qmlObj = new KDeclarative::QmlObjectSharedEngine(this); + qmlObj->setInitializationDelayed(true); + qmlObj->setSource(alternativesQML); + + AlternativesHelper *helper = new AlternativesHelper(applet, qmlObj); + qmlObj->rootContext()->setContextProperty(QStringLiteral("alternativesHelper"), helper); + + qmlObj->completeInitialization(); + + auto dialog = qobject_cast(qmlObj->rootObject()); + if (!dialog) { + qCWarning(PLASMASHELL) << "Alternatives UI does not inherit from Dialog"; + delete qmlObj; + return; + } + connect(applet, &Plasma::Applet::destroyedChanged, qmlObj, [qmlObj](bool destroyed) { + if (!destroyed) { + return; + } + qmlObj->deleteLater(); + }); + connect(dialog, &PlasmaQuick::Dialog::visibleChanged, qmlObj, [qmlObj](bool visible) { + if (visible) { + return; + } + qmlObj->deleteLater(); + }); +} + +void ShellCorona::unload() +{ + if (m_shell.isEmpty()) { + return; + } + qDeleteAll(m_desktopViewForScreen); + m_desktopViewForScreen.clear(); + qDeleteAll(m_panelViews); + m_panelViews.clear(); + m_waitingPanels.clear(); + m_activityContainmentPlugins.clear(); + + while (!containments().isEmpty()) { + // Some applets react to destroyedChanged rather just destroyed, + // give them the possibility to react + // deleting a containment will remove it from the list due to QObject::destroyed connect in Corona + // this form doesn't crash, while qDeleteAll(containments()) does + // And is more correct anyways to use destroy() + containments().constFirst()->destroy(); + } +} + +KSharedConfig::Ptr ShellCorona::applicationConfig() +{ + return KSharedConfig::openConfig(); +} + +void ShellCorona::requestApplicationConfigSync() +{ + m_appConfigSyncTimer.start(); +} + +void ShellCorona::loadDefaultLayout() +{ + // pre-startup scripts + QString script = m_lookAndFeelPackage.filePath("layouts", shell() + "-prelayout.js"); + if (!script.isEmpty()) { + QFile file(script); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString code = file.readAll(); + qDebug() << "evaluating pre-startup script:" << script; + + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + if (!scriptEngine.evaluateScript(code, script)) { + qCWarning(PLASMASHELL) << "failed to initialize layout properly:" << script; + } + } + } + + // NOTE: Is important the containments already exist for each screen + // at the moment of the script execution,the same loop in :load() + // is executed too late + const auto screens = m_screenPool->screens(); + for (QScreen *screen : screens) { + addOutput(screen); + } + + script = m_testModeLayout; + + if (script.isEmpty()) { + script = m_lookAndFeelPackage.filePath("layouts", shell() + "-layout.js"); + } + if (script.isEmpty()) { + script = kPackage().filePath("defaultlayout"); + } + + QFile file(script); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString code = file.readAll(); + qDebug() << "evaluating startup script:" << script; + + // We need to know which activities are here in order for + // the scripting engine to work. activityAdded does not mind + // if we pass it the same activity multiple times + const QStringList existingActivities = m_activityController->activities(); + for (const QString &id : existingActivities) { + activityAdded(id); + } + + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + if (!scriptEngine.evaluateScript(code, script)) { + qCWarning(PLASMASHELL) << "failed to initialize layout properly:" << script; + } + } + + Q_EMIT startupCompleted(); +} + +void ShellCorona::processUpdateScripts() +{ + const QStringList scripts = WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this); + if (scripts.isEmpty()) { + return; + } + + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + + for (const QString &script : scripts) { + QFile file(script); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString code = file.readAll(); + scriptEngine.evaluateScript(code); + } else { + qCWarning(PLASMASHELL) << "Unable to open the script file" << script << "for reading"; + } + } +} + +int ShellCorona::numScreens() const +{ + return m_screenPool->screens().count(); +} + +QRect ShellCorona::screenGeometry(int id) const +{ + QScreen *screen = m_screenPool->screenForId(id); + DesktopView *view = screen ? m_desktopViewForScreen.value(screen) : nullptr; + if (!screen || !view) { + qWarning() << "requesting unexisting screen" << id; + QScreen *s = m_screenPool->primaryScreen(); + return s ? s->geometry() : QRect(); + } + return view->geometry(); +} + +QRegion ShellCorona::availableScreenRegion(int id) const +{ + return m_strutManager->availableScreenRegion(id); +} + +QRegion ShellCorona::_availableScreenRegion(int id) const +{ + QScreen *screen = m_screenPool->screenForId(id); + DesktopView *view = screen ? m_desktopViewForScreen.value(screen) : nullptr; + if (!screen || !view) { + // each screen should have a view + qWarning() << "requesting unexisting screen" << id; + QScreen *s = m_screenPool->primaryScreen(); + return s ? s->availableGeometry() : QRegion(); + } + + QRegion r = view->geometry(); + for (const PanelView *v : m_panelViews) { + if (v->isVisible() && view->screen() == v->screen() && v->visibilityMode() != PanelView::AutoHide) { + // if the panel is being moved around, we still want to calculate it from the edge + r -= v->geometryByDistance(0); + } + } + return r; +} + +QRect ShellCorona::availableScreenRect(int id) const +{ + return m_strutManager->availableScreenRect(id); +} + +QRect ShellCorona::_availableScreenRect(int id) const +{ + QScreen *screen = m_screenPool->screenForId(id); + DesktopView *view = screen ? m_desktopViewForScreen.value(screen) : nullptr; + if (!screen || !view) { + // each screen should have a view + qWarning() << "requesting unexisting screen" << id; + QScreen *s = m_screenPool->primaryScreen(); + return s ? s->availableGeometry() : QRect(); + } + + QRect r = view->geometry(); + for (PanelView *v : m_panelViews) { + if (v->isVisible() && v->screen() == view->screen() && v->visibilityMode() != PanelView::AutoHide) { + switch (v->location()) { + case Plasma::Types::LeftEdge: + r.setLeft(r.left() + v->thickness()); + break; + case Plasma::Types::RightEdge: + r.setRight(r.right() - v->thickness()); + break; + case Plasma::Types::TopEdge: + r.setTop(r.top() + v->thickness()); + break; + case Plasma::Types::BottomEdge: + r.setBottom(r.bottom() - v->thickness()); + default: + break; + } + } + } + return r; +} + +QStringList ShellCorona::availableActivities() const +{ + return m_activityContainmentPlugins.keys(); +} + +void ShellCorona::removeDesktop(DesktopView *desktopView) +{ + const int idx = m_screenPool->id(desktopView->screenToFollow()->name()); + + // Iterate instead of going by index: QScreen can be recyced to be the "fake" output + // so searching by id won't work anymore + auto deskIt = m_desktopViewForScreen.begin(); + while (deskIt != m_desktopViewForScreen.end()) { + DesktopView *view = deskIt.value(); + + if (view == desktopView) { + break; + } + ++deskIt; + } + Q_ASSERT(deskIt != m_desktopViewForScreen.end()); + + QMutableHashIterator it(m_panelViews); + while (it.hasNext()) { + it.next(); + PanelView *panelView = it.value(); + + if (panelView->containment()->screen() == idx) { + m_waitingPanels << panelView->containment(); + it.remove(); + panelView->destroy(); + } + } + + m_desktopViewForScreen.erase(deskIt); + desktopView->destroy(); + + Q_EMIT screenRemoved(idx); +} + +PanelView *ShellCorona::panelView(Plasma::Containment *containment) const +{ + return m_panelViews.value(containment); +} + +///// SLOTS + +QList ShellCorona::panelsForScreen(QScreen *screen) const +{ + QList ret; + for (PanelView *v : m_panelViews) { + if (v->screenToFollow() == screen) { + ret += v; + } + } + return ret; +} + +DesktopView *ShellCorona::desktopForScreen(QScreen *screen) const +{ + DesktopView *view = m_desktopViewForScreen.value(screen); + //An output may have been renamed, fall back to a linear check + if (view) { + return view; + } else { + for (DesktopView *v : m_desktopViewForScreen) { + if (v->screenToFollow() == screen) { // FIXME: whe should never hit this? + return v; + } + } + } + return nullptr; +} + +void ShellCorona::handleScreenRemoved(QScreen *screen) +{ + if (DesktopView *v = desktopForScreen(screen)) { + removeDesktop(v); + } +#ifndef NDEBUG + m_invariantsTimer.start(); +#endif +} + +void ShellCorona::addOutput(QScreen *screen) +{ + Q_ASSERT(screen); + + if (m_desktopViewForScreen.contains(screen)) { + return; + } + Q_ASSERT(!screen->geometry().isNull()); +#ifndef NDEBUG + connect(screen, &QScreen::geometryChanged, &m_invariantsTimer, static_cast(&QTimer::start), Qt::UniqueConnection); +#endif + int insertPosition = m_screenPool->id(screen->name()); + Q_ASSERT(insertPosition >= 0); + + DesktopView *view = new DesktopView(this, screen); + + if (view->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { + connect(view, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); + } + connect(view, &DesktopView::geometryChanged, this, [=]() { + const int id = m_screenPool->id(view->screen()->name()); + if (id >= 0) { + Q_EMIT screenGeometryChanged(id); + Q_EMIT availableScreenRegionChanged(); + Q_EMIT availableScreenRectChanged(); + } + }); + + Plasma::Containment *containment = createContainmentForActivity(m_activityController->currentActivity(), insertPosition); + Q_ASSERT(containment); + + QAction *removeAction = containment->actions()->action(QStringLiteral("remove")); + if (removeAction) { + removeAction->deleteLater(); + } + + connect(containment, &Plasma::Containment::uiReadyChanged, this, &ShellCorona::checkAllDesktopsUiReady); + + m_desktopViewForScreen[screen] = view; + view->setContainment(containment); + view->show(); + Q_ASSERT(screen == view->screen()); + + // need to specifically call the reactToScreenChange, since when the screen is shown it's not yet + // in the list. We still don't want to have an invisible view added. + containment->reactToScreenChange(); + + // were there any panels for this screen before it popped up? + if (!m_waitingPanels.isEmpty()) { + m_waitingPanelsTimer.start(); + } + + Q_EMIT availableScreenRectChanged(); + Q_EMIT screenAdded(m_screenPool->id(screen->name())); +#ifndef NDEBUG + m_invariantsTimer.start(); +#endif +} + +void ShellCorona::checkAllDesktopsUiReady(bool ready) +{ + if (!ready) + return; + for (auto v : qAsConst(m_desktopViewForScreen)) { + if (!v->containment()->isUiReady()) + return; + + qDebug() << "Plasma Shell startup completed"; + QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), + QStringLiteral("/KSplash"), + QStringLiteral("org.kde.KSplash"), + QStringLiteral("setStage")); + ksplashProgressMessage.setArguments(QList() << QStringLiteral("desktop")); + QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); + } +} + +Plasma::Containment *ShellCorona::createContainmentForActivity(const QString &activity, int screenNum) +{ + const auto containments = containmentsForActivity(activity); + for (Plasma::Containment *cont : containments) { + // in the case of a corrupt config file + // with multiple containments with same lastScreen + // it can happen two insertContainment happen for + // the same screen, leading to the old containment + // to be destroyed + if (!cont->destroyed() && cont->screen() == screenNum) { + return cont; + } + } + + QString plugin = m_activityContainmentPlugins.value(activity); + + if (plugin.isEmpty()) { + plugin = defaultContainmentPlugin(); + } + + Plasma::Containment *containment = containmentForScreen(screenNum, activity, plugin, QVariantList()); + Q_ASSERT(containment); + + if (containment) { + insertContainment(activity, screenNum, containment); + } + + return containment; +} + +void ShellCorona::createWaitingPanels() +{ + QList stillWaitingPanels; + + for (Plasma::Containment *cont : qAsConst(m_waitingPanels)) { + // ignore non existing (yet?) screens + int requestedScreen = cont->lastScreen(); + if (requestedScreen < 0) { + requestedScreen = 0; + } + + QScreen *screen = m_screenPool->screenForId(requestedScreen); + DesktopView *desktopView = screen ? m_desktopViewForScreen.value(screen) : nullptr; + if (!screen || !desktopView) { + stillWaitingPanels << cont; + continue; + } + + // TODO: does a similar check make sense? + // Q_ASSERT(qBound(0, requestedScreen, m_screenPool->count() - 1) == requestedScreen); + PanelView *panel = new PanelView(this, screen); + if (panel->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { + connect(panel, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); + } + connect(panel, &QWindow::visibleChanged, this, &Plasma::Corona::availableScreenRectChanged); + connect(panel, &QWindow::screenChanged, this, &Plasma::Corona::availableScreenRectChanged); + connect(panel, &PanelView::locationChanged, this, &Plasma::Corona::availableScreenRectChanged); + connect(panel, &PanelView::visibilityModeChanged, this, &Plasma::Corona::availableScreenRectChanged); + connect(panel, &PanelView::thicknessChanged, this, &Plasma::Corona::availableScreenRectChanged); + + m_panelViews[cont] = panel; + panel->setContainment(cont); + cont->reactToScreenChange(); + + connect(cont, &QObject::destroyed, this, &ShellCorona::panelContainmentDestroyed); + } + m_waitingPanels = stillWaitingPanels; + Q_EMIT availableScreenRectChanged(); +} + +void ShellCorona::panelContainmentDestroyed(QObject *cont) +{ + auto view = m_panelViews.take(static_cast(cont)); + delete view; + // don't make things relayout when the application is quitting + // NOTE: qApp->closingDown() is still false here + if (!m_closingDown) { + Q_EMIT availableScreenRectChanged(); + } +} + +void ShellCorona::handleContainmentAdded(Plasma::Containment *c) +{ + connect(c, &Plasma::Containment::showAddWidgetsInterface, this, &ShellCorona::toggleWidgetExplorer); + connect(c, &Plasma::Containment::appletAlternativesRequested, this, &ShellCorona::showAlternativesForApplet); + connect(c, &Plasma::Containment::appletCreated, this, [this, c](Plasma::Applet *applet) { + executeSetupPlasmoidScript(c, applet); + }); + + // When a containment is removed, remove the thumbnail as well + connect(c, &Plasma::Containment::destroyedChanged, this, [this, c](bool destroyed) { + if (!destroyed) { + return; + } + const QString snapshotPath = containmentPreviewPath(c); + if (!snapshotPath.isEmpty()) { + QFile f(snapshotPath); + f.remove(); + } + }); +} + +void ShellCorona::executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet) +{ + if (!applet->pluginMetaData().isValid() || !containment->pluginMetaData().isValid()) { + return; + } + + const QString scriptFile = m_lookAndFeelPackage.filePath("plasmoidsetupscripts", applet->pluginMetaData().pluginId() + ".js"); + + if (scriptFile.isEmpty()) { + return; + } + + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + + QFile file(scriptFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(PLASMASHELL) << "Unable to load script file:" << scriptFile; + return; + } + + QString script = file.readAll(); + if (script.isEmpty()) { + // qDebug() << "script is empty"; + return; + } + + scriptEngine.globalObject().setProperty(QStringLiteral("applet"), scriptEngine.wrap(applet)); + scriptEngine.globalObject().setProperty(QStringLiteral("containment"), scriptEngine.wrap(containment)); + scriptEngine.evaluateScript(script, scriptFile); +} + +void ShellCorona::toggleWidgetExplorer() +{ + // FIXME: This does not work on wayland + const QPoint cursorPos = QCursor::pos(); + for (DesktopView *view : qAsConst(m_desktopViewForScreen)) { + if (view->screen()->geometry().contains(cursorPos)) { + // The view QML has to provide something to display the widget explorer + view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender()))); + return; + } + } +} + +void ShellCorona::toggleActivityManager() +{ + const QPoint cursorPos = QCursor::pos(); + for (DesktopView *view : qAsConst(m_desktopViewForScreen)) { + if (view->screen()->geometry().contains(cursorPos)) { + // The view QML has to provide something to display the activity explorer + view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleActivityManager", Qt::QueuedConnection); + return; + } + } +} + +void ShellCorona::syncAppConfig() +{ + applicationConfig()->sync(); +} + +void ShellCorona::setDashboardShown(bool show) +{ + KWindowSystem::setShowingDesktop(show); +} + +void ShellCorona::toggleDashboard() +{ + setDashboardShown(!KWindowSystem::showingDesktop()); +} + +QString ShellCorona::evaluateScript(const QString &script) +{ + if (calledFromDBus()) { + if (immutability() == Plasma::Types::SystemImmutable) { + sendErrorReply(QDBusError::Failed, QStringLiteral("Widgets are locked")); + return QString(); + } else if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) { + sendErrorReply(QDBusError::Failed, QStringLiteral("Administrative policies prevent script execution")); + return QString(); + } + } + + WorkspaceScripting::ScriptEngine scriptEngine(this); + QString buffer; + QTextStream bufferStream(&buffer, QIODevice::WriteOnly | QIODevice::Text); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [&bufferStream](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + bufferStream << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [&bufferStream](const QString &msg) { + qDebug() << msg; + bufferStream << msg; + }); + + scriptEngine.evaluateScript(script); + + bufferStream.flush(); + + if (calledFromDBus() && !scriptEngine.errorString().isEmpty()) { + sendErrorReply(QDBusError::Failed, scriptEngine.errorString()); + return QString(); + } + + return buffer; +} + +void ShellCorona::checkActivities() +{ + KActivities::Controller::ServiceStatus status = m_activityController->serviceStatus(); + // qDebug() << "$%$%$#%$%$%Status:" << status; + if (status != KActivities::Controller::Running) { + // panic and give up - better than causing a mess + qDebug() << "ShellCorona::checkActivities is called whilst activity daemon is still connecting"; + return; + } + + const QStringList existingActivities = m_activityController->activities(); + for (const QString &id : existingActivities) { + activityAdded(id); + } + + // Checking whether the result we got is valid. Just in case. + Q_ASSERT_X(!existingActivities.isEmpty(), "isEmpty", "There are no activities, and the service is running"); + Q_ASSERT_X(existingActivities[0] != QLatin1String("00000000-0000-0000-0000-000000000000"), "null uuid", "There is a nulluuid activity present"); + + // Killing the unassigned containments + const auto conts = containments(); + for (Plasma::Containment *cont : conts) { + if ((cont->containmentType() == Plasma::Types::DesktopContainment || cont->containmentType() == Plasma::Types::CustomContainment) + && !existingActivities.contains(cont->activity())) { + cont->destroy(); + } + } +} + +void ShellCorona::currentActivityChanged(const QString &newActivity) +{ + // qDebug() << "Activity changed:" << newActivity; + + for (auto it = m_desktopViewForScreen.constBegin(); it != m_desktopViewForScreen.constEnd(); ++it) { + Plasma::Containment *c = createContainmentForActivity(newActivity, m_screenPool->id(it.key()->name())); + + QAction *removeAction = c->actions()->action(QStringLiteral("remove")); + if (removeAction) { + removeAction->deleteLater(); + } + (*it)->setContainment(c); + } +} + +void ShellCorona::activityAdded(const QString &id) +{ + // TODO more sanity checks + if (m_activityContainmentPlugins.contains(id)) { + qCWarning(PLASMASHELL) << "Activity added twice" << id; + return; + } + + m_activityContainmentPlugins.insert(id, defaultContainmentPlugin()); +} + +void ShellCorona::activityRemoved(const QString &id) +{ + m_activityContainmentPlugins.remove(id); + const QList containments = containmentsForActivity(id); + for (auto cont : containments) { + cont->destroy(); + } +} + +void ShellCorona::insertActivity(const QString &id, const QString &plugin) +{ + activityAdded(id); + + // TODO: This needs to go away! + // The containment creation API does not know when we have a + // new activity to create a containment for, we need to pretend + // that the current activity has been changed + QFuture currentActivity = m_activityController->setCurrentActivity(id); + awaitFuture(currentActivity); + + if (!currentActivity.result()) { + qDebug() << "Failed to create and switch to the activity"; + return; + } + + while (m_activityController->currentActivity() != id) { + QCoreApplication::processEvents(); + } + + m_activityContainmentPlugins.insert(id, plugin); + for (auto it = m_desktopViewForScreen.constBegin(); it != m_desktopViewForScreen.constEnd(); ++it) { + Plasma::Containment *c = createContainmentForActivity(id, m_screenPool->id(it.key()->name())); + if (c) { + c->config().writeEntry("lastScreen", m_screenPool->id(it.key()->name())); + } + } +} + +Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin) +{ + // search but not create + Plasma::Containment *oldContainment = containmentForScreen(screen, m_activityController->currentActivity(), QString()); + + // no valid containment in given screen, giving up + if (!oldContainment) { + return nullptr; + } + + if (plugin.isEmpty()) { + return oldContainment; + } + + DesktopView *view = nullptr; + for (DesktopView *v : qAsConst(m_desktopViewForScreen)) { + if (v->containment() == oldContainment) { + view = v; + break; + } + } + + // no view? give up + if (!view) { + return oldContainment; + } + + // create a new containment + Plasma::Containment *newContainment = createContainmentDelayed(plugin); + + // if creation failed or invalid plugin, give up + if (!newContainment) { + return oldContainment; + } else if (!newContainment->pluginMetaData().isValid()) { + newContainment->deleteLater(); + return oldContainment; + } + + newContainment->setWallpaper(oldContainment->wallpaper()); + + // At this point we have a valid new containment from plugin and a view + // copy all configuration groups (excluded applets) + KConfigGroup oldCg = oldContainment->config(); + + // newCg *HAS* to be from a KSharedConfig, because some KConfigSkeleton will need to be synced + // this makes the configscheme work + KConfigGroup newCg(KSharedConfig::openConfig(oldCg.config()->name()), "Containments"); + newCg = KConfigGroup(&newCg, QString::number(newContainment->id())); + + // this makes containment->config() work, is a separate thing from its configscheme + KConfigGroup newCg2 = newContainment->config(); + + const auto groups = oldCg.groupList(); + for (const QString &group : groups) { + if (group != QLatin1String("Applets")) { + KConfigGroup subGroup(&oldCg, group); + KConfigGroup newSubGroup(&newCg, group); + subGroup.copyTo(&newSubGroup); + + KConfigGroup newSubGroup2(&newCg2, group); + subGroup.copyTo(&newSubGroup2); + } + } + + newContainment->init(); + newCg.writeEntry("activityId", oldContainment->activity()); + newContainment->restore(newCg); + newContainment->updateConstraints(Plasma::Types::StartupCompletedConstraint); + newContainment->flushPendingConstraintsEvents(); + Q_EMIT containmentAdded(newContainment); + + // Move the applets + const auto applets = oldContainment->applets(); + for (Plasma::Applet *applet : applets) { + newContainment->addApplet(applet); + } + + // remove the "remove" action + QAction *removeAction = newContainment->actions()->action(QStringLiteral("remove")); + if (removeAction) { + removeAction->deleteLater(); + } + view->setContainment(newContainment); + newContainment->setActivity(oldContainment->activity()); + insertContainment(oldContainment->activity(), screen, newContainment); + + // removing the focus from the item that is going to be destroyed + // fixes a crash + // delayout the destruction of the old containment fixes another crash + view->rootObject()->setFocus(true, Qt::MouseFocusReason); + QTimer::singleShot(2500, oldContainment, &Plasma::Applet::destroy); + + // Save now as we now have a screen, so lastScreen will not be -1 + newContainment->save(newCg); + requestConfigSync(); + Q_EMIT availableScreenRectChanged(); + + return newContainment; +} + +void ShellCorona::checkAddPanelAction() +{ + delete m_addPanelAction; + m_addPanelAction = nullptr; + + m_addPanelsMenu.reset(nullptr); + + const QList panelContainmentPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel")); + + auto filter = [](const KPluginMetaData &md) -> bool { + return !md.rawData().value(QStringLiteral("NoDisplay")).toBool() + && md.value(QStringLiteral("X-Plasma-ContainmentCategories"), QStringList()).contains(QLatin1String("panel")); + }; + QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); + + if (panelContainmentPlugins.count() + templates.count() == 1) { + m_addPanelAction = new QAction(this); + connect(m_addPanelAction, &QAction::triggered, this, qOverload<>(&ShellCorona::addPanel)); + } else if (!panelContainmentPlugins.isEmpty()) { + m_addPanelAction = new QAction(this); + m_addPanelsMenu.reset(new QMenu); + m_addPanelAction->setMenu(m_addPanelsMenu.data()); + connect(m_addPanelsMenu.data(), &QMenu::aboutToShow, this, &ShellCorona::populateAddPanelsMenu); + connect(m_addPanelsMenu.data(), &QMenu::triggered, this, qOverload(&ShellCorona::addPanel)); + } + + if (m_addPanelAction) { + m_addPanelAction->setText(i18n("Add Panel")); + m_addPanelAction->setData(Plasma::Types::AddAction); + m_addPanelAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); + actions()->addAction(QStringLiteral("add panel"), m_addPanelAction); + } +} + +void ShellCorona::populateAddPanelsMenu() +{ + m_addPanelsMenu->clear(); + const KPluginMetaData emptyInfo; + + const QList panelContainmentPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel")); + QMap> sorted; + for (const KPluginMetaData &plugin : panelContainmentPlugins) { + if (plugin.rawData().value(QStringLiteral("NoDisplay")).toBool()) { + continue; + } + sorted.insert(plugin.name(), qMakePair(plugin, KPluginMetaData())); + } + + auto filter = [](const KPluginMetaData &md) -> bool { + return !md.rawData().value(QStringLiteral("NoDisplay")).toBool() + && md.value(QStringLiteral("X-Plasma-ContainmentCategories"), QStringList()).contains(QLatin1String("panel")); + }; + const QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); + for (const auto &tpl : templates) { + sorted.insert(tpl.name(), qMakePair(emptyInfo, tpl)); + } + + QMapIterator> it(sorted); + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate")); + while (it.hasNext()) { + it.next(); + QPair pair = it.value(); + if (pair.first.isValid()) { + KPluginMetaData plugin = pair.first; + QAction *action = m_addPanelsMenu->addAction(i18n("Empty %1", plugin.name())); + if (!plugin.iconName().isEmpty()) { + action->setIcon(QIcon::fromTheme(plugin.iconName())); + } + + action->setData(plugin.pluginId()); + } else { + KPluginMetaData plugin(pair.second); + package.setPath(plugin.pluginId()); + const QString scriptFile = package.filePath("mainscript"); + if (!scriptFile.isEmpty()) { + QAction *action = m_addPanelsMenu->addAction(plugin.name()); + action->setData(QStringLiteral("plasma-desktop-template:%1").arg(plugin.pluginId())); + } + } + } +} + +void ShellCorona::addPanel() +{ + const QList panelPlugins = Plasma::PluginLoader::listContainmentsMetaDataOfType(QStringLiteral("Panel")); + + if (!panelPlugins.isEmpty()) { + addPanel(panelPlugins.first().pluginId()); + } +} + +void ShellCorona::addPanel(QAction *action) +{ + const QString plugin = action->data().toString(); + if (plugin.startsWith(QLatin1String("plasma-desktop-template:"))) { + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + const QString templateName = plugin.right(plugin.length() - qstrlen("plasma-desktop-template:")); + + scriptEngine.evaluateScript(QStringLiteral("loadTemplate(\"%1\")").arg(templateName)); + } else if (!plugin.isEmpty()) { + addPanel(plugin); + } +} + +Plasma::Containment *ShellCorona::addPanel(const QString &plugin) +{ + Plasma::Containment *panel = createContainment(plugin); + if (!panel) { + return nullptr; + } + + // find out what screen this panel should go on + QScreen *wantedScreen = qGuiApp->focusWindow() ? qGuiApp->focusWindow()->screen() : m_screenPool->primaryScreen(); + + QList availableLocations; + availableLocations << Plasma::Types::LeftEdge << Plasma::Types::TopEdge << Plasma::Types::RightEdge << Plasma::Types::BottomEdge; + + for (auto it = m_panelViews.constBegin(); it != m_panelViews.constEnd(); ++it) { + if ((*it)->screenToFollow() == wantedScreen) { + availableLocations.removeAll((*it)->location()); + } + } + + Plasma::Types::Location loc; + if (availableLocations.isEmpty()) { + loc = Plasma::Types::TopEdge; + } else { + loc = availableLocations.first(); + } + + panel->setLocation(loc); + switch (loc) { + case Plasma::Types::LeftEdge: + case Plasma::Types::RightEdge: + panel->setFormFactor(Plasma::Types::Vertical); + break; + default: + panel->setFormFactor(Plasma::Types::Horizontal); + break; + } + + Q_ASSERT(panel); + m_waitingPanels << panel; + // immediately create the panel here so that we have access to the panel view + createWaitingPanels(); + + if (m_panelViews.contains(panel)) { + m_panelViews.value(panel)->setScreenToFollow(wantedScreen); + } + + return panel; +} + +void ShellCorona::swapDesktopScreens(int oldScreen, int newScreen) +{ + for (auto *containment : containmentsForScreen(oldScreen)) { + if (containment->containmentType() != Plasma::Types::PanelContainment + && containment->containmentType() != Plasma::Types::CustomPanelContainment) { + setScreenForContainment(containment, newScreen); + } + } +} + +void ShellCorona::setScreenForContainment(Plasma::Containment *containment, int newScreenId) +{ + if (newScreenId < 0 || !m_screenPool->knownIds().contains(newScreenId)) { + qCWarning(PLASMASHELL) << "Invalid screen id requested:" << newScreenId << "for containment" << containment; + return; + } + const int oldScreenId = containment->screen() >= 0 ? containment->screen() : containment->lastScreen(); + + if (oldScreenId == newScreenId) { + return; + } + + m_pendingScreenChanges[containment] = newScreenId; + + if (containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment) { + // Panel Case + containment->reactToScreenChange(); + auto *panelView = m_panelViews.value(containment); + // If newScreen != nullptr we are also assured it won't be redundant + QScreen *newScreen = m_screenPool->screenForId(newScreenId); + + if (panelView) { + // There was an existing panel view + if (newScreen) { + panelView->setScreenToFollow(newScreen); + } else { + // Not on a connected screen: destroy the panel + if (!m_waitingPanels.contains(containment)) { + m_waitingPanels << containment; + } + m_panelViews.remove(containment); + panelView->destroy(); + } + } else { + // Didn't have a view, createWaitingPanels() will create it if needed + createWaitingPanels(); + } + + } else { + // Desktop case: a bit more complicate because we may have to swap + Plasma::Containment *contSwap = containmentForScreen(newScreenId, containment->activity(), "org.kde.plasma.folder"); + Q_ASSERT(contSwap); + + // Perform the lastScreen changes + containment->reactToScreenChange(); + if (contSwap) { + m_pendingScreenChanges[contSwap] = oldScreenId; + contSwap->reactToScreenChange(); + } + + // If those will be != nullptr we are also assured they won't be redundant + QScreen *oldScreen = m_screenPool->screenForId(oldScreenId); + QScreen *newScreen = m_screenPool->screenForId(newScreenId); + + // Actually perform a view swap when we are dealing with containments of current activity + if (contSwap && containment->activity() == m_activityController->currentActivity()) { + auto *containmentView = m_desktopViewForScreen.value(oldScreen); + auto *contSwapView = m_desktopViewForScreen.value(newScreen); + m_desktopViewForScreen.remove(newScreen); + m_desktopViewForScreen.remove(oldScreen); + + const QString containmentConnector = m_screenPool->knownIds().contains(oldScreenId) ? m_screenPool->connector(oldScreenId) : ""; + const QString contSwapConnector = m_screenPool->knownIds().contains(newScreenId) ? m_screenPool->connector(newScreenId) : ""; + Q_ASSERT(!oldScreen || oldScreen->name() == containmentConnector); + Q_ASSERT(!newScreen || newScreen->name() == contSwapConnector); + + if (containmentView) { + if (newScreen) { + containmentView->setScreenToFollow(newScreen); + m_desktopViewForScreen[newScreen] = containmentView; + } else { + containmentView->destroy(); + } + } else if (newScreen) { + addOutput(newScreen); + } + + if (contSwapView) { + if (oldScreen) { + contSwapView->setScreenToFollow(oldScreen); + m_desktopViewForScreen[oldScreen] = contSwapView; + } else { + contSwapView->destroy(); + } + } else if (oldScreen) { + addOutput(oldScreen); + } + } + } + + m_pendingScreenChanges.clear(); +} + +int ShellCorona::screenForContainment(const Plasma::Containment *containment) const +{ + // TODO: when we can depend on a new framework, use a p-f method to actuall set lastScreen instead of this? + // m_pendingScreenChanges controls an explicit user-determined screen change + if (!m_pendingScreenChanges.isEmpty() && m_pendingScreenChanges.contains(containment)) { + return m_pendingScreenChanges.value(containment); + } + + // case in which this containment is child of an applet, hello systray :) + if (Plasma::Applet *parentApplet = qobject_cast(containment->parent())) { + if (Plasma::Containment *cont = parentApplet->containment()) { + return screenForContainment(cont); + } else { + return -1; + } + } + + // if the desktop views already exist, base the decision upon them + for (auto it = m_desktopViewForScreen.constBegin(), end = m_desktopViewForScreen.constEnd(); it != end; ++it) { + if (it.value()->containment() == containment && containment->activity() == m_activityController->currentActivity()) { + return m_screenPool->id(it.value()->screenToFollow()->name()); + } + } + + // if the panel views already exist, base upon them + PanelView *view = m_panelViews.value(containment); + if (view && view->screenToFollow()) { + return m_screenPool->id(view->screenToFollow()->name()); + } + + // Failed? fallback on lastScreen() + // lastScreen() is the correct screen for panels + // It is also correct for desktops *that have the correct activity()* + // a containment with lastScreen() == 0 but another activity, + // won't be associated to a screen + // qDebug() << "ShellCorona screenForContainment: " << containment << " Last screen is " << containment->lastScreen(); + + const auto screens = m_screenPool->screens(); + for (auto screen : screens) { + // containment->lastScreen() == m_screenPool->id(screen->name()) to check if the lastScreen refers to a screen that exists/it's known + if (containment->lastScreen() == m_screenPool->id(screen->name()) + && (containment->activity() == m_activityController->currentActivity() || containment->containmentType() == Plasma::Types::PanelContainment + || containment->containmentType() == Plasma::Types::CustomPanelContainment)) { + return containment->lastScreen(); + } + } + + return -1; +} + +void ShellCorona::grabContainmentPreview(Plasma::Containment *containment) +{ + QQuickWindow *viewToGrab = nullptr; + QScreen *containmentQScreen = m_screenPool->screenForId(containment->screen()); + if (containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment) { + // Panel Containment + auto it = m_panelViews.constBegin(); + while (it != m_panelViews.constEnd()) { + if (it.key() == containment) { + viewToGrab = it.value(); + break; + } + it++; + } + } else if (containmentQScreen && m_desktopViewForScreen.contains(containmentQScreen)) { + // Desktop Containment + viewToGrab = m_desktopViewForScreen[containmentQScreen]; + } + if (viewToGrab) { + QSize size(512, 512); + size = viewToGrab->size().scaled(size, Qt::KeepAspectRatio); + auto result = viewToGrab->contentItem()->grabToImage(size); + + if (result) { + connect(result.data(), &QQuickItemGrabResult::ready, this, [this, result, containment]() { + // DataLocation is plasmashell, we need just "plasma" + QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/plasma/"); + QDir dir(path); + dir.mkdir(QStringLiteral("containmentpreviews")); + path += + QStringLiteral("containmentpreviews/") + QString::number(containment->id()) + QChar('-') + containment->activity() + QStringLiteral(".png"); + result->saveToFile(path); + emit containmentPreviewReady(containment, path); + }); + } + } +} + +QString ShellCorona::containmentPreviewPath(Plasma::Containment *containment) const +{ + const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/plasma/containmentpreviews/") + + QString::number(containment->id()) + QChar('-') + containment->activity() + QStringLiteral(".png"); + if (QFile::exists(path)) { + return path; + } else { + return QString(); + } +} + +void ShellCorona::nextActivity() +{ + m_activityController->nextActivity(); +} + +void ShellCorona::previousActivity() +{ + m_activityController->previousActivity(); +} + +void ShellCorona::stopCurrentActivity() +{ + const QStringList list = m_activityController->activities(KActivities::Info::Running); + if (list.isEmpty()) { + return; + } + + m_activityController->stopActivity(m_activityController->currentActivity()); +} + +void ShellCorona::insertContainment(const QString &activity, int screenNum, Plasma::Containment *containment) +{ + Plasma::Containment *cont = nullptr; + const auto candidates = containmentsForActivity(activity); + for (Plasma::Containment *c : candidates) { + // using lastScreen() instead of screen() catches also containments of activities that aren't the current one, so not assigned to a screen right now + if (c->lastScreen() == screenNum) { + cont = c; + if (containment == cont) { + return; + } + break; + } + } + + Q_ASSERT(containment != cont); + + // if there was a duplicate containment destroy the old one + // the new one replaces it + // FIXME: this whole function is probably redundant now + if (cont) { + cont->destroy(); + } +} + +/** + * @internal + * + * The DismissPopupEventFilter class monitors mouse button press events and + * when needed dismisses the active popup widget. + * + * plasmashell uses both QtQuick and QtWidgets under a single roof, QtQuick is + * used for most of things, while QtWidgets is used for things such as context + * menus, etc. + * + * If user clicks outside a popup window, it's expected that the popup window + * will be closed. On X11, it's achieved by establishing both a keyboard grab + * and a pointer grab. But on Wayland, you can't grab keyboard or pointer. If + * user clicks a surface of another app, the compositor will dismiss the popup + * surface. However, if user clicks some surface of the same application, the + * popup surface won't be dismissed, it's up to the application to decide + * whether the popup must be closed. In 99% cases, it must. + * + * Qt has some code that dismisses the active popup widget if another window + * of the same app has been clicked. But, that code works only if the + * application uses solely Qt widgets. See QTBUG-83972. For plasma it doesn't + * work, because as we said previously, it uses both Qt Quick and Qt Widgets. + * + * Ideally, this bug needs to be fixed upstream, but given that it'll involve + * major changes in Qt, the chances of it being fixed any time soon are slim. + * + * In order to work around the popup dismissal bug, we install an event filter + * that monitors Qt::MouseButtonPress events. If it happens that user has + * clicked outside an active popup widget, that popup will be closed. This + * event filter is not needed on X11! + */ +class DismissPopupEventFilter : public QObject +{ + Q_OBJECT + +public: + explicit DismissPopupEventFilter(QObject *parent = nullptr); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + bool m_filterMouseEvents = false; +}; + +DismissPopupEventFilter::DismissPopupEventFilter(QObject *parent) + : QObject(parent) +{ +} + +bool DismissPopupEventFilter::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::MouseButtonPress) { + if (m_filterMouseEvents) { + // Eat events until all mouse buttons are released. + return true; + } + + QWidget *popup = QApplication::activePopupWidget(); + if (!popup) { + return false; + } + + QWindow *window = qobject_cast(watched); + if (popup->windowHandle() == window) { + // The popup window handles mouse events before the widget. + return false; + } + + QWidget *widget = qobject_cast(watched); + if (widget) { + // Let the popup widget handle the mouse press event. + return false; + } + + popup->close(); + m_filterMouseEvents = true; + return true; + + } else if (event->type() == QEvent::MouseButtonRelease) { + if (m_filterMouseEvents) { + // Eat events until all mouse buttons are released. + QMouseEvent *mouseEvent = static_cast(event); + if (mouseEvent->buttons() == Qt::NoButton) { + m_filterMouseEvents = false; + } + return true; + } + } + + return false; +} + +void ShellCorona::setupWaylandIntegration() +{ + if (!KWindowSystem::isPlatformWayland()) { + return; + } + using namespace KWayland::Client; + ConnectionThread *connection = ConnectionThread::fromApplication(this); + if (!connection) { + return; + } + Registry *registry = new Registry(this); + registry->create(connection); + connect(registry, &Registry::plasmaShellAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_waylandPlasmaShell = registry->createPlasmaShell(name, version, this); + }); + registry->setup(); + connection->roundtrip(); + qApp->installEventFilter(new DismissPopupEventFilter(this)); +} + +KWayland::Client::PlasmaShell *ShellCorona::waylandPlasmaShellInterface() const +{ + return m_waylandPlasmaShell; +} + +ScreenPool *ShellCorona::screenPool() const +{ + return m_screenPool; +} + +QList ShellCorona::screenIds() const +{ + return m_screenPool->knownIds(); +} + +QString ShellCorona::defaultContainmentPlugin() const +{ + QString plugin = m_lnfDefaultsConfig.readEntry("Containment", QString()); + if (plugin.isEmpty()) { + plugin = m_desktopDefaultsConfig.readEntry("Containment", "org.kde.desktopcontainment"); + } + return plugin; +} + +void ShellCorona::updateStruts() +{ + for (PanelView *view : qAsConst(m_panelViews)) { + view->updateStruts(); + } +} + +void ShellCorona::configurationChanged(const QString &path) +{ + if (path == m_configPath) { + m_config->reparseConfiguration(); + } +} + +void ShellCorona::activateLauncherMenu() +{ + auto activateLauncher = [](Plasma::Applet *applet) -> bool { + const auto provides = applet->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList()); + if (provides.contains(QLatin1String("org.kde.plasma.launchermenu"))) { + if (!applet->globalShortcut().isEmpty()) { + Q_EMIT applet->activated(); + return true; + } + } + return false; + }; + + for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { + const auto applets = it.key()->applets(); + for (auto applet : applets) { + if (activateLauncher(applet)) { + return; + } + } + if (activateLauncher((*it)->containment())) { + return; + } + } + for (auto it = m_desktopViewForScreen.constBegin(), itEnd = m_desktopViewForScreen.constEnd(); it != itEnd; ++it) { + if (activateLauncher((*it)->containment())) { + return; + } + } +} + +void ShellCorona::activateTaskManagerEntry(int index) +{ + auto activateTaskManagerEntryOnContainment = [](const Plasma::Containment *c, int index) { + const auto &applets = c->applets(); + for (auto *applet : applets) { + const auto &provides = applet->pluginMetaData().value(QStringLiteral("X-Plasma-Provides"), QStringList()); + if (provides.contains(QLatin1String("org.kde.plasma.multitasking"))) { + if (QQuickItem *appletInterface = applet->property("_plasma_graphicObject").value()) { + const auto &childItems = appletInterface->childItems(); + if (childItems.isEmpty()) { + continue; + } + + for (QQuickItem *item : childItems) { + if (auto *metaObject = item->metaObject()) { + // not using QMetaObject::invokeMethod to avoid warnings when calling + // this on applets that don't have it or other child items since this + // is pretty much trial and error. + + // Also, "var" arguments are treated as QVariant in QMetaObject + int methodIndex = metaObject->indexOfMethod("activateTaskAtIndex(QVariant)"); + if (methodIndex == -1) { + continue; + } + + QMetaMethod method = metaObject->method(methodIndex); + if (method.invoke(item, Q_ARG(QVariant, index))) { + return true; + } + } + } + } + } + } + return false; + }; + + // To avoid overly complex configuration, we'll try to get the 90% usecase to work + // which is activating a task on the task manager on a panel on the primary screen. + + for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { + if (it.value()->screen() != m_screenPool->primaryScreen()) { + continue; + } + if (activateTaskManagerEntryOnContainment(it.key(), index)) { + return; + } + } + + // we didn't find anything on primary, try all the panels + for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { + if (activateTaskManagerEntryOnContainment(it.key(), index)) { + return; + } + } +} + +QString ShellCorona::defaultShell() +{ + KSharedConfig::Ptr startupConf = KSharedConfig::openConfig(QStringLiteral("plasmashellrc")); + KConfigGroup startupConfGroup(startupConf, "Shell"); + const QString defaultValue = qEnvironmentVariable("PLASMA_DEFAULT_SHELL", "org.kde.plasma.desktop"); + QString value = startupConfGroup.readEntry("ShellPackage", defaultValue); + + // In the global theme an empty value was written, make sure we still return a shell package + return value.isEmpty() ? defaultValue : value; +} + +void ShellCorona::refreshCurrentShell() +{ + KSharedConfig::openConfig(QStringLiteral("plasmashellrc"))->reparseConfiguration(); + // FIXME: setShell(defaultShell()); + QProcess::startDetached("plasmashell", {"--replace"}); +} + +// Desktop corona handler + +#include "moc_shellcorona.cpp" +#include "shellcorona.moc" diff --git a/plasma/workspace/shell/shellcorona.h b/plasma/workspace/shell/shellcorona.h new file mode 100644 index 0000000000..fa73e57e4f --- /dev/null +++ b/plasma/workspace/shell/shellcorona.h @@ -0,0 +1,274 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + SPDX-FileCopyrightText: 2013 Sebastian Kügler + SPDX-FileCopyrightText: 2013 Ivan Cukic + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "plasma/corona.h" + +#include +#include +#include +#include +#include + +#include + +class DesktopView; +class PanelView; +class QMenu; +class QScreen; +class ScreenPool; +class StrutManager; +class ShellContainmentConfig; + +namespace KActivities +{ +class Controller; +} // namespace KActivities + +namespace KDeclarative +{ +class QmlObjectSharedEngine; +} // namespace KDeclarative + +namespace KScreen +{ +class Output; +} // namespace KScreen + +namespace Plasma +{ +class Applet; +} // namespace Plasma + +namespace KWayland +{ +namespace Client +{ +class PlasmaShell; +} +} + +class ShellCorona : public Plasma::Corona, QDBusContext +{ + Q_OBJECT + Q_PROPERTY(QString shell READ shell WRITE setShell) + Q_PROPERTY(int numScreens READ numScreens) + Q_CLASSINFO("D-Bus Interface", "org.kde.PlasmaShell") + +public: + explicit ShellCorona(QObject *parent = nullptr); + ~ShellCorona() override; + + KPackage::Package lookAndFeelPackage(); + void init(); + + /** + * Where to save global configuration that doesn't have anything to do with the scene (e.g. views) + */ + KSharedConfig::Ptr applicationConfig(); + + int numScreens() const override; + Q_INVOKABLE QRect screenGeometry(int id) const override; + Q_INVOKABLE QRegion availableScreenRegion(int id) const override; + Q_INVOKABLE QRect availableScreenRect(int id) const override; + + // plasmashellCorona's value + QRegion _availableScreenRegion(int id) const; + QRect _availableScreenRect(int id) const; + + Q_INVOKABLE QStringList availableActivities() const; + + PanelView *panelView(Plasma::Containment *containment) const; + + // This one is a bit of an hack but are just for desktop scripting + void insertActivity(const QString &id, const QString &plugin); + + Plasma::Containment *setContainmentTypeForScreen(int screen, const QString &plugin); + + void removeDesktop(DesktopView *desktopView); + + /** + * @returns a new containment associated with the specified @p activity and @p screen. + */ + Plasma::Containment *createContainmentForActivity(const QString &activity, int screenNum); + + KWayland::Client::PlasmaShell *waylandPlasmaShellInterface() const; + + ScreenPool *screenPool() const; + + QList screenIds() const; + + QString defaultContainmentPlugin() const; + + static QString defaultShell(); + + // Set all Desktop containments (for all activities) associated to a given screen to this new screen. + // swapping ones that had newScreen to oldScreen if existing. Panels are ignored + void swapDesktopScreens(int oldScreen, int newScreen); + + // Set a single containment to a new screen. + // If it is a Desktop contaiment, swap it with the other containment that was associated with same screen and activity if existent + void setScreenForContainment(Plasma::Containment *containment, int screen); + + // Grab a screenshot of the contaiment if it has a view in an async fashion + // containmentPreviewReady will be emitted when done + // If there is no view, this will have no effect + void grabContainmentPreview(Plasma::Containment *containment); + + // If a containment preview has been grabbed, for this containment, return its path + QString containmentPreviewPath(Plasma::Containment *containment) const; + +Q_SIGNALS: + void glInitializationFailed(); + // A preview for this containment has been rendered and saved to disk + void containmentPreviewReady(Plasma::Containment *containment, const QString &path); + +public Q_SLOTS: + /** + * Request saving applicationConfig on disk, it's event compressed, not immediate + */ + void requestApplicationConfigSync(); + + /** + * Sets the shell that the corona should display + */ + void setShell(const QString &shell); + + /** + * Gets the currently shown shell + */ + QString shell() const; + + /// DBUS methods + void toggleDashboard(); + void setDashboardShown(bool show); + void toggleActivityManager(); + void toggleWidgetExplorer(); + QString evaluateScript(const QString &string); + void activateLauncherMenu(); + + QByteArray dumpCurrentLayoutJS() const; + + /** + * loads the shell layout from a look and feel package, + * resetting it to the default layout exported in the + * look and feel package + */ + void loadLookAndFeelDefaultLayout(const QString &layout); + + Plasma::Containment *addPanel(const QString &plugin); + + void nextActivity(); + void previousActivity(); + void stopCurrentActivity(); + + void setTestModeLayout(const QString &layout) + { + m_testModeLayout = layout; + } + + int panelCount() const + { + return m_panelViews.count(); + } + + void refreshCurrentShell(); + +protected Q_SLOTS: + /** + * Loads the layout and performs the needed checks + */ + void load(); + + /** + * Unloads everything + */ + void unload(); + + /** + * Loads the default (system wide) layout for this user + **/ + void loadDefaultLayout() override; + + /** + * Execute any update script + */ + void processUpdateScripts(); + + int screenForContainment(const Plasma::Containment *containment) const override; + + void showAlternativesForApplet(Plasma::Applet *applet); + +private Q_SLOTS: + void createWaitingPanels(); + void handleContainmentAdded(Plasma::Containment *c); + void syncAppConfig(); + void checkActivities(); + void currentActivityChanged(const QString &newActivity); + void activityAdded(const QString &id); + void activityRemoved(const QString &id); + void checkAddPanelAction(); + void addPanel(); + void addPanel(QAction *action); + void populateAddPanelsMenu(); + + void addOutput(QScreen *screen); + void primaryScreenChanged(QScreen *oldScreen, QScreen *newScreen); + + void panelContainmentDestroyed(QObject *cont); + void handleScreenRemoved(QScreen *screen); + + void activateTaskManagerEntry(int index); + +private: + void updateStruts(); + void configurationChanged(const QString &path); + QList panelsForScreen(QScreen *screen) const; + DesktopView *desktopForScreen(QScreen *screen) const; + void setupWaylandIntegration(); + void executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet); + void checkAllDesktopsUiReady(bool ready); + +#ifndef NDEBUG + void screenInvariants() const; +#endif + + void insertContainment(const QString &activity, int screenNum, Plasma::Containment *containment); + + KSharedConfig::Ptr m_config; + QString m_configPath; + + ScreenPool *m_screenPool; + QString m_shell; + KActivities::Controller *m_activityController; + QHash m_panelViews; + // map from QScreen to desktop view + QHash m_desktopViewForScreen; + QHash m_pendingScreenChanges; + KConfigGroup m_desktopDefaultsConfig; + KConfigGroup m_lnfDefaultsConfig; + QList m_waitingPanels; + QHash m_activityContainmentPlugins; + QAction *m_addPanelAction; + QScopedPointer m_addPanelsMenu; + KPackage::Package m_lookAndFeelPackage; + + QTimer m_waitingPanelsTimer; + QTimer m_appConfigSyncTimer; +#ifndef NDEBUG + QTimer m_invariantsTimer; +#endif + KWayland::Client::PlasmaShell *m_waylandPlasmaShell; + bool m_closingDown : 1; + QString m_testModeLayout; + + StrutManager *m_strutManager; + QPointer m_shellContainmentConfig; +}; diff --git a/plasma/workspace/shell/softwarerendernotifier.cpp b/plasma/workspace/shell/softwarerendernotifier.cpp new file mode 100644 index 0000000000..67d3e2de5c --- /dev/null +++ b/plasma/workspace/shell/softwarerendernotifier.cpp @@ -0,0 +1,49 @@ +#include "softwarerendernotifier.h" +#include +#include +#include +#include +#include +#include +#include + +#include + +void SoftwareRendererNotifier::notifyIfRelevant() +{ + if (QQuickWindow::sceneGraphBackend() == QLatin1String("software")) { + auto group = KSharedConfig::openConfig()->group(QStringLiteral("softwarerenderer")); + bool neverShow = group.readEntry("neverShow", false); + if (neverShow) { + return; + } + new SoftwareRendererNotifier(qApp); + } +} + +SoftwareRendererNotifier::SoftwareRendererNotifier(QObject *parent) + : KStatusNotifierItem(parent) +{ + setTitle(i18n("Software Renderer In Use")); + setToolTipTitle(i18nc("Tooltip telling user their GL drivers are broken", "Software Renderer In Use")); + setToolTipSubTitle(i18nc("Tooltip telling user their GL drivers are broken", "Rendering may be degraded")); + setIconByName(QStringLiteral("video-card-inactive")); + setStatus(KStatusNotifierItem::Active); + setStandardActionsEnabled(false); + + connect(this, &KStatusNotifierItem::activateRequested, this, []() { + QProcess::startDetached(QStringLiteral("kcmshell5"), {QStringLiteral("qtquicksettings")}); + }); + + auto menu = new QMenu; // ownership is transferred in setContextMenu + auto action = new QAction(i18n("Never show again")); + connect(action, &QAction::triggered, this, [this]() { + auto group = KSharedConfig::openConfig()->group(QStringLiteral("softwarerenderer")); + group.writeEntry("neverShow", true); + deleteLater(); + }); + menu->addAction(action); + setContextMenu(menu); +} + +SoftwareRendererNotifier::~SoftwareRendererNotifier() = default; diff --git a/plasma/workspace/shell/softwarerendernotifier.h b/plasma/workspace/shell/softwarerendernotifier.h new file mode 100644 index 0000000000..e22dc9c89c --- /dev/null +++ b/plasma/workspace/shell/softwarerendernotifier.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +/** + * Responsible for showing an SNI if the software renderer is used + * to allow the a user to open the KCM + */ + +class SoftwareRendererNotifier : public KStatusNotifierItem +{ + Q_OBJECT +public: + // only exposed as void static constructor as internally it is self memory managing + static void notifyIfRelevant(); + +private: + SoftwareRendererNotifier(QObject *parent = nullptr); + ~SoftwareRendererNotifier(); +}; diff --git a/plasma/workspace/shell/standaloneappcorona.cpp b/plasma/workspace/shell/standaloneappcorona.cpp new file mode 100644 index 0000000000..4a9d07cf8b --- /dev/null +++ b/plasma/workspace/shell/standaloneappcorona.cpp @@ -0,0 +1,229 @@ +/* + SPDX-FileCopyrightText: 2014 Bhushan Shah + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "standaloneappcorona.h" +#include "debug.h" +#include "panelview.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "scripting/scriptengine.h" + +StandaloneAppCorona::StandaloneAppCorona(const QString &coronaPlugin, QObject *parent) + : Plasma::Corona(parent) + , m_coronaPlugin(coronaPlugin) + , m_activityConsumer(new KActivities::Consumer(this)) + , m_view(nullptr) +{ + qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Desktop", QStringLiteral("It is not possible to create objects of type Desktop")); + qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Panel", QStringLiteral("It is not possible to create objects of type Panel")); + + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell")); + package.setPath(m_coronaPlugin); + package.setAllowExternalPaths(true); + if (!package.isValid()) { + qCritical() << "starting invalid corona" << m_coronaPlugin; + } + setKPackage(package); + + Plasma::Theme theme; + theme.setUseGlobalSettings(false); + + KConfigGroup lnfCfg = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Theme"); + theme.setThemeName(lnfCfg.readEntry("name", "default")); + + m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Desktop"); + + m_view = new DesktopView(this); + + connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, &StandaloneAppCorona::currentActivityChanged); + connect(m_activityConsumer, &KActivities::Consumer::activityAdded, this, &StandaloneAppCorona::activityAdded); + connect(m_activityConsumer, &KActivities::Consumer::activityRemoved, this, &StandaloneAppCorona::activityRemoved); + + connect(m_activityConsumer, &KActivities::Consumer::serviceStatusChanged, this, &StandaloneAppCorona::load); +} + +StandaloneAppCorona::~StandaloneAppCorona() +{ + delete m_view; +} + +QRect StandaloneAppCorona::screenGeometry(int id) const +{ + Q_UNUSED(id); + if (m_view) { + return m_view->geometry(); + } else { + return QRect(); + } +} + +void StandaloneAppCorona::load() +{ + loadLayout("plasma-" + m_coronaPlugin + "-appletsrc"); + + bool found = false; + for (auto c : containments()) { + if (c->containmentType() == Plasma::Types::DesktopContainment || c->containmentType() == Plasma::Types::CustomContainment) { + found = true; + break; + } + } + + if (!found) { + qDebug() << "Loading default layout"; + loadDefaultLayout(); + saveLayout("plasma-" + m_coronaPlugin + "-appletsrc"); + } + + for (auto c : containments()) { + qDebug() << "containment found"; + if (c->containmentType() == Plasma::Types::DesktopContainment || c->containmentType() == Plasma::Types::CustomContainment) { + QAction *removeAction = c->actions()->action(QStringLiteral("remove")); + if (removeAction) { + removeAction->deleteLater(); + } + m_view->setContainment(c); + m_view->show(); + connect(m_view, &QWindow::visibleChanged, [=](bool visible) { + if (!visible) { + deleteLater(); + } + }); + break; + } + } +} + +void StandaloneAppCorona::loadDefaultLayout() +{ + const QString script = kPackage().filePath("defaultlayout"); + QFile file(script); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString code = file.readAll(); + qDebug() << "evaluating startup script:" << script; + + WorkspaceScripting::ScriptEngine scriptEngine(this); + + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { + qCWarning(PLASMASHELL) << msg; + }); + connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { + qDebug() << msg; + }); + scriptEngine.evaluateScript(code); + } +} + +Plasma::Containment *StandaloneAppCorona::createContainmentForActivity(const QString &activity, int screenNum) +{ + for (Plasma::Containment *cont : containments()) { + if (cont->activity() == activity + && (cont->containmentType() == Plasma::Types::DesktopContainment || cont->containmentType() == Plasma::Types::CustomContainment)) { + return cont; + } + } + + Plasma::Containment *containment = + containmentForScreen(screenNum, activity, m_desktopDefaultsConfig.readEntry("Containment", "org.kde.desktopcontainment"), QVariantList()); + Q_ASSERT(containment); + + if (containment) { + containment->setActivity(activity); + } + + return containment; +} + +void StandaloneAppCorona::activityAdded(const QString &id) +{ + // TODO more sanity checks + if (m_activityContainmentPlugins.contains(id)) { + qCWarning(PLASMASHELL) << "Activity added twice" << id; + return; + } + + m_activityContainmentPlugins.insert(id, QString()); +} + +void StandaloneAppCorona::activityRemoved(const QString &id) +{ + m_activityContainmentPlugins.remove(id); +} + +void StandaloneAppCorona::currentActivityChanged(const QString &newActivity) +{ + // qDebug() << "Activity changed:" << newActivity; + + if (containments().isEmpty()) { + return; + } + + Plasma::Containment *c = createContainmentForActivity(newActivity, 0); + + connect(c, &Plasma::Containment::showAddWidgetsInterface, this, &StandaloneAppCorona::toggleWidgetExplorer); + + QAction *removeAction = c->actions()->action(QStringLiteral("remove")); + if (removeAction) { + removeAction->deleteLater(); + } + m_view->setContainment(c); +} + +void StandaloneAppCorona::toggleWidgetExplorer() +{ + // The view QML has to provide something to display the widget explorer + m_view->rootObject()->metaObject()->invokeMethod(m_view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender()))); + return; +} + +QStringList StandaloneAppCorona::availableActivities() const +{ + return m_activityContainmentPlugins.keys(); +} + +void StandaloneAppCorona::insertActivity(const QString &id, const QString &plugin) +{ + m_activityContainmentPlugins.insert(id, plugin); + Plasma::Containment *c = createContainmentForActivity(id, 0); + if (c) { + c->config().writeEntry("lastScreen", 0); + } +} + +Plasma::Containment *StandaloneAppCorona::addPanel(const QString &plugin) +{ + // this creates a panel that wwill be used for nothing + // it's needed by the scriptengine to create + // a corona useful also when launched in fullshell + Plasma::Containment *panel = createContainment(plugin); + if (!panel) { + return nullptr; + } + + return panel; +} + +int StandaloneAppCorona::screenForContainment(const Plasma::Containment *containment) const +{ + // this simple corona doesn't have multiscreen support + if (containment->containmentType() != Plasma::Types::PanelContainment && containment->containmentType() != Plasma::Types::CustomPanelContainment) { + if (containment->activity() != m_activityConsumer->currentActivity()) { + return -1; + } + } + return 0; +} diff --git a/plasma/workspace/shell/standaloneappcorona.h b/plasma/workspace/shell/standaloneappcorona.h new file mode 100644 index 0000000000..eca36eb8c2 --- /dev/null +++ b/plasma/workspace/shell/standaloneappcorona.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2014 Bhushan Shah + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "desktopview.h" +#include + +namespace KActivities +{ +class Consumer; +} + +class StandaloneAppCorona : public Plasma::Corona +{ + Q_OBJECT + +public: + explicit StandaloneAppCorona(const QString &coronaPlugin, QObject *parent = nullptr); + ~StandaloneAppCorona() override; + + QRect screenGeometry(int id) const override; + + void loadDefaultLayout() override; + + Plasma::Containment *createContainmentForActivity(const QString &activity, int screenNum); + + void insertActivity(const QString &id, const QString &plugin); + Plasma::Containment *addPanel(const QString &plugin); + + Q_INVOKABLE QStringList availableActivities() const; + +public Q_SLOTS: + void load(); + + void currentActivityChanged(const QString &newActivity); + void activityAdded(const QString &id); + void activityRemoved(const QString &id); + void toggleWidgetExplorer(); + +protected Q_SLOTS: + int screenForContainment(const Plasma::Containment *containment) const override; + +private: + QString m_coronaPlugin; + KActivities::Consumer *m_activityConsumer; + KConfigGroup m_desktopDefaultsConfig; + DesktopView *m_view; + QHash m_activityContainmentPlugins; +}; diff --git a/plasma/workspace/shell/strutmanager.cpp b/plasma/workspace/shell/strutmanager.cpp new file mode 100644 index 0000000000..31cc145256 --- /dev/null +++ b/plasma/workspace/shell/strutmanager.cpp @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Tranter Madi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "strutmanager.h" +#include "screenpool.h" +#include "shellcorona.h" + +#include +#include +#include +#include + +StrutManager::StrutManager(ShellCorona *plasmashellCorona) + : QObject(plasmashellCorona) + , m_plasmashellCorona(plasmashellCorona) + , m_serviceWatcher(new QDBusServiceWatcher(this)) +{ + qDBusRegisterMetaType>(); + + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject("/StrutManager", this, QDBusConnection::ExportAllSlots); + m_serviceWatcher->setConnection(dbus); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, [=](const QString &service) { + m_availableScreenRects.remove(service); + m_availableScreenRegions.remove(service); + m_serviceWatcher->removeWatchedService(service); + + Q_EMIT m_plasmashellCorona->availableScreenRectChanged(); + }); +} + +QRect StrutManager::availableScreenRect(int id) const +{ + QRect r = m_plasmashellCorona->_availableScreenRect(id); + QHash service; + foreach (service, m_availableScreenRects) { + if (!service.value(id).isNull()) { + r &= service[id]; + } + } + return r; +} + +QRect StrutManager::availableScreenRect(const QString &screenName) const +{ + return availableScreenRect(m_plasmashellCorona->screenPool()->id(screenName)); +} + +QRegion StrutManager::availableScreenRegion(int id) const +{ + QRegion r = m_plasmashellCorona->_availableScreenRegion(id); + QHash service; + foreach (service, m_availableScreenRegions) { + if (!service.value(id).isNull()) { + r &= service[id]; + } + } + return r; +} + +void StrutManager::setAvailableScreenRect(const QString &service, const QString &screenName, const QRect &rect) +{ + int id = m_plasmashellCorona->screenPool()->id(screenName); + if (id == -1 || m_availableScreenRects.value(service).value(id) == rect || !addWatchedService(service)) { + return; + } + m_availableScreenRects[service][id] = rect; + Q_EMIT m_plasmashellCorona->availableScreenRectChanged(); +} + +void StrutManager::setAvailableScreenRegion(const QString &service, const QString &screenName, const QList &rects) +{ + int id = m_plasmashellCorona->screenPool()->id(screenName); + QRegion region; + foreach (QRect rect, rects) { + region += rect; + } + + if (id == -1 || m_availableScreenRegions.value(service).value(id) == region || !addWatchedService(service)) { + return; + } + m_availableScreenRegions[service][id] = region; + Q_EMIT m_plasmashellCorona->availableScreenRegionChanged(); +} + +bool StrutManager::addWatchedService(const QString &service) +{ + if (!m_serviceWatcher->watchedServices().contains(service)) { + if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(service)) { + return false; + } + m_serviceWatcher->addWatchedService(service); + } + return true; +} diff --git a/plasma/workspace/shell/strutmanager.h b/plasma/workspace/shell/strutmanager.h new file mode 100644 index 0000000000..78c14687b9 --- /dev/null +++ b/plasma/workspace/shell/strutmanager.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Tranter Madi + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class QDBusServiceWatcher; +class ShellCorona; + +class StrutManager : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.PlasmaShell.StrutManager") + +public: + explicit StrutManager(ShellCorona *plasmashellCorona); + + QRect availableScreenRect(int id) const; + QRegion availableScreenRegion(int id) const; + +public Q_SLOTS: + QRect availableScreenRect(const QString &screenName) const; + + void setAvailableScreenRect(const QString &service, const QString &screenName, const QRect &rect); + void setAvailableScreenRegion(const QString &service, const QString &screenName, const QList &rects); + +private: + ShellCorona *m_plasmashellCorona; + + QDBusServiceWatcher *m_serviceWatcher; + bool addWatchedService(const QString &service); + + QHash> m_availableScreenRects; + QHash> m_availableScreenRegions; +}; diff --git a/plasma/workspace/shell/tests/CMakeLists.txt b/plasma/workspace/shell/tests/CMakeLists.txt new file mode 100644 index 0000000000..1d6940fb46 --- /dev/null +++ b/plasma/workspace/shell/tests/CMakeLists.txt @@ -0,0 +1,30 @@ + +include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/..) + +set(screenpoolmanualtest_SRCS + screenpooltest.cpp + ../screenpool.cpp + ${CMAKE_CURRENT_BINARY_DIR}/../screenpool-debug.cpp + ../primaryoutputwatcher.cpp + ) +ecm_add_qtwayland_client_protocol(screenpoolmanualtest_SRCS + PROTOCOL ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-primary-output-v1.xml + BASENAME kde-primary-output-v1 + ) +add_executable(screenpoolmanualtest ${screenpoolmanualtest_SRCS}) +target_link_libraries(screenpoolmanualtest + Qt::Test + Qt::Gui + KF5::Service + KF5::WindowSystem + KF5::WaylandClient + Wayland::Client + ) +if(HAVE_X11) + target_link_libraries(screenpoolmanualtest XCB::XCB XCB::RANDR) + target_link_libraries(screenpoolmanualtest Qt::X11Extras) +endif() +if(QT_QTOPENGL_FOUND) + target_link_libraries(screenpoolmanualtest Qt::OpenGL) +endif() + diff --git a/plasma/workspace/shell/tests/plasma/shells/org.kde.plasmashelltest/metadata.desktop b/plasma/workspace/shell/tests/plasma/shells/org.kde.plasmashelltest/metadata.desktop new file mode 100644 index 0000000000..21d0985780 --- /dev/null +++ b/plasma/workspace/shell/tests/plasma/shells/org.kde.plasmashelltest/metadata.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Comment=TEST ALL THE THINGS! ;) +Name=TestShell +Type=Service +Icon=user-desktop + +X-KDE-ServiceTypes=Plasma/Shell +X-KDE-PluginInfo-Name=org.kde.plasmashelltest +X-Plasma-MainScript=layout.js diff --git a/plasma/workspace/shell/tests/screenpooltest.cpp b/plasma/workspace/shell/tests/screenpooltest.cpp new file mode 100644 index 0000000000..f6c541cec5 --- /dev/null +++ b/plasma/workspace/shell/tests/screenpooltest.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include + +#include +#include +#include + +#include "../screenpool.h" + +class ScreenPoolTester : public QObject +{ + Q_OBJECT +public: + ScreenPoolTester(QObject *parent = nullptr); + +private: + void handleScreenAdded(QScreen *screen); + void handleScreenRemoved(QScreen *screen); + void handlePrimaryScreenChanged(QScreen *oldPrimary, QScreen *newPrimary); + + ScreenPool *m_screenPool = nullptr; +}; + +ScreenPoolTester::ScreenPoolTester(QObject *parent) + : QObject(parent) + , m_screenPool(new ScreenPool(KSharedConfig::openConfig("plasmashellrc"))) +{ + connect(m_screenPool, &ScreenPool::screenAdded, this, &ScreenPoolTester::handleScreenAdded); + connect(m_screenPool, &ScreenPool::screenRemoved, this, &ScreenPoolTester::handleScreenRemoved); + connect(m_screenPool, &ScreenPool::primaryScreenChanged, this, &ScreenPoolTester::handlePrimaryScreenChanged); + m_screenPool->load(); + qWarning() << "Load completed"; + qWarning() << m_screenPool; +} + +void ScreenPoolTester::handleScreenAdded(QScreen *screen) +{ + qWarning() << "SCREEN ADDED" << screen; + qWarning() << m_screenPool; +} + +void ScreenPoolTester::handleScreenRemoved(QScreen *screen) +{ + qWarning() << "SCREEN REMOVED" << screen; + qWarning() << m_screenPool; +} + +void ScreenPoolTester::handlePrimaryScreenChanged(QScreen *oldPrimary, QScreen *newPrimary) +{ + qWarning() << "PRIMARY SCREEN CHANGED:" << oldPrimary << "-->" << newPrimary; + qWarning() << m_screenPool; +} + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ + if (!qEnvironmentVariableIsSet("PLASMA_USE_QT_SCALING")) { + qunsetenv("QT_DEVICE_PIXEL_RATIO"); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + } else { + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + } + + QGuiApplication::setApplicationDisplayName(QStringLiteral("ScreenPool test")); + + QApplication app(argc, argv); + + ScreenPoolTester screenPoolTester(); + + return app.exec(); +} + +#include "screenpooltest.moc" diff --git a/plasma/workspace/shell/userfeedback.cpp b/plasma/workspace/shell/userfeedback.cpp new file mode 100644 index 0000000000..26893004a5 --- /dev/null +++ b/plasma/workspace/shell/userfeedback.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + SPDX-FileCopyrightText: 2020 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "userfeedback.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "shellcorona.h" + +class PanelCountSource : public KUserFeedback::AbstractDataSource +{ +public: + /*! Create a new start count data source. */ + PanelCountSource(ShellCorona *corona) + : AbstractDataSource(QStringLiteral("panelCount"), KUserFeedback::Provider::DetailedSystemInformation) + , corona(corona) + { + } + + QString name() const override + { + return i18n("Panel Count"); + } + QString description() const override + { + return i18n("Counts the panels"); + } + + QVariant data() override + { + return QVariantMap{{QStringLiteral("panelCount"), corona->panelCount()}}; + } + +private: + ShellCorona *const corona; +}; + +UserFeedback::UserFeedback(ShellCorona *corona, QObject *parent) + : QObject(parent) + , m_provider(new KUserFeedback::Provider(this)) +{ + m_provider->setProductIdentifier(QStringLiteral("org.kde.plasmashell")); + m_provider->setFeedbackServer(QUrl(QStringLiteral("https://telemetry.kde.org/"))); + m_provider->setSubmissionInterval(7); + m_provider->setApplicationStartsUntilEncouragement(5); + m_provider->setEncouragementDelay(30); + m_provider->addDataSource(new KUserFeedback::ApplicationVersionSource); + m_provider->addDataSource(new KUserFeedback::CompilerInfoSource); + m_provider->addDataSource(new KUserFeedback::PlatformInfoSource); + m_provider->addDataSource(new KUserFeedback::QtVersionSource); + m_provider->addDataSource(new KUserFeedback::UsageTimeSource); + m_provider->addDataSource(new KUserFeedback::OpenGLInfoSource); + m_provider->addDataSource(new KUserFeedback::ScreenInfoSource); + m_provider->addDataSource(new KUserFeedback::QPAInfoSource); + m_provider->addDataSource(new PanelCountSource(corona)); + + auto plasmaConfig = KSharedConfig::openConfig(QStringLiteral("PlasmaUserFeedback")); + m_provider->setTelemetryMode( + KUserFeedback::Provider::TelemetryMode(plasmaConfig->group("Global").readEntry("FeedbackLevel", int(KUserFeedback::Provider::NoTelemetry)))); +} + +QString UserFeedback::describeDataSources() const +{ + return m_provider->describeDataSources(); +} diff --git a/plasma/workspace/shell/userfeedback.h b/plasma/workspace/shell/userfeedback.h new file mode 100644 index 0000000000..32990c129f --- /dev/null +++ b/plasma/workspace/shell/userfeedback.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + SPDX-FileCopyrightText: 2020 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ShellCorona; + +namespace KUserFeedback +{ +class Provider; +} + +class UserFeedback : public QObject +{ + Q_OBJECT +public: + UserFeedback(ShellCorona *corona, QObject *parent); + ~UserFeedback() override = default; + QString describeDataSources() const; + +private: + KUserFeedback::Provider *m_provider; +}; diff --git a/plasma/workspace/solidautoeject/CMakeLists.txt b/plasma/workspace/solidautoeject/CMakeLists.txt new file mode 100644 index 0000000000..dd7e83163f --- /dev/null +++ b/plasma/workspace/solidautoeject/CMakeLists.txt @@ -0,0 +1,9 @@ +########### next target ############### +add_definitions(-DTRANSLATION_DOMAIN=\"solidautoeject\") + +set(kded_solidautoeject_SRCS + solidautoeject.cpp +) + +kcoreaddons_add_plugin(solidautoeject SOURCES ${kded_solidautoeject_SRCS} INSTALL_NAMESPACE "kf5/kded") +target_link_libraries(solidautoeject KF5::Solid KF5::DBusAddons KF5::CoreAddons) diff --git a/plasma/workspace/solidautoeject/Messages.sh b/plasma/workspace/solidautoeject/Messages.sh new file mode 100644 index 0000000000..a0444880ea --- /dev/null +++ b/plasma/workspace/solidautoeject/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/solidautoeject.pot diff --git a/plasma/workspace/solidautoeject/solidautoeject.cpp b/plasma/workspace/solidautoeject/solidautoeject.cpp new file mode 100644 index 0000000000..6ed1fc4caf --- /dev/null +++ b/plasma/workspace/solidautoeject/solidautoeject.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "solidautoeject.h" + +#include + +#include +#include +#include + +K_PLUGIN_CLASS_WITH_JSON(SolidAutoEject, "solidautoeject.json") + +SolidAutoEject::SolidAutoEject(QObject *parent, const QList &) + : KDEDModule(parent) +{ + const QList drives = Solid::Device::listFromType(Solid::DeviceInterface::OpticalDrive); + for (const Solid::Device &drive : drives) { + connectDevice(drive); + } + + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &SolidAutoEject::onDeviceAdded); +} + +SolidAutoEject::~SolidAutoEject() +{ +} + +void SolidAutoEject::onDeviceAdded(const QString &udi) +{ + connectDevice(Solid::Device(udi)); +} + +void SolidAutoEject::onEjectPressed(const QString &udi) +{ + Solid::Device dev(udi); + dev.as()->eject(); +} + +void SolidAutoEject::connectDevice(const Solid::Device &device) +{ + connect(device.as(), &Solid::OpticalDrive::ejectPressed, this, &SolidAutoEject::onEjectPressed); +} + +#include "solidautoeject.moc" diff --git a/plasma/workspace/solidautoeject/solidautoeject.h b/plasma/workspace/solidautoeject/solidautoeject.h new file mode 100644 index 0000000000..9ad1327987 --- /dev/null +++ b/plasma/workspace/solidautoeject/solidautoeject.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include + +namespace Solid +{ +class Device; +} + +class SolidAutoEject : public KDEDModule +{ + Q_OBJECT + +public: + SolidAutoEject(QObject *parent, const QList &); + ~SolidAutoEject() override; + +private Q_SLOTS: + void onDeviceAdded(const QString &udi); + void onEjectPressed(const QString &udi); + +private: + void connectDevice(const Solid::Device &device); +}; diff --git a/plasma/workspace/solidautoeject/solidautoeject.json b/plasma/workspace/solidautoeject/solidautoeject.json new file mode 100644 index 0000000000..f81047d72c --- /dev/null +++ b/plasma/workspace/solidautoeject/solidautoeject.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Description": "Automatically releases drives when their eject button is pushed", + "Description[ar]": "أطلِق الأجهزة آليًا عندما يُضغَط زر الإخراج", + "Description[az]": "Çıxart düyməsinə vurduqda daşıyıcıları avtomatik ayırır", + "Description[ca]": "Allibera unitats automàticament quan es prem el seu botó d'expulsió", + "Description[cs]": "Umožňuje automaticky uvolnit mechaniku při stisknutí tlačítka vysunutí", + "Description[de]": "Löst die Laufwerkseinbindung automatisch bei Betätigung des Auswurfknopfes", + "Description[en_GB]": "Automatically releases drives when their eject button is pushed", + "Description[es]": "Libera automáticamente las unidades cuando se pulsa el botón de expulsión", + "Description[eu]": "Unitateak automatikoki askatzen ditu haien egozteko botoia sakatzen denean", + "Description[fi]": "Irrottaa asemat automaattisesti niiden poistopainiketta painettaessa", + "Description[fr]": "Libère automatiquement les disques lors d'un appui sur leur bouton d'éjection", + "Description[hu]": "Automatikusan kiadja a meghajtókat a kiadógomb megnyomásakor", + "Description[ia]": "Il lassa fora automaticamente le drives quando lor button de expeller es pressate.", + "Description[it]": "Rilascia automaticamente le unità quando viene premuto il loro pulsante di espulsione", + "Description[ko]": "꺼내기 단추를 눌렀을 때 자동으로 드라이브 꺼내기", + "Description[lt]": "Automatiškai atjungia diskus, kai paspaudžiamas jų išstūmimo mygtukas", + "Description[nl]": "Geeft automatisch apparaten vrij als hun uitwerpknop wordt ingedrukt", + "Description[nn]": "Løyser automatisk ut stasjonar når det vert trykt på utløysingsknappen deira", + "Description[pa]": "ਜਦੋਂ ਬਾਹਰ ਕੱਢੋ ਬਟਨ ਦੱਬਿਆ ਜਾਵੇ ਤਾਂ ਆਟੋਮੈਟਿਕ ਹੀ ਡਰਾਇਵਾਂ ਨੂੰ ਛੱਡ ਦਿਓ", + "Description[pl]": "Samoczynnie wysuwa napędy po naciśnięciu właściwego przycisku", + "Description[pt_BR]": "Libera as unidades automaticamente quando o botão de ejeção for pressionado", + "Description[ro]": "Eliberează automat unitățile la apăsarea butonului de deschidere al acestora", + "Description[ru]": "Выбрасывает сменный носитель из привода при нажатии кнопки извлечения", + "Description[sk]": "Umožňuje automaticky uvoľniť mechaniku pri stlačení tlačidla vysunúť", + "Description[sl]": "Samodejno odklopi pogon, ko pritisnjen gumb za izmet", + "Description[sv]": "Frisläpper automatiskt enheter vid tryck på deras utmatningsknapp", + "Description[ta]": "வட்டுகளின் வெளியேற்ற பட்டன் அழுத்தப்படும்போது அவற்றை வெளியேற்றும் நிரல்", + "Description[tr]": "Çıkarma düğmesine basıldığında sürücüleri otomatik olarak ayırır", + "Description[uk]": "Автоматичне вилучення файли пристроїв, після натискання на них кнопки виштовхування", + "Description[vi]": "Tự động xuất các ổ ra khi nút rút của chúng được ấn", + "Description[x-test]": "xxAutomatically releases drives when their eject button is pushedxx", + "Description[zh_CN]": "当按下弹出按钮时自动释放驱动器", + "Name": "Drive Ejector", + "Name[ar]": "مُخرِج الأجهزة", + "Name[az]": "Daşıyıcıların Çıxarılması", + "Name[bg]": "Изваждане на устройства", + "Name[bs]": "Izbacivač jedinica", + "Name[ca@valencia]": "Expulsor d'unitats", + "Name[ca]": "Expulsor d'unitats", + "Name[cs]": "Vysunutí mechaniky", + "Name[da]": "Udskubning af drev", + "Name[de]": "Laufwerksauswurf", + "Name[el]": "Οδηγός Εξαγωγέα", + "Name[en_GB]": "Drive Ejector", + "Name[eo]": "Elĵetilo de diskingoj", + "Name[es]": "Eyector de unidad", + "Name[et]": "Plaadiväljasti", + "Name[eu]": "Unitate-egozlea", + "Name[fi]": "Aseman poistaja", + "Name[fr]": "Éjecteur de disque", + "Name[ga]": "Díchurthóir Tiomántáin", + "Name[gl]": "Expulsor de unidades", + "Name[gu]": "ડ્રાઇવ બહાર કાઢનાર", + "Name[he]": "מוציא הכוננים", + "Name[hi]": "ड्राइव इजेक्टर", + "Name[hr]": "Izbacivač pogona", + "Name[hsb]": "Wućisnjenje diskow a podobnych gratow", + "Name[hu]": "Meghajtókiadó", + "Name[ia]": "Ejector de Drive", + "Name[id]": "Pengeluar Drive", + "Name[is]": "Diskaútspýting", + "Name[it]": "Espulsore di unità", + "Name[ja]": "ドライブイジェクタ", + "Name[kk]": "Тасушысын алып-шығу", + "Name[km]": "កម្មវិធីច្រាន​ដ្រាយ​ចេញ", + "Name[kn]": "ಡ್ರೈವ್ ಹೊರತಳ್ಳುಕ", + "Name[ko]": "장치 꺼내기", + "Name[lt]": "Diskų išstūmimo įrankis", + "Name[lv]": "Disku izgrūdējs", + "Name[ml]": "ഡ്രൈവ് പുറത്തെടുക്കുവാന്‍", + "Name[mr]": "ड्राइव्ह बाहेर काढा", + "Name[nb]": "Drevutløser", + "Name[nds]": "Schuuvlaad rutfohren", + "Name[nl]": "Uitwerpen uit apparaat", + "Name[nn]": "Stasjonsutløysing", + "Name[pa]": "ਡਰਾਇਵਰ ਇੰਜੈਂਕਟਰ", + "Name[pl]": "Wysuwanie napędów", + "Name[pt]": "Ejecção de Unidades", + "Name[pt_BR]": "Ejeção de unidades", + "Name[ro]": "Ejector unitate", + "Name[ru]": "Извлечение дисков", + "Name[si]": "ධාවකය ඉවත් කරනය", + "Name[sk]": "Vysunutie mechaniky", + "Name[sl]": "Izmetalo pogona", + "Name[sr@ijekavian]": "Избацивач јединица", + "Name[sr@ijekavianlatin]": "Izbacivač jedinica", + "Name[sr@latin]": "Izbacivač jedinica", + "Name[sr]": "Избацивач јединица", + "Name[sv]": "Utmatning av enheter", + "Name[ta]": "வட்டு வெளியேற்ற நிரல்", + "Name[th]": "ตัวดันถาดไดรฟ์", + "Name[tr]": "Sürücü Çıkarıcı", + "Name[ug]": "قوزغاتقۇچ قاڭقىتقۇچ", + "Name[uk]": "Вилучення дисків", + "Name[vi]": "Trình rút ổ", + "Name[wa]": "Rexheu d' plake", + "Name[x-test]": "xxDrive Ejectorxx", + "Name[zh_CN]": "驱动器弹出程序", + "Name[zh_TW]": "碟片跳出管理" + }, + "X-KDE-Kded-autoload": true +} diff --git a/plasma/workspace/soliduiserver/CMakeLists.txt b/plasma/workspace/soliduiserver/CMakeLists.txt new file mode 100644 index 0000000000..9628947d8f --- /dev/null +++ b/plasma/workspace/soliduiserver/CMakeLists.txt @@ -0,0 +1,20 @@ +########### next target ############### +add_definitions(-DTRANSLATION_DOMAIN=\"soliduiserver5\") + +kcoreaddons_add_plugin(soliduiserver SOURCES soliduiserver.cpp INSTALL_NAMESPACE "kf5/kded") +target_link_libraries(soliduiserver + KF5::Solid + KF5::DBusAddons + KF5::Wallet + KF5::KIOCore + KF5::WindowSystem + KF5::I18n + KF5::WidgetsAddons +) + + +ecm_qt_declare_logging_category(soliduiserver + HEADER soliduiserver_debug.h + IDENTIFIER SOLIDUISERVER_DEBUG + CATEGORY_NAME org.kde.plasma.soliduiserver +) diff --git a/plasma/workspace/soliduiserver/Messages.sh b/plasma/workspace/soliduiserver/Messages.sh new file mode 100644 index 0000000000..82787c6c81 --- /dev/null +++ b/plasma/workspace/soliduiserver/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp +$XGETTEXT *.cpp -o $podir/soliduiserver5.pot diff --git a/plasma/workspace/soliduiserver/soliduiserver.cpp b/plasma/workspace/soliduiserver/soliduiserver.cpp new file mode 100644 index 0000000000..6231ffb700 --- /dev/null +++ b/plasma/workspace/soliduiserver/soliduiserver.cpp @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + SPDX-FileCopyrightText: 2007 Alexis Ménard + SPDX-FileCopyrightText: 2011, 2014 Lukas Tinkl + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "soliduiserver.h" +#include "config-X11.h" +#include "soliduiserver_debug.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +// solid specific includes +#include +#include +#include +#include +#include + +K_PLUGIN_CLASS_WITH_JSON(SolidUiServer, "soliduiserver.json") + +SolidUiServer::SolidUiServer(QObject *parent, const QList &) + : KDEDModule(parent) +{ +} + +SolidUiServer::~SolidUiServer() +{ +} + +void SolidUiServer::showPassphraseDialog(const QString &udi, const QString &returnService, const QString &returnObject, uint wId, const QString &appId) +{ + if (m_idToPassphraseDialog.contains(returnService + ':' + udi)) { + KPasswordDialog *dialog = m_idToPassphraseDialog[returnService + ':' + udi]; + dialog->activateWindow(); + return; + } + + Solid::Device device(udi); + + KPasswordDialog *dialog = new KPasswordDialog(nullptr, KPasswordDialog::ShowKeepPassword); + + QString label = device.vendor(); + if (!label.isEmpty()) + label += ' '; + label += device.product(); + + dialog->setPrompt(i18n("'%1' needs a password to be accessed. Please enter a password.", label)); + dialog->setIcon(QIcon::fromTheme(device.icon())); + dialog->setProperty("soliduiserver.udi", udi); + dialog->setProperty("soliduiserver.returnService", returnService); + dialog->setProperty("soliduiserver.returnObject", returnObject); + + QString uuid; + if (device.is()) + uuid = device.as()->uuid(); + + // read the password from wallet and prefill it to the dialog + if (!uuid.isEmpty()) { + dialog->setProperty("soliduiserver.uuid", uuid); + + KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), (WId)wId); + const QString folderName = QString::fromLatin1("SolidLuks"); + if (wallet && wallet->hasFolder(folderName)) { + wallet->setFolder(folderName); + QString savedPassword; + if (wallet->readPassword(uuid, savedPassword) == 0) { + dialog->setKeepPassword(true); + dialog->setPassword(savedPassword); + } + wallet->closeWallet(wallet->walletName(), false); + } + delete wallet; + } + + connect(dialog, &KPasswordDialog::gotPassword, this, &SolidUiServer::onPassphraseDialogCompleted); + connect(dialog, &KPasswordDialog::rejected, this, &SolidUiServer::onPassphraseDialogRejected); + + m_idToPassphraseDialog[returnService + ':' + udi] = dialog; + + reparentDialog(dialog, (WId)wId, appId, true); + dialog->show(); +} + +void SolidUiServer::onPassphraseDialogCompleted(const QString &pass, bool keep) +{ + KPasswordDialog *dialog = qobject_cast(sender()); + + if (dialog) { + QString returnService = dialog->property("soliduiserver.returnService").toString(); + QString returnObject = dialog->property("soliduiserver.returnObject").toString(); + QDBusInterface returnIface(returnService, returnObject); + + QDBusReply reply = returnIface.call(QStringLiteral("passphraseReply"), pass); + + QString udi = dialog->property("soliduiserver.udi").toString(); + m_idToPassphraseDialog.remove(returnService + ':' + udi); + + if (!reply.isValid()) { + qCWarning(SOLIDUISERVER_DEBUG) << "Impossible to send the passphrase to the application, D-Bus said: " << reply.error().name() << ", " + << reply.error().message() << Qt::endl; + return; // don't save into wallet if an error occurs + } + + if (keep) { // save the password into the wallet + KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), 0); + if (wallet) { + const QString folderName = QString::fromLatin1("SolidLuks"); + const QString uuid = dialog->property("soliduiserver.uuid").toString(); + if (!wallet->hasFolder(folderName)) + wallet->createFolder(folderName); + if (wallet->setFolder(folderName)) + wallet->writePassword(uuid, pass); + wallet->closeWallet(wallet->walletName(), false); + delete wallet; + } + } + } +} + +void SolidUiServer::onPassphraseDialogRejected() +{ + onPassphraseDialogCompleted(QString(), false); +} + +void SolidUiServer::reparentDialog(QWidget *dialog, WId wId, const QString &appId, bool modal) +{ + Q_UNUSED(appId); + // Code borrowed from kwalletd + + dialog->setAttribute(Qt::WA_NativeWindow, true); + KWindowSystem::setMainWindow(dialog->windowHandle(), wId); // correct, set dialog parent + +#if HAVE_X11 + if (modal) { + KWindowSystem::setState(dialog->winId(), NET::Modal); + } else { + KWindowSystem::clearState(dialog->winId(), NET::Modal); + } +#endif + + // allow dialog activation even if it interrupts, better than trying hacks + // with keeping the dialog on top or on all desktops + KUserTimestamp::updateUserTimestamp(); +} + +#include "soliduiserver.moc" diff --git a/plasma/workspace/soliduiserver/soliduiserver.h b/plasma/workspace/soliduiserver/soliduiserver.h new file mode 100644 index 0000000000..a974ee1dc7 --- /dev/null +++ b/plasma/workspace/soliduiserver/soliduiserver.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2005 Jean-Remy Falleri + SPDX-FileCopyrightText: 2005-2007 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include + +class DeviceActionsDialog; +class KPasswordDialog; +class QWidget; + +class SolidUiServer : public KDEDModule +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.SolidUiServer") + +public: + SolidUiServer(QObject *parent, const QList &); + ~SolidUiServer() override; + +public Q_SLOTS: + Q_SCRIPTABLE void showPassphraseDialog(const QString &udi, const QString &returnService, const QString &returnObject, uint wId, const QString &appId); + +private Q_SLOTS: + void onPassphraseDialogCompleted(const QString &pass, bool keep); + void onPassphraseDialogRejected(); + +private: + void reparentDialog(QWidget *dialog, WId wId, const QString &appId, bool modal); + + QMap m_idToPassphraseDialog; +}; \ No newline at end of file diff --git a/plasma/workspace/soliduiserver/soliduiserver.json b/plasma/workspace/soliduiserver/soliduiserver.json new file mode 100644 index 0000000000..61e60b007a --- /dev/null +++ b/plasma/workspace/soliduiserver/soliduiserver.json @@ -0,0 +1,92 @@ +{ + "KPlugin": { + "Description": "Provides a user interface for hardware events", + "Description[ar]": "يوفّر واجهة مستخدِم لأحداث العتاد", + "Description[az]": "Avadanlıqlarla iş üçün istifadəçi interfeysini təqdim edir", + "Description[ca]": "Proporciona una interfície d'usuari per als esdeveniments del maquinari", + "Description[cs]": "Poskytuje uživatelské rozhraní pro hardwarové události", + "Description[de]": "Stellt eine Benutzeroberfläche für Hardwareereignisse zur Verfügung", + "Description[en_GB]": "Provides a user interface for hardware events", + "Description[es]": "Proporciona una interfaz de usuario para eventos del hardware", + "Description[eu]": "Hardware gertaerentzako erabiltzaile interfaze bat hornitzen du", + "Description[fi]": "Tarjoaa käyttöliittymän laitteistotapahtumiin", + "Description[fr]": "Fournit une interface utilisateur pour les évènements matériels", + "Description[hu]": "Felhasználói felületet biztosít a hardvereseményekhez", + "Description[ia]": "Il forni un interface de usator pro eventos hardware", + "Description[it]": "Fornisce un'interfaccia utente per gli eventi hardware", + "Description[ko]": "하드웨어 이벤트 사용자 인터페이스 제공", + "Description[lt]": "Suteikia naudotojo sąsają aparatinės įrangos įvykiams", + "Description[nl]": "Levert een gebruikersinterface voor hardware gebeurtenissen", + "Description[nn]": "Eit brukargrensesnitt for maskinvarehendingar", + "Description[pa]": "ਹਾਰਡਵੇਅਰ ਈਵੈਂਟ ਲਈ ਵਰਤੋਂਕਾਰ ਇੰਟਰਫੇਸ ਦਿੰਦਾ ਹੈ", + "Description[pl]": "Zapewnia interfejs dla zdarzeń związanych ze sprzętem", + "Description[pt_BR]": "Fornece uma interface de usuário para os eventos do hardware", + "Description[ro]": "Furnizează o interfață cu utilizatorul pentru evenimente de echipament", + "Description[ru]": "Обеспечивает пользовательский интерфейс для событий с оборудованием", + "Description[sk]": "Poskytuje používateľské rozhranie pre hardvérové udalosti", + "Description[sl]": "Ponuja uporabniški vmesnik za dogodke strojne opreme", + "Description[sv]": "Tillhandahåller ett användargränssnitt för händelser i hårdvaran", + "Description[ta]": "வன்பொருள் சார்ந்த நடப்புகளுக்கு பயனர் இடைமுகப்பை வழங்கும்", + "Description[tr]": "Donanım etkinlikleri için kullanıcı arayüzü sunar", + "Description[uk]": "Забезпечує роботу інтерфейсу користувача для подій, пов’язаних з обладнанням", + "Description[vi]": "Cung cấp một giao diện người dùng cho các sự kiện phần cứng", + "Description[x-test]": "xxProvides a user interface for hardware eventsxx", + "Description[zh_CN]": "为硬件事件提供用户界面", + "Name": "Hardware Detection", + "Name[ar]": "اكتشاف العتاد", + "Name[ast]": "Deteición de hardware", + "Name[az]": "Avadanlığın aşkar edilməsi", + "Name[bs]": "Detekcija hardvera", + "Name[ca@valencia]": "Detecció de maquinari", + "Name[ca]": "Detecció de maquinari", + "Name[cs]": "Detekce hardwaru", + "Name[da]": "Hardware-detektion", + "Name[de]": "Hardwareerkennung", + "Name[el]": "Εντοπισμός υλικού", + "Name[en_GB]": "Hardware Detection", + "Name[es]": "Detección de hardware", + "Name[et]": "Riistvara tuvastamine", + "Name[eu]": "Hardware-detekzioa", + "Name[fi]": "Laitteiston tunnistus", + "Name[fr]": "Détection du matériel", + "Name[gl]": "Detección de soporte físico", + "Name[he]": "זיהוי חומרה", + "Name[hi]": "हार्डवेयर का पता लगाना", + "Name[hsb]": "Wotkrywanje gratow", + "Name[hu]": "Hardverfelismerés", + "Name[ia]": "Discoperta de Hardware", + "Name[id]": "Deteksi Hardware", + "Name[is]": "Vélbúnaðarskynjun", + "Name[it]": "Rilevamento hardware", + "Name[ja]": "ハードウェア検出", + "Name[ko]": "하드웨어 감지", + "Name[lt]": "Aparatinės įrangos aptikimas", + "Name[ml]": "ഹാർഡ്‍വെയർ കണ്ടെത്തല്‍", + "Name[nb]": "Maskinvareoppdaging", + "Name[nds]": "Reedschappen opdecken", + "Name[nl]": "Hardware-detectie", + "Name[nn]": "Maskinvareoppdaging", + "Name[pa]": "ਹਾਰਡਵੇਅਰ ਖੋਜ", + "Name[pl]": "Wykrywanie sprzętu", + "Name[pt]": "Detecção do 'Hardware'", + "Name[pt_BR]": "Detecção do hardware", + "Name[ro]": "Detectare echipament", + "Name[ru]": "Обнаружение оборудования", + "Name[sk]": "Detekcia hardvéru", + "Name[sl]": "Zaznavanje strojne opreme", + "Name[sr@ijekavian]": "Откривање хардвера", + "Name[sr@ijekavianlatin]": "Otkrivanje hardvera", + "Name[sr@latin]": "Otkrivanje hardvera", + "Name[sr]": "Откривање хардвера", + "Name[sv]": "Hårdvarudetektering", + "Name[ta]": "வன்பொருட்களை கண்டறிதல்", + "Name[tr]": "Donanım Algılaması", + "Name[uk]": "Виявлення обладнання", + "Name[vi]": "Phát hiện phần cứng", + "Name[x-test]": "xxHardware Detectionxx", + "Name[zh_CN]": "硬件检测", + "Name[zh_TW]": "硬體偵測" + }, + "X-KDE-Kded-autoload": false, + "X-KDE-Kded-load-on-demand": true +} diff --git a/plasma/workspace/startkde/CMakeLists.txt b/plasma/workspace/startkde/CMakeLists.txt new file mode 100644 index 0000000000..406aedbd38 --- /dev/null +++ b/plasma/workspace/startkde/CMakeLists.txt @@ -0,0 +1,57 @@ +add_subdirectory(plasmaautostart) +add_subdirectory(kcminit) +add_subdirectory(waitforname) + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_subdirectory(systemd) +endif() + +add_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII) +add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) + +qt_add_dbus_interface( + startplasma_SRCS + ${CMAKE_SOURCE_DIR}/ksplash/ksplashqml/org.kde.KSplash.xml + ksplashinterface +) +ecm_qt_declare_logging_category(startplasma_SRCS HEADER debug.h IDENTIFIER PLASMA_STARTUP CATEGORY_NAME org.kde.startup) + +add_library(startplasma OBJECT startplasma.cpp ${startplasma_SRCS}) +target_link_libraries(startplasma PUBLIC + Qt::Core + Qt::DBus + KF5::ConfigCore + KF5::Notifications + KF5::Package + ${PHONON_LIBRARIES} + PW::KWorkspace + lookandfeelmanager +) + +add_executable(startplasma-x11 ${START_PLASMA_COMMON_SRCS} startplasma-x11.cpp kcheckrunning/kcheckrunning.cpp) +add_executable(startplasma-wayland ${START_PLASMA_COMMON_SRCS} startplasma-wayland.cpp) + +target_link_libraries(startplasma-x11 PRIVATE + startplasma + X11::X11 # for kcheckrunning +) + +target_link_libraries(startplasma-wayland PRIVATE + startplasma +) + +add_subdirectory(plasma-session) +add_subdirectory(plasma-shutdown) + +#FIXME: reconsider, looks fishy +if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") + set_property(SOURCE startplasma.cpp APPEND PROPERTY COMPILE_DEFINITIONS + XCURSOR_PATH="${KDE_INSTALL_FULL_DATAROOTDIR}/icons:$XCURSOR_PATH:~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons") +endif() + +configure_file(config-startplasma.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-startplasma.h) + +install(TARGETS startplasma-x11 ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS startplasma-wayland ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(PROGRAMS plasma-sourceenv.sh DESTINATION ${KDE_INSTALL_LIBEXECDIR}) +install(PROGRAMS plasma-dbus-run-session-if-needed DESTINATION ${KDE_INSTALL_LIBEXECDIR}) diff --git a/plasma/workspace/startkde/README b/plasma/workspace/startkde/README new file mode 100644 index 0000000000..f386087348 --- /dev/null +++ b/plasma/workspace/startkde/README @@ -0,0 +1,9 @@ +StartKDE + +Startup script used on X + +StartPlasmaCompositor + StartPlasma + +Startup script used on Wayland. +StartPlasmaCompostior is run before kwin and sets up kwin's env +StartPlasma is then invoked by kwin diff --git a/plasma/workspace/startkde/config-startplasma.h.cmake b/plasma/workspace/startkde/config-startplasma.h.cmake new file mode 100644 index 0000000000..1c9c5d65d9 --- /dev/null +++ b/plasma/workspace/startkde/config-startplasma.h.cmake @@ -0,0 +1,9 @@ +#pragma once + +#define CMAKE_INSTALL_FULL_BINDIR "@CMAKE_INSTALL_FULL_BINDIR@" +#define KDE_INSTALL_FULL_DATAROOTDIR "@KDE_INSTALL_FULL_DATAROOTDIR@" +#define CMAKE_INSTALL_FULL_LIBEXECDIR "@CMAKE_INSTALL_FULL_LIBEXECDIR@" +#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "@CMAKE_INSTALL_FULL_LIBEXECDIR_KF5@" +#define KWIN_WAYLAND_BIN_PATH "@KWIN_WAYLAND_BIN_PATH@" + +#define KWIN_BIN "${KWIN_BIN}" diff --git a/plasma/workspace/startkde/kcheckrunning/kcheckrunning.cpp b/plasma/workspace/startkde/kcheckrunning/kcheckrunning.cpp new file mode 100644 index 0000000000..6792e6b973 --- /dev/null +++ b/plasma/workspace/startkde/kcheckrunning/kcheckrunning.cpp @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2005 Lubos Lunak + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kcheckrunning.h" +#include + +/* + Return 0 when KDE is running, 1 when KDE is not running but it is possible + to connect to X, 2 when it's not possible to connect to X. +*/ +CheckRunningState kCheckRunning() +{ + Display *dpy = XOpenDisplay(nullptr); + if (dpy == nullptr) + return NoX11; + Atom atom = XInternAtom(dpy, "_KDE_RUNNING", False); + return XGetSelectionOwner(dpy, atom) != None ? PlasmaRunning : NoPlasmaRunning; +} diff --git a/plasma/workspace/startkde/kcheckrunning/kcheckrunning.h b/plasma/workspace/startkde/kcheckrunning/kcheckrunning.h new file mode 100644 index 0000000000..88acfa7a22 --- /dev/null +++ b/plasma/workspace/startkde/kcheckrunning/kcheckrunning.h @@ -0,0 +1,15 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +enum CheckRunningState { + PlasmaRunning, + NoPlasmaRunning, + NoX11, +}; + +CheckRunningState kCheckRunning(); diff --git a/plasma/workspace/startkde/kcminit/CMakeLists.txt b/plasma/workspace/startkde/kcminit/CMakeLists.txt new file mode 100644 index 0000000000..da9dae49f8 --- /dev/null +++ b/plasma/workspace/startkde/kcminit/CMakeLists.txt @@ -0,0 +1,24 @@ +########### next target ############### + +set(kcminit_SRCS main.cpp) + +add_executable(kcminit ${kcminit_SRCS}) + +target_link_libraries(kcminit Qt::Core Qt::Gui Qt::DBus KF5::CoreAddons KF5::Service KF5::I18n PW::KWorkspace) + +install(TARGETS kcminit ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) + +########### next target ############### + +# TODO might be simpler to make _startup to be a symlink to + +set(kcminit_startup_SRCS main.cpp) + +add_executable(kcminit_startup ${kcminit_startup_SRCS}) + +ecm_install_configured_files(INPUT plasma-kcminit-phase1.service.in plasma-kcminit.service.in + DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) + +target_link_libraries(kcminit_startup Qt::Core Qt::Gui Qt::DBus KF5::CoreAddons KF5::Service KF5::I18n PW::KWorkspace) + +install(TARGETS kcminit_startup ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) diff --git a/plasma/workspace/startkde/kcminit/Messages.sh b/plasma/workspace/startkde/kcminit/Messages.sh new file mode 100644 index 0000000000..59f7b2dd43 --- /dev/null +++ b/plasma/workspace/startkde/kcminit/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT *.cpp -o $podir/kcminit.pot diff --git a/plasma/workspace/startkde/kcminit/main.cpp b/plasma/workspace/startkde/kcminit/main.cpp new file mode 100644 index 0000000000..d39b496074 --- /dev/null +++ b/plasma/workspace/startkde/kcminit/main.cpp @@ -0,0 +1,181 @@ +/* + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include "main.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static int ready[2]; +static bool startup = false; + +static void sendReady() +{ + if (ready[1] == -1) + return; + char c = 0; + write(ready[1], &c, 1); + close(ready[1]); + ready[1] = -1; +} + +static void waitForReady() +{ + char c = 1; + close(ready[1]); + read(ready[0], &c, 1); + close(ready[0]); +} + +bool KCMInit::runModule(const KPluginMetaData &data) +{ + QString path = QPluginLoader(data.fileName()).fileName(); + + // get the kcminit_ function + QFunctionPointer init = QLibrary::resolve(path, "kcminit"); + if (!init) { + qWarning() << "Module" << data.fileName() << "does not actually have a kcminit function"; + return false; + } + + // initialize the module + qDebug() << "Initializing " << data.fileName(); + init(); + return true; +} + +void KCMInit::runModules(int phase) +{ + for (const KPluginMetaData &data : qAsConst(m_list)) { + // see ksmserver's README for the description of the phases + int libphase = data.value(QStringLiteral("X-KDE-Init-Phase"), 1); + + if (libphase > 1) { + libphase = 1; + } + + if (phase != -1 && libphase != phase) + continue; + + // try to load the library + if (!m_alreadyInitialized.contains(data.pluginId())) { + runModule(data); + m_alreadyInitialized.append(data.pluginId()); + } + } +} + +KCMInit::KCMInit(const QCommandLineParser &args) +{ + QString arg; + if (args.positionalArguments().size() == 1) { + arg = args.positionalArguments().first(); + } + + if (args.isSet(QStringLiteral("list"))) { + m_list = KPluginMetaData::findPlugins(QStringLiteral("plasma/kcminit")); + for (const KPluginMetaData &data : qAsConst(m_list)) { + printf("%s\n", QFile::encodeName(data.fileName()).data()); + } + return; + } + + if (!arg.isEmpty()) { + if (KPluginMetaData data(arg); data.isValid()) { + m_list << arg; + } + } else { + m_list = KPluginMetaData::findPlugins(QStringLiteral("plasma/kcminit")); + } + + if (startup) { + runModules(0); + // Tell KSplash that KCMInit has started + QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), + QStringLiteral("/KSplash"), + QStringLiteral("org.kde.KSplash"), + QStringLiteral("setStage")); + ksplashProgressMessage.setArguments(QList() << QStringLiteral("kcminit")); + QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); + + sendReady(); + QTimer::singleShot(300 * 1000, qApp, &QCoreApplication::quit); // just in case + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/kcminit"), this, QDBusConnection::ExportScriptableContents); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kcminit")); + + qApp->exec(); // wait for runPhase1() + } else + runModules(-1); // all phases +} + +KCMInit::~KCMInit() +{ + sendReady(); +} + +void KCMInit::runPhase1() +{ + runModules(1); + qApp->exit(0); +} + +int main(int argc, char *argv[]) +{ + // plasma-session startup waits for kcminit to finish running phase 0 kcms + // (theoretically that is only important kcms that need to be started very + // early in the login process), the rest is delayed, so fork and make parent + // return after the initial phase + pipe(ready); + if (fork() != 0) { + waitForReady(); + return 0; + } + close(ready[0]); + + const QString executableName = QString::fromUtf8(argv[0]); + startup = executableName.endsWith(QLatin1String("kcminit_startup")); // started from startkde? + + KWorkSpace::detectPlatform(argc, argv); + QGuiApplication::setDesktopSettingsAware(false); + QGuiApplication app(argc, argv); // gui is needed for several modules + KLocalizedString::setApplicationDomain("kcminit"); + KAboutData about(QStringLiteral("kcminit"), + i18n("KCMInit"), + QString(), + i18n("KCMInit - runs startup initialization for Control Modules."), + KAboutLicense::GPL); + KAboutData::setApplicationData(about); + + QCommandLineParser parser; + about.setupCommandLine(&parser); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("list"), i18n("List modules that are run at startup"))); + parser.addPositionalArgument(QStringLiteral("module"), i18n("Configuration module to run")); + + parser.process(app); + about.processCommandLine(&parser); + + KCMInit kcminit(parser); + return 0; +} diff --git a/plasma/workspace/startkde/kcminit/main.h b/plasma/workspace/startkde/kcminit/main.h new file mode 100644 index 0000000000..26d5d20bde --- /dev/null +++ b/plasma/workspace/startkde/kcminit/main.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 1999 Matthias Hoelzer-Kluepfel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class KCMInit : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KCMInit") +public Q_SLOTS: // dbus + Q_SCRIPTABLE void runPhase1(); + +public: + explicit KCMInit(const QCommandLineParser &args); + ~KCMInit() override; + +private: + bool runModule(const KPluginMetaData &data); + void runModules(int phase); + QVector m_list; + QStringList m_alreadyInitialized; +}; diff --git a/plasma/workspace/startkde/kcminit/plasma-kcminit-phase1.service.in b/plasma/workspace/startkde/kcminit/plasma-kcminit-phase1.service.in new file mode 100644 index 0000000000..b8bc811010 --- /dev/null +++ b/plasma/workspace/startkde/kcminit/plasma-kcminit-phase1.service.in @@ -0,0 +1,10 @@ +[Unit] +Description=KDE Configuration Module Initialization (Phase 1) +Requires=plasma-kcminit.service +After=plasma-kcminit.service plasma-kded.service +PartOf=graphical-session.target + +[Service] +Type=oneshot +ExecStart=@QtBinariesDir@/qdbus org.kde.kcminit /kcminit org.kde.KCMInit.runPhase1 +Slice=session.slice diff --git a/plasma/workspace/startkde/kcminit/plasma-kcminit.service.in b/plasma/workspace/startkde/kcminit/plasma-kcminit.service.in new file mode 100644 index 0000000000..198f81fbd1 --- /dev/null +++ b/plasma/workspace/startkde/kcminit/plasma-kcminit.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=KDE Config Module Initialization +PartOf=graphical-session.target +After=plasma-kwin_wayland.service + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/kcminit_startup +Restart=no +Type=forking +Slice=session.slice + +[Install] +Alias=plasma-workspace.service diff --git a/plasma/workspace/startkde/plasma-dbus-run-session-if-needed b/plasma/workspace/startkde/plasma-dbus-run-session-if-needed new file mode 100644 index 0000000000..b37c154f5c --- /dev/null +++ b/plasma/workspace/startkde/plasma-dbus-run-session-if-needed @@ -0,0 +1,10 @@ +#!/bin/sh +# Usage: plasma-dbus-run-session-if-needed PROGRAM [ARGUMENTS] +# If the session bus is not available it is spawned and wrapper round our program +# Otherwise we spawn our program directly +drs= +if [ -z "${DBUS_SESSION_BUS_ADDRESS}" ] +then + drs=dbus-run-session +fi +exec ${drs} "$@" diff --git a/plasma/workspace/startkde/plasma-session/CMakeLists.txt b/plasma/workspace/startkde/plasma-session/CMakeLists.txt new file mode 100644 index 0000000000..3bfb198723 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/CMakeLists.txt @@ -0,0 +1,33 @@ +add_subdirectory(plasma-autostart-list) + +set(plasma_session_SRCS + main.cpp + autostart.cpp + startup.cpp + sessiontrack.cpp + signalhandler.cpp +) + +ecm_qt_declare_logging_category(plasma_session_SRCS HEADER debug.h IDENTIFIER PLASMA_SESSION CATEGORY_NAME org.kde.plasma.session) + +qt_add_dbus_adaptor( plasma_session_SRCS org.kde.Startup.xml startup.h Startup) + +set(kcminit_adaptor ${plasma-workspace_SOURCE_DIR}/startkde/kcminit/main.h) +set(kcminit_xml ${CMAKE_CURRENT_BINARY_DIR}/org.kde.KCMinit.xml) +qt5_generate_dbus_interface( ${kcminit_adaptor} ${kcminit_xml} ) +qt_add_dbus_interface( plasma_session_SRCS ${kcminit_xml} kcminit_interface ) +qt_add_dbus_interface( plasma_session_SRCS ${KDED_DBUS_INTERFACE} kded_interface ) + +qt_add_dbus_interface( plasma_session_SRCS ../../ksmserver/org.kde.KSMServerInterface.xml ksmserver_interface ) + +add_executable(plasma_session ${plasma_session_SRCS}) + +target_include_directories(plasma_session PRIVATE ${CMAKE_SOURCE_DIR}/startkde ${CMAKE_BINARY_DIR}/startkde) +target_link_libraries(plasma_session + startplasma + KF5::KIOCore + PlasmaAutostart +) + +install(TARGETS plasma_session ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + diff --git a/plasma/workspace/startkde/plasma-session/README b/plasma/workspace/startkde/plasma-session/README new file mode 100644 index 0000000000..02e3b13a04 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/README @@ -0,0 +1,3 @@ +This application runs all kcminit/autostart scripts needed for a plasma session. + +This application should only contain tasks relating to starting executables and setting environment variables so that an implementation of systemd units would not cause duplication. diff --git a/plasma/workspace/startkde/plasma-session/autostart.cpp b/plasma/workspace/startkde/plasma-session/autostart.cpp new file mode 100644 index 0000000000..9ed7e7b5c1 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/autostart.cpp @@ -0,0 +1,141 @@ +/* + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "autostart.h" + +#include "../plasmaautostart/plasmaautostart.h" + +#include +#include +#include + +AutoStart::AutoStart() + : m_phase(-1) + , m_phasedone(false) +{ + loadAutoStartList(); +} + +AutoStart::~AutoStart() +{ +} + +void AutoStart::setPhase(int phase) +{ + if (phase > m_phase) { + m_phase = phase; + m_phasedone = false; + } +} + +void AutoStart::setPhaseDone() +{ + m_phasedone = true; +} + +static QString extractName(QString path) // krazy:exclude=passbyvalue +{ + int i = path.lastIndexOf(QLatin1Char('/')); + if (i >= 0) { + path = path.mid(i + 1); + } + i = path.lastIndexOf(QLatin1Char('.')); + if (i >= 0) { + path.truncate(i); + } + return path; +} + +void AutoStart::loadAutoStartList() +{ + // XDG autostart dirs + + // Make unique list of relative paths + QHash files; + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericConfigLocation, QStringLiteral("autostart"), QStandardPaths::LocateDirectory); + for (const QString &dir : dirs) { + const QDir d(dir); + const QStringList fileNames = d.entryList(QStringList() << QStringLiteral("*.desktop")); + for (const QString &file : fileNames) { + if (!files.contains(file)) { + files.insert(file, d.absoluteFilePath(file)); + } + } + } + + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { + PlasmaAutostart config(*it); + if (!config.autostarts(QStringLiteral("KDE"), PlasmaAutostart::CheckAll)) { + continue; + } + + AutoStartItem item; + item.service = *it; + item.name = extractName(it.key()); + item.startAfter = config.startAfter(); + item.phase = qMax(PlasmaAutostart::BaseDesktop, config.startPhase()); + m_startList.append(item); + } +} + +QString AutoStart::startService() +{ + if (m_startList.isEmpty()) { + return QString(); + } + + while (!m_started.isEmpty()) { + // Check for items that depend on previously started items + QString lastItem = m_started[0]; + QMutableVectorIterator it(m_startList); + while (it.hasNext()) { + const auto &item = it.next(); + if (item.phase == m_phase && item.startAfter == lastItem) { + m_started.prepend(item.name); + QString service = item.service; + it.remove(); + return service; + } + } + m_started.removeFirst(); + } + + // Check for items that don't depend on anything + QMutableVectorIterator it(m_startList); + while (it.hasNext()) { + const auto &item = it.next(); + if (item.phase == m_phase && item.startAfter.isEmpty()) { + m_started.prepend(item.name); + QString service = item.service; + it.remove(); + return service; + } + } + + // Just start something in this phase + it = m_startList; + while (it.hasNext()) { + const auto &item = it.next(); + if (item.phase == m_phase) { + m_started.prepend(item.name); + QString service = item.service; + it.remove(); + return service; + } + } + + return QString(); +} + +QVector AutoStart::startList() const +{ + QVector ret; + for (const auto &asi : m_startList) { + if (asi.phase == m_phase) + ret << asi; + } + return ret; +} diff --git a/plasma/workspace/startkde/plasma-session/autostart.h b/plasma/workspace/startkde/plasma-session/autostart.h new file mode 100644 index 0000000000..3d5e0e75f5 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/autostart.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +class AutoStartItem +{ +public: + QString name; + QString service; + QString startAfter; + int phase; +}; + +class AutoStart +{ +public: + AutoStart(); + ~AutoStart(); + + QString startService(); + void setPhase(int phase); + void setPhaseDone(); + int phase() const + { + return m_phase; + } + bool phaseDone() const + { + return m_phasedone; + } + QVector startList() const; + +private: + void loadAutoStartList(); + QVector m_startList; + QStringList m_started; + int m_phase; + bool m_phasedone; +}; diff --git a/plasma/workspace/startkde/plasma-session/main.cpp b/plasma/workspace/startkde/plasma-session/main.cpp new file mode 100644 index 0000000000..b0e424d571 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "startup.h" + +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + new Startup(&app); + app.exec(); +} diff --git a/plasma/workspace/startkde/plasma-session/org.kde.Startup.xml b/plasma/workspace/startkde/plasma-session/org.kde.Startup.xml new file mode 100644 index 0000000000..7ef689111b --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/org.kde.Startup.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plasma/workspace/startkde/plasma-session/plasma-autostart-list/CMakeLists.txt b/plasma/workspace/startkde/plasma-session/plasma-autostart-list/CMakeLists.txt new file mode 100644 index 0000000000..f0d882b3b9 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/plasma-autostart-list/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(plasma-autostart-list main.cpp ../autostart.cpp) +target_link_libraries(plasma-autostart-list KF5::Service PlasmaAutostart) diff --git a/plasma/workspace/startkde/plasma-session/plasma-autostart-list/main.cpp b/plasma/workspace/startkde/plasma-session/plasma-autostart-list/main.cpp new file mode 100644 index 0000000000..33bc0c7dd2 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/plasma-autostart-list/main.cpp @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "../autostart.h" +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + AutoStart as; + + QTextStream cout(stdout); + auto printPhase = [&cout, &as](int phase) -> bool { + AutoStart asN(as); + asN.setPhase(phase); + cout << "phase: " << phase << '\n'; + bool foundThings = true; + for (const auto &asi : asN.startList()) { + foundThings = false; + cout << "- " << asi.name << ' ' << asi.service; + if (!asi.startAfter.isEmpty()) + cout << ", startAfter:" << asi.startAfter; + cout << '\n'; + } + cout << '\n'; + return !foundThings; + }; + + printPhase(0); + printPhase(1); + printPhase(2); + return 0; +} diff --git a/plasma/workspace/startkde/plasma-session/sessiontrack.cpp b/plasma/workspace/startkde/plasma-session/sessiontrack.cpp new file mode 100644 index 0000000000..850422066f --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/sessiontrack.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "sessiontrack.h" +#include "signalhandler.h" +#include +#include +#include +#include + +SessionTrack::SessionTrack(const QVector &processes) + : m_processes(processes) +{ + SignalHandler::self()->addSignal(SIGTERM); + connect(SignalHandler::self(), &SignalHandler::signalReceived, QCoreApplication::instance(), [](int signal) { + if (signal == SIGTERM) { + QCoreApplication::instance()->exit(0); + } + }); + + for (auto process : std::as_const(m_processes)) { + connect(process, &QProcess::finished, this, [this] { + m_processes.removeAll(static_cast(sender())); + }); + } + + QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &SessionTrack::deleteLater); +} + +SessionTrack::~SessionTrack() +{ + disconnect(this, nullptr, QCoreApplication::instance(), nullptr); + + for (auto process : std::as_const(m_processes)) { + process->terminate(); + } + for (auto process : std::as_const(m_processes)) { + if (process->state() == QProcess::Running && !process->waitForFinished(500)) { + process->kill(); + } else { + delete process; + } + } +} diff --git a/plasma/workspace/startkde/plasma-session/sessiontrack.h b/plasma/workspace/startkde/plasma-session/sessiontrack.h new file mode 100644 index 0000000000..0983c17707 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/sessiontrack.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +class SessionTrack : public QObject +{ + Q_OBJECT +public: + SessionTrack(const QVector &processes); + ~SessionTrack() override; + +private: + QVector m_processes; + QEventLoopLocker m_lock; +}; diff --git a/plasma/workspace/startkde/plasma-session/signalhandler.cpp b/plasma/workspace/startkde/plasma-session/signalhandler.cpp new file mode 100644 index 0000000000..e1f4e84756 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/signalhandler.cpp @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "signalhandler.h" +#include "debug.h" + +#include +#include +#include +#include + +int SignalHandler::signalFd[2]; + +SignalHandler::SignalHandler() +{ + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signalFd)) { + qCWarning(PLASMA_SESSION) << "Couldn't create a socketpair"; + return; + } + + m_handler = new QSocketNotifier(signalFd[1], QSocketNotifier::Read, this); + connect(m_handler, &QSocketNotifier::activated, this, &SignalHandler::handleSignal); +} + +SignalHandler::~SignalHandler() +{ + for (int sig : std::as_const(m_signalsRegistered)) { + signal(sig, nullptr); + } + close(signalFd[0]); + close(signalFd[1]); +} + +void SignalHandler::addSignal(int signalToTrack) +{ + m_signalsRegistered.insert(signalToTrack); + signal(signalToTrack, signalHandler); +} + +void SignalHandler::signalHandler(int signal) +{ + ::write(signalFd[0], &signal, sizeof(signal)); +} + +void SignalHandler::handleSignal() +{ + m_handler->setEnabled(false); + int signal; + ::read(signalFd[1], &signal, sizeof(signal)); + m_handler->setEnabled(true); + + Q_EMIT signalReceived(signal); +} + +SignalHandler *SignalHandler::self() +{ + static SignalHandler s_self; + return &s_self; +} diff --git a/plasma/workspace/startkde/plasma-session/signalhandler.h b/plasma/workspace/startkde/plasma-session/signalhandler.h new file mode 100644 index 0000000000..c0f8428b47 --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/signalhandler.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include + +/** + * Class to be able to receive ANSI C signals and forward them onto the Qt eventloop + * + * It's a singleton as it relies on static data getting defined. + */ +class SignalHandler : public QObject +{ + Q_OBJECT +public: + ~SignalHandler() override; + void addSignal(int signal); + + static SignalHandler *self(); + +Q_SIGNALS: + void signalReceived(int signal); + +private: + SignalHandler(); + void handleSignal(); + static void signalHandler(int signal); + + QSet m_signalsRegistered; + static int signalFd[2]; + QSocketNotifier *m_handler = nullptr; +}; diff --git a/plasma/workspace/startkde/plasma-session/startup.cpp b/plasma/workspace/startkde/plasma-session/startup.cpp new file mode 100644 index 0000000000..9078c1f16f --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/startup.cpp @@ -0,0 +1,450 @@ +/* + SPDX-FileCopyrightText: 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2005 Lubos Lunak + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-FileContributor: Oswald Buddenhagen + + some code taken from the dcopserver (part of the KDE libraries), which is + SPDX-FileCopyrightText: 1999 Matthias Ettrich + SPDX-FileCopyrightText: 1999 Preston Brown + + SPDX-License-Identifier: MIT +*/ + +#include "startup.h" + +#include "debug.h" + +#include + +#include "kcminit_interface.h" +#include "kded_interface.h" +#include "ksmserver_interface.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "sessiontrack.h" +#include "startupadaptor.h" + +#include "../config-startplasma.h" +#include "startplasma.h" + +class Phase : public KCompositeJob +{ + Q_OBJECT +public: + Phase(const AutoStart &autostart, QObject *parent) + : KCompositeJob(parent) + , m_autostart(autostart) + { + } + + bool addSubjob(KJob *job) override + { + bool rc = KCompositeJob::addSubjob(job); + job->start(); + return rc; + } + + void slotResult(KJob *job) override + { + KCompositeJob::slotResult(job); + if (!hasSubjobs()) { + emitResult(); + } + } + +protected: + const AutoStart m_autostart; +}; + +class StartupPhase0 : public Phase +{ + Q_OBJECT +public: + StartupPhase0(const AutoStart &autostart, QObject *parent) + : Phase(autostart, parent) + { + } + void start() override + { + qCDebug(PLASMA_SESSION) << "Phase 0"; + addSubjob(new AutoStartAppsJob(m_autostart, 0)); + addSubjob(new KCMInitJob()); + addSubjob(new SleepJob()); + } +}; + +class StartupPhase1 : public Phase +{ + Q_OBJECT +public: + StartupPhase1(const AutoStart &autostart, QObject *parent) + : Phase(autostart, parent) + { + } + void start() override + { + qCDebug(PLASMA_SESSION) << "Phase 1"; + addSubjob(new AutoStartAppsJob(m_autostart, 1)); + } +}; + +class StartupPhase2 : public Phase +{ + Q_OBJECT +public: + StartupPhase2(const AutoStart &autostart, QObject *parent) + : Phase(autostart, parent) + { + } + void migrateKDE4Autostart(); + + void start() override + { + qCDebug(PLASMA_SESSION) << "Phase 2"; + migrateKDE4Autostart(); + addSubjob(new AutoStartAppsJob(m_autostart, 2)); + addSubjob(new KDEDInitJob()); + } +}; + +SleepJob::SleepJob() +{ +} + +void SleepJob::start() +{ + auto t = new QTimer(this); + connect(t, &QTimer::timeout, this, [this]() { + emitResult(); + }); + t->start(100); +} + +Startup::Startup(QObject *parent) + : QObject(parent) +{ + Q_ASSERT(!s_self); + s_self = this; + new StartupAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Startup"), QStringLiteral("org.kde.Startup"), this); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Startup")); + + const AutoStart autostart; + + KJob *x11WindowManagerJob = nullptr; + if (qEnvironmentVariable("XDG_SESSION_TYPE") != QLatin1String("wayland")) { + QString windowManager; + if (qEnvironmentVariableIsSet("KDEWM")) { + windowManager = qEnvironmentVariable("KDEWM"); + } + if (windowManager.isEmpty()) { + windowManager = QStringLiteral(KWIN_BIN); + } + + if (windowManager == QLatin1String(KWIN_BIN)) { + x11WindowManagerJob = new StartServiceJob(windowManager, {}, QStringLiteral("org.kde.KWin")); + } else { + x11WindowManagerJob = new StartServiceJob(windowManager, {}, {}); + } + } else { + // This must block until started as it sets the WAYLAND_DISPLAY/DISPLAY env variables needed for the rest of the boot + // fortunately it's very fast as it's just starting a wrapper + StartServiceJob kwinWaylandJob(QStringLiteral("kwin_wayland_wrapper"), {QStringLiteral("--xwayland")}, QStringLiteral("org.kde.KWinWrapper")); + kwinWaylandJob.exec(); + // kslpash is only launched in plasma-session from the wayland mode, for X it's in startplasma-x11 + + const KConfig cfg(QStringLiteral("ksplashrc")); + // the splashscreen and progress indicator + KConfigGroup ksplashCfg = cfg.group("KSplash"); + if (ksplashCfg.readEntry("Engine", QStringLiteral("KSplashQML")) == QLatin1String("KSplashQML")) { + QProcess::startDetached(QStringLiteral("ksplashqml"), {}); + } + } + + // Keep for KF5; remove in KF6 (KInit will be gone then) + QProcess::execute(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/start_kdeinit_wrapper"), QStringList()); + + KJob *phase1 = nullptr; + m_lock.reset(new QEventLoopLocker); + + const QVector sequence = { + new StartProcessJob(QStringLiteral("kcminit_startup"), {}), + new StartServiceJob(QStringLiteral("kded5"), {}, QStringLiteral("org.kde.kded5"), {}), + x11WindowManagerJob, + new StartServiceJob(QStringLiteral("ksmserver"), QCoreApplication::instance()->arguments().mid(1), QStringLiteral("org.kde.ksmserver")), + new StartupPhase0(autostart, this), + phase1 = new StartupPhase1(autostart, this), + new RestoreSessionJob(), + new StartupPhase2(autostart, this), + }; + KJob *last = nullptr; + for (KJob *job : sequence) { + if (!job) { + continue; + } + if (last) { + connect(last, &KJob::finished, job, &KJob::start); + } + last = job; + } + + connect(sequence.last(), &KJob::finished, this, &Startup::finishStartup); + sequence.first()->start(); + + // app will be closed when all KJobs finish thanks to the QEventLoopLocker in each KJob +} + +void Startup::upAndRunning(const QString &msg) +{ + QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), + QStringLiteral("/KSplash"), + QStringLiteral("org.kde.KSplash"), + QStringLiteral("setStage")); + ksplashProgressMessage.setArguments(QList() << msg); + QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); +} + +void Startup::finishStartup() +{ + qCDebug(PLASMA_SESSION) << "Finished"; + upAndRunning(QStringLiteral("ready")); + + playStartupSound(this); + new SessionTrack(m_processes); + deleteLater(); +} + +void Startup::updateLaunchEnv(const QString &key, const QString &value) +{ + qputenv(key.toLatin1(), value.toLatin1()); +} + +bool Startup::startDetached(const QString &program, const QStringList &args) +{ + QProcess *p = new QProcess(); + p->setProgram(program); + p->setArguments(args); + return startDetached(p); +} + +bool Startup::startDetached(QProcess *process) +{ + process->setProcessChannelMode(QProcess::ForwardedChannels); + process->start(); + const bool ret = process->waitForStarted(); + if (ret) { + m_processes << process; + } + return ret; +} + +Startup *Startup::s_self = nullptr; + +KCMInitJob::KCMInitJob() + : KJob() +{ +} + +void KCMInitJob::start() +{ + org::kde::KCMInit kcminit(QStringLiteral("org.kde.kcminit"), QStringLiteral("/kcminit"), QDBusConnection::sessionBus()); + kcminit.setTimeout(10 * 1000); + + QDBusPendingReply pending = kcminit.runPhase1(); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this]() { + emitResult(); + }); + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QObject::deleteLater); +} + +KDEDInitJob::KDEDInitJob() +{ +} + +void KDEDInitJob::start() +{ + qCDebug(PLASMA_SESSION()); + org::kde::kded5 kded(QStringLiteral("org.kde.kded5"), QStringLiteral("/kded"), QDBusConnection::sessionBus()); + auto pending = kded.loadSecondPhase(); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this]() { + emitResult(); + }); + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QObject::deleteLater); +} + +RestoreSessionJob::RestoreSessionJob() + : KJob() +{ +} + +void RestoreSessionJob::start() +{ + OrgKdeKSMServerInterfaceInterface ksmserverIface(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QDBusConnection::sessionBus()); + auto pending = ksmserverIface.restoreSession(); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this]() { + emitResult(); + }); + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QObject::deleteLater); +} + +void StartupPhase2::migrateKDE4Autostart() +{ + // Migrate user autostart from kde4 + Kdelibs4Migration migration; + if (!migration.kdeHomeFound()) { + return; + } + + const QString autostartFolder = + QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QDir::separator() + QStringLiteral("autostart-scripts"); + QDir dir(autostartFolder); + if (!dir.exists()) { + dir.mkpath(QStringLiteral(".")); + } + + // KDEHOME/Autostart was the default value for KGlobalSettings::autostart() + QString oldAutostart = migration.kdeHome() + QStringLiteral("/Autostart"); + // That path could be customized in kdeglobals + const QString oldKdeGlobals = migration.locateLocal("config", QStringLiteral("kdeglobals")); + if (!oldKdeGlobals.isEmpty()) { + oldAutostart = KConfig(oldKdeGlobals).group("Paths").readEntry("Autostart", oldAutostart); + } + + const QDir oldFolder(oldAutostart); + qCDebug(PLASMA_SESSION) << "Copying autostart files from" << oldFolder.path(); + const QStringList entries = oldFolder.entryList(QDir::Files); + for (const QString &file : entries) { + const QString src = oldFolder.absolutePath() + QLatin1Char('/') + file; + const QString dest = autostartFolder + QLatin1Char('/') + file; + QFileInfo info(src); + bool success; + if (info.isSymLink()) { + // This will only work with absolute symlink targets + success = QFile::link(info.symLinkTarget(), dest); + } else { + success = QFile::copy(src, dest); + } + if (!success) { + qCWarning(PLASMA_SESSION) << "Error copying" << src << "to" << dest; + } + } + return; +} + +AutoStartAppsJob::AutoStartAppsJob(const AutoStart &autostart, int phase) + : m_autoStart(autostart) +{ + m_autoStart.setPhase(phase); +} + +void AutoStartAppsJob::start() +{ + qCDebug(PLASMA_SESSION); + + QTimer::singleShot(0, this, [=]() { + do { + QString serviceName = m_autoStart.startService(); + if (serviceName.isEmpty()) { + // Done + if (!m_autoStart.phaseDone()) { + m_autoStart.setPhaseDone(); + } + emitResult(); + return; + } + KService service(serviceName); + auto arguments = KIO::DesktopExecParser(service, QList()).resultingArguments(); + if (arguments.isEmpty()) { + qCWarning(PLASMA_SESSION) << "failed to parse" << serviceName << "for autostart"; + continue; + } + qCInfo(PLASMA_SESSION) << "Starting autostart service " << serviceName << arguments; + auto program = arguments.takeFirst(); + if (!Startup::self()->startDetached(program, arguments)) + qCWarning(PLASMA_SESSION) << "could not start" << serviceName << ":" << program << arguments; + } while (true); + }); +} + +StartServiceJob::StartServiceJob(const QString &process, const QStringList &args, const QString &serviceId, const QProcessEnvironment &additionalEnv) + : KJob() + , m_process(new QProcess) + , m_serviceId(serviceId) + , m_additionalEnv(additionalEnv) +{ + m_process->setProgram(process); + m_process->setArguments(args); + + auto watcher = new QDBusServiceWatcher(serviceId, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &StartServiceJob::emitResult); +} + +void StartServiceJob::start() +{ + auto env = QProcessEnvironment::systemEnvironment(); + env.insert(m_additionalEnv); + m_process->setProcessEnvironment(env); + + if (!m_serviceId.isEmpty() && QDBusConnection::sessionBus().interface()->isServiceRegistered(m_serviceId)) { + qCDebug(PLASMA_SESSION) << m_process << "already running"; + emitResult(); + return; + } + qCDebug(PLASMA_SESSION) << "Starting " << m_process->program() << m_process->arguments(); + if (!Startup::self()->startDetached(m_process)) { + qCWarning(PLASMA_SESSION) << "error starting process" << m_process->program() << m_process->arguments(); + emitResult(); + } + + if (m_serviceId.isEmpty()) { + emitResult(); + } +} + +StartProcessJob::StartProcessJob(const QString &process, const QStringList &args, const QProcessEnvironment &additionalEnv) + : KJob() + , m_process(new QProcess(this)) +{ + m_process->setProgram(process); + m_process->setArguments(args); + m_process->setProcessChannelMode(QProcess::ForwardedChannels); + auto env = QProcessEnvironment::systemEnvironment(); + env.insert(additionalEnv); + m_process->setProcessEnvironment(env); + + connect(m_process, &QProcess::finished, [this](int exitCode) { + qCInfo(PLASMA_SESSION) << "process job " << m_process->program() << "finished with exit code " << exitCode; + emitResult(); + }); +} + +void StartProcessJob::start() +{ + qCDebug(PLASMA_SESSION) << "Starting " << m_process->program() << m_process->arguments(); + + m_process->start(); +} + +#include "startup.moc" diff --git a/plasma/workspace/startkde/plasma-session/startup.h b/plasma/workspace/startkde/plasma-session/startup.h new file mode 100644 index 0000000000..da3ab8421b --- /dev/null +++ b/plasma/workspace/startkde/plasma-session/startup.h @@ -0,0 +1,124 @@ +/* + ksmserver - the KDE session management server + + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include +#include +#include +#include + +#include "autostart.h" + +class Startup : public QObject +{ + Q_OBJECT +public: + Startup(QObject *parent); + void upAndRunning(const QString &msg); + void finishStartup(); + + static Startup *self() + { + Q_ASSERT(s_self); + return s_self; + } + + bool startDetached(const QString &program, const QStringList &args); + bool startDetached(QProcess *process); + +public Q_SLOTS: + // alternatively we could drop this and have a rule that we /always/ launch everything through klauncher + // need resolution from frameworks discussion on kdeinit + void updateLaunchEnv(const QString &key, const QString &value); + +private: + void autoStart(int phase); + + QVector m_processes; + QScopedPointer m_lock; + static Startup *s_self; +}; + +class SleepJob : public KJob +{ + Q_OBJECT +public: + SleepJob(); + void start() override; +}; + +class KCMInitJob : public KJob +{ + Q_OBJECT +public: + KCMInitJob(); + void start() override; +}; + +class KDEDInitJob : public KJob +{ + Q_OBJECT +public: + KDEDInitJob(); + void start() override; +}; + +class AutoStartAppsJob : public KJob +{ + Q_OBJECT +public: + AutoStartAppsJob(const AutoStart &autoStart, int phase); + void start() override; + +private: + AutoStart m_autoStart; +}; + +/** + * Launches a process, and waits for the process to start + */ +class StartProcessJob : public KJob +{ + Q_OBJECT +public: + StartProcessJob(const QString &process, const QStringList &args, const QProcessEnvironment &additionalEnv = QProcessEnvironment()); + void start() override; + +private: + QProcess *m_process; +}; + +/** + * Launches a process, and waits for the service to appear on the session bus + */ +class StartServiceJob : public KJob +{ + Q_OBJECT +public: + StartServiceJob(const QString &process, + const QStringList &args, + const QString &serviceId, + const QProcessEnvironment &additionalEnv = QProcessEnvironment()); + void start() override; + +private: + QProcess *m_process; + const QString m_serviceId; + const QProcessEnvironment m_additionalEnv; +}; + +class RestoreSessionJob : public KJob +{ + Q_OBJECT +public: + RestoreSessionJob(); + void start() override; + +private: +}; diff --git a/plasma/workspace/startkde/plasma-shutdown/CMakeLists.txt b/plasma/workspace/startkde/plasma-shutdown/CMakeLists.txt new file mode 100644 index 0000000000..a91a490fb0 --- /dev/null +++ b/plasma/workspace/startkde/plasma-shutdown/CMakeLists.txt @@ -0,0 +1,24 @@ +set(plasma_shutdown_SRCS + main.cpp + shutdown.cpp +) + +ecm_qt_declare_logging_category(plasma_shutdown_SRCS HEADER debug.h IDENTIFIER PLASMA_SESSION CATEGORY_NAME org.kde.plasma.shutdown) + +qt_add_dbus_adaptor(plasma_shutdown_SRCS org.kde.Shutdown.xml shutdown.h Shutdown) +qt_add_dbus_interface(plasma_shutdown_SRCS org.kde.Shutdown.xml shutdown_interface) +qt_add_dbus_interface( plasma_shutdown_SRCS ../../ksmserver/org.kde.KSMServerInterface.xml ksmserver_interface ) +qt_add_dbus_interface( plasma_shutdown_SRCS ../../ksmserver/org.kde.KWin.Session.xml kwin_interface ) + +add_executable(plasma-shutdown ${plasma_shutdown_SRCS}) + +target_link_libraries(plasma-shutdown + Qt::Core + Qt::DBus + KF5::ConfigCore + PW::KWorkspace +) + +kdbusaddons_generate_dbus_service_file(plasma-shutdown org.kde.Shutdown ${KDE_INSTALL_FULL_BINDIR}) +install(TARGETS plasma-shutdown ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + diff --git a/plasma/workspace/startkde/plasma-shutdown/main.cpp b/plasma/workspace/startkde/plasma-shutdown/main.cpp new file mode 100644 index 0000000000..c62cb7ff44 --- /dev/null +++ b/plasma/workspace/startkde/plasma-shutdown/main.cpp @@ -0,0 +1,16 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "shutdown.h" + +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + new Shutdown(&app); + app.exec(); +} diff --git a/plasma/workspace/startkde/plasma-shutdown/org.kde.Shutdown.xml b/plasma/workspace/startkde/plasma-shutdown/org.kde.Shutdown.xml new file mode 100644 index 0000000000..7184f73ab8 --- /dev/null +++ b/plasma/workspace/startkde/plasma-shutdown/org.kde.Shutdown.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plasma/workspace/startkde/plasma-shutdown/shutdown.cpp b/plasma/workspace/startkde/plasma-shutdown/shutdown.cpp new file mode 100644 index 0000000000..f5609dcfbf --- /dev/null +++ b/plasma/workspace/startkde/plasma-shutdown/shutdown.cpp @@ -0,0 +1,105 @@ +#include "shutdown.h" +#include "shutdownadaptor.h" + +#include +#include +#include +#include +#include + +#include "debug.h" +#include "ksmserver_interface.h" +#include "kwin_interface.h" +#include "sessionmanagementbackend.h" + +Shutdown::Shutdown(QObject *parent) + : QObject(parent) +{ + new ShutdownAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Shutdown"), QStringLiteral("org.kde.Shutdown"), this); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Shutdown")); +} + +void Shutdown::logout() +{ + startLogout(KWorkSpace::ShutdownTypeNone); +} + +void Shutdown::logoutAndShutdown() +{ + startLogout(KWorkSpace::ShutdownTypeHalt); +} + +void Shutdown::logoutAndReboot() +{ + startLogout(KWorkSpace::ShutdownTypeReboot); +} + +void Shutdown::startLogout(KWorkSpace::ShutdownType shutdownType) +{ + m_shutdownType = shutdownType; + + OrgKdeKSMServerInterfaceInterface ksmserverIface(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QDBusConnection::sessionBus()); + ksmserverIface.setTimeout( + INT32_MAX); // KSMServer closeSession can take a long time to reply, as apps may have prompts. Value corresponds to DBUS_TIMEOUT_INFINITE + + auto closeSessionReply = ksmserverIface.closeSession(); + auto watcher = new QDBusPendingCallWatcher(closeSessionReply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [closeSessionReply, watcher, this]() { + watcher->deleteLater(); + if (closeSessionReply.isError()) { + qCWarning(PLASMA_SESSION) << "ksmserver failed to complete logout"; + qApp->quit(); + } + if (closeSessionReply.value()) { + logoutComplete(); + } else { + logoutCancelled(); + } + }); +} + +void Shutdown::logoutCancelled() +{ + m_shutdownType = KWorkSpace::ShutdownTypeNone; + qApp->quit(); +} + +void Shutdown::logoutComplete() +{ + runShutdownScripts(); + + // technically this isn't needed in the systemd managed mode, but it seems harmless for now. Guard if it becomes an issue + OrgKdeKWinSessionInterface kwinInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Session"), QDBusConnection::sessionBus()); + QDBusPendingReply<> reply = kwinInterface.quit(); + reply.waitForFinished(); + + if (m_shutdownType == KWorkSpace::ShutdownTypeHalt) { + SessionBackend::self()->shutdown(); + } else if (m_shutdownType == KWorkSpace::ShutdownTypeReboot) { + SessionBackend::self()->reboot(); + } else { // logout + qApp->quit(); + } +} + +void Shutdown::runShutdownScripts() +{ + const QStringList shutdownFolders = + QStandardPaths::locateAll(QStandardPaths::GenericConfigLocation, QStringLiteral("plasma-workspace/shutdown"), QStandardPaths::LocateDirectory); + for (const QString &shutDownFolder : shutdownFolders) { + QDir dir(shutDownFolder); + + const QStringList entries = dir.entryList(QDir::Files); + for (const QString &file : entries) { + // Don't execute backup files + if (!file.endsWith(QLatin1Char('~')) && !file.endsWith(QLatin1String(".bak")) && (file[0] != QLatin1Char('%') || !file.endsWith(QLatin1Char('%'))) + && (file[0] != QLatin1Char('#') || !file.endsWith(QLatin1Char('#')))) { + const QString fullPath = dir.absolutePath() + QLatin1Char('/') + file; + + qCDebug(PLASMA_SESSION) << "running shutdown script" << fullPath; + QProcess::execute(fullPath, QStringList()); + } + } + } +} diff --git a/plasma/workspace/startkde/plasma-shutdown/shutdown.h b/plasma/workspace/startkde/plasma-shutdown/shutdown.h new file mode 100644 index 0000000000..3bf17f19d7 --- /dev/null +++ b/plasma/workspace/startkde/plasma-shutdown/shutdown.h @@ -0,0 +1,30 @@ +/* + ksmserver - the KDE session management server + + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include +#include + +class Shutdown : public QObject +{ + Q_OBJECT +public: + Shutdown(QObject *parent = nullptr); + void logout(); + void logoutAndShutdown(); + void logoutAndReboot(); +private Q_SLOTS: + void logoutCancelled(); + void logoutComplete(); + +private: + void startLogout(KWorkSpace::ShutdownType shutdownType); + void runShutdownScripts(); + KWorkSpace::ShutdownType m_shutdownType; +}; diff --git a/plasma/workspace/startkde/plasma-sourceenv.sh b/plasma/workspace/startkde/plasma-sourceenv.sh new file mode 100644 index 0000000000..4a00f15f42 --- /dev/null +++ b/plasma/workspace/startkde/plasma-sourceenv.sh @@ -0,0 +1,7 @@ +for i in $@ +do + . $i >/dev/null +done + +# env may not support -0, fall back to GNU env +env -0 2>/dev/null || genv -0 diff --git a/plasma/workspace/startkde/plasmaautostart/CMakeLists.txt b/plasma/workspace/startkde/plasmaautostart/CMakeLists.txt new file mode 100644 index 0000000000..8c383990fa --- /dev/null +++ b/plasma/workspace/startkde/plasmaautostart/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(PlasmaAutostart STATIC plasmaautostart.cpp) +target_link_libraries(PlasmaAutostart KF5::CoreAddons KF5::ConfigCore) diff --git a/plasma/workspace/startkde/plasmaautostart/plasmaautostart.cpp b/plasma/workspace/startkde/plasmaautostart/plasmaautostart.cpp new file mode 100644 index 0000000000..ad36780284 --- /dev/null +++ b/plasma/workspace/startkde/plasmaautostart/plasmaautostart.cpp @@ -0,0 +1,204 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2006 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "plasmaautostart.h" + +#include +#include + +#include +#include +#include + +void PlasmaAutostart::copyIfNeeded() +{ + if (copyIfNeededChecked) { + return; + } + + const QString local = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/autostart/") + name; + + if (!QFile::exists(local)) { + const QString global = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QLatin1String("autostart/") + name); + if (!global.isEmpty()) { + KDesktopFile *newDf = df->copyTo(local); + delete df; + delete newDf; // Force sync-to-disk + df = new KDesktopFile(QStandardPaths::GenericConfigLocation, QStringLiteral("autostart/") + name); // Recreate from disk + } + } + + copyIfNeededChecked = true; +} + +PlasmaAutostart::PlasmaAutostart(const QString &entryName, QObject *parent) + : QObject(parent) +{ + const bool isAbsolute = QDir::isAbsolutePath(entryName); + if (isAbsolute) { + name = entryName.mid(entryName.lastIndexOf(QLatin1Char('/')) + 1); + } else { + if (entryName.isEmpty()) { + name = QCoreApplication::applicationName(); + } else { + name = entryName; + } + + if (!name.endsWith(QLatin1String(".desktop"))) { + name.append(QLatin1String(".desktop")); + } + } + + const QString path = isAbsolute ? entryName : QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QLatin1String("autostart/") + name); + if (path.isEmpty()) { + // just a new KDesktopFile, since we have nothing to use + df = new KDesktopFile(QStandardPaths::GenericConfigLocation, QLatin1String("autostart/") + name); + copyIfNeededChecked = true; + } else { + df = new KDesktopFile(path); + } +} + +PlasmaAutostart::~PlasmaAutostart() = default; + +void PlasmaAutostart::setAutostarts(bool autostart) +{ + bool currentAutostartState = !df->desktopGroup().readEntry("Hidden", false); + if (currentAutostartState == autostart) { + return; + } + + copyIfNeeded(); + df->desktopGroup().writeEntry("Hidden", !autostart); +} + +bool PlasmaAutostart::autostarts(const QString &environment, Conditions check) const +{ + // check if this is actually a .desktop file + bool starts = df->desktopGroup().exists(); + + // check the hidden field + starts = starts && !df->desktopGroup().readEntry("Hidden", false); + + if (!environment.isEmpty()) { + starts = starts && checkAllowedEnvironment(environment); + } + + if (check & CheckCommand) { + starts = starts && df->tryExec(); + } + + if (check & CheckCondition) { + starts = starts && checkStartCondition(); + } + + return starts; +} + +bool PlasmaAutostart::checkStartCondition() const +{ + return PlasmaAutostart::isStartConditionMet(df->desktopGroup().readEntry("X-KDE-autostart-condition")); +} + +bool PlasmaAutostart::isStartConditionMet(const QString &condition) +{ + if (condition.isEmpty()) { + return true; + } + + const QStringList list = condition.split(QLatin1Char(':')); + if (list.count() < 4) { + return true; + } + + if (list[0].isEmpty() || list[2].isEmpty()) { + return true; + } + + KConfig config(list[0], KConfig::NoGlobals); + KConfigGroup cg(&config, list[1]); + + const bool defaultValue = (list[3].toLower() == QLatin1String("true")); + return cg.readEntry(list[2], defaultValue); +} + +bool PlasmaAutostart::checkAllowedEnvironment(const QString &environment) const +{ + const QStringList allowed = allowedEnvironments(); + if (!allowed.isEmpty()) { + return allowed.contains(environment); + } + + const QStringList excluded = excludedEnvironments(); + if (!excluded.isEmpty()) { + return !excluded.contains(environment); + } + + return true; +} + +QString PlasmaAutostart::command() const +{ + return df->desktopGroup().readEntry("Exec", QString()); +} + +void PlasmaAutostart::setCommand(const QString &command) +{ + if (df->desktopGroup().readEntry("Exec", QString()) == command) { + return; + } + + copyIfNeeded(); + df->desktopGroup().writeEntry("Exec", command); +} + +bool PlasmaAutostart::isServiceRegistered(const QString &entryName) +{ + const QString localDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/autostart/"); + return QFile::exists(localDir + entryName + QLatin1String(".desktop")); +} + +// do not specialize the readEntry template - +// http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100911 +static PlasmaAutostart::StartPhase readEntry(const KConfigGroup &group, const char *key, PlasmaAutostart::StartPhase aDefault) +{ + const QByteArray data = group.readEntry(key, QByteArray()); + + if (data.isNull()) { + return aDefault; + } + + if (data == "0" || data == "BaseDesktop") { + return PlasmaAutostart::BaseDesktop; + } else if (data == "1" || data == "DesktopServices") { + return PlasmaAutostart::DesktopServices; + } else if (data == "2" || data == "Applications") { + return PlasmaAutostart::Applications; + } + + return aDefault; +} + +PlasmaAutostart::StartPhase PlasmaAutostart::startPhase() const +{ + return readEntry(df->desktopGroup(), "X-KDE-autostart-phase", Applications); +} + +QStringList PlasmaAutostart::allowedEnvironments() const +{ + return df->desktopGroup().readXdgListEntry("OnlyShowIn"); +} + +QStringList PlasmaAutostart::excludedEnvironments() const +{ + return df->desktopGroup().readXdgListEntry("NotShowIn"); +} + +QString PlasmaAutostart::startAfter() const +{ + return df->desktopGroup().readEntry("X-KDE-autostart-after"); +} diff --git a/plasma/workspace/startkde/plasmaautostart/plasmaautostart.h b/plasma/workspace/startkde/plasmaautostart/plasmaautostart.h new file mode 100644 index 0000000000..88537a0b3a --- /dev/null +++ b/plasma/workspace/startkde/plasmaautostart/plasmaautostart.h @@ -0,0 +1,199 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2006 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KDELIBS_KAUTOSTART_H +#define KDELIBS_KAUTOSTART_H + +#include +#include + +class KDesktopFile; + +/* + * This class was imported from KService at commit c2cedaaeba0a53939c96a1273ab92ed1d6ca7bcf + */ +class PlasmaAutostart : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new KAutostart object that represents the autostart + * service "entryName". If the service already exists in the system + * then the values associated with that service, such as the executable + * command, will be loaded as well. + * + * Note that unless this service is explicitly set to autostart, + * simply creating a KAutostart object will not result in the + * service being autostarted on next log in. + * + * If no such service is already registered and the command to be + * executed on startup is not the same as entryName, then you will want + * to set the associated command with setExec(const QString&) + * @see setExec + * @param entryName the name used to identify the service. If none is + * provided then it uses the name registered with KAboutData. + * @param parent QObject + */ + explicit PlasmaAutostart(const QString &entryName = QString(), QObject *parent = nullptr); + ~PlasmaAutostart(); + + /** + * Flags for each of the conditions that may affect whether or not + * a service actually autostarted on login + * @see Conditions + */ + enum Condition { + /** + * no conditions set + */ + NoConditions = 0x0, + /** + * an executable that is checked for existence by name + */ + CheckCommand = 0x1, + /** + * autostart condition will be checked too (KDE-specific) + * @since 4.3 + */ + CheckCondition = 0x2, + /** + * all necessary conditions will be checked + * @since 4.3 + */ + CheckAll = 0xff, + }; + /** + * Stores a combination of #Condition values. + */ + Q_DECLARE_FLAGS(Conditions, Condition) + + /** + * Enumerates the various autostart phases that occur during start-up. + */ + enum StartPhase { + /** + * the essential desktop services such as panels and window managers + */ + BaseDesktop = 0, + /** + * services that should be available before most interactive + * applications start but that aren't part of the base desktop. + * This would include things such as clipboard managers and + * mouse gesture tools. + */ + DesktopServices = 1, + /** + * everything else that doesn't belong in the above two categories, + * including most system tray applications, system monitors and + * interactive applications + */ + Applications = 2, + }; + + /** + * Sets the given exec to start automatically at login + * @param autostart will register with the autostart facility when true + * and deregister when false + * @see autostarts() + */ + void setAutostarts(bool autostart); + + /** + * Returns whether or not the service represented by entryName in the + * autostart system is set to autostart at login or not + * @param environment if provided the check will be performed as if + * being loaded in that environment + * @param check autostart conditions to check for (see commandToCheck()) + * @see setAutostarts() + */ + bool autostarts(const QString &environment = QString(), Conditions check = NoConditions) const; + + /** + * Returns the associated command for this autostart service + * @see setCommand() + */ + QString command() const; + /** + * Set the associated command for this autostart service + * @see command() + */ + void setCommand(const QString &command); + + /** + * Checks whether or not a service by the given name @p entryName is registered + * with the autostart system. Does not check whether or not it is + * set to actually autostart or not. + * @param entryName the name of the service to check for + */ + static bool isServiceRegistered(const QString &entryName); + + /** + * Returns the autostart phase this service is started in. + * + * Note that this is KDE specific and may not work in other + * environments. + * + * @see StartPhase, setStartPhase() + */ + StartPhase startPhase() const; + + /** + * Returns the list of environments (e.g. "KDE") this service is allowed + * to start in. Use checkAllowedEnvironment() or autostarts() for actual + * checks. + * + * This does not take other autostart conditions + * into account. If any environment is added to the allowed environments + * list, then only those environments will be allowed to + * autoload the service. It is not allowed to specify both allowed and excluded + * environments at the same time. + * @see setAllowedEnvironments() + */ + QStringList allowedEnvironments() const; + + /** + * Returns the list of environments this service is explicitly not + * allowed to start in. Use checkAllowedEnvironment() or autostarts() for actual + * checks. + * + * This does not take other autostart conditions + * such as into account. It is not allowed to specify both allowed and excluded + * environments at the same time. + * @see setExcludedEnvironments() + */ + QStringList excludedEnvironments() const; + + /** + * Returns the name of another service that should be autostarted + * before this one (if that service would be autostarted). + * @internal + */ + QString startAfter() const; + + /** + * Checks whether autostart is allowed in the given environment, + * depending on allowedEnvironments() and excludedEnvironments(). + */ + bool checkAllowedEnvironment(const QString &environment) const; + + /** + * Checks that a given autostart configuration condition is met. + * @param condition: config in the format "rcfile:group:entry:default" + */ + static bool isStartConditionMet(const QString &condition); + +private: + bool checkStartCondition() const; + void copyIfNeeded(); + QString name; + KDesktopFile *df; + bool copyIfNeededChecked; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(PlasmaAutostart::Conditions) +#endif diff --git a/plasma/workspace/startkde/startplasma-wayland.cpp b/plasma/workspace/startkde/startplasma-wayland.cpp new file mode 100644 index 0000000000..afa099f4d3 --- /dev/null +++ b/plasma/workspace/startkde/startplasma-wayland.cpp @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "debug.h" +#include "startplasma.h" +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + createConfigDirectory(); + setupCursor(true); + signal(SIGTERM, sigtermHandler); + + { + KConfig fonts(QStringLiteral("kcmfonts")); + KConfigGroup group = fonts.group("General"); + auto dpiSetting = group.readEntry("forceFontDPIWayland", 96); + auto dpi = dpiSetting == 0 ? 96 : dpiSetting; + qputenv("QT_WAYLAND_FORCE_DPI", QByteArray::number(dpi)); + } + + // Query whether org.freedesktop.locale1 is available. If it is, try to + // set XKB_DEFAULT_{MODEL,LAYOUT,VARIANT,OPTIONS} accordingly. + { + const QString locale1Service = QStringLiteral("org.freedesktop.locale1"); + const QString locale1Path = QStringLiteral("/org/freedesktop/locale1"); + QDBusMessage message = + QDBusMessage::createMethodCall(locale1Service, locale1Path, QStringLiteral("org.freedesktop.DBus.Properties"), QLatin1String("GetAll")); + message << locale1Service; + QDBusMessage resultMessage = QDBusConnection::systemBus().call(message); + if (resultMessage.type() == QDBusMessage::ReplyMessage) { + QVariantMap result; + QDBusArgument dbusArgument = resultMessage.arguments().at(0).value(); + while (!dbusArgument.atEnd()) { + dbusArgument >> result; + } + + auto queryAndSet = [&](const QByteArray &var, const QString &value) { + const auto r = result.value(value).toString(); + if (!r.isEmpty()) + qputenv(var, r.toUtf8()); + }; + + queryAndSet("XKB_DEFAULT_MODEL", QStringLiteral("X11Model")); + queryAndSet("XKB_DEFAULT_LAYOUT", QStringLiteral("X11Layout")); + queryAndSet("XKB_DEFAULT_VARIANT", QStringLiteral("X11Variant")); + queryAndSet("XKB_DEFAULT_OPTIONS", QStringLiteral("X11Options")); + } else { + qCWarning(PLASMA_STARTUP) << "not a reply org.freedesktop.locale1" << resultMessage; + } + } + runEnvironmentScripts(); + + if (!qEnvironmentVariableIsSet("DBUS_SESSION_BUS_ADDRESS")) { + out << "startplasmacompositor: Could not start D-Bus. Can you call qdbus?\n"; + return 1; + } + setupPlasmaEnvironment(); + runStartupConfig(); + qputenv("PLASMA_USE_QT_SCALING", "1"); + qputenv("GDK_SCALE", "1"); + qputenv("GDK_DPI_SCALE", "1"); + + qputenv("XDG_SESSION_TYPE", "wayland"); + + auto oldSystemdEnvironment = getSystemdEnvironment(); + if (!syncDBusEnvironment()) { + out << "Could not sync environment to dbus.\n"; + return 1; + } + + // We import systemd environment after we sync the dbus environment here. + // Otherwise it may leads to some unwanted order of applying environment + // variables (e.g. LANG and LC_*) + importSystemdEnvrionment(); + + if (!startPlasmaSession(true)) + return 4; + + // Anything after here is logout + // It is not called after shutdown/restart + waitForKonqi(); + out << "startplasma-wayland: Shutting down...\n"; + + // Keep for KF5; remove in KF6 (KInit will be gone then) + runSync(QStringLiteral("kdeinit5_shutdown"), {}); + + out << "startplasmacompositor: Shutting down...\n"; + cleanupPlasmaEnvironment(oldSystemdEnvironment); + out << "startplasmacompositor: Done.\n"; + + return 0; +} diff --git a/plasma/workspace/startkde/startplasma-x11.cpp b/plasma/workspace/startkde/startplasma-x11.cpp new file mode 100644 index 0000000000..817de35113 --- /dev/null +++ b/plasma/workspace/startkde/startplasma-x11.cpp @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "startplasma.h" + +#include +#include +#include +#include + +void sighupHandler(int) +{ + out << "GOT SIGHUP\n"; +} + +int main(int argc, char **argv) +{ + // When the X server dies we get a HUP signal from xinit. We must ignore it + // because we still need to do some cleanup. + signal(SIGHUP, sighupHandler); + + QCoreApplication app(argc, argv); + + // Check if a Plasma session already is running and whether it's possible to connect to X + switch (kCheckRunning()) { + case NoX11: + out << "$DISPLAY is not set or cannot connect to the X server.\n"; + return 1; + case PlasmaRunning: + messageBox(QStringLiteral("Plasma seems to be already running on this display.\n")); + return 1; + case NoPlasmaRunning: + break; + } + + createConfigDirectory(); + runStartupConfig(); + + // Do not sync any of this section with the wayland versions as there scale factors are + // sent properly over wl_output + + { + KConfig cfg(QStringLiteral("kdeglobals")); + + KConfigGroup kscreenGroup = cfg.group("KScreen"); + const auto screenScaleFactors = kscreenGroup.readEntry("ScreenScaleFactors", QByteArray()); + if (!screenScaleFactors.isEmpty()) { + qputenv("QT_SCREEN_SCALE_FACTORS", screenScaleFactors); + qreal scaleFactor = qFloor(kscreenGroup.readEntry("ScaleFactor", 1.0)); + if (scaleFactor > 1) { + qputenv("GDK_SCALE", QByteArray::number(scaleFactor, 'g', 0)); + qputenv("GDK_DPI_SCALE", QByteArray::number(1.0 / scaleFactor, 'g', 3)); + } + } + } + + setupCursor(false); + QScopedPointer ksplash(setupKSplash()); + + runEnvironmentScripts(); + + out << "startkde: Starting up...\n"; + + setupPlasmaEnvironment(); + setupX11(); + + auto oldSystemdEnvironment = getSystemdEnvironment(); + if (!syncDBusEnvironment()) { + // Startup error + messageBox(QStringLiteral("Could not sync environment to dbus.\n")); + return 1; + } + + // We import systemd environment after we sync the dbus environment here. + // Otherwise it may leads to some unwanted order of applying environment + // variables (e.g. LANG and LC_*) + importSystemdEnvrionment(); + + if (!startPlasmaSession(false)) + return 1; + + // Anything after here is logout + // It is not called after shutdown/restart + waitForKonqi(); + + out << "startkde: Shutting down...\n"; + + // Keep for KF5; remove in KF6 (KInit will be gone then) + runSync(QStringLiteral("kdeinit5_shutdown"), {}); + + cleanupPlasmaEnvironment(oldSystemdEnvironment); + + out << "startkde: Done.\n"; + + return 0; +} diff --git a/plasma/workspace/startkde/startplasma.cpp b/plasma/workspace/startkde/startplasma.cpp new file mode 100644 index 0000000000..0f4879567b --- /dev/null +++ b/plasma/workspace/startkde/startplasma.cpp @@ -0,0 +1,791 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include + +#include "startplasma.h" + +#include "../config-workspace.h" +#include "../kcms/lookandfeel/lookandfeelmanager.h" +#include "debug.h" + +QTextStream out(stderr); + +void sigtermHandler(int signalNumber) +{ + Q_UNUSED(signalNumber) + if (QCoreApplication::instance()) { + QCoreApplication::instance()->exit(-1); + } +} + +void messageBox(const QString &text) +{ + out << text; + runSync(QStringLiteral("xmessage"), {QStringLiteral("-geometry"), QStringLiteral("500x100"), text}); +} + +QStringList allServices(const QLatin1String &prefix) +{ + QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface(); + const QStringList services = bus->registeredServiceNames(); + QMap servicesWithAliases; + + for (const QString &serviceName : services) { + QDBusReply reply = bus->serviceOwner(serviceName); + QString owner = reply; + if (owner.isEmpty()) + owner = serviceName; + servicesWithAliases[owner].append(serviceName); + } + + QStringList names; + for (auto it = servicesWithAliases.constBegin(); it != servicesWithAliases.constEnd(); ++it) { + if (it.value().startsWith(prefix)) + names << it.value(); + } + names.removeDuplicates(); + names.sort(); + return names; +} + +void gentleTermination(QProcess *p) +{ + if (p->state() != QProcess::Running) { + return; + } + + p->close(); + p->terminate(); + + // Wait longer for a session than a greeter + if (!p->waitForFinished(5000)) { + p->kill(); + if (!p->waitForFinished(5000)) { + qCWarning(PLASMA_STARTUP) << "Could not fully finish the process" << p->program(); + } + } +} + +int runSync(const QString &program, const QStringList &args, const QStringList &env) +{ + QProcess p; + if (!env.isEmpty()) + p.setEnvironment(QProcess::systemEnvironment() << env); + p.setProcessChannelMode(QProcess::ForwardedChannels); + p.start(program, args); + + QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &p, [&p] { + gentleTermination(&p); + }); + // qCDebug(PLASMA_STARTUP) << "started..." << program << args; + p.waitForFinished(-1); + if (p.exitCode()) { + qCWarning(PLASMA_STARTUP) << program << args << "exited with code" << p.exitCode(); + } + return p.exitCode(); +} + +bool isShellVariable(const QByteArray &name) +{ + return name == "_" || name.startsWith("SHLVL"); +} + +bool isSessionVariable(const QByteArray &name) +{ + // Check is variable is specific to session. + return name == "DISPLAY" || name == "XAUTHORITY" || // + name == "WAYLAND_DISPLAY" || name == "WAYLAND_SOCKET" || // + name.startsWith("XDG_"); +} + +void setEnvironmentVariable(const QByteArray &name, const QByteArray &value) +{ + if (qgetenv(name) != value) { + qputenv(name, value); + } +} + +void sourceFiles(const QStringList &files) +{ + QStringList filteredFiles; + std::copy_if(files.begin(), files.end(), std::back_inserter(filteredFiles), [](const QString &i) { + return QFileInfo(i).isReadable(); + }); + + if (filteredFiles.isEmpty()) + return; + + filteredFiles.prepend(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR "/plasma-sourceenv.sh")); + + QProcess p; + p.start(QStringLiteral("/bin/sh"), filteredFiles); + p.waitForFinished(-1); + + const auto fullEnv = p.readAllStandardOutput(); + auto envs = fullEnv.split('\0'); + + for (auto &env : envs) { + const int idx = env.indexOf('='); + if (Q_UNLIKELY(idx <= 0)) { + continue; + } + + const auto name = env.left(idx); + if (isShellVariable(name)) { + continue; + } + setEnvironmentVariable(name, env.mid(idx + 1)); + } +} + +void createConfigDirectory() +{ + const QString configDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + if (!QDir().mkpath(configDir)) + out << "Could not create config directory XDG_CONFIG_HOME: " << configDir << '\n'; +} + +void runStartupConfig() +{ + // export LC_* variables set by kcmshell5 formats into environment + // so it can be picked up by QLocale and friends. + KConfig config(QStringLiteral("plasma-localerc")); + KConfigGroup formatsConfig = KConfigGroup(&config, "Formats"); + + const auto lcValues = {"LANG", "LC_NUMERIC", "LC_TIME", "LC_MONETARY", "LC_MEASUREMENT", "LC_COLLATE", "LC_CTYPE"}; + for (auto lc : lcValues) { + const QString value = formatsConfig.readEntry(lc, QString()); + if (!value.isEmpty()) { + qputenv(lc, value.toUtf8()); + } + } + + KConfigGroup languageConfig = KConfigGroup(&config, "Translations"); + const QString value = languageConfig.readEntry("LANGUAGE", QString()); + if (!value.isEmpty()) { + qputenv("LANGUAGE", value.toUtf8()); + } + + if (!formatsConfig.hasKey("LANG") && !qEnvironmentVariableIsEmpty("LANG")) { + formatsConfig.writeEntry("LANG", qgetenv("LANG")); + formatsConfig.sync(); + } +} + +void setupCursor(bool wayland) +{ + const KConfig cfg(QStringLiteral("kcminputrc")); + const KConfigGroup inputCfg = cfg.group("Mouse"); + + const auto kcminputrc_mouse_cursorsize = inputCfg.readEntry("cursorSize", 24); + const auto kcminputrc_mouse_cursortheme = inputCfg.readEntry("cursorTheme", QStringLiteral("breeze_cursors")); + if (!kcminputrc_mouse_cursortheme.isEmpty()) { +#ifdef XCURSOR_PATH + QByteArray path(XCURSOR_PATH); + path.replace("$XCURSOR_PATH", qgetenv("XCURSOR_PATH")); + qputenv("XCURSOR_PATH", path); +#endif + } + + // TODO: consider linking directly + const int applyMouseStatus = + wayland ? 0 : runSync(QStringLiteral("kapplymousetheme"), {kcminputrc_mouse_cursortheme, QString::number(kcminputrc_mouse_cursorsize)}); + if (applyMouseStatus == 10) { + qputenv("XCURSOR_THEME", "breeze_cursors"); + } else if (!kcminputrc_mouse_cursortheme.isEmpty()) { + qputenv("XCURSOR_THEME", kcminputrc_mouse_cursortheme.toUtf8()); + } + qputenv("XCURSOR_SIZE", QByteArray::number(kcminputrc_mouse_cursorsize)); +} + +std::optional getSystemdEnvironment() +{ + QStringList list; + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + msg << QStringLiteral("org.freedesktop.systemd1.Manager") << QStringLiteral("Environment"); + auto reply = QDBusConnection::sessionBus().call(msg); + if (reply.type() == QDBusMessage::ErrorMessage) { + return std::nullopt; + } + + // Make sure the returned type is correct. + auto arguments = reply.arguments(); + if (arguments.isEmpty() || arguments[0].userType() != qMetaTypeId()) { + return std::nullopt; + } + auto variant = qdbus_cast(arguments[0]); + if (variant.type() != QVariant::StringList) { + return std::nullopt; + } + + const auto assignmentList = variant.toStringList(); + QProcessEnvironment ret; + for (auto &env : assignmentList) { + const int idx = env.indexOf(QLatin1Char('=')); + if (Q_LIKELY(idx > 0)) { + ret.insert(env.left(idx), env.mid(idx + 1)); + } + } + + return ret; +} + +// Import systemd user environment. +// +// Systemd read ~/.config/environment.d which applies to all systemd user unit. +// But it won't work if plasma is not started by systemd. +void importSystemdEnvrionment() +{ + const auto environment = getSystemdEnvironment(); + if (!environment) { + return; + } + + for (auto &nameStr : environment.value().keys()) { + const auto name = nameStr.toLocal8Bit(); + if (!isShellVariable(name) && !isSessionVariable(name)) { + setEnvironmentVariable(name, environment.value().value(nameStr).toLocal8Bit()); + } + } +} + +// Source scripts found in /plasma-workspace/env/*.sh +// (where correspond to the system and user's configuration +// directory. +// +// Scripts are sourced in reverse order of priority of their directory, as defined +// by `QStandardPaths::standardLocations`. This ensures that high-priority scripts +// (such as those in the user's home directory) are sourced last and take precedence +// over lower-priority scripts (such as system defaults). Scripts in the same +// directory are sourced in lexical order of their filename. +// +// This is where you can define environment variables that will be available to +// all KDE programs, so this is where you can run agents using e.g. eval `ssh-agent` +// or eval `gpg-agent --daemon`. +// Note: if you do that, you should also put "ssh-agent -k" as a shutdown script +// +// (see end of this file). +// For anything else (that doesn't set env vars, or that needs a window manager), +// better use the Autostart folder. + +void runEnvironmentScripts() +{ + QStringList scripts; + auto locations = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation); + + //`standardLocations()` returns locations sorted by "order of priority". We iterate in reverse + // order so that high-priority scripts are sourced last and their modifications take precedence. + for (auto loc = locations.crbegin(); loc != locations.crend(); loc++) { + QDir dir(*loc); + if (!dir.cd(QStringLiteral("./plasma-workspace/env"))) { + // Skip location if plasma-workspace/env subdirectory does not exist + continue; + } + const auto dirScripts = dir.entryInfoList({QStringLiteral("*.sh")}, QDir::Files, QDir::Name); + for (const auto &script : dirScripts) { + scripts << script.absoluteFilePath(); + } + } + sourceFiles(scripts); +} + +// Mark that full KDE session is running (e.g. Konqueror preloading works only +// with full KDE running). The KDE_FULL_SESSION property can be detected by +// any X client connected to the same X session, even if not launched +// directly from the KDE session but e.g. using "ssh -X", kdesu. $KDE_FULL_SESSION +// however guarantees that the application is launched in the same environment +// like the KDE session and that e.g. KDE utilities/libraries are available. +// KDE_FULL_SESSION property is also only available since KDE 3.5.5. +// The matching tests are: +// For $KDE_FULL_SESSION: +// if test -n "$KDE_FULL_SESSION"; then ... whatever +// For KDE_FULL_SESSION property (on X11): +// xprop -root | grep "^KDE_FULL_SESSION" >/dev/null 2>/dev/null +// if test $? -eq 0; then ... whatever +// +// Additionally there is $KDE_SESSION_UID with the uid +// of the user running the KDE session. It should be rarely needed (e.g. +// after sudo to prevent desktop-wide functionality in the new user's kded). +// +// Since KDE4 there is also KDE_SESSION_VERSION, containing the major version number. +// + +void setupPlasmaEnvironment() +{ + // Manually disable auto scaling because we are scaling above + // otherwise apps that manually opt in for high DPI get auto scaled by the developer AND manually scaled by us + qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0"); + + qputenv("KDE_FULL_SESSION", "true"); + qputenv("KDE_SESSION_VERSION", "5"); + qputenv("KDE_SESSION_UID", QByteArray::number(getuid())); + qputenv("XDG_CURRENT_DESKTOP", "KDE"); + + qputenv("KDE_APPLICATIONS_AS_SCOPE", "1"); + + // Add kdedefaults dir to allow config defaults overriding from a writable location + QByteArray currentConfigDirs = qgetenv("XDG_CONFIG_DIRS"); + if (currentConfigDirs.isEmpty()) { + currentConfigDirs = "/etc/xdg"; + } + const QString extraConfigDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kdedefaults"); + QDir().mkpath(extraConfigDir); + qputenv("XDG_CONFIG_DIRS", QFile::encodeName(extraConfigDir) + ':' + currentConfigDirs); + + const KConfig globals; + const QString currentLnf = KConfigGroup(&globals, QStringLiteral("KDE")).readEntry("LookAndFeelPackage", QStringLiteral("org.kde.breeze.desktop")); + QFile activeLnf(extraConfigDir + QLatin1String("/package")); + activeLnf.open(QIODevice::ReadOnly); + if (activeLnf.readLine() != currentLnf.toUtf8()) { + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), currentLnf); + LookAndFeelManager lnfManager; + lnfManager.setMode(LookAndFeelManager::Mode::Defaults); + lnfManager.save(package, KPackage::Package()); + } + // check if colors changed, if so apply them and discard palsma cache + { + LookAndFeelManager lnfManager; + lnfManager.setMode(LookAndFeelManager::Mode::Apply); + KConfig globals(QStringLiteral("kdeglobals")); // Reload the config + KConfigGroup generalGroup(&globals, QStringLiteral("General")); + const QString colorScheme = generalGroup.readEntry("ColorScheme", QStringLiteral("BreezeLight")); + QString path = lnfManager.colorSchemeFile(colorScheme); + + if (!path.isEmpty()) { + QFile f(path); + QCryptographicHash hash(QCryptographicHash::Sha1); + if (f.open(QFile::ReadOnly) && hash.addData(&f)) { + const QString fileHash = QString::fromUtf8(hash.result().toHex()); + if (fileHash != generalGroup.readEntry("ColorSchemeHash", QString())) { + lnfManager.setColors(colorScheme, path); + generalGroup.writeEntry("ColorSchemeHash", fileHash); + generalGroup.sync(); + const QString svgCache = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + QStringLiteral("plasma-svgelements"); + if (!svgCache.isEmpty()) { + QFile::remove(svgCache); + } + } + } + } + } +} + +void setupX11() +{ + // Set a left cursor instead of the standard X11 "X" cursor, since I've heard + // from some users that they're confused and don't know what to do. This is + // especially necessary on slow machines, where starting KDE takes one or two + // minutes until anything appears on the screen. + // + // If the user has overwritten fonts, the cursor font may be different now + // so don't move this up. + + runSync(QStringLiteral("xsetroot"), {QStringLiteral("-cursor_name"), QStringLiteral("left_ptr")}); +} + +void cleanupPlasmaEnvironment(const std::optional &oldSystemdEnvironment) +{ + qunsetenv("KDE_FULL_SESSION"); + qunsetenv("KDE_SESSION_VERSION"); + qunsetenv("KDE_SESSION_UID"); + + if (!oldSystemdEnvironment) { + return; + } + + auto currentEnv = getSystemdEnvironment(); + if (!currentEnv) { + return; + } + + // According to systemd documentation: + // If a variable is listed in both, the variable is set after this method returns, i.e. the set list overrides the unset list. + // So this will effectively restore the state to the values in oldSystemdEnvironment. + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("UnsetAndSetEnvironment")); + message.setArguments({currentEnv.value().keys(), oldSystemdEnvironment.value().toStringList()}); + + // The session program gonna quit soon, ensure the message is flushed. + auto reply = QDBusConnection::sessionBus().asyncCall(message); + reply.waitForFinished(); +} + +// Drop session-specific variables from the systemd environment. +// Those can be leftovers from previous sessions, which can interfere with the session +// we want to start now, e.g. $DISPLAY might break kwin_wayland. +static void dropSessionVarsFromSystemdEnvironment() +{ + const auto environment = getSystemdEnvironment(); + if (!environment) { + return; + } + + QStringList varsToDrop; + for (auto &nameStr : environment.value().keys()) { + // If it's set in this process, it'll be overwritten by the following UpdateLaunchEnvJob + const auto name = nameStr.toLocal8Bit(); + if (!qEnvironmentVariableIsSet(name) && isSessionVariable(name)) { + varsToDrop.append(nameStr); + } + } + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("UnsetEnvironment")); + msg << varsToDrop; + auto reply = QDBusConnection::sessionBus().call(msg); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCWarning(PLASMA_STARTUP) << "Failed to unset systemd environment variables:" << reply.errorName() << reply.errorMessage(); + } +} + +// kwin_wayland can possibly also start dbus-activated services which need env variables. +// In that case, the update in startplasma might be too late. +bool syncDBusEnvironment() +{ + dropSessionVarsFromSystemdEnvironment(); + + // At this point all environment variables are set, let's send it to the DBus session server to update the activation environment + auto job = new UpdateLaunchEnvJob(QProcessEnvironment::systemEnvironment()); + return job->exec(); +} + +static bool desktopLockedAtStart = false; + +QProcess *setupKSplash() +{ + const auto dlstr = qgetenv("DESKTOP_LOCKED"); + desktopLockedAtStart = dlstr == "true" || dlstr == "1"; + qunsetenv("DESKTOP_LOCKED"); // Don't want it in the environment + + QProcess *p = nullptr; + if (!desktopLockedAtStart) { + const KConfig cfg(QStringLiteral("ksplashrc")); + // the splashscreen and progress indicator + KConfigGroup ksplashCfg = cfg.group("KSplash"); + if (ksplashCfg.readEntry("Engine", QStringLiteral("KSplashQML")) == QLatin1String("KSplashQML")) { + p = new QProcess; + p->setProcessChannelMode(QProcess::ForwardedChannels); + p->start(QStringLiteral("ksplashqml"), {ksplashCfg.readEntry("Theme", QStringLiteral("Breeze"))}); + } + } + return p; +} + +// If something went on an endless restart crash loop it will get blacklisted, as this is a clean login we will want to reset those counters +// This is independent of whether we use the Plasma systemd boot +void resetSystemdFailedUnits() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("ResetFailed")); + QDBusConnection::sessionBus().call(message); +} + +bool hasSystemdService(const QString &serviceName) +{ + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("ListUnitFilesByPatterns")); + msg << QStringList({QStringLiteral("enabled"), + QStringLiteral("static"), + QStringLiteral("linked"), + QStringLiteral("linked-runtime")}); + msg << QStringList({serviceName}); + QDBusReply>> reply = QDBusConnection::sessionBus().call(msg); + if (!reply.isValid()) { + return false; + } + // if we have a service returned then it must have found it + return !reply.value().isEmpty(); +} + +bool useSystemdBoot() +{ + auto config = KSharedConfig::openConfig(QStringLiteral("startkderc"), KConfig::NoGlobals); + const QString configValue = config->group(QStringLiteral("General")).readEntry("systemdBoot", QStringLiteral("true")).toLower(); + + if (configValue == QLatin1String("false")) { + return false; + } + + if (configValue == QLatin1String("force")) { + qInfo() << "Systemd boot forced"; + return true; + } + + if (!hasSystemdService(QStringLiteral("plasma-workspace.target"))) { + return false; + } + + // xdg-desktop-autostart.target is shipped with an systemd 246 and provides a generator + // for creating units out of existing autostart files + // only enable our systemd boot if that exists, unless the user has forced the systemd boot above + return hasSystemdService(QStringLiteral("xdg-desktop-autostart.target")); +} + +void startKSplashViaSystemd() +{ + const KConfig cfg(QStringLiteral("ksplashrc")); + // the splashscreen and progress indicator + KConfigGroup ksplashCfg = cfg.group("KSplash"); + if (ksplashCfg.readEntry("Engine", QStringLiteral("KSplashQML")) == QLatin1String("KSplashQML")) { + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("StartUnit")); + msg << QStringLiteral("plasma-ksplash.service") << QStringLiteral("fail"); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + } +} + +static void migrateUserScriptsAutostart() +{ + QDir configLocation(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)); + QDir autostartScriptsLocation(configLocation.filePath(QStringLiteral("autostart-scripts"))); + if (!autostartScriptsLocation.exists()) { + return; + } + const QDir autostartScriptsMovedLocation(configLocation.filePath(QStringLiteral("old-autostart-scripts"))); + const auto entries = autostartScriptsLocation.entryInfoList(QDir::Files); + for (const auto &info : entries) { + const auto scriptName = info.fileName(); + const auto scriptPath = info.absoluteFilePath(); + const auto scriptMovedPath = autostartScriptsMovedLocation.filePath(scriptName); + + // Don't migrate backup files + if (scriptName.endsWith(QLatin1Char('~')) || scriptName.endsWith(QLatin1String(".bak")) + || (scriptName[0] == QLatin1Char('%') && scriptName.endsWith(QLatin1Char('%'))) + || (scriptName[0] == QLatin1Char('#') && scriptName.endsWith(QLatin1Char('#')))) { + qCDebug(PLASMA_STARTUP) << "Not migrating backup autostart script" << scriptName; + continue; + } + + // Migrate autostart script to a standard .desktop autostart file + AutostartScriptDesktopFile desktopFile(scriptName, info.isSymLink() ? info.symLinkTarget() : scriptMovedPath); + qCInfo(PLASMA_STARTUP) << "Migrated legacy autostart script" << scriptPath << "to" << desktopFile.fileName(); + + if (info.isSymLink() && QFile::remove(scriptPath)) { + qCInfo(PLASMA_STARTUP) << "Removed legacy autostart script" << scriptPath << "that pointed to" << info.symLinkTarget(); + } + } + // Delete or rename autostart-scripts to old-autostart-scripts to avoid running the migration again + if (autostartScriptsLocation.entryInfoList(QDir::Files).empty()) { + autostartScriptsLocation.removeRecursively(); + } else { + configLocation.rename(autostartScriptsLocation.dirName(), autostartScriptsMovedLocation.dirName()); + } + // Reload systemd so that the XDG autostart generator is run again to pick up the new .desktop files + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("Reload")); + QDBusConnection::sessionBus().call(message); +} + +bool startPlasmaSession(bool wayland) +{ + resetSystemdFailedUnits(); + OrgKdeKSplashInterface iface(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QDBusConnection::sessionBus()); + iface.setStage(QStringLiteral("startPlasma")); + // finally, give the session control to the session manager + // see kdebase/ksmserver for the description of the rest of the startup sequence + // if the KDEWM environment variable has been set, then it will be used as KDE's + // window manager instead of kwin. + // if KDEWM is not set, ksmserver will ensure kwin is started. + // kwrapper5 is used to reduce startup time and memory usage + // kwrapper5 does not return useful error codes such as the exit code of ksmserver. + // We only check for 255 which means that the ksmserver process could not be + // started, any problems thereafter, e.g. ksmserver failing to initialize, + // will remain undetected. + // If the session should be locked from the start (locked autologin), + // lock now and do the rest of the KDE startup underneath the locker. + + bool rc = true; + QEventLoop e; + + QDBusServiceWatcher serviceWatcher; + serviceWatcher.setConnection(QDBusConnection::sessionBus()); + + // We want to exit when both ksmserver and plasma-session-shutdown have finished + // This also closes if ksmserver crashes unexpectedly, as in those cases plasma-shutdown is not running + if (wayland) { + serviceWatcher.addWatchedService(QStringLiteral("org.kde.KWinWrapper")); + } else { + serviceWatcher.addWatchedService(QStringLiteral("org.kde.ksmserver")); + } + serviceWatcher.addWatchedService(QStringLiteral("org.kde.Shutdown")); + serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + + QObject::connect(&serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, [&]() { + const QStringList watchedServices = serviceWatcher.watchedServices(); + bool plasmaSessionRunning = std::any_of(watchedServices.constBegin(), watchedServices.constEnd(), [](const QString &service) { + return QDBusConnection::sessionBus().interface()->isServiceRegistered(service); + }); + if (!plasmaSessionRunning) { + e.quit(); + } + }); + + // Create .desktop files for the scripts in .config/autostart-scripts + migrateUserScriptsAutostart(); + + QScopedPointer startPlasmaSession; + if (!useSystemdBoot()) { + startPlasmaSession.reset(new QProcess); + qCDebug(PLASMA_STARTUP) << "Using classic boot"; + + QStringList plasmaSessionOptions; + if (wayland) { + plasmaSessionOptions << QStringLiteral("--no-lockscreen"); + } else { + if (desktopLockedAtStart) { + plasmaSessionOptions << QStringLiteral("--lockscreen"); + } + } + + startPlasmaSession->setProcessChannelMode(QProcess::ForwardedChannels); + QObject::connect(startPlasmaSession.data(), &QProcess::finished, &e, [&rc](int exitCode, QProcess::ExitStatus) { + if (exitCode == 255) { + // Startup error + messageBox(QStringLiteral("startkde: Could not start plasma_session. Check your installation.\n")); + rc = false; + } + }); + + startPlasmaSession->start(QStringLiteral(CMAKE_INSTALL_FULL_BINDIR "/plasma_session"), plasmaSessionOptions); + } else { + qCDebug(PLASMA_STARTUP) << "Using systemd boot"; + const QString platform = wayland ? QStringLiteral("wayland") : QStringLiteral("x11"); + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QStringLiteral("StartUnit")); + msg << QStringLiteral("plasma-workspace-%1.target").arg(platform) << QStringLiteral("fail"); + QDBusReply reply = QDBusConnection::sessionBus().call(msg); + if (!reply.isValid()) { + qCWarning(PLASMA_STARTUP) << "Could not start systemd managed Plasma session:" << reply.error().name() << reply.error().message(); + messageBox(QStringLiteral("startkde: Could not start Plasma session.\n")); + rc = false; + } else { + playStartupSound(&e); + } + if (wayland) { + startKSplashViaSystemd(); + } + } + if (rc) { + QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &e, &QEventLoop::quit); + e.exec(); + } + return rc; +} + +void waitForKonqi() +{ + const KConfig cfg(QStringLiteral("startkderc")); + const KConfigGroup grp = cfg.group("WaitForDrKonqi"); + bool wait_drkonqi = grp.readEntry("Enabled", true); + if (wait_drkonqi) { + // wait for remaining drkonqi instances with timeout (in seconds) + const int wait_drkonqi_timeout = grp.readEntry("Timeout", 900) * 1000; + QElapsedTimer wait_drkonqi_counter; + wait_drkonqi_counter.start(); + QStringList services = allServices(QLatin1String("org.kde.drkonqi-")); + while (!services.isEmpty()) { + sleep(5); + services = allServices(QLatin1String("org.kde.drkonqi-")); + if (wait_drkonqi_counter.elapsed() >= wait_drkonqi_timeout) { + // ask remaining drkonqis to die in a graceful way + for (const auto &service : qAsConst(services)) { + QDBusInterface iface(service, QStringLiteral("/MainApplication")); + iface.call(QStringLiteral("quit")); + } + break; + } + } + } +} + +void playStartupSound(QObject *parent) +{ + KNotifyConfig notifyConfig(QStringLiteral("plasma_workspace"), QList>(), QStringLiteral("startkde")); + const QString action = notifyConfig.readEntry(QStringLiteral("Action")); + if (action.isEmpty() || !action.split(QLatin1Char('|')).contains(QLatin1String("Sound"))) { + // no startup sound configured + return; + } + Phonon::AudioOutput *audioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, parent); + + QString soundFilename = notifyConfig.readEntry(QStringLiteral("Sound")); + if (soundFilename.isEmpty()) { + qCWarning(PLASMA_STARTUP) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification"; + audioOutput->deleteLater(); + return; + } + + QUrl soundURL; + const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const QString &dataLocation : dataLocations) { + soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile); + if (soundURL.isLocalFile() && QFile::exists(soundURL.toLocalFile())) { + break; + } else if (!soundURL.isLocalFile() && soundURL.isValid()) { + break; + } + soundURL.clear(); + } + if (soundURL.isEmpty()) { + qCWarning(PLASMA_STARTUP) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification"; + audioOutput->deleteLater(); + return; + } + + Phonon::MediaObject *mediaObject = new Phonon::MediaObject(parent); + Phonon::createPath(mediaObject, audioOutput); + QObject::connect(mediaObject, &Phonon::MediaObject::finished, audioOutput, &QObject::deleteLater); + QObject::connect(mediaObject, &Phonon::MediaObject::finished, mediaObject, &QObject::deleteLater); + + mediaObject->setCurrentSource(soundURL); + mediaObject->play(); +} diff --git a/plasma/workspace/startkde/startplasma.h b/plasma/workspace/startkde/startplasma.h new file mode 100644 index 0000000000..79b3b402cd --- /dev/null +++ b/plasma/workspace/startkde/startplasma.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "config-startplasma.h" +#include "kcheckrunning/kcheckrunning.h" +#include +#include + +extern QTextStream out; + +void sigtermHandler(int signalNumber); +QStringList allServices(const QLatin1String &prefix); +int runSync(const QString &program, const QStringList &args, const QStringList &env = {}); +void sourceFiles(const QStringList &files); +void messageBox(const QString &text); + +void createConfigDirectory(); +void runStartupConfig(); +void setupCursor(bool wayland); +std::optional getSystemdEnvironment(); +void importSystemdEnvrionment(); +void runEnvironmentScripts(); +void setupPlasmaEnvironment(); +void cleanupPlasmaEnvironment(const std::optional &oldSystemdEnvironment); +bool syncDBusEnvironment(); +void setupFontDpi(); +QProcess *setupKSplash(); +void setupX11(); + +bool startPlasmaSession(bool wayland); + +void waitForKonqi(); + +void playStartupSound(QObject *parent); + +void gentleTermination(QProcess *process); + +struct KillBeforeDeleter { + static inline void cleanup(QProcess *pointer) + { + if (pointer) { + gentleTermination(pointer); + } + delete pointer; + } +}; diff --git a/plasma/workspace/startkde/systemd/CMakeLists.txt b/plasma/workspace/startkde/systemd/CMakeLists.txt new file mode 100644 index 0000000000..419c184d40 --- /dev/null +++ b/plasma/workspace/startkde/systemd/CMakeLists.txt @@ -0,0 +1,11 @@ +ecm_install_configured_files(INPUT plasma-ksplash-ready.service.in @ONLY + DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) + +install(FILES plasma-core.target DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) +install(FILES plasma-workspace.target DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) +install(FILES plasma-workspace-wayland.target DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) +install(FILES plasma-workspace-x11.target DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) + +add_executable(kde-systemd-start-condition kde-systemd-start-condition.cpp) +target_link_libraries(kde-systemd-start-condition PUBLIC KF5::ConfigCore KF5::Service PlasmaAutostart) +install(TARGETS kde-systemd-start-condition DESTINATION ${KDE_INSTALL_BINDIR}) diff --git a/plasma/workspace/startkde/systemd/README.md b/plasma/workspace/startkde/systemd/README.md new file mode 100644 index 0000000000..41dce4b316 --- /dev/null +++ b/plasma/workspace/startkde/systemd/README.md @@ -0,0 +1,30 @@ +# Startup + +Startup can be summarised as being: + +plasma-core.target +plasma-workspace@.target +graphical-session.target + +plasma-workspace@ is the target explicitly activated. + +## X11 and wayland + +plasma-workspace@ is a template file that ends with x11 or wayland. That will then require the correct kwin_ +startup order can be different between the two. + +## Wants & Order +Note that in systemd dependencies (wants/wantedby) counter-intuitively do not determine order. + +plasma-workspace wants graphical-session, meaning it will make it something started by it, but it also explicitly comes before graphical-session. + +The order of events is: +plasma-core does anything that adjusts environment variables +plasma-workspace@ starts all runtime services +graphical-session is at a point where everything including runtime services are up + +## Adding a new service + +If it should only be used on plasma it should be wanted by plasma-core or plasma-workspace@. + +That service is responsible for setting "After=plasma-core.target" if we need envs set up. diff --git a/plasma/workspace/startkde/systemd/kde-systemd-start-condition.cpp b/plasma/workspace/startkde/systemd/kde-systemd-start-condition.cpp new file mode 100644 index 0000000000..ddd179039d --- /dev/null +++ b/plasma/workspace/startkde/systemd/kde-systemd-start-condition.cpp @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2020 Henri Chain + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "../plasmaautostart/plasmaautostart.h" +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + // If invoked on gnome we should always return success + // this is because a desktop file that has X-KDE-AutostartCondition + // probably has an X-Gnome- equivalent and we only want one to run + // this would match non systemd behaviour + if (!qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(QLatin1Char(':')).contains("kde", Qt::CaseInsensitive)) { + return 0; + } + QCommandLineParser parser; + parser.setApplicationDescription(QStringLiteral("Checks start condition for a KDE systemd service")); + parser.addHelpOption(); + QCommandLineOption option{QStringLiteral("condition"), + QStringLiteral("start condition, in the format 'rcfile:group:entry:default'."), + QStringLiteral("condition")}; + parser.addOption(option); + parser.process(app); + + if (!parser.isSet(option)) { + parser.showHelp(255); + } + + if (PlasmaAutostart::isStartConditionMet(parser.value(option))) { + return 0; + } else { + return 1; + } +} diff --git a/plasma/workspace/startkde/systemd/plasma-core.target b/plasma/workspace/startkde/systemd/plasma-core.target new file mode 100644 index 0000000000..4cc4cb2185 --- /dev/null +++ b/plasma/workspace/startkde/systemd/plasma-core.target @@ -0,0 +1,7 @@ +[Unit] +Description=KDE Plasma Workspace Core +Wants=plasma-plasmashell.service plasma-kcminit.service plasma-kded.service plasma-kcminit-phase1.service graphical-session-pre.target +Requires=plasma-ksmserver.service +After=graphical-session-pre.target plasma-kwin_wayland.service +RefuseManualStart=yes +StopWhenUnneeded=true diff --git a/plasma/workspace/startkde/systemd/plasma-ksplash-ready.service.in b/plasma/workspace/startkde/systemd/plasma-ksplash-ready.service.in new file mode 100644 index 0000000000..3f6744f378 --- /dev/null +++ b/plasma/workspace/startkde/systemd/plasma-ksplash-ready.service.in @@ -0,0 +1,10 @@ +[Unit] +Description=KSplash "ready" Stage +Wants=plasma-core.target +After=plasma-core.target +PartOf=graphical-session.target + +[Service] +Type=oneshot +ExecStart=-@QtBinariesDir@/qdbus org.kde.KSplash /KSplash org.kde.KSplash.setStage ready +Slice=session.slice diff --git a/plasma/workspace/startkde/systemd/plasma-workspace-wayland.target b/plasma/workspace/startkde/systemd/plasma-workspace-wayland.target new file mode 100644 index 0000000000..d960413fe5 --- /dev/null +++ b/plasma/workspace/startkde/systemd/plasma-workspace-wayland.target @@ -0,0 +1,4 @@ +[Unit] +Requires=plasma-workspace.target +Requires=plasma-kwin_wayland.service +BindsTo=plasma-kwin_wayland.service diff --git a/plasma/workspace/startkde/systemd/plasma-workspace-x11.target b/plasma/workspace/startkde/systemd/plasma-workspace-x11.target new file mode 100644 index 0000000000..154b0a0bb1 --- /dev/null +++ b/plasma/workspace/startkde/systemd/plasma-workspace-x11.target @@ -0,0 +1,4 @@ +[Unit] +Wants=plasma-kwin_x11.service +Requires=plasma-workspace.target +BindsTo=plasma-ksmserver.service diff --git a/plasma/workspace/startkde/systemd/plasma-workspace.target b/plasma/workspace/startkde/systemd/plasma-workspace.target new file mode 100644 index 0000000000..7c5fa6ad58 --- /dev/null +++ b/plasma/workspace/startkde/systemd/plasma-workspace.target @@ -0,0 +1,8 @@ +[Unit] +Description=KDE Plasma Workspace +Requires=plasma-core.target graphical-session.target +Wants=plasma-restoresession.service plasma-xembedsniproxy.service plasma-gmenudbusmenuproxy.service plasma-powerdevil.service plasma-ksplash-ready.service plasma-polkit-agent.service kde-baloo.service plasma-foreground-booster.service plasma-kwallet-pam.service xdg-desktop-autostart.target +BindsTo=graphical-session.target +Before=graphical-session.target xdg-desktop-autostart.target plasma-ksplash-ready.service plasma-restoresession.service +RefuseManualStart=yes +StopWhenUnneeded=true diff --git a/plasma/workspace/startkde/waitforname/CMakeLists.txt b/plasma/workspace/startkde/waitforname/CMakeLists.txt new file mode 100644 index 0000000000..ba34c3199f --- /dev/null +++ b/plasma/workspace/startkde/waitforname/CMakeLists.txt @@ -0,0 +1,28 @@ + +set(plasma_waitforname_SRCS + waiter.cpp + main.cpp + ) + +ecm_qt_declare_logging_category(plasma_waitforname_SRCS HEADER debug_p.h IDENTIFIER LOG_PLASMA CATEGORY_NAME org.kde.knotifications) + +add_executable(plasma_waitforname ${plasma_waitforname_SRCS}) +ecm_mark_nongui_executable(plasma_waitforname) + +target_link_libraries(plasma_waitforname + Qt::DBus + ) + +configure_file(org.kde.plasma.Notifications.service.in + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasma.Notifications.service) + +configure_file(org.kde.KSplash.service.in + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.KSplash.service) + +install(TARGETS plasma_waitforname ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasma.Notifications.service + DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.KSplash.service + DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) diff --git a/plasma/workspace/startkde/waitforname/main.cpp b/plasma/workspace/startkde/waitforname/main.cpp new file mode 100644 index 0000000000..180fb08034 --- /dev/null +++ b/plasma/workspace/startkde/waitforname/main.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2017 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "waiter.h" +#include + +void sigtermHandler(int signalNumber) +{ + Q_UNUSED(signalNumber) + if (QCoreApplication::instance()) { + QCoreApplication::instance()->exit(-1); + } +} +int main(int argc, char **argv) +{ + Waiter app(argc, argv); + signal(SIGTERM, sigtermHandler); + + if (!app.waitForService()) { + return 0; + } + + return app.exec(); +} diff --git a/plasma/workspace/startkde/waitforname/org.kde.KSplash.service.in b/plasma/workspace/startkde/waitforname/org.kde.KSplash.service.in new file mode 100644 index 0000000000..6d2d2e0acb --- /dev/null +++ b/plasma/workspace/startkde/waitforname/org.kde.KSplash.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.kde.KSplash +Exec=@KDE_INSTALL_FULL_BINDIR@/plasma_waitforname org.kde.KSplash diff --git a/plasma/workspace/startkde/waitforname/org.kde.plasma.Notifications.service.in b/plasma/workspace/startkde/waitforname/org.kde.plasma.Notifications.service.in new file mode 100644 index 0000000000..2350ebb0b3 --- /dev/null +++ b/plasma/workspace/startkde/waitforname/org.kde.plasma.Notifications.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Notifications +Exec=@KDE_INSTALL_FULL_BINDIR@/plasma_waitforname org.freedesktop.Notifications diff --git a/plasma/workspace/startkde/waitforname/waiter.cpp b/plasma/workspace/startkde/waitforname/waiter.cpp new file mode 100644 index 0000000000..fc70ae3871 --- /dev/null +++ b/plasma/workspace/startkde/waitforname/waiter.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2017 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include +#include + +#include +#include +#include + +#include + +#include "debug_p.h" +#include "waiter.h" + +constexpr static const char dbusServiceName[] = "org.freedesktop.Notifications"; + +Waiter::Waiter(int argc, char **argv) + : QCoreApplication(argc, argv) + , mService(dbusServiceName) +{ + setApplicationName(QStringLiteral("plasma_waitforname")); + setApplicationVersion(QStringLiteral("1.0")); + + QCommandLineParser parser; + parser.setApplicationDescription( + QStringLiteral("Waits for D-Bus registration of the Notifications service.\n" + "Prevents notifications from being processed before the desktop is ready.")); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument(QStringLiteral("service"), + QStringLiteral("Optionally listen for a service different than '%1'").arg(mService), + QStringLiteral("[service]")); + parser.process(*this); + + const QStringList args = parser.positionalArguments(); + if (!args.isEmpty()) { + mService = args.at(0); + } +} + +bool Waiter::waitForService() +{ + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + + if (sessionBus.interface()->isServiceRegistered(mService)) { + qCDebug(LOG_PLASMA) << "WaitForName: Service" << mService << "is already registered"; + return false; + } + + qCDebug(LOG_PLASMA) << "WaitForName: Waiting for appearance of service" << mService << "for" << dbusTimeoutSec << "seconds"; + + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); + watcher->setConnection(sessionBus); + watcher->setWatchMode(QDBusServiceWatcher::WatchForRegistration); + watcher->addWatchedService(mService); + connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &Waiter::registered); + + mTimeoutTimer.setSingleShot(true); + mTimeoutTimer.setInterval(dbusTimeoutSec * 1000); + connect(&mTimeoutTimer, &QTimer::timeout, this, &Waiter::timeout); + mTimeoutTimer.start(); + + return true; +} + +void Waiter::registered() +{ + qCDebug(LOG_PLASMA) << "WaitForName: Service was registered after" << (dbusTimeoutSec - (mTimeoutTimer.remainingTime() / 1000)) << "seconds"; + mTimeoutTimer.stop(); + exit(0); +} + +void Waiter::timeout() +{ + qCInfo(LOG_PLASMA) << "WaitForName: Service was not registered within timeout"; + exit(1); +} diff --git a/plasma/workspace/startkde/waitforname/waiter.h b/plasma/workspace/startkde/waitforname/waiter.h new file mode 100644 index 0000000000..8dcfb07f5b --- /dev/null +++ b/plasma/workspace/startkde/waitforname/waiter.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2017 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include +#include + +class Waiter : public QCoreApplication +{ + Q_OBJECT + +public: + Waiter(int argc, char **argv); + bool waitForService(); + +private Q_SLOTS: + void registered(); + void timeout(); + +private: + constexpr static const int dbusTimeoutSec = 60; + + QString mService = QStringLiteral("org.freedesktop.Notifications"); + QTimer mTimeoutTimer; +}; diff --git a/plasma/workspace/statusnotifierwatcher/CMakeLists.txt b/plasma/workspace/statusnotifierwatcher/CMakeLists.txt new file mode 100644 index 0000000000..ee7abbde34 --- /dev/null +++ b/plasma/workspace/statusnotifierwatcher/CMakeLists.txt @@ -0,0 +1,18 @@ +add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII") + +set(kded_statusnotifierwatcher_SRCS statusnotifierwatcher.cpp ) + +qt_add_dbus_adaptor(kded_statusnotifierwatcher_SRCS ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierWatcher.xml + statusnotifierwatcher.h StatusNotifierWatcher) + + +set(statusnotifieritem_xml ${KNOTIFICATIONS_DBUS_INTERFACES_DIR}/kf5_org.kde.StatusNotifierItem.xml) +set_source_files_properties(${statusnotifieritem_xml} PROPERTIES + NO_NAMESPACE false + INCLUDE "systemtraytypedefs.h" + CLASSNAME OrgKdeStatusNotifierItemInterface +) +qt_add_dbus_interface(kded_statusnotifierwatcher_SRCS ${statusnotifieritem_xml} statusnotifieritem_interface) + +kcoreaddons_add_plugin(statusnotifierwatcher SOURCES ${kded_statusnotifierwatcher_SRCS} INSTALL_NAMESPACE "kf5/kded") +target_link_libraries(statusnotifierwatcher Qt::DBus KF5::DBusAddons KF5::CoreAddons) diff --git a/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.cpp b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.cpp new file mode 100644 index 0000000000..4a727f02d8 --- /dev/null +++ b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.cpp @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "statusnotifierwatcher.h" + +#include +#include +#include + +#include + +#include "statusnotifieritem_interface.h" +#include "statusnotifierwatcheradaptor.h" + +K_PLUGIN_CLASS_WITH_JSON(StatusNotifierWatcher, "statusnotifierwatcher.json") + +StatusNotifierWatcher::StatusNotifierWatcher(QObject *parent, const QList &) + : KDEDModule(parent) +{ + setModuleName(QStringLiteral("StatusNotifierWatcher")); + new StatusNotifierWatcherAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/StatusNotifierWatcher"), this); + dbus.registerService(QStringLiteral("org.kde.StatusNotifierWatcher")); + + m_serviceWatcher = new QDBusServiceWatcher(this); + m_serviceWatcher->setConnection(dbus); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &StatusNotifierWatcher::serviceUnregistered); +} + +StatusNotifierWatcher::~StatusNotifierWatcher() +{ + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.unregisterService(QStringLiteral("org.kde.StatusNotifierWatcher")); +} + +void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString &serviceOrPath) +{ + QString service; + QString path; + if (serviceOrPath.startsWith(QLatin1Char('/'))) { + service = message().service(); + path = serviceOrPath; + } else { + service = serviceOrPath; + path = QStringLiteral("/StatusNotifierItem"); + } + QString notifierItemId = service + path; + if (m_registeredServices.contains(notifierItemId)) { + return; + } + m_serviceWatcher->addWatchedService(service); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(service).value()) { + // check if the service has registered a SystemTray object + org::kde::StatusNotifierItem trayclient(service, path, QDBusConnection::sessionBus()); + if (trayclient.isValid()) { + qDebug() << "Registering" << notifierItemId << "to system tray"; + m_registeredServices.append(notifierItemId); + Q_EMIT StatusNotifierItemRegistered(notifierItemId); + } else { + m_serviceWatcher->removeWatchedService(service); + } + } else { + m_serviceWatcher->removeWatchedService(service); + } +} + +QStringList StatusNotifierWatcher::RegisteredStatusNotifierItems() const +{ + return m_registeredServices; +} + +void StatusNotifierWatcher::serviceUnregistered(const QString &name) +{ + qDebug() << "Service " << name << "unregistered"; + m_serviceWatcher->removeWatchedService(name); + + QString match = name + QLatin1Char('/'); + QStringList::Iterator it = m_registeredServices.begin(); + while (it != m_registeredServices.end()) { + if (it->startsWith(match)) { + QString name = *it; + it = m_registeredServices.erase(it); + Q_EMIT StatusNotifierItemUnregistered(name); + } else { + ++it; + } + } + + if (m_statusNotifierHostServices.contains(name)) { + m_statusNotifierHostServices.remove(name); + Q_EMIT StatusNotifierHostUnregistered(); + } +} + +void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString &service) +{ + if (service.contains(QLatin1String("org.kde.StatusNotifierHost-")) && QDBusConnection::sessionBus().interface()->isServiceRegistered(service).value() + && !m_statusNotifierHostServices.contains(service)) { + qDebug() << "Registering" << service << "as system tray"; + + m_statusNotifierHostServices.insert(service); + m_serviceWatcher->addWatchedService(service); + Q_EMIT StatusNotifierHostRegistered(); + } +} + +bool StatusNotifierWatcher::IsStatusNotifierHostRegistered() const +{ + return !m_statusNotifierHostServices.isEmpty(); +} + +int StatusNotifierWatcher::ProtocolVersion() const +{ + return 0; +} + +#include "statusnotifierwatcher.moc" diff --git a/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.h b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.h new file mode 100644 index 0000000000..cb9311265a --- /dev/null +++ b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include +#include +#include + +class QDBusServiceWatcher; + +class StatusNotifierWatcher : public KDEDModule, protected QDBusContext +{ + Q_OBJECT + Q_PROPERTY(QStringList RegisteredStatusNotifierItems READ RegisteredStatusNotifierItems) + Q_PROPERTY(bool IsStatusNotifierHostRegistered READ IsStatusNotifierHostRegistered) + Q_PROPERTY(int ProtocolVersion READ ProtocolVersion) + +public: + StatusNotifierWatcher(QObject *parent, const QList &); + ~StatusNotifierWatcher() override; + + QStringList RegisteredStatusNotifierItems() const; + + bool IsStatusNotifierHostRegistered() const; + + int ProtocolVersion() const; + +public Q_SLOTS: + void RegisterStatusNotifierItem(const QString &service); + + void RegisterStatusNotifierHost(const QString &service); + +protected Q_SLOTS: + void serviceUnregistered(const QString &name); + +Q_SIGNALS: + void StatusNotifierItemRegistered(const QString &service); + // TODO: decide if this makes sense, the systray itself could notice the vanishing of items, but looks complete putting it here + void StatusNotifierItemUnregistered(const QString &service); + void StatusNotifierHostRegistered(); + void StatusNotifierHostUnregistered(); + +private: + QDBusServiceWatcher *m_serviceWatcher = nullptr; + QStringList m_registeredServices; + QSet m_statusNotifierHostServices; +}; \ No newline at end of file diff --git a/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.json b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.json new file mode 100644 index 0000000000..29d940ee16 --- /dev/null +++ b/plasma/workspace/statusnotifierwatcher/statusnotifierwatcher.json @@ -0,0 +1,103 @@ +{ + "KPlugin": { + "Description": "Manages services that provide status notifier user interfaces", + "Description[ar]": "أدِر الخدمات التي توفّر واجهات مستخدِم لمُخطِر الحالة", + "Description[az]": "İstifadəçi interfeysinin vəziyyəti haqqında bildirişləri təmin edən xidmətlər menecri", + "Description[ca]": "Gestiona serveis que proporcionen notificacions d'estat a les interfícies d'usuari", + "Description[cs]": "Nastavení služeb poskytujících rozhraní pro oznamování stavu", + "Description[de]": "Verwaltet Dienste, die eine Schnittstelle für Status-Informationen bereitstellen", + "Description[en_GB]": "Manages services that provide status notifier user interfaces", + "Description[es]": "Gestiona servicios que proporcionan interfaces de usuario para el notificador de estado", + "Description[eu]": "Egora-jakinarazlerako erabiltzaile-interfazeak hornitzen dituen zerbitzuak kudeatzen ditu", + "Description[fi]": "Hallitsee tilailmoitinkäyttöliittymän tarjoavia palveluja", + "Description[fr]": "Gère les services fournissant des interfaces utilisateurs pour la notification d'état", + "Description[hu]": "Kezeli az állapotértesítést nyújtó szolgáltatásokat", + "Description[ia]": "Il gere servicios que forni interfacies de usator pro notificator de stato", + "Description[it]": "Gestisce i servizi che forniscono le interfacce utente al notificatore di stato", + "Description[ko]": "상태 알림 인터페이스를 제공하는 서비스를 관리합니다", + "Description[lt]": "Valdo tarnybas, teikiančias būsenos pranešėjo naudotojo sąsajas", + "Description[nl]": "Beheert services die statusmeldingen leveren aan gebruikersinterfaces", + "Description[nn]": "Handterer tenester som tilbyr brukargrensesnitt for statusvarslingar", + "Description[pa]": "ਸਰਵਿਸਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰਦਾ ਹੈ, ਜੋ ਕਿ ਹਾਲਤ ਨੋਟੀਫਾਇਰ ਵਰਤੋਂਕਾਰ ਇੰਟਰਫੇਸ ਦਿੰਦਾ ਹੈ", + "Description[pl]": "Zarządzanie usługami, które dostarczają powiadomień o stanie poprzez interfejs użytkownika", + "Description[pt_BR]": "Gerencia os serviços que fornecem interfaces de notificação do status", + "Description[ro]": "Administrează serviciile care furnizează interfețe utilizator pentru notificări de stare", + "Description[ru]": "Управление службами интерфейсов уведомлений о состоянии", + "Description[sk]": "Spravuje služby, ktoré poskytujú užívateľské rozhrania pre oznámenie stavu", + "Description[sl]": "Upravlja s storitvami, ki ponujajo uporabniške vmesnike obvestilnika o stanju", + "Description[sv]": "Hanterar tjänster som tillhandahåller användargränssnitt för statusunderrättelser", + "Description[ta]": "நிலையை அறிவிக்கும் (SNI) சேவைகளை நிர்வகிக்கும்", + "Description[tr]": "Durum bildirimi kullanıcı arayüzünü sunan hizmetleri yönetir", + "Description[uk]": "Керує службами інтерфейсів користувача для сповіщення про стан", + "Description[vi]": "Quản lí các dịch vụ cung cấp giao diện thông báo trạng thái", + "Description[x-test]": "xxManages services that provide status notifier user interfacesxx", + "Description[zh_CN]": "管理提供状态通知用户界面的服务", + "Name": "Status Notifier Manager", + "Name[ar]": "مدير مُخطِر الحالة", + "Name[ast]": "Xestor del avisador d'estaos", + "Name[az]": "Vəziyyət Bildirişləri Meneceri", + "Name[bn]": "স্ট্যাটাস বিজ্ঞপ্তি ম্যানেজার", + "Name[bs]": "Menadžer izveštavača o stanju", + "Name[ca@valencia]": "Gestor del notificador d'estat", + "Name[ca]": "Gestor del notificador d'estat", + "Name[cs]": "Správce oznamování stavu", + "Name[da]": "Håndtering af statusbekendtgørelser", + "Name[de]": "Statusbenachrichtigungs-Verwaltung", + "Name[el]": "Διαχειριστής των ειδοποιήσεων κατάστασης", + "Name[en_GB]": "Status Notifier Manager", + "Name[es]": "Gestor del notificador del estado", + "Name[et]": "Oleku märguannete haldur", + "Name[eu]": "Egoera-jakinarazlearen kudeatzailea", + "Name[fi]": "Tilailmoittimen hallinta", + "Name[fr]": "Gestionnaire du notificateur d'état", + "Name[gl]": "Xestor das notificacións de estado", + "Name[he]": "מנהל שירותי הודעות מצב", + "Name[hi]": "स्थिति संकेत प्रबंधक", + "Name[hr]": "Upravitelj glasnika stanja", + "Name[hu]": "Állapotértesítés-kezelő", + "Name[ia]": "Gerente de notificator de stato", + "Name[id]": "Pengelola Penotifikasi Status", + "Name[is]": "Stöðutilkynningaþjónn", + "Name[it]": "Gestore del notificatore di stato", + "Name[ja]": "ステータス通知マネージャ", + "Name[kk]": "Күй-жайы туралы құлақтандырғыш менеджері", + "Name[km]": "កម្មវិធី​គ្រប់គ្រង​ធាតុជូន​ដំណឹង​អំពី​ស្ថានភាព", + "Name[kn]": " ಸ್ಥಿತಿ ಸೂಚನಾ ವ್ಯವಸ್ಥಾಪಕ", + "Name[ko]": "상태 알림 관리자", + "Name[lt]": "Būsenos pranešėjo tvarkytuvė", + "Name[lv]": "Statusa paziņojumu pārvaldītājs", + "Name[ml]": "അവസ്ഥ അറിയിപ്പു കാര്യസ്ഥന്‍", + "Name[mr]": "स्थिती निदर्शक व्यवस्थापक", + "Name[nb]": "Håndtering av statusvarsler", + "Name[nds]": "Statusbescheed-Beluern", + "Name[nl]": "Statusmeldingenbeheerder", + "Name[nn]": "Handsaming av statusvarslingar", + "Name[pa]": "ਹਾਲਤ ਨੋਟੀਫਾਇਰ ਮੈਨੇਜਰ", + "Name[pl]": "Zarządzanie powiadomieniami o stanie", + "Name[pt]": "Gestor das Notificações de Estado", + "Name[pt_BR]": "Gerenciador de notificações de status", + "Name[ro]": "Gestionar notificator de stare", + "Name[ru]": "Диспетчер уведомлений о состоянии", + "Name[si]": "තත්ව දැන්වීමේ කළමනාකරු", + "Name[sk]": "Správca oznámení stavu", + "Name[sl]": "Upravljalnik obvestilnika o stanju", + "Name[sr@ijekavian]": "Менаџер извјештавача о стању", + "Name[sr@ijekavianlatin]": "Menadžer izvještavača o stanju", + "Name[sr@latin]": "Menadžer izveštavača o stanju", + "Name[sr]": "Менаџер извештавача о стању", + "Name[sv]": "Hantering av statusunderrättelser", + "Name[ta]": "நிலை அறிவிப்பு மேலாளர்", + "Name[th]": "ตัวจัดการการแจ้งสถานะให้ทราบ", + "Name[tr]": "Durum Bildirim Yöneticisi", + "Name[ug]": "ھالەت بىلدۈرگۈ باشقۇرغۇچ", + "Name[uk]": "Керування сповіщенням про стан", + "Name[vi]": "Trình quản lí thông báo trạng thái", + "Name[wa]": "Manaedjeu do notifiaedje d' estat", + "Name[x-test]": "xxStatus Notifier Managerxx", + "Name[zh_CN]": "状态通知管理器", + "Name[zh_TW]": "狀態通知管理員" + }, + "X-KDE-Kded-autoload": true, + "X-KDE-Kded-load-on-demand": false, + "X-KDE-Kded-phase": 1 +} diff --git a/plasma/workspace/statusnotifierwatcher/systemtraytypedefs.h b/plasma/workspace/statusnotifierwatcher/systemtraytypedefs.h new file mode 100644 index 0000000000..2afb8c48f2 --- /dev/null +++ b/plasma/workspace/statusnotifierwatcher/systemtraytypedefs.h @@ -0,0 +1,32 @@ +/* + + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +struct KDbusImageStruct { + int width; + int height; + QByteArray data; +}; + +typedef QVector KDbusImageVector; + +struct KDbusToolTipStruct { + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; +}; + +Q_DECLARE_METATYPE(KDbusImageStruct) +Q_DECLARE_METATYPE(KDbusImageVector) +Q_DECLARE_METATYPE(KDbusToolTipStruct) diff --git a/plasma/workspace/systemmonitor/CMakeLists.txt b/plasma/workspace/systemmonitor/CMakeLists.txt new file mode 100644 index 0000000000..6004546cc4 --- /dev/null +++ b/plasma/workspace/systemmonitor/CMakeLists.txt @@ -0,0 +1,28 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"systemmonitor\") + +add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII") + +kcoreaddons_add_plugin(ksysguard SOURCES kdedksysguard.cpp INSTALL_NAMESPACE "kf5/kded") + +target_link_libraries(ksysguard + KF5::DBusAddons + KF5::I18n + KF5::XmlGui + KF5::GlobalAccel +) + +add_executable(systemmonitor ksystemactivitydialog.cpp main.cpp) + +target_link_libraries(systemmonitor + KSysGuard::ProcessUi + KF5::CoreAddons + KF5::ConfigCore + KF5::I18n + KF5::XmlGui + KF5::GlobalAccel + KF5::WindowSystem + PW::KWorkspace +) + +install(TARGETS systemmonitor DESTINATION ${KDE_INSTALL_BINDIR}) +install(PROGRAMS org.kde.systemmonitor.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/plasma/workspace/systemmonitor/Messages.sh b/plasma/workspace/systemmonitor/Messages.sh new file mode 100644 index 0000000000..86cbd2aad7 --- /dev/null +++ b/plasma/workspace/systemmonitor/Messages.sh @@ -0,0 +1,2 @@ +#! /bin/sh +$XGETTEXT *.cpp -o $podir/systemmonitor.pot diff --git a/plasma/workspace/systemmonitor/kdedksysguard.cpp b/plasma/workspace/systemmonitor/kdedksysguard.cpp new file mode 100644 index 0000000000..7d801a8ede --- /dev/null +++ b/plasma/workspace/systemmonitor/kdedksysguard.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2014 Vishesh Handa + SPDX-FileCopyrightText: 2006 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "kdedksysguard.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +K_PLUGIN_CLASS_WITH_JSON(KDEDKSysGuard, "ksysguard.json") + +KDEDKSysGuard::KDEDKSysGuard(QObject *parent, const QVariantList &) + : KDEDModule(parent) +{ + QTimer::singleShot(0, this, &KDEDKSysGuard::init); +} + +KDEDKSysGuard::~KDEDKSysGuard() +{ +} + +void KDEDKSysGuard::init() +{ + KActionCollection *actionCollection = new KActionCollection(this); + + QAction *action = actionCollection->addAction(QStringLiteral("Show System Activity")); + action->setText(i18n("Show System Activity")); + connect(action, &QAction::triggered, this, &KDEDKSysGuard::showTaskManager); + + KGlobalAccel::self()->setGlobalShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Escape)); +} + +void KDEDKSysGuard::showTaskManager() +{ + QDBusConnection con = QDBusConnection::sessionBus(); + QDBusConnectionInterface *interface = con.interface(); + if (interface->isServiceRegistered(QStringLiteral("org.kde.systemmonitor"))) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.systemmonitor"), + QStringLiteral("/"), + QStringLiteral("org.qtproject.Qt.QWidget"), + QStringLiteral("close")); + + con.asyncCall(msg); + } else { + const QString exe = QStandardPaths::findExecutable(QStringLiteral("systemmonitor")); + QProcess::startDetached(exe, QStringList()); + } +} + +#include "kdedksysguard.moc" diff --git a/plasma/workspace/systemmonitor/kdedksysguard.h b/plasma/workspace/systemmonitor/kdedksysguard.h new file mode 100644 index 0000000000..705296cf95 --- /dev/null +++ b/plasma/workspace/systemmonitor/kdedksysguard.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2014 Vishesh Handa + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include + +class KDEDKSysGuard : public KDEDModule +{ + Q_OBJECT + +public: + explicit KDEDKSysGuard(QObject *parent, const QVariantList &); + ~KDEDKSysGuard() override; + +private Q_SLOTS: + void init(); + void showTaskManager(); +}; diff --git a/plasma/workspace/systemmonitor/ksysguard.json b/plasma/workspace/systemmonitor/ksysguard.json new file mode 100644 index 0000000000..503e48d92f --- /dev/null +++ b/plasma/workspace/systemmonitor/ksysguard.json @@ -0,0 +1,93 @@ +{ + "KPlugin": { + "Description": "Launches KSysguard on Ctrl + Escape", + "Description[ar]": "يُطلِق مرقاب نظام.ك عند الضغط على Ctrl+Escape", + "Description[az]": "Ctrl + Escape-lə KSysguard-ı başladır", + "Description[ca]": "Llança el KSysguard amb Ctrl + Esc", + "Description[cs]": "Při Ctrl+Esc spustí KSysguard", + "Description[de]": "Startet KSysguard mit Strg+Esc", + "Description[en_GB]": "Launches KSysguard on Ctrl + Escape", + "Description[es]": "Lanza KSysguard al pulsar Ctrl + Escape", + "Description[eu]": "KSysguard jaurtitzen du Ktrl + Ihes sakatzean", + "Description[fi]": "Käynnistää Ctrl+Esc-painalluksella KSysGuardin", + "Description[fr]": "Lancer KSysguard avec « CTRL » + « Échap »", + "Description[hu]": "KSysguard futtatása a Ctrl+Esc megnyomásakor", + "Description[ia]": "Lancea KSysguard sur Ctrl + Escape", + "Description[it]": "Avvia KSysguard con Ctrl + Esc", + "Description[ko]": "Ctrl+Esc 키를 눌렀을 때 KSysguard 실행", + "Description[lt]": "Paleidžia KSysguard, paspaudus Vald + Gr (Ctrl + Esc)", + "Description[nl]": "Start KSysguard bij Ctrl + Escape", + "Description[nn]": "Startar KSysGuard ved «Ctrl + Escape»", + "Description[pa]": "Ctrl + Escape ਨਾਲ ਕੇਸਿਸ-ਗਾਰਡ ਚੱਲਦਾ ਹੈ", + "Description[pl]": "Uruchamia Monitor Systemowy na Ctrl + Escape", + "Description[pt_BR]": "Carrega o KSysguard com o Ctrl + Escape", + "Description[ro]": "Lansează KSysguard cu Ctrl + Escape", + "Description[ru]": "Запускает системный монитор при нажатии Ctrl+Esc", + "Description[sk]": "Spustí KSysGuard s Ctrl + Escape", + "Description[sl]": "Zažene KSysguard ob pritisku Ctrl + Escape", + "Description[sv]": "Startar KDE:s systemövervakare vid Ctrl+Esc", + "Description[ta]": "Ctrl + Escape அழுத்தப்படும்போது கேஸிஸ்கார்டை இயக்கும்", + "Description[tr]": "Ctrl + Escape ile KSysguard'ı başlatır", + "Description[uk]": "Запускає KSysguard у відповідь на натискання Ctrl + Esc", + "Description[vi]": "Khởi chạy KSysguard khi ấn Ctrl + Escape", + "Description[x-test]": "xxLaunches KSysguard on Ctrl + Escapexx", + "Description[zh_CN]": "Ctrl + Esc 启动系统守护程序", + "Icon": "preferences-system-power-management", + "Name": "KSysguard", + "Name[ar]": "مرقاب نظام.ك", + "Name[ast]": "KSysguard", + "Name[az]": "KSysguard", + "Name[bs]": "KSysguard", + "Name[ca@valencia]": "KSysguard", + "Name[ca]": "KSysguard", + "Name[cs]": "KSysGuard", + "Name[da]": "KSysguard", + "Name[de]": "KSysguard", + "Name[el]": "KSysGuard", + "Name[en_GB]": "KSysguard", + "Name[es]": "KSysguard", + "Name[et]": "KSysguard", + "Name[eu]": "KSysguard", + "Name[fi]": "KSysguard", + "Name[fr]": "KSysguard", + "Name[gl]": "KSysguard", + "Name[he]": "מנהל תהליכים", + "Name[hi]": "के-सिसगार्ड", + "Name[hu]": "KSysGuard", + "Name[ia]": "KSysGuard", + "Name[id]": "KSysguard", + "Name[is]": "KDE kerfisvörður", + "Name[it]": "Monitor di sistema", + "Name[ja]": "KSysguard", + "Name[ko]": "KSysguard", + "Name[lt]": "KSysguard", + "Name[ml]": "കെസിസ്ഗാർഡ്", + "Name[nb]": "KSysguard", + "Name[nds]": "KSysguard", + "Name[nl]": "KSysguard", + "Name[nn]": "KSysguard", + "Name[pa]": "ਕੇ-ਸਿਸਗਾਰਡ", + "Name[pl]": "Monitor Systemowy", + "Name[pt]": "KSysguard", + "Name[pt_BR]": "KSysguard", + "Name[ro]": "KSysguard", + "Name[ru]": "Запуск системного монитора", + "Name[sk]": "KSysguard", + "Name[sl]": "KSysguard", + "Name[sr@ijekavian]": "К‑систембран", + "Name[sr@ijekavianlatin]": "K‑sistembran", + "Name[sr@latin]": "K‑sistembran", + "Name[sr]": "К‑систембран", + "Name[sv]": "KDE:s systemövervakare", + "Name[ta]": "கேஸிஸ்கார்ட்", + "Name[tr]": "KSysGuard", + "Name[uk]": "KSysguard", + "Name[vi]": "KSysguard", + "Name[x-test]": "xxKSysguardxx", + "Name[zh_CN]": "KDE 系统守护程序", + "Name[zh_TW]": "KSysguard" + }, + "X-KDE-Kded-autoload": true, + "X-KDE-Kded-load-on-demand": false, + "X-KDE-Kded-phase": 1 +} diff --git a/plasma/workspace/systemmonitor/ksystemactivitydialog.cpp b/plasma/workspace/systemmonitor/ksystemactivitydialog.cpp new file mode 100644 index 0000000000..3ff47db0d5 --- /dev/null +++ b/plasma/workspace/systemmonitor/ksystemactivitydialog.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2007-2010 John Tapsell + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#ifndef Q_WS_WIN + +#include "ksystemactivitydialog.h" + +#include "processui/ksysguardprocesslist.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +KSystemActivityDialog::KSystemActivityDialog(QWidget *parent) + : KMainWindow(parent) + , m_configGroup(KSharedConfig::openConfig()->group("TaskDialog")) +{ + setWindowTitle(i18n("System Activity")); + setWindowIcon(QIcon::fromTheme(QStringLiteral("utilities-system-monitor"))); + setAutoSaveSettings(); + + m_processList = new KSysGuardProcessList; + m_processList->setScriptingEnabled(true); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(m_processList); + + QWidget *mainWidget = new QWidget; + mainWidget->setLayout(mainLayout); + setCentralWidget(mainWidget); + + QAction *closeAction = new QAction; + closeAction->setShortcuts({ QKeySequence::Quit, Qt::Key_Escape }); + connect(closeAction, &QAction::triggered, this, &KSystemActivityDialog::close); + addAction(closeAction); + + if (m_configGroup.exists()) { + m_processList->loadSettings(m_configGroup); + } + + QDBusConnection con = QDBusConnection::sessionBus(); + con.registerObject(QStringLiteral("/"), this, QDBusConnection::ExportAllSlots); +} + +void KSystemActivityDialog::run() +{ + show(); + raise(); + KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop()); + KWindowSystem::forceActiveWindow(winId()); +} + +void KSystemActivityDialog::setFilterText(const QString &filterText) +{ + m_processList->filterLineEdit()->setText(filterText); + m_processList->filterLineEdit()->setFocus(); +} + +QString KSystemActivityDialog::filterText() const +{ + return m_processList->filterLineEdit()->text(); +} + +void KSystemActivityDialog::closeEvent(QCloseEvent *event) +{ + m_processList->saveSettings(m_configGroup); + KSharedConfig::openConfig()->sync(); + + KMainWindow::closeEvent(event); +} + +QSize KSystemActivityDialog::sizeHint() const +{ + return QSize(650, 420); +} + +#endif // not Q_WS_WIN diff --git a/plasma/workspace/systemmonitor/ksystemactivitydialog.h b/plasma/workspace/systemmonitor/ksystemactivitydialog.h new file mode 100644 index 0000000000..2cc5898054 --- /dev/null +++ b/plasma/workspace/systemmonitor/ksystemactivitydialog.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2007-2010 John Tapsell + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#ifndef Q_WS_WIN + +#include +#include + +class KSysGuardProcessList; + +/** This creates a simple dialog box with a KSysguardProcessList + * + * It remembers the size and position of the dialog, and sets + * the dialog to always be over the other windows + */ +class KSystemActivityDialog : public KMainWindow +{ + Q_OBJECT +public: + explicit KSystemActivityDialog(QWidget *parent = nullptr); + + /** Show the dialog and set the focus + * + * This can be called even when the dialog is already showing to bring it + * to the front again and move it to the current desktop etc. + */ + void run(); + + /** Set the text in the filter line in the process list widget */ + void setFilterText(const QString &filterText); + QString filterText() const; + + QSize sizeHint() const override; + +protected: + /** Save the settings if the user clicks (x) button on the window */ + void closeEvent(QCloseEvent *event) override; + +private: + KSysGuardProcessList *m_processList = nullptr; + KConfigGroup m_configGroup; +}; +#endif // not Q_WS_WIN diff --git a/plasma/workspace/systemmonitor/main.cpp b/plasma/workspace/systemmonitor/main.cpp new file mode 100644 index 0000000000..38d9954592 --- /dev/null +++ b/plasma/workspace/systemmonitor/main.cpp @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2014 Vishesh Handa + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include +#include +#include + +#include + +#include "ksystemactivitydialog.h" + +int main(int argc, char **argv) +{ + KWorkSpace::detectPlatform(argc, argv); + QApplication app(argc, argv); + KLocalizedString::setApplicationDomain("systemmonitor"); + + app.setOrganizationDomain(QStringLiteral("kde.org")); + app.setDesktopFileName(QStringLiteral("org.kde.systemmonitor")); + + app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); + + QDBusConnection con = QDBusConnection::sessionBus(); + if (!con.registerService(QStringLiteral("org.kde.systemmonitor"))) { + return 0; + } + + KSystemActivityDialog *dialog = new KSystemActivityDialog; + dialog->show(); + + return app.exec(); +} diff --git a/plasma/workspace/systemmonitor/org.kde.systemmonitor.desktop b/plasma/workspace/systemmonitor/org.kde.systemmonitor.desktop new file mode 100644 index 0000000000..f2f9e82ff8 --- /dev/null +++ b/plasma/workspace/systemmonitor/org.kde.systemmonitor.desktop @@ -0,0 +1,91 @@ +# KDE Config File +[Desktop Entry] +Type=Application +Exec=systemmonitor +Name=System Monitor +Name[af]=Stelsel Monitor +Name[ar]=مرقاب النظام +Name[ast]=Supervisor del sistema +Name[az]=Sistem İzləyici +Name[be]=Сістэмны назіральнік +Name[be@latin]=Systemny nazirańnik +Name[bg]=Наблюдение на системата +Name[bn]=সিস্টেম মনিটর +Name[bn_IN]=সিস্টেম নিরীক্ষণ ব্যবস্থা +Name[bs]=Monitor sistema +Name[ca]=Monitor del sistema +Name[ca@valencia]=Monitor del sistema +Name[cs]=Monitor systému +Name[csb]=Mònitór systemë +Name[da]=Systemovervågning +Name[de]=Systemmonitor +Name[el]=Επόπτης συστήματος +Name[en_GB]=System Monitor +Name[eo]=Sistemstato-programo +Name[es]=Monitor del sistema +Name[et]=Süsteemi jälgija +Name[eu]=Sistema-monitorea +Name[fa]=نمایشگر سیستم +Name[fi]=Järjestelmän valvonta +Name[fr]=Surveillance du système +Name[fy]=Systeemmonitor +Name[ga]=Monatóir an Chórais +Name[gl]=Vixilante do sistema +Name[gu]=સિસ્ટમ દેખરેખ +Name[he]=מוניטור המערכת +Name[hi]=तंत्र मॉनीटर +Name[hne]=तंत्र मानीटर +Name[hr]=Nadzor sustava +Name[hsb]=Systemowy monitor +Name[hu]=Rendszermonitor +Name[ia]=Monitor de systema +Name[id]=Pemantau Sistem +Name[is]=Kerfiseftirlit +Name[it]=Monitor di sistema +Name[ja]=システムモニタ +Name[kk]=Жүйе мониторы +Name[km]=កម្មវិធី​ត្រួត​​ពិនិត្យ​ប្រព័ន្ធ +Name[kn]=ವ್ಯವಸ್ಥೆಯ ಪ್ರದರ್ಶಕ +Name[ko]=시스템 모니터 +Name[ku]=Temaşekerê Pergalê +Name[lt]=Sistemos prižiūryklė +Name[lv]=Sistēmas monitors +Name[mai]=सिस्टम मानीटर +Name[mk]=Системски монитор +Name[ml]=സിസ്റ്റം നിരീക്ഷകന്‍ +Name[mr]=प्रणाली मॉनिटर +Name[nb]=Systemovervåker +Name[nds]=Systeemkieker +Name[ne]=प्रणाली मनिटर +Name[nl]=Systeemmonitor +Name[nn]=Systemovervaking +Name[oc]=Monitor sistèma +Name[pa]=ਸਿਸਟਮ ਮਾਨੀਟਰ +Name[pl]=Monitor systemowy +Name[pt]=Monitor do Sistema +Name[pt_BR]=Monitor do sistema +Name[ro]=Monitor de sistem +Name[ru]=Системный монитор +Name[se]=Vuogádatgoziheaddji +Name[si]=පද්ධති නිරීක්‍ෂකය +Name[sk]=Monitor systému +Name[sl]=Sistemski nadzornik +Name[sr]=надзорник система +Name[sr@ijekavian]=надзорник система +Name[sr@ijekavianlatin]=nadzornik sistema +Name[sr@latin]=nadzornik sistema +Name[sv]=Systemövervakare +Name[ta]=கணினி நோட்டம் +Name[te]=సిస్టమ్ మానిటర్ +Name[tg]=Назорати низом +Name[th]=ติดตามการทำงานของระบบ +Name[tr]=Sistem İzleyici +Name[ug]=سىستېما كۆزەتكۈچ +Name[uk]=Монітор системи +Name[vi]=Trình giám sát hệ thống +Name[wa]=Corwaitoe do sistinme +Name[x-test]=xxSystem Monitorxx +Name[zh_CN]=系统监视器 +Name[zh_TW]=系統監視器 +Icon=utilities-system-monitor +NoDisplay=true diff --git a/plasma/workspace/themes/CMakeLists.txt b/plasma/workspace/themes/CMakeLists.txt new file mode 100644 index 0000000000..fe92958b45 --- /dev/null +++ b/plasma/workspace/themes/CMakeLists.txt @@ -0,0 +1,22 @@ + +set( themes_RC + qtcde.themerc + qtcleanlooks.themerc + qtmotif.themerc + qtplastique.themerc + qtwindows.themerc +) + +if (X11_FOUND) + set( themes_RC ${themes_RC} + qtgtk.themerc + ) +endif () + +########### install files ############### + +install( FILES ${themes_RC} DESTINATION ${KDE_INSTALL_DATADIR}/kstyle/themes ) + + + + diff --git a/plasma/workspace/themes/qtcde.themerc b/plasma/workspace/themes/qtcde.themerc new file mode 100644 index 0000000000..bdcf62e72d --- /dev/null +++ b/plasma/workspace/themes/qtcde.themerc @@ -0,0 +1,172 @@ +[Misc] +Name=CDE +Name[af]=CDE +Name[ar]=CDE +Name[as]=CDE +Name[az]=CDE +Name[be]=CDE +Name[be@latin]=CDE +Name[bg]=CDE +Name[bn]=CDE +Name[bn_IN]=CDE +Name[br]=CDE +Name[bs]=CDE +Name[ca]=CDE +Name[ca@valencia]=CDE +Name[cs]=CDE +Name[csb]=CDE +Name[cy]=CDE +Name[da]=CDE +Name[de]=CDE +Name[el]=CDE +Name[en_GB]=CDE +Name[eo]=CDE +Name[es]=CDE +Name[et]=CDE +Name[eu]=CDE +Name[fi]=CDE +Name[fr]=CDE +Name[fy]=CDE +Name[ga]=CDE +Name[gl]=CDE +Name[gu]=CDE +Name[he]=CDE +Name[hi]=सीडीई +Name[hne]=सीडीई +Name[hr]=CDE +Name[hsb]=CDE +Name[hu]=CDE +Name[ia]=CDE +Name[id]=CDE +Name[is]=CDE +Name[it]=CDE +Name[ja]=CDE +Name[ka]=CDE +Name[kk]=CDE +Name[km]=CDE +Name[kn]=ಸಿಡಿಇ +Name[ko]=CDE +Name[ku]=CDE +Name[lt]=CDE +Name[lv]=CDE +Name[mai]=CDE +Name[mk]=CDE +Name[ml]=സിഡിഇ +Name[mr]=CDE +Name[ms]=CDE +Name[nb]=CDE +Name[nds]=CDE +Name[ne]=CDE +Name[nl]=CDE +Name[nn]=CDE +Name[or]=CDE +Name[pa]=CDE +Name[pl]=CDE +Name[pt]=CDE +Name[pt_BR]=CDE +Name[ro]=CDE +Name[ru]=CDE +Name[se]=CDE +Name[si]=CDE +Name[sk]=CDE +Name[sl]=CDE +Name[sr]=ЦДЕ +Name[sr@ijekavian]=ЦДЕ +Name[sr@ijekavianlatin]=CDE +Name[sr@latin]=CDE +Name[sv]=CDE +Name[ta]=CDE +Name[te]=సిడిఈ +Name[th]=รูปแบบ CDE +Name[tr]=CDE +Name[ug]=CDE +Name[uk]=CDE +Name[uz]=CDE +Name[uz@cyrillic]=CDE +Name[vi]=CDE +Name[wa]=CDE +Name[xh]=CDE +Name[x-test]=xxCDExx +Name[zh_CN]=CDE 风格 +Name[zh_TW]=CDE +Comment=Built-in unthemed CDE style +Comment[ar]=نمط CDE مدمج بدون سمة +Comment[az]=CDE mövzusuz üslubu +Comment[be]=Убудаваны стыль CDE +Comment[be@latin]=Styl asiarodździa „CDE” +Comment[bg]=Вграден CDE стил +Comment[bn_IN]=থিমবিহীন বিল্ট-ইন CDE বিন্যাস +Comment[bs]=Ugrađeni bestematski stil CDE‑a +Comment[ca]=Estil integrat CDE sense temes +Comment[ca@valencia]=Estil integrat CDE sense temes +Comment[cs]=Zabudovaný styl CDE bez motivu +Comment[csb]=Wbùdowóny sztél CDE +Comment[da]=Indbygget, utematiseret CDE-stil +Comment[de]=Eingebautes CDE-Design +Comment[el]=Ενσωματωμένο στιλ παρόμοιο με το CDE +Comment[en_GB]=Built-in unthemed CDE style +Comment[eo]=Debaza senetosa stilo de CDE +Comment[es]=Estilo CDE +Comment[et]=Sisseehitatud teematu CDE stiil +Comment[eu]=Gairik gabeko CDE estilo barneratua +Comment[fi]=Sisäinen teematon CDE-tyyli +Comment[fr]=Style CDE intégré sans thème +Comment[fy]=Ynboude, temaleaze CDE-styl +Comment[ga]=Stíl insuite CDE gan téamaí +Comment[gl]=Estilo como CDE sen tema +Comment[gu]=સાથે-આપેલ થીમ વગરની CDE શૈલી +Comment[he]=מובנה בסגנון CDE +Comment[hi]=अंतर्निर्मित बगैर-प्रसंग सीडीई शैली +Comment[hne]=बिना थीम के भीतरे मं बने सीडीई सैली +Comment[hr]=Ugrađeni netematski CDE stil +Comment[hsb]=Integrowany CDE-stil bjez temow +Comment[hu]=Beépített CDE stílus +Comment[ia]=Stilo includite CDE sin themas +Comment[id]=Bawaan gaya CDE tak bertema +Comment[is]=Innbyggður óþemaður CDE stíll +Comment[it]=Stile integrato di CDE +Comment[ja]=ビルトイン CDE スタイル (テーマなし) +Comment[kk]=Құрамындағы нақыштарсыз CDE стилі +Comment[km]=រចនាប័ទ្ម​ជាប់​ជាមួយ CDE ដែល​គ្មាន​ស្បែក​ +Comment[kn]=ಅಂತರ್ಭೂತ ಪರಿಸರವಿನ್ಯಾಸವಿಲ್ಲದಂತಹ (ಥೀಮ್) ಸಿಡಿಇ ವೈಖರಿ +Comment[ko]=내장된 테마를 적용하지 않은 CDE 스타일 +Comment[ku]=Bi teşeyê bê dirb yê CDE ava-bike +Comment[lt]=Įtaisytas neapipavidalintas CDE stilius +Comment[lv]=Iebūvētais, netēmotais CDE stils +Comment[mai]=अंतर्निर्मित बगैर-प्रसंग सीडीई शैली +Comment[mk]=Вграден стил CDE, без тема +Comment[ml]=രംഗവിതാനമൊന്നും തെരഞ്ഞെടുക്കാത്ത സിഡിഇയുടെ തനതു് രീതി +Comment[mr]=अंतर्भूत अनथीम्ड CDE शैली +Comment[nb]=Innebygd CDE-stil uten tema +Comment[nds]=Inbuut Stil "CDE" ahn Muster +Comment[ne]=ढाँचाबद्ध नगरिएको सीडीई शैलीमा निर्माण गरिएको +Comment[nl]=Ingebouwde themaloze CDE-stijl +Comment[nn]=Innebygd temalaus CDE-stil +Comment[or]=ସନ୍ନିହିତ ଅପ୍ରାସଙ୍ଗିକ CDE ଶୈଳୀ +Comment[pa]=ਬਿਲਟ-ਇਨ ਗ਼ੈਰ-ਥੀਮ CDE ਸਟਾਇਲ +Comment[pl]=Wbudowany Wygląd CDE, , bez zdobień +Comment[pt]=O estilo incorporado sem tema do CDE +Comment[pt_BR]=Estilo embutido CDE sem tema +Comment[ro]=Stil încorporat CDE fără temă +Comment[ru]=Стиль CDE (без темы) +Comment[se]=Sisahuksejuvvon, fáddákeahtes CDE-stiila +Comment[si]=තිළැලි තේමාරහිත CDE රටාව +Comment[sk]=Zabudovaný CDE štýl bez témy +Comment[sl]=Vgrajeni običajni slog CDE +Comment[sr]=Уграђени бестематски стил ЦДЕ‑а +Comment[sr@ijekavian]=Уграђени бестематски стил ЦДЕ‑а +Comment[sr@ijekavianlatin]=Ugrađeni bestematski stil CDE‑a +Comment[sr@latin]=Ugrađeni bestematski stil CDE‑a +Comment[sv]=Inbyggd otemad CDE-stil +Comment[te]=అంతర్-నిర్మిత అన్‌థీమ్‌డ్ CDE శైలి +Comment[th]=รูปแบบซึ่งรวมมาในชุดอยู่แล้ว เลียนแบบ CDE +Comment[tr]=Dahili temalandırılmamış CDE biçimi +Comment[ug]=ئىچكى ئۆرنەكسىز CDE ئۇسلۇبى +Comment[uk]=Вбудований стиль CDE без теми +Comment[vi]=Kiểu cách CDE không chủ đề tích hợp sẵn +Comment[wa]=Stîle CDE tchôkî-dvins sins tinme +Comment[x-test]=xxBuilt-in unthemed CDE stylexx +Comment[zh_CN]=内建的无主题的 CDE 风格 +Comment[zh_TW]=內建的 CDE 樣式 +[KDE] +WidgetStyle=CDE diff --git a/plasma/workspace/themes/qtcleanlooks.themerc b/plasma/workspace/themes/qtcleanlooks.themerc new file mode 100644 index 0000000000..c7c0cdc41f --- /dev/null +++ b/plasma/workspace/themes/qtcleanlooks.themerc @@ -0,0 +1,137 @@ +[Misc] +Name=Cleanlooks +Name[ar]=كلين لوك +Name[az]=Cleanlooks +Name[bg]=Cleanlooks +Name[bs]=Čistina +Name[ca]=Cleanlooks +Name[ca@valencia]=Cleanlooks +Name[cs]=Cleanlooks +Name[csb]=Czësti wëzdrzatk +Name[da]=Cleanlooks +Name[de]=Cleanlooks +Name[el]=Καθαρή όψη +Name[en_GB]=Cleanlooks +Name[eo]=Cleanlooks +Name[es]=Cleanlooks +Name[et]=Cleanlooks +Name[eu]=Cleanlooks +Name[fi]=Cleanlooks +Name[fr]=Cleanlooks +Name[fy]=Cleanlooks +Name[ga]=Cleanlooks +Name[gl]=Cleanlooks +Name[he]=Cleanlooks +Name[hi]=क्लीनलुक +Name[hr]=Cleanlooks +Name[hsb]=Cleanlooks +Name[hu]=Cleanlooks +Name[ia]=Cleanlooks +Name[id]=Cleanlooks +Name[is]=Cleanlooks +Name[it]=Cleanlooks +Name[ja]=Cleanlooks +Name[kk]=Cleanlooks +Name[km]=រូបរាង​ច្បាស់ +Name[kn]=Cleanlooks +Name[ko]=Cleanlooks +Name[lt]=Cleanlooks +Name[lv]=Cleanlooks +Name[mk]=Чист изглед +Name[ml]=ക്ലീന്‍ലുക്സ് +Name[mr]=क्लीन लूक्स +Name[nb]=Cleanlooks +Name[nds]=Cleanlooks +Name[nl]=Cleanlooks +Name[nn]=Cleanlooks +Name[pa]=ਸਾਫ਼ ਦਿੱਖ +Name[pl]=Cleanlooks +Name[pt]=Cleanlooks +Name[pt_BR]=Cleanlooks +Name[ro]=Aspect curat +Name[ru]=Cleanlooks +Name[si]=පැහැදිලි දසුන +Name[sk]=Cleanlooks +Name[sl]=Cleanlooks +Name[sr]=Чистина +Name[sr@ijekavian]=Чистина +Name[sr@ijekavianlatin]=Čistina +Name[sr@latin]=Čistina +Name[sv]=Cleanlooks +Name[th]=Cleanlooks +Name[tr]=Cleanlooks +Name[ug]=Cleanlooks +Name[uk]=Cleanlooks +Name[vi]=Cleanlooks +Name[wa]=Cleanlooks +Name[x-test]=xxCleanlooksxx +Name[zh_CN]=Cleanlook 清爽 +Name[zh_TW]=乾淨外觀 +Comment=Built-in unthemed style similar to Clearlooks from GNOME +Comment[ar]=نمط مدمج بدون سمة مشابه لكلير لوك من غنوم +Comment[az]=GNOME mühitindəki kimi "Aydın Baxış" üslubuna oxşar quraşdırılmış üslub +Comment[bg]=Вграден стил, подобен на Clearlooks от GNOME +Comment[bs]=Ugrađeni bestematski stil nalik na Bistar izgled iz Gnoma +Comment[ca]=Un estil sense tema integrat, similar al Clearlooks de GNOME +Comment[ca@valencia]=Un estil sense tema integrat, similar a Clearlooks de GNOME +Comment[cs]=Zabudovaný styl bez motivu podobný Clearlooks z GNOME +Comment[csb]=Wbùdowóny sztél szlachùjący za Czëstim sztélã z GNOME +Comment[da]=Indbygget ikke-tematiseret stil som ligner Clearlooks fra GNOME +Comment[de]=Eingebauter Stil ähnlich zu Clearlooks von GNOME +Comment[el]=Ενσωματωμένο στιλ παρόμοιο με το Clearlooks από το GNOME +Comment[en_GB]=Built-in unthemed style similar to Clearlooks from GNOME +Comment[eo]=Stilo integrita kiu similas al Clearlook de GNOME +Comment[es]=Estilo integrado sin tema similar al Clearlooks de GNOME +Comment[et]=Kaasa pandud teemata stiil, mis sarnaneb GNOME Clearlooksiga +Comment[eu]=GNOMEren Clearlooks-en antzeko gairik gabeko estilo barneratua +Comment[fi]=Sisäinen teematon tyyli, joka muistuttaa Gnomen Clearlooksia +Comment[fr]=Style intégré sans thème, similaire à « Clear-looks » de GNOME +Comment[fy]=Ynboude temaleaze styl lyk oan Clearlooks fan GNOME +Comment[ga]=Stíl insuite gan téamaí cosúil le Clearlooks ó GNOME +Comment[gl]=Estilo incorporado sen tema similar ao Clearlooks de GNOME +Comment[he]=סגנון מובנה וללא ערכות־נושא הדומה ל־Clearlooks מגנום. +Comment[hi]=गनोम से क्लियरलुक्स के समान अंतर्निर्मित बिना थीम वाली शैली +Comment[hr]=Ugrađen netematski stil sličan Clearlooksu na GNOMEu +Comment[hsb]=Integrowany bjeztemowy stil podobny na Clearlooks wot GNOME +Comment[hu]=A Clearlooks GNOME stílusához hasonló beépített stílus +Comment[ia]=Stilo includite simil a Clearlookds de Gnome sin themas +Comment[id]=Bawaan gaya tak bertema mirip Clearlooks dari GNOME +Comment[is]=Innbyggður óþemaður stíll í stíl við Clearlooks frá GNOME +Comment[it]=Stile integrato simile al tema Clearlooks di GNOME +Comment[ja]=GNOME の Clearlooks に似たビルトインスタイル (テーマなし) +Comment[kk]=Құрамындағы GNOME Clearlooks тәрізді нақышы өзгертілмейтін стилі +Comment[km]=រចនាប័ទ្ម unthemed ជាប់​ដូច​គ្នា​ទៅនឹង​រូបរាង​ច្បាស់​ពី GNOME +Comment[ko]=그놈의 Clearlooks와 닮은 내장 스타일 +Comment[lt]=Įtaisytas neapipavidalintas stilius, panašus į GNOME Clearlooks stilių +Comment[lv]=Iebūvēts netēmots stils, līdzīgs Clearlooks no GNOME +Comment[mk]=Вграден стил без тема сличен на „Clearlooks“ од GNOME +Comment[ml]=ഗ്നോമിലെ ക്ലിയര്‍ലുക്സുമായി സാദൃശ്യമുള്ള പ്രമേയമില്ലാത്ത ഒപ്പം വരുന്ന ശൈലി +Comment[mr]=अंतर्भूत अनथीम्ड शैली GNOME मधील क्लीअर लूक्स कडून +Comment[nb]=Innebygd stil uten tema, likner på Clearlooks fra GNOME +Comment[nds]=Inbuut Stil ahn Muster, liek to »Clearlooks« bi GNOME +Comment[nl]=Ingebouwde stijl zonder thema gelijkend op Clearlooks uit GNOME +Comment[nn]=Innebygd temalaus stil som liknar på Clearlooks i GNOME +Comment[pl]=Wbudowany wygląd do Clearlooks z GNOME, bez zdobień +Comment[pt]=Um estilo incorporado sem tema, semelhante ao Clearlooks do GNOME +Comment[pt_BR]=Estilo embutido sem tema, semelhante ao Clearlooks do GNOME +Comment[ro]=Stil fără tematică încorporat, similar Clearlooks din GNOME +Comment[ru]=Встроенный стиль, похожий на стиль «Ясность» из среды GNOME +Comment[si]=GNOME විසින් තිළැලි තේමාරහිත පැහැදිලි දසුනට සමාන රටාවක් +Comment[sk]=Zabudovaný štýl podobný Clearlooks z GNOME bez témy +Comment[sl]=Vgrajeni običajni slog, ki je podoben Clearlooks iz GNOME +Comment[sr]=Уграђени бестематски стил налик на Бистар изглед из Гнома +Comment[sr@ijekavian]=Уграђени бестематски стил налик на Бистар изглед из Гнома +Comment[sr@ijekavianlatin]=Ugrađeni bestematski stil nalik na Bistar izgled iz Gnomea +Comment[sr@latin]=Ugrađeni bestematski stil nalik na Bistar izgled iz Gnomea +Comment[sv]=Inbyggd stil utan tema som liknar Clearlooks från GNOME +Comment[th]=รูปแบบซึ่งรวมมาในชุดอยู่แล้ว ที่คล้ายชุดตกแต่ง Clearlooks ของ GNOME +Comment[tr]=GNOME'daki Clearlooks'a benzer gömülü, temasız stil +Comment[ug]=ئىچكى ئۆرنەكسىز ئۇسلۇبتىكى GNOME نىڭ Clearlooks ئۇسلۇبى بىلەن ئوخشاش +Comment[uk]=Вбудований стиль без тем, подібний до Clearlooks у GNOME +Comment[vi]=Kiểu cách không chủ đề tích hợp sẵn tương tự Clearlooks của GNOME +Comment[wa]=Stîle ki n' est pus on tinme basti dins KDE mins k' rishonne a Clearlooks di GNOME +Comment[x-test]=xxBuilt-in unthemed style similar to Clearlooks from GNOMExx +Comment[zh_CN]=内建的无主题风格,和 GNOME 的 Clearlooks 风格类似 +Comment[zh_TW]=內建的無主題樣式,類似 GNOME 的 Clearlooks +[KDE] +WidgetStyle=Cleanlooks diff --git a/plasma/workspace/themes/qtgtk.themerc b/plasma/workspace/themes/qtgtk.themerc new file mode 100644 index 0000000000..fe520cb4af --- /dev/null +++ b/plasma/workspace/themes/qtgtk.themerc @@ -0,0 +1,141 @@ +[Misc] +Name=GTK+ Style +Name[ar]=نمط جتك+ +Name[az]=GTK+ üslubu +Name[bg]=Стил GTK+ +Name[bs]=GTK+ stil +Name[ca]=Estil GTK+ +Name[ca@valencia]=Estil GTK+ +Name[cs]=GTK+ styl +Name[csb]=Sztél GTK+ +Name[da]=GTK+-stil +Name[de]=GTK+-Stil +Name[el]=GTK+ Στιλ +Name[en_GB]=GTK+ Style +Name[eo]=GTK+ Stilo +Name[es]=Estilo GTK+ +Name[et]=GTK+ stiil +Name[eu]=GTK+ estiloa +Name[fi]=GTK+-tyyli +Name[fr]=Style GTK+ +Name[fy]=GTK+ Styl +Name[ga]=Stíl GTK+ +Name[gl]=Estilo GTK+ +Name[gu]=GTK+ શૈલી +Name[he]=סגנון GTK+ +Name[hi]=GTK+ शैली +Name[hr]=GTK+ stil +Name[hsb]=GTK+-stil +Name[hu]=GTK+ stílus +Name[ia]=Stilo GTK+ +Name[id]=Gaya GTK+ +Name[is]=GTK+ stíll +Name[it]=Stile GTK+ +Name[ja]=GTK+ スタイル +Name[kk]=GTK+ стилі +Name[km]=GTK+ Style +Name[kn]=GTK+ ವೈಖರಿ +Name[ko]=GTK+ 스타일 +Name[lt]=GTK+ Stilius +Name[lv]=GTK+ stils +Name[mai]=GTK+ शैली +Name[mk]=Стил GTK+ +Name[ml]=ജിടികെ+ ശൈലി +Name[mr]=GTK+ शैली +Name[nb]=GTK+-stil +Name[nds]=GTK+-Stil +Name[nl]=GTK+ Stijl +Name[nn]=GTK+-stil +Name[pa]=GTK+ ਸਟਾਇਲ +Name[pl]=Wygląd GTK+ +Name[pt]=Estilo GTK+ +Name[pt_BR]=Estilo GTK+ +Name[ro]=Stil GTK+ +Name[ru]=Стиль GTK+ +Name[si]=GTK+ රටාව +Name[sk]=GTK+ štýl +Name[sl]=Slog GTK+ +Name[sr]=ГТК+ стил +Name[sr@ijekavian]=ГТК+ стил +Name[sr@ijekavianlatin]=GTK+ stil +Name[sr@latin]=GTK+ stil +Name[sv]=GTK+ stil +Name[tg]=Услуби GTK+ +Name[th]=รูปลักษณ์ของ GTK+ +Name[tr]=GTK+ Biçimi +Name[ug]=GTK+ ئۇسلۇبى +Name[uk]=Стиль GTK+ +Name[vi]=Kiểu cách GTK+ +Name[wa]=Stîle GTK+ +Name[x-test]=xxGTK+ Stylexx +Name[zh_CN]=GTK+ 风格 +Name[zh_TW]=GTK+ 風格 +Comment=Style that uses the GTK+ theming engine +Comment[ar]=نمط يستخدِم محرّك سمات جتك+ +Comment[az]=GTK+ movzusunda istfadə olunan üslub +Comment[bg]=Стил, използващ ядрото за изгледи на GTK+ +Comment[bs]=Stil koji koristi tematski motor GTK+-a +Comment[ca]=Estil que usa el motor de temes GTK+ +Comment[ca@valencia]=Estil que utilitza el motor de temes GTK+ +Comment[cs]=Styl, který používá systém motivů GTK+ +Comment[csb]=Sztél brëkùjący mòtór GTK+ +Comment[da]=Stil der bruger tematiseringsmotoren fra GTK+ +Comment[de]=Stil, der das GTK+-Designmodul verwendet +Comment[el]=Στιλ που χρησιμοποιεί τον μηχανισμό θεμάτων GTK+ +Comment[en_GB]=Style that uses the GTK+ theming engine +Comment[eo]=Stiloj kiuj uzas la GTK+ etosan modulon +Comment[es]=Estilo que usa el motor de temas de GTK+ +Comment[et]=GTK+ teemamootorit kasutav stiil +Comment[eu]=GTK+en gaietarako motorra erabiltzen duen estiloa +Comment[fi]=GTK+-teemamoottoria käyttävä tyyli +Comment[fr]=Style utilisant le moteur de rendu de thèmes de GTK+ +Comment[fy]=Styl dat de GTK+ tema motor brûkt +Comment[ga]=Stíl a úsáideann an t-inneall téamaí GTK+ +Comment[gl]=Un estilo que emprega o motor de temas de GTK+ +Comment[gu]=શૈલી જે GTK+ થીમ એન્જિન વાપરે છે +Comment[he]=סגנון המשתמש במנוע התצוגה של GTK+ +Comment[hi]=शैली जो जीटीके+ प्रसंग इंजन का उपयोग करती है +Comment[hr]=Stil koji koristi GTK+-ov tematski mehanizam +Comment[hu]=A GTK+ témakezelőjét használó stílus +Comment[ia]=Stilo que usa le motor de themas de GTK+ +Comment[id]=Gaya yang menggunakan mesin pertemaan GTK+ +Comment[is]=Stíll sem notar GTK+ þemavélina +Comment[it]=Stile che usa il motore dei temi GTK+ +Comment[ja]=GTK+ テーマエンジンを使うスタイル +Comment[kk]=GTK+ нақыштар тетігін қолданатын стилі +Comment[km]=រចនាប័ទ្ម​ដែលប្រើ​ម៉ាស៊ីន GTK+ theming +Comment[ko]=GTK+ 테마 엔진을 사용하는 스타일 +Comment[lt]=Stilius, naudojantis GTK+ apipavidalinimų variklį +Comment[lv]=Stils, kas izmanto GTK+ tēmošanas dzinēju +Comment[mk]=Стил што ја користи машината на теми од GTK+ +Comment[ml]=ജിടികെ+ പ്രമേയ എഞ്ചിന്‍ ഉപയോഗിയ്ക്കുന്ന ശൈലി +Comment[mr]=GTK+ थीमिंग इंजिन वापरणारी शैली +Comment[nb]=Stil som bruker temamotoren i GTK+ +Comment[nds]=Stil, de den GTK+-Musterkarn bruukt +Comment[nl]=Stijl die de GTK+ thema-engine gebruikt +Comment[nn]=Stil som brukar GTK+-temamotoren +Comment[pa]=ਸਟਾਇਲ, ਜੋ ਕਿ GTK+ ਥੀਮ ਇੰਜਣ ਵਰਤਦਾ ਹੈ +Comment[pl]=Wygląd wykorzystujący silnik wyglądów GTK+ +Comment[pt]=Um estilo que usa o motor de temas do GTK+ +Comment[pt_BR]=Estilo que usa o mecanismo de temas do GTK+ +Comment[ro]=Stil ce utilizează motorul de tematici GTK+ +Comment[ru]=Стиль, использующий темы GTK+ +Comment[si]=GTK+ තේමා එන්ජිම යොදාගන්නා රටාවක් +Comment[sk]=Štýl, ktorý používa GTK+ tému +Comment[sl]=Slog, ki za izris teme uporablja pogon iz GTK+ +Comment[sr]=Стил који користи тематски мотор ГТК+-а +Comment[sr@ijekavian]=Стил који користи тематски мотор ГТК+-а +Comment[sr@ijekavianlatin]=Stil koji koristi tematski motor GTK+-a +Comment[sr@latin]=Stil koji koristi tematski motor GTK+-a +Comment[sv]=Stil som använder GTK+ tema +Comment[th]=รูปแบบที่ใช้กลไกชุดตกแต่งรูปลักษณ์ของ GTK+ +Comment[tr]=GTK+ temalandırma motoru kullanan bir biçim +Comment[ug]=GTK+ ئۆرنەك ماتورىنى ئىشلەتكەن ئۇسلۇب +Comment[uk]=Стиль, що використовує рушій тем GTK+ +Comment[vi]=Kiểu cách sử dụng dụng cụ đặt chủ đề của GTK+ +Comment[wa]=Stîle ki s' sieve do moteur di tinmes di GTK+ +Comment[x-test]=xxStyle that uses the GTK+ theming enginexx +Comment[zh_CN]=使用 GTK+ 主题引擎的风格 +Comment[zh_TW]=GTK+ 主題引擎使用的樣式 +[KDE] +WidgetStyle=Gtk+ diff --git a/plasma/workspace/themes/qtmotif.themerc b/plasma/workspace/themes/qtmotif.themerc new file mode 100644 index 0000000000..be2fe4fc40 --- /dev/null +++ b/plasma/workspace/themes/qtmotif.themerc @@ -0,0 +1,165 @@ +[Misc] +Name=Motif +Name[af]=Motif +Name[ar]=موتِف +Name[az]=Motif +Name[be]=Motif +Name[be@latin]=Motif +Name[bg]=Motif +Name[bn]=মোটিফ +Name[bn_IN]=Motif +Name[bs]=Motif +Name[ca]=Motif +Name[ca@valencia]=Motif +Name[cs]=Motif +Name[csb]=Motif +Name[da]=Motif +Name[de]=Motif +Name[el]=Motif +Name[en_GB]=Motif +Name[eo]=Motif +Name[es]=Motif +Name[et]=Motif +Name[eu]=Motif +Name[fa]=موتیف +Name[fi]=Motif +Name[fr]=Motif +Name[fy]=Motif +Name[ga]=Motif +Name[gl]=Motif +Name[gu]=મોટિફ +Name[he]=Motif +Name[hi]=मोटिफ़ +Name[hne]=मोटिफ +Name[hr]=Motif +Name[hsb]=Motif +Name[hu]=Motif +Name[ia]=Motif +Name[id]=Motif +Name[is]=Mótíf +Name[it]=Motif +Name[ja]=Motif +Name[kk]=Motif +Name[km]=Motif +Name[kn]=ಮೋಟಿಫ್ +Name[ko]=Motif +Name[ku]=Motif +Name[lt]=Motif +Name[lv]=Motif +Name[mai]=Motif +Name[mk]=Мотиф +Name[ml]=മോട്ടിഫ് +Name[mr]=Motif +Name[nb]=Motif +Name[nds]=Motif +Name[ne]=नमूना +Name[nl]=Motif +Name[nn]=Motif +Name[or]=Motif +Name[pa]=Motif +Name[pl]=Motif +Name[pt]=Motif +Name[pt_BR]=Motif +Name[ro]=Motif +Name[ru]=Motif +Name[se]=Motif +Name[si]=Motif +Name[sk]=Motif +Name[sl]=Motif +Name[sr]=Мотиф +Name[sr@ijekavian]=Мотиф +Name[sr@ijekavianlatin]=Motif +Name[sr@latin]=Motif +Name[sv]=Motif +Name[te]=మోటిఫ్ +Name[th]=รูปแบบ Motif +Name[tr]=Motif +Name[ug]=Motif +Name[uk]=Motif +Name[uz]=Motif +Name[uz@cyrillic]=Motif +Name[vi]=Motif +Name[wa]=Motif +Name[x-test]=xxMotifxx +Name[zh_CN]=Motif 风格 +Name[zh_TW]=Motif +Comment=Built-in unthemed Motif style +Comment[ar]=نمط موتف مدمج بدون سمة +Comment[az]=Mövzusuz Motif üslubu +Comment[be]=Убудаваны стыль Motif +Comment[be@latin]=Styl widžetaŭ „Motif” +Comment[bg]=Вграден стил Motif +Comment[bs]=Ugrađeni bestematski stil Motifa +Comment[ca]=Estil integrat Motif sense temes +Comment[ca@valencia]=Estil integrat Motif sense temes +Comment[cs]=Zabudovaný styl bez motivu podobný Motifu +Comment[csb]=Wbùdowóny sztél Motif +Comment[da]=Indbygget, utematiseret Motif-stil +Comment[de]=Eingebautes Motif-Design +Comment[el]=Ενσωματωμένο στιλ χωρίς θέματα παρόμοιο με το Motif +Comment[en_GB]=Built-in unthemed Motif style +Comment[eo]=Debaza senetosa stilo de Motif +Comment[es]=Estilo Motif sin tema +Comment[et]=Sisseehitatud teematu Motifi stiil +Comment[eu]=Gairik gabeko Motif estilo barneratua +Comment[fi]=Sisäinen teematon Motif-tyyli +Comment[fr]=Style « Motif » intégré sans thème +Comment[fy]=Ynboude, temaleaze Motif-styl +Comment[ga]=Stíl insuite Motif gan téamaí +Comment[gl]=Estilo como Motif sen tema +Comment[gu]=સાથે-આપેલ થીમ વગરની મોટિફ શૈલી +Comment[he]=מובנה בסגנון Motif ללא ערכת נושא +Comment[hi]=अंतर्निर्मित बगैर-प्रसंग मोटिफ शैली +Comment[hne]=बिना थीम के भीतरे मं बने मोटिफ सैली +Comment[hr]=Ugrađeni netematski Motif stil +Comment[hsb]=Integrowany Motif-stil bjez temy +Comment[hu]=Beépített Motif stílus +Comment[ia]=Stilo includite Motif sin themas +Comment[id]=Bawaan gaya Motif tak bertema +Comment[is]=Innbyggður óþemaður Mótíf stíll +Comment[it]=Stile integrato Motif +Comment[ja]=ビルトイン Motif スタイル (テーマなし) +Comment[kk]=Құрамындағы нақыштарсыз Motif стилі +Comment[km]=រចនាប័ទ្ម​ជាប់​ជា​មួយ Motif ដែល​គ្មាន​ស្បែក​ +Comment[kn]=ಅಂತರ್ಭೂತ ಪರಿಸರವಿನ್ಯಾಸವಿಲ್ಲದಂತಹ (ಥೀಮ್) ಮೋಟಿಫ್ ವೈಖರಿ +Comment[ko]=내장된 테마를 적용하지 않은 Motif 스타일 +Comment[ku]=Bi teşeyê bê dirb yê Motif ava-bike +Comment[lt]=Įtaisytas neapipavidalintas Motif stilius +Comment[lv]=Iebūvēts, netēmots Motif stils +Comment[mai]=अंतर्निर्मित बगैर-प्रसंग मोटिफ शैली +Comment[mk]=Вграден стил Мотиф, без тема +Comment[ml]=രംഗവിതാനമൊന്നും തെരഞ്ഞെടുക്കാത്ത മോട്ടിഫിന്റെ തനതു് രീതി +Comment[mr]=अंतर्भूतीत अनथीम्ड Motif शैली +Comment[nb]=Innebygget Motif-stil uten tema +Comment[nds]=Inbuut Stil "Motif" ahn Muster +Comment[ne]=ढाँचाबद्ध नगरिएको नमूना शैलीमा निर्माण गरियो +Comment[nl]=Ingebouwde themaloze Motif-stijl +Comment[nn]=Innebygd temalaus Motfi-stil +Comment[or]=ସନ୍ନିହିତ ଅପ୍ରାସଙ୍ଗିକ Motif ଶୈଳୀ +Comment[pa]=ਬਿਲਟ-ਇਨ ਗ਼ੈਰ-ਥੀਮ ਮੋਟਿਫ ਸਟਾਇਲ +Comment[pl]=Wbudowany wygląd Motif, bez zdobień +Comment[pt]=O estilo incorporado sem tema do Motif +Comment[pt_BR]=Estilo embutido Motif sem tema +Comment[ro]=Stil încorporat Motif fără temă +Comment[ru]=Стиль Motif (без темы) +Comment[se]=Sisahuksejuvvon, fáddákeahtes Motif-stiila +Comment[si]=තිළැලි තේමා රහිත Motif රටාව +Comment[sk]=Zabudovaný Motif štýl bez témy +Comment[sl]=Vgrajen običajni slog Motif +Comment[sr]=Уграђени бестематски стил Мотифа +Comment[sr@ijekavian]=Уграђени бестематски стил Мотифа +Comment[sr@ijekavianlatin]=Ugrađeni bestematski stil Motifa +Comment[sr@latin]=Ugrađeni bestematski stil Motifa +Comment[sv]=Inbyggd otemad Motif-stil +Comment[te]=అంతర్-నిర్మిత అన్‌థీమ్‌డ్ మోటిఫ్ శైలి +Comment[th]=รูปแบบซึ่งรวมมาในชุดอยู่แล้ว เลียนแบบรูปลักษณ์ของ Motif +Comment[tr]=Dahili temalandırılmamış Motif biçimi +Comment[ug]=ئىچكى ئۆرنەكسىز Motif ئۇسلۇبى +Comment[uk]=Вбудований стиль Motif без теми +Comment[vi]=Kiểu cách Motif không chủ đề tích hợp sẵn +Comment[wa]=Stîle Motif tchôkî-dvins sins tinme +Comment[x-test]=xxBuilt-in unthemed Motif stylexx +Comment[zh_CN]=内建的无主题的 Motif 风格 +Comment[zh_TW]=內建的 Motif 樣式 +[KDE] +WidgetStyle=Motif diff --git a/plasma/workspace/themes/qtplastique.themerc b/plasma/workspace/themes/qtplastique.themerc new file mode 100644 index 0000000000..5bac5fa792 --- /dev/null +++ b/plasma/workspace/themes/qtplastique.themerc @@ -0,0 +1,137 @@ +[Misc] +Name=Plastique +Name[ar]=بلاستك +Name[az]=Plastik +Name[bg]=Пластик +Name[bs]=Plastika KuT +Name[ca]=Plastique +Name[ca@valencia]=Plastique +Name[cs]=Plastique +Name[csb]=Plastik +Name[da]=Plastique +Name[de]=Plastique +Name[el]=Πλαστικό +Name[en_GB]=Plastique +Name[eo]=Plastik +Name[es]=Plastique +Name[et]=Plastique +Name[eu]=Plastique +Name[fi]=Plastique +Name[fr]=Plastique +Name[fy]=Plastyk +Name[ga]=Plastique +Name[gl]=Plastique +Name[gu]=પ્લાસ્ટિક +Name[he]=Plastique +Name[hi]=प्लास्टिक +Name[hr]=Plastique +Name[hu]=Plastique +Name[ia]=Plastique +Name[id]=Plastique +Name[is]=Plastik +Name[it]=Plastica +Name[ja]=Plastique +Name[kk]=Plastique +Name[km]=ប្ល៉ាស្ទិក +Name[kn]=Plastique +Name[ko]=Plastique +Name[lt]=Plastique +Name[lv]=Plastique +Name[mai]=प्लास्टिक +Name[mk]=Plastique +Name[ml]=പ്ലാസ്റ്റിക് +Name[mr]=प्लास्टिक +Name[nb]=Plastique +Name[nds]=Plastique +Name[nl]=Plastik +Name[nn]=Plastique +Name[pa]=ਪਲਾਸਟਿਕਿਉ +Name[pl]=Plastik +Name[pt]=Plastique +Name[pt_BR]=Plastique +Name[ro]=Plastic +Name[ru]=Пластик +Name[si]=Plastique +Name[sk]=Plastique +Name[sl]=Plastique +Name[sr]=Пластика КуТ +Name[sr@ijekavian]=Пластика КуТ +Name[sr@ijekavianlatin]=Plastika Qt +Name[sr@latin]=Plastika Qt +Name[sv]=Plastique +Name[th]=รูปแบบพลาสติก +Name[tr]=Plastik +Name[ug]=Plastique +Name[uk]=Plastique +Name[vi]=Plastique +Name[wa]=Plastike +Name[x-test]=xxPlastiquexx +Name[zh_CN]=Plastique 塑料 +Name[zh_TW]=Plastique +Comment=Built-in unthemed style similar to Plastik from KDE3 +Comment[ar]=نمط مدمج بدون سمة مشابه لبلاستك من كدي٣ +Comment[az]=KDE3-dəki Plastik-ə oxşar quraşdırılmış üslub +Comment[bg]=Вграден стил, подобен на Plastik от KDE3 +Comment[bs]=Ugrađeni bestematski stil nalik na Plastiku iz KDE‑a 3 +Comment[ca]=Un estil sense tema integrat, similar Plastik del KDE 3 +Comment[ca@valencia]=Un estil sense tema integrat, similar a Plastik de KDE3 +Comment[cs]=Zabudovaný styl bez motivu podobný Plastik z KDE3 +Comment[csb]=Wbùdowóny sztél szlachùjący za Plastikã z KDE3 +Comment[da]=Indbygget ikke-tematiseret stil som ligner Plastik fra KDE3 +Comment[de]=Eingebauter Stil ähnlich zu Plastik aus KDE 3 +Comment[el]=Ενσωματωμένο στιλ χωρίς θέματα παρόμοιο με το Plastik από το KDE3 +Comment[en_GB]=Built-in unthemed style similar to Plastik from KDE3 +Comment[eo]=Stilo integrita kiu similas al Plastik de KDE3 +Comment[es]=Estilo integrado sin tema similar al Plastik de KDE3 +Comment[et]=Kaasa pandud teemata stiil, mis sarnaneb KDE3 Plastikuga +Comment[eu]=KDE3ren Plastik-en antzeko gairik gabeko estilo barneratua +Comment[fi]=Sisäinen teematon tyyli, joka muistuttaa KDE 3:n Plastikia +Comment[fr]=Style intégré sans thème similaire à « Plastik » de KDE3 +Comment[fy]=Ynboude temaleaze styl lyk oan plastyk fan KDE3 +Comment[ga]=Stíl insuite gan téamaí cosúil le Plastik ó KDE3 +Comment[gl]=Un estilo incorporado sen tema similar ao Plastik de KDE3 +Comment[he]=סגנון מובנה דומה ל־Plastik מ־KDE3 +Comment[hi]=केडीइ३ से प्लास्टिक के समान बगैर-प्रसंग वाली अंतर्निर्मित शैली +Comment[hr]=Ugrađen netematski stil sličan Plastiku iz KDE3 +Comment[hu]=A KDE3 Plastik stílusához hasonló beépített stílus +Comment[ia]=Stilo includite simil Plastik de KDE3 sin themas +Comment[id]=Bawaan gaya tak bertema mirip dengan Plastik dari KDE3 +Comment[is]=Innbyggður óþemaður stíll í stíl við Plastik frá KDE3 +Comment[it]=Stile integrato simile a Plastica di KDE 3 +Comment[ja]=KDE3 の Plastik に似たビルトインスタイル (テーマなし) +Comment[kk]=Құрамындағы KDE3 Plastik-қа ұқсас нақыштарсыз стилі +Comment[km]=រចនាប័ទ្ម unthemed ដែល​ជាប់​ដូច​គ្នា​ទៅ​នឹង​ប្លាស្ទីក​ពី KDE3 +Comment[ko]=KDE 3의 Plastik과 닮은 내장 스타일 +Comment[lt]=Įtaisytas neapipavidalintas stilius, panašus į KDE3 Plastik stilių +Comment[lv]=Iebūvēts netēmots stils, līdzīgs Plastik no KDE3 +Comment[mk]=Вграден стил без тема сличен на „Пластик“ од KDE3 +Comment[ml]=കെഡിഇ3യിലെ പ്ലാസ്റ്റിക്കിനോടു് സദൃശ്യമായ പ്രമേയമില്ലാത്ത ഒപ്പം വരുന്ന ശൈലി +Comment[mr]=अंतर्भूत अनथीम्ड शैली केडीई3 मधील प्लास्टिक सारखी +Comment[nb]=Innebygd stil uten tema, likner på Plastik fra KDE3 +Comment[nds]=Inbuut Stil ahn Muster, liek to »Plastik« bi KDE3 +Comment[nl]=Ingebouwde stijl zonder thema gelijkend op Plastik uit KDE3 +Comment[nn]=Innebygd temalaus stil som liknar på Plastik i KDE 3 +Comment[pl]=Wbudowany wygląd podobny do Plastik z KDE3, bez zdobień +Comment[pt]=Um estilo sem tema incorporado, semelhante ao Plastik do KDE3 +Comment[pt_BR]=Estilo embutido sem tema, semelhante ao Plastik do KDE3 +Comment[ro]=Stil fără tematică încorporat, similar Plasticului din KDE3 +Comment[ru]=Встроенный стиль, похожий на Пластик из KDE3 +Comment[si]=KDE3 වෙතින් තිළැලි තේමා රහිත Plastik ට සමාන රටාවක් +Comment[sk]=Zabudovaný štýl podobný Plastik z KDE3 bez témy +Comment[sl]=Vgrajeni običajni slog, ki je podoben Plastik iz KDE 3 +Comment[sr]=Уграђени бестематски стил налик на Пластику из КДЕ‑а 3 +Comment[sr@ijekavian]=Уграђени бестематски стил налик на Пластику из КДЕ‑а 3 +Comment[sr@ijekavianlatin]=Ugrađeni bestematski stil nalik na Plastiku iz KDE‑a 3 +Comment[sr@latin]=Ugrađeni bestematski stil nalik na Plastiku iz KDE‑a 3 +Comment[sv]=Inbyggd stil utan tema som liknar Plastik från KDE 3 +Comment[th]=รูปแบบซึ่งรวมมาในชุดอยู่แล้ว ที่คล้ายชุดตกแต่งพลาสติกของ KDE3 +Comment[tr]=KDE3'teki Plastik stiline benzer gömülü temasız stil +Comment[ug]=ئىچكى ئۆرنەكسىز ئۇسلۇبتىكى KDE3 نىڭ Plastik ئۇسلۇبى بىلەن ئوخشاش +Comment[uk]=Вбудований стиль без тем, подібний до Plastik у KDE3 +Comment[vi]=Kiểu cách không chủ đề tích hợp sẵn tương tự Plastik của KDE3 +Comment[wa]=Stîle ki n' est pus on tinme basti dins KDE4 mins k' rishonne a Plastik di KDE3 +Comment[x-test]=xxBuilt-in unthemed style similar to Plastik from KDE3xx +Comment[zh_CN]=内建的无主题风格,和 KDE3 的 Plastik 风格类似 +Comment[zh_TW]=內建的無主題樣式,類似 KDE3 的 Plastik +[KDE] +WidgetStyle=Plastique diff --git a/plasma/workspace/themes/qtwindows.themerc b/plasma/workspace/themes/qtwindows.themerc new file mode 100644 index 0000000000..8b794c74b8 --- /dev/null +++ b/plasma/workspace/themes/qtwindows.themerc @@ -0,0 +1,168 @@ +[Misc] +Name=MS Windows 9x +Name[af]=MS Windows 9x +Name[ar]=مايكروسوفت ويندوز 9x +Name[as]=MS Windows 9x +Name[ast]=MS Windows 9x +Name[az]=MS Windows 9x +Name[be]=MS Windows 9x +Name[be@latin]=MS Windows 9x +Name[bg]=MS Windows 9x +Name[bn]=MS Windows 9x +Name[bn_IN]=MS Windows 9x +Name[bs]=MS Vindouz 9h +Name[ca]=MS Windows 9x +Name[ca@valencia]=MS Windows 9x +Name[cs]=MS Windows 9x +Name[csb]=MS Windows 9x +Name[da]=MS Windows 9x +Name[de]=MS Windows 9x +Name[el]=MS Windows 9x +Name[en_GB]=MS Windows 9x +Name[eo]=MS Vindozo 9x +Name[es]=MS Windows 9x +Name[et]=MS Windows 9x +Name[eu]=MS Windows 9x +Name[fi]=MS Windows 9X +Name[fr]=MS Windows 9x +Name[fy]=MS Windows 9x +Name[ga]=MS Windows 9x +Name[gl]=MS Windows 9x +Name[gu]=MS વિન્ડોઝ 9x +Name[he]=חלונות משנות התשעים +Name[hi]=एमएस विंडोज़ 9x +Name[hne]=एमएस विंडोज ९x +Name[hr]=MS Windows 9x +Name[hsb]=MS Windows 9x +Name[hu]=MS Windows 9x +Name[ia]=MS Windows 9x +Name[id]=MS Windows 9x +Name[is]=MS Windows 9x +Name[it]=MS Windows 9x +Name[ja]=MS Windows 9x +Name[kk]=MS Windows 9x +Name[km]=MS Windows 9x +Name[kn]=ಎಮ್ ಎಸ್ ವಿಂಡೋಸ್ ೯ಕ್ಸ +Name[ko]=MS Windows 9x +Name[ku]=MS Windows 9x +Name[lt]=MS Windows 9x +Name[lv]=MS Windows 9x +Name[mai]=MS Windows 9x +Name[mk]=MS Windows 9x +Name[ml]=എംഎസ് വിന്‍ഡോസ് 9എക്സ് +Name[mr]=MS विंडोज 9x +Name[nb]=MS Windows 9x +Name[nds]=MS Windows 9x +Name[ne]=एमएस विन्डोज् 9x +Name[nl]=MS Windows 9x +Name[nn]=MS Windows 9x +Name[oc]=MS Windows 9x +Name[or]=MS Windows 9x +Name[pa]=MS Windows 9x +Name[pl]=MS Windows 9x +Name[pt]=MS Windows 9x +Name[pt_BR]=MS Windows 9x +Name[ro]=MS Windows 9x +Name[ru]=Microsoft Windows 9x +Name[se]=MS Windows 9x +Name[si]=MS Windows 9x +Name[sk]=MS Windows 9x +Name[sl]=MS Windows 9x +Name[sr]=МС Виндоуз 9х +Name[sr@ijekavian]=МС Виндоуз 9х +Name[sr@ijekavianlatin]=MS Windows 9x +Name[sr@latin]=MS Windows 9x +Name[sv]=MS Windows 9x +Name[te]=MS Windows 9x +Name[tg]=MS Windows 9x +Name[th]=รูปแบบ MS Windows 9x +Name[tr]=MS Windows 9x +Name[ug]=MS Windows 9x +Name[uk]=MS Windows 9x +Name[uz]=MS Windows 9x +Name[uz@cyrillic]=MS Windows 9x +Name[vi]=MS Windows 9x +Name[wa]=MS Windows 9x +Name[x-test]=xxMS Windows 9xxx +Name[zh_CN]=MS Windows 9x 风格 +Name[zh_TW]=MS Windows 9x +Comment=Built-in unthemed Windows 9x style +Comment[ar]=نمط ويندوز ٩x مدمج بدون سمة +Comment[az]=Mövzusuz Windows 9x üslubu +Comment[be]=Убудаваны стыль Windows 9x +Comment[be@latin]=Styl systemy „Windows 9x” +Comment[bg]=Вграден стил, подобен на Windows 9x +Comment[bs]=Ugrađeni bestematski stil Vindouza 9x +Comment[ca]=Estil integrat Windows 9x sense temes +Comment[ca@valencia]=Estil integrat Windows 9x sense temes +Comment[cs]=Zabudovaný styl Windows 9x bez motivu +Comment[csb]=Wbùdowóny sztél szlachùjący za Windows 9x +Comment[da]=Indbygget, utematiseret Windows 9x-stil +Comment[de]=Eingebautes Design im Stil von Windows 9x +Comment[el]=Ενσωματωμένο στιλ χωρίς θέματα παρόμοιο με τα Windows 9x +Comment[en_GB]=Built-in unthemed Windows 9x style +Comment[eo]=Debaza senetosa stilo de Vindozo 9x +Comment[es]=Estilo de Windows 9x sin tema +Comment[et]=Sisseehitatud teematu Windows 9x stiil +Comment[eu]=Gairik gabeko Windows 9x estilo barneratua +Comment[fi]=Sisäinen teematon Windows 9x -tyyli +Comment[fr]=Style intégré « Windows 9x » sans thème +Comment[fy]=Ynboude, temaleaze Windows 9x-styl +Comment[ga]=Stíl insuite Windows 9x gan téamaí +Comment[gl]=Estilo como Windows 9x +Comment[gu]=સાથે-આપેલ થીમ વગરની વિન્ડોઝ 9x શૈલી +Comment[he]=ערכת הנושא הבררית מחדל של Windows 9x +Comment[hi]=अंतर्निर्मित विंडोज़ 9x शैली +Comment[hne]=बिना थीम के भीतरे मं बने विंडोज९xीई सैली +Comment[hr]=Ugrađeni netematski Windows 9x stil +Comment[hsb]=Integrowany WIndows 9x stil bjez temow +Comment[hu]=Beépített Windows 9x stílus +Comment[ia]=Stilo de Windows 9x includite sin themas +Comment[id]=Bawaan gaya Windows 9x tak bertema +Comment[is]=Innbyggður óþemaður Windows 9x stíll +Comment[it]=Stile integrato Windows 9x +Comment[ja]=ビルトイン Windows 9x スタイル (テーマなし) +Comment[kk]=Құрамындағы нақыштарсыз Windows 9x стилі +Comment[km]=រចនាប័ទ្ម​ Windows 9x ដែល​គ្មាន​ស្បែក​ +Comment[kn]=ಅಂತರ್ಭೂತ ಪರಿಸರವಿನ್ಯಾಸವಿಲ್ಲದಂತಹ (ಥೀಮ್) ಎಮ್ ಎಸ್ ವಿಂಡೋಸ್ ೯ಕ್ಸ ವೈಖರಿ +Comment[ko]=기본적으로 포함된 테마를 적용하지 않은 Windows 9x 스타일 +Comment[ku]=Teşeyê bê-dirb yê Windows 9x ya hundirî +Comment[lt]=Įtaisytas neapipavidalintas Windows 9x stilius +Comment[lv]=Iebūvētais, netēmotais Windows 9x stils +Comment[mai]=अंतर्निर्मित विंडोज़ 9x शैली +Comment[mk]=Вграден стил Windows 9x, без тема +Comment[ml]=വിന്‍ഡോസ് 9എക്സിന്റെ പ്രമോയമില്ലാത്ത ഒപ്പം വരുന്ന ശൈലി +Comment[mr]=अंतर्भूत अनथीम्ड विंडोज 9x शैली +Comment[nb]=Innebygget Windows 9x-stil uten tema +Comment[nds]=Inbuut "Windows 9x"-Stil ahn Muster +Comment[ne]=ढाँचाबद्ध नगरिएको विन्डोज् 9x शैलीमा निर्माण गरियो +Comment[nl]=Ingebouwde themaloze Windows 9x-stijl +Comment[nn]=Innebygd temalaus Windows 9x-stil +Comment[or]=ସନ୍ନିହିତ ଅପ୍ରାସଙ୍ଗିକ Windows 9x ଶୈଳୀ +Comment[pa]=ਬਿਲਡ-ਇਨ ਗ਼ੈਰ-ਥੀਮ ਵਿੰਡੋਜ਼ 9x ਸਟਾਇਲ +Comment[pl]=Wbudowany wygląd Windows 9x, bez zdobień +Comment[pt]=O estilo incorporado sem tema do Windows 9x +Comment[pt_BR]=Estilo embutido MS Windows 9x sem tema +Comment[ro]=Stil încorporat Windows 9x fără temă +Comment[ru]=Стиль Windows 9x (без темы) +Comment[se]=Sisahuksejuvvon, fáddákeahtes Windows 9x-stiila +Comment[si]=තිළැලි තේමාරහිත Windows 9x රටාව +Comment[sk]=Zabudovaný Windows 9x štýl bez témy +Comment[sl]=Vgrajeni običajni slog Windows 9x +Comment[sr]=Уграђени бестематски стил Виндоуза 9x +Comment[sr@ijekavian]=Уграђени бестематски стил Виндоуза 9x +Comment[sr@ijekavianlatin]=Ugrađeni bestematski stil Windowsa 9x +Comment[sr@latin]=Ugrađeni bestematski stil Windowsa 9x +Comment[sv]=Inbyggd otemad Windows 9x-stil +Comment[te]=అంతర్-నిర్మిత అన్‌థీమ్‌డ్ Windows 9x శైలి +Comment[th]=รูปแบบซึ่งรวมมาในชุดอยู่แล้ว เลียนแบบวินโดวส์ 9x +Comment[tr]=Dahili temalandırılmamış Windows 9x biçimi +Comment[ug]=ئىچكى ئۆرنەكسىز Windows 9x ئۇسلۇبى +Comment[uk]=Вбудований стиль Windows 9x без теми +Comment[vi]=Kiểu cách Windows 9x không chủ đề tích hợp sẵn +Comment[wa]=Stîle Windows 9x tchôkî-dvins sins tinme +Comment[x-test]=xxBuilt-in unthemed Windows 9x stylexx +Comment[zh_CN]=内建的无主题的 Windows 9x 风格 +Comment[zh_TW]=內建的 Windows 9x 樣式 +[KDE] +WidgetStyle=Windows diff --git a/plasma/workspace/wallpapers/CMakeLists.txt b/plasma/workspace/wallpapers/CMakeLists.txt new file mode 100644 index 0000000000..ad0bfe6d4c --- /dev/null +++ b/plasma/workspace/wallpapers/CMakeLists.txt @@ -0,0 +1,3 @@ + +add_subdirectory(image) +plasma_install_package(color org.kde.color wallpapers wallpaper) diff --git a/plasma/workspace/wallpapers/color/Messages.sh b/plasma/workspace/wallpapers/color/Messages.sh new file mode 100644 index 0000000000..f601d1bc76 --- /dev/null +++ b/plasma/workspace/wallpapers/color/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_wallpaper_org.kde.color.pot diff --git a/plasma/workspace/wallpapers/color/contents/config/main.xml b/plasma/workspace/wallpapers/color/contents/config/main.xml new file mode 100644 index 0000000000..927dc73789 --- /dev/null +++ b/plasma/workspace/wallpapers/color/contents/config/main.xml @@ -0,0 +1,15 @@ + + + + + + + + #1d99f3 + + + + diff --git a/plasma/workspace/wallpapers/color/contents/ui/config.qml b/plasma/workspace/wallpapers/color/contents/ui/config.qml new file mode 100644 index 0000000000..d729630a41 --- /dev/null +++ b/plasma/workspace/wallpapers/color/contents/ui/config.qml @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import org.kde.kquickcontrols 2.0 as KQuickControls +import org.kde.kirigami 2.5 as Kirigami + +Kirigami.FormLayout { + id: root + twinFormLayouts: parentLayout + + property alias cfg_Color: colorButton.color + property alias formLayout: root + + KQuickControls.ColorButton { + id: colorButton + Kirigami.FormData.label: i18nd("plasma_wallpaper_org.kde.color", "Color:") + dialogTitle: i18nd("plasma_wallpaper_org.kde.color", "Select Background Color") + } +} diff --git a/plasma/workspace/wallpapers/color/contents/ui/main.qml b/plasma/workspace/wallpapers/color/contents/ui/main.qml new file mode 100644 index 0000000000..ad2fcc3942 --- /dev/null +++ b/plasma/workspace/wallpapers/color/contents/ui/main.qml @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +Rectangle { + id: root + color: wallpaper.configuration.Color + + Behavior on color { + ColorAnimation { duration: PlasmaCore.Units.longDuration } + } +} diff --git a/plasma/workspace/wallpapers/color/metadata.json b/plasma/workspace/wallpapers/color/metadata.json new file mode 100644 index 0000000000..3d4fa8471c --- /dev/null +++ b/plasma/workspace/wallpapers/color/metadata.json @@ -0,0 +1,135 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "", + "Description": "Plain color wallpaper", + "Description[ar]": "خلفية لون صِرف", + "Description[az]": "Sadə, bir rəngli divar kağızları", + "Description[ca]": "Fons de pantalla en color llis", + "Description[cs]": "Tapeta v prosté barvě", + "Description[de]": "Einfarbiges Hintergrundbild", + "Description[en_GB]": "Plain colour wallpaper", + "Description[es]": "Fondo del escritorio de color liso", + "Description[eu]": "Kolore bakarreko horma-papera", + "Description[fi]": "Yksivärinen tausta", + "Description[fr]": "Fond d'écran de couleur unie", + "Description[hu]": "Egyszínű háttérkép", + "Description[ia]": "Tapete de papiro de Color plan", + "Description[it]": "Sfondo con colore semplice", + "Description[ko]": "단색 배경 그림", + "Description[lt]": "Vientisos spalvos darbalaukio fonas", + "Description[nl]": "Vlakke kleur achtergrond", + "Description[nn]": "Rein farge som bakgrunn", + "Description[pa]": "ਇੱਕ ਰੰਗ ਦਾ ਵਾਲਪੇਪਰ", + "Description[pl]": "Tapeta zwykłego koloru", + "Description[pt_BR]": "Papel de parede de cor simples", + "Description[ro]": "Tapet de o culoare", + "Description[ru]": "Простые одноцветные обои", + "Description[sk]": "Tapeta obyčajná farba", + "Description[sl]": "Ozadje s preprosto barvo", + "Description[sv]": "Skrivbordsunderlägg med enkel färg", + "Description[ta]": "ஒரே ஒரு நிறத்தை கொண்ட பின்புலம்", + "Description[tr]": "Düz renk duvar kağıdı", + "Description[uk]": "Звичайне одноколірне тло", + "Description[vi]": "Phông nền dạng màu trơn", + "Description[x-test]": "xxPlain color wallpaperxx", + "Description[zh_CN]": "纯色壁纸", + "Icon": "preferences-desktop-color", + "Id": "org.kde.color", + "License": "GPLv2+", + "Name": "Plain Color", + "Name[ar]": "لون صِرف", + "Name[ast]": "Color planu", + "Name[az]": "Bir rəngli divar kağızları", + "Name[bs]": "Obična boja", + "Name[ca@valencia]": "Color llis", + "Name[ca]": "Color llis", + "Name[cs]": "Prostá barva", + "Name[da]": "Almindelig farve", + "Name[de]": "Einfarbig", + "Name[el]": "Απλό χρώμα", + "Name[en_GB]": "Plain Colour", + "Name[es]": "Color sencillo", + "Name[et]": "Puhas värv", + "Name[eu]": "Kolore bakarra", + "Name[fi]": "Yksivärinen", + "Name[fr]": "Couleur pleine", + "Name[gl]": "Cor simple", + "Name[he]": "צבע רגיל", + "Name[hi]": "सादा रंग", + "Name[hu]": "Egyszerű szín", + "Name[ia]": "Color plan", + "Name[id]": "Warna Polos", + "Name[is]": "Hreinn litur", + "Name[it]": "Colore semplice", + "Name[ja]": "単色", + "Name[kk]": "Жәй түсі", + "Name[ko]": "단색", + "Name[lt]": "Vientisa spalva", + "Name[lv]": "Vienkārša krāsa", + "Name[ml]": "പ്ലെയിൻ നിറം", + "Name[nb]": "Ren farge", + "Name[nds]": "Eenfach Klöör", + "Name[nl]": "Vlakke kleur", + "Name[nn]": "Rein farge", + "Name[pa]": "ਇੱਕ ਰੰਗ", + "Name[pl]": "Zwykły kolor", + "Name[pt]": "Cor Simples", + "Name[pt_BR]": "Cor simples", + "Name[ro]": "Culoare simplă", + "Name[ru]": "Одноцветные обои", + "Name[sk]": "Obyčajná farba", + "Name[sl]": "Preprosta barva", + "Name[sr@ijekavian]": "проста боја", + "Name[sr@ijekavianlatin]": "prosta boja", + "Name[sr@latin]": "prosta boja", + "Name[sr]": "проста боја", + "Name[sv]": "Enkel färg", + "Name[ta]": "வெறும் நிறம்", + "Name[tr]": "Düz Renk", + "Name[uk]": "Звичайний колір", + "Name[vi]": "Màu trơn", + "Name[x-test]": "xxPlain Colorxx", + "Name[zh_CN]": "纯色", + "Name[zh_TW]": "普通顏色", + "Website": "https://kde.org/plasma-desktop" + }, + "Keywords": "", + "X-KDE-ParentApp": "org.kde.plasmashell" +} diff --git a/plasma/workspace/wallpapers/image/CMakeLists.txt b/plasma/workspace/wallpapers/image/CMakeLists.txt new file mode 100644 index 0000000000..c4c1155a24 --- /dev/null +++ b/plasma/workspace/wallpapers/image/CMakeLists.txt @@ -0,0 +1,59 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_wallpaper_org.kde.image\") + +set(image_SRCS + image.cpp + imageplugin.cpp + backgroundlistmodel.cpp + slidemodel.cpp + slidefiltermodel.cpp +) + +ecm_qt_declare_logging_category(image_SRCS HEADER debug.h + IDENTIFIER IMAGEWALLPAPER + CATEGORY_NAME kde.wallpapers.image + DEFAULT_SEVERITY Info) + +add_library(plasma_wallpaper_imageplugin SHARED ${image_SRCS}) + +target_link_libraries(plasma_wallpaper_imageplugin + Qt::Core + Qt::Quick + Qt::Qml + KF5::Plasma + KF5::KIOCore + KF5::KIOWidgets + KF5::I18n + KF5::KIOCore + KF5::KIOGui + KF5::NewStuff + KF5::Notifications + ) + +set(plasma-apply-wallpaperimage_SRCS + plasma-apply-wallpaperimage.cpp +) +add_executable(plasma-apply-wallpaperimage ${plasma-apply-wallpaperimage_SRCS}) +target_link_libraries(plasma-apply-wallpaperimage + Qt::Core + Qt::DBus + KF5::I18n +) + +if(BUILD_TESTING) + add_subdirectory(autotests) +endif() + +install(TARGETS plasma_wallpaper_imageplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/wallpapers/image) +install(TARGETS plasma-apply-wallpaperimage DESTINATION ${KDE_INSTALL_BINDIR}) + +install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/wallpapers/image) + +install(FILES wallpaper.knsrc wallpaper-mobile.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) + +plasma_install_package(imagepackage org.kde.image wallpapers wallpaper) +plasma_install_package(slideshowpackage org.kde.slideshow wallpapers wallpaper) + +configure_file(imagepackage/setaswallpaper.desktop.in imagepackage/setaswallpaper.desktop) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/imagepackage/setaswallpaper.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}/ServiceMenus) + +install(DIRECTORY imagepackage/contents/ui DESTINATION ${PLASMA_DATA_INSTALL_DIR}/wallpapers/org.kde.slideshow/contents PATTERN .svn EXCLUDE PATTERN CMakeLists.txt EXCLUDE PATTERN Messages.sh EXCLUDE) diff --git a/plasma/workspace/wallpapers/image/Messages.sh b/plasma/workspace/wallpapers/image/Messages.sh new file mode 100644 index 0000000000..42cb9bdbe8 --- /dev/null +++ b/plasma/workspace/wallpapers/image/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_wallpaper_org.kde.image.pot diff --git a/plasma/workspace/wallpapers/image/autotests/CMakeLists.txt b/plasma/workspace/wallpapers/image/autotests/CMakeLists.txt new file mode 100644 index 0000000000..f11e5b1ec1 --- /dev/null +++ b/plasma/workspace/wallpapers/image/autotests/CMakeLists.txt @@ -0,0 +1,11 @@ +set(testfindpreferredimage_SRCS + testfindpreferredimage.cpp + ../image.cpp + ../backgroundlistmodel.cpp + ) + +add_executable(testfindpreferredimage EXCLUDE_FROM_ALL ${testfindpreferredimage_SRCS}) + +target_link_libraries(testfindpreferredimage + plasma_wallpaper_imageplugin + Qt::Test) diff --git a/plasma/workspace/wallpapers/image/autotests/testfindpreferredimage.cpp b/plasma/workspace/wallpapers/image/autotests/testfindpreferredimage.cpp new file mode 100644 index 0000000000..d378145907 --- /dev/null +++ b/plasma/workspace/wallpapers/image/autotests/testfindpreferredimage.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2016 Antonio Larrosa + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "image.h" +#include +#include + +extern QSize resSize(const QString &str); + +QString formatResolution(const QString &str) +{ + QSize size = resSize(str); + float aspectRatio = (size.height() > 0) ? size.width() / (float)size.height() : 0; + return QStringLiteral("%1 (%2)").arg(str, 9).arg(aspectRatio, 7); +} + +class TestResolutions : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testResolutions_data(); + void testResolutions(); + +protected: + Image m_image; + QStringList m_images; +}; + +void TestResolutions::testResolutions_data() +{ + // The list of available wallpaper image sizes + m_images << QStringLiteral("1280x1024") << QStringLiteral("1350x1080") << QStringLiteral("1440x1080") << QStringLiteral("1600x1200") + << QStringLiteral("1920x1080") << QStringLiteral("1920x1200") << QStringLiteral("3840x2400"); + qDebug() << "Available images:"; + foreach (auto image, m_images) { + qDebug() << formatResolution(image); + } + + // The list of possible screen resolutions to test and the appropriate images that should be chosen + QTest::addColumn("resolution"); + QTest::addColumn("expected"); + QTest::newRow("1280x1024") << QStringLiteral("1280x1024") << QStringLiteral("1280x1024"); + QTest::newRow("1350x1080") << QStringLiteral("1350x1080") << QStringLiteral("1350x1080"); + QTest::newRow("1440x1080") << QStringLiteral("1440x1080") << QStringLiteral("1440x1080"); + QTest::newRow("1600x1200") << QStringLiteral("1600x1200") << QStringLiteral("1600x1200"); + QTest::newRow("1920x1080") << QStringLiteral("1920x1080") << QStringLiteral("1920x1080"); + QTest::newRow("1920x1200") << QStringLiteral("1920x1200") << QStringLiteral("1920x1200"); + QTest::newRow("3840x2400") << QStringLiteral("3840x2400") << QStringLiteral("3840x2400"); + QTest::newRow("4096x2160") << QStringLiteral("4096x2160") << QStringLiteral("1920x1080"); + QTest::newRow("3840x2160") << QStringLiteral("3840x2160") << QStringLiteral("1920x1080"); + QTest::newRow("3200x1800") << QStringLiteral("3200x1800") << QStringLiteral("1920x1080"); + QTest::newRow("2048x1080") << QStringLiteral("2048x1080") << QStringLiteral("1920x1080"); + QTest::newRow("1680x1050") << QStringLiteral("1680x1050") << QStringLiteral("1920x1200"); + QTest::newRow("1400x1050") << QStringLiteral("1400x1050") << QStringLiteral("1440x1080"); + QTest::newRow("1440x900") << QStringLiteral("1440x900") << QStringLiteral("1920x1200"); + QTest::newRow("1280x960") << QStringLiteral("1280x960") << QStringLiteral("1440x1080"); + QTest::newRow("1280x854") << QStringLiteral("1280x854") << QStringLiteral("1920x1200"); + QTest::newRow("1280x800") << QStringLiteral("1280x800") << QStringLiteral("1920x1200"); + QTest::newRow("1280x720") << QStringLiteral("1280x720") << QStringLiteral("1920x1080"); + QTest::newRow("1152x768") << QStringLiteral("1152x768") << QStringLiteral("1920x1200"); + QTest::newRow("1024x768") << QStringLiteral("1024x768") << QStringLiteral("1440x1080"); + QTest::newRow("800x600") << QStringLiteral("800x600") << QStringLiteral("1440x1080"); + QTest::newRow("848x480") << QStringLiteral("848x480") << QStringLiteral("1920x1080"); + QTest::newRow("720x480") << QStringLiteral("720x480") << QStringLiteral("1920x1200"); + QTest::newRow("640x480") << QStringLiteral("640x480") << QStringLiteral("1440x1080"); + QTest::newRow("1366x768") << QStringLiteral("1366x768") << QStringLiteral("1920x1080"); + QTest::newRow("1600x814") << QStringLiteral("1600x814") << QStringLiteral("1920x1080"); +} + +void TestResolutions::testResolutions() +{ + QFETCH(QString, resolution); + QFETCH(QString, expected); + + m_image.setTargetSize(resSize(resolution)); + QString preferred = m_image.findPreferedImage(m_images); + + qDebug() << "For a screen size of " << formatResolution(resolution) << " the " << formatResolution(preferred) << " wallpaper was preferred"; + + QCOMPARE(preferred, expected); +} + +QTEST_MAIN(TestResolutions) +#include "testfindpreferredimage.moc" diff --git a/plasma/workspace/wallpapers/image/backgroundlistmodel.cpp b/plasma/workspace/wallpapers/image/backgroundlistmodel.cpp new file mode 100644 index 0000000000..33ba39ec75 --- /dev/null +++ b/plasma/workspace/wallpapers/image/backgroundlistmodel.cpp @@ -0,0 +1,557 @@ +/* + SPDX-FileCopyrightText: 2007 Paolo Capriotti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef BACKGROUNDLISTMODEL_CPP +#define BACKGROUNDLISTMODEL_CPP + +#include "backgroundlistmodel.h" +#include "debug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include + +QStringList BackgroundFinder::s_suffixes; +QMutex BackgroundFinder::s_suffixMutex; + +ImageSizeFinder::ImageSizeFinder(const QString &path, QObject *parent) + : QObject(parent) + , m_path(path) +{ +} + +void ImageSizeFinder::run() +{ + QImageReader reader(m_path); + Q_EMIT sizeFound(m_path, reader.size()); +} + +BackgroundListModel::BackgroundListModel(Image *wallpaper, QObject *parent) + : QAbstractListModel(parent) + , m_wallpaper(wallpaper) +{ + m_imageCache.setMaxCost(10 * 1024 * 1024); // 10 MiB + + connect(&m_dirwatch, &KDirWatch::deleted, this, &BackgroundListModel::removeBackground); + + // TODO: on Qt 4.4 use the ui scale factor + QFontMetrics fm(QGuiApplication::font()); + m_screenshotSize = fm.horizontalAdvance('M') * 15; +} + +BackgroundListModel::~BackgroundListModel() = default; + +QHash BackgroundListModel::BackgroundListModel::roleNames() const +{ + return { + {Qt::DisplayRole, "display"}, + {Qt::DecorationRole, "decoration"}, + {AuthorRole, "author"}, + {ScreenshotRole, "screenshot"}, + {ResolutionRole, "resolution"}, + {PathRole, "path"}, + {PackageNameRole, "packageName"}, + {RemovableRole, "removable"}, + {PendingDeletionRole, "pendingDeletion"}, + }; +} + +void BackgroundListModel::removeBackground(const QString &path) +{ + int index = -1; + while ((index = indexOf(path)) >= 0) { + beginRemoveRows(QModelIndex(), index, index); + m_packages.removeAt(index); + endRemoveRows(); + Q_EMIT countChanged(); + } +} + +void BackgroundListModel::reload() +{ + reload(QStringList()); +} + +void BackgroundListModel::reload(const QStringList &selected) +{ + if (!m_wallpaper) { + beginRemoveRows(QModelIndex(), 0, m_packages.count() - 1); + m_packages.clear(); + endRemoveRows(); + Q_EMIT countChanged(); + return; + } + + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/"), QStandardPaths::LocateDirectory); + + BackgroundFinder *finder = new BackgroundFinder(m_wallpaper.data(), dirs); + const auto token = finder->token(); + connect(finder, &BackgroundFinder::backgroundsFound, this, [this, selected, token](const QStringList &wallpapersFound) { + if (token != m_findToken || !m_wallpaper) { + return; + } + + processPaths(selected + wallpapersFound); + m_removableWallpapers = QSet(selected.constBegin(), selected.constEnd()); + }); + m_findToken = token; + finder->start(); +} + +void BackgroundListModel::processPaths(const QStringList &paths) +{ + beginResetModel(); + m_packages.clear(); + + QList newPackages; + newPackages.reserve(paths.count()); + for (QString file : paths) { + // check if the path is a symlink and if it is, + // work with the target rather than the symlink + QFileInfo info(file); + if (info.isSymLink()) { + file = info.symLinkTarget(); + } + // now check if the path contains "contents" part + // which could indicate that the file is part of some other + // package (could have been symlinked) and we should work + // with the package (which can already be present) rather + // than just one file from it + int contentsIndex = file.indexOf(QLatin1String("contents")); + + // FIXME: additionally check for metadata.desktop being present + // which would confirm a package but might be slowing things + if (contentsIndex != -1) { + file.truncate(contentsIndex); + } + + // so now we have a path to a package, check if we're not + // processing the same path twice (this is different from + // the "!contains(file)" call lower down, that one checks paths + // already in the model and does not include the paths + // that are being checked in here); we want to check for duplicates + // if and only if we actually changed the path (so the conditions from above + // are reused here as that means we did change the path) + if ((info.isSymLink() || contentsIndex != -1) && paths.contains(file)) { + continue; + } + + if (!contains(file) && QFile::exists(file)) { + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + package.setPath(file); + if (package.isValid()) { + m_wallpaper->findPreferedImageInPackage(package); + newPackages << package; + } + } + } + + // add new files to dirwatch + for (const KPackage::Package &b : qAsConst(newPackages)) { + if (!m_dirwatch.contains(b.path())) { + m_dirwatch.addDir(b.path()); + } + } + + if (!newPackages.isEmpty()) { + m_packages.append(newPackages); + } + endResetModel(); + Q_EMIT countChanged(); + // qCDebug(IMAGEWALLPAPER) << t.elapsed(); +} + +void BackgroundListModel::addBackground(const QString &path) +{ + if (!m_wallpaper || !contains(path)) { + if (!m_dirwatch.contains(path)) { + m_dirwatch.addFile(path); + } + beginInsertRows(QModelIndex(), 0, 0); + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + + m_removableWallpapers.insert(path); + package.setPath(path); + m_wallpaper->findPreferedImageInPackage(package); + qCDebug(IMAGEWALLPAPER) << "Background added " << path << package.isValid(); + m_packages.prepend(package); + endInsertRows(); + Q_EMIT countChanged(); + } +} + +int BackgroundListModel::indexOf(const QString &path) const +{ + for (int i = 0; i < m_packages.size(); i++) { + // packages will end with a '/', but the path passed in may not + QString package = m_packages[i].path(); + if (package.endsWith(QChar::fromLatin1('/'))) { + package.chop(1); + } + // remove eventual file:/// + const QString filteredPath = QUrl(path).path(); + + if (filteredPath.startsWith(package)) { + // FIXME: ugly hack to make a difference between local files in the same dir + // package->path does not contain the actual file name + qCDebug(IMAGEWALLPAPER) << "prefix" << m_packages[i].contentsPrefixPaths() << m_packages[i].filePath("preferred") << package << filteredPath; + QStringList ps = m_packages[i].contentsPrefixPaths(); + bool prefixempty = ps.count() == 0; + if (!prefixempty) { + prefixempty = ps[0].isEmpty(); + } + + // For local files (user wallpapers) filteredPath == m_packages[i].filePath("preferred") + // E.X. filteredPath = "/home/kde/next.png" + // m_packages[i].filePath("preferred") = "/home/kde/next.png" + // + // But for the system wallpapers this is not the case. filteredPath != m_packages[i].filePath("preferred") + // E.X. filteredPath = /usr/share/wallpapers/Next/" + // m_packages[i].filePath("preferred") = "/usr/share/wallpapers/Next/contents/images/1920x1080.png" + if ((filteredPath == m_packages[i].filePath("preferred")) || m_packages[i].filePath("preferred").contains(filteredPath)) { + return i; + } + } + } + return -1; +} + +bool BackgroundListModel::contains(const QString &path) const +{ + // qCDebug(IMAGEWALLPAPER) << "WP contains: " << path << indexOf(path).isValid(); + return indexOf(path) >= 0; +} + +int BackgroundListModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_packages.size(); +} + +QSize BackgroundListModel::bestSize(const KPackage::Package &package) const +{ + if (m_sizeCache.contains(package.path())) { + return m_sizeCache.value(package.path()); + } + + const QString image = package.filePath("preferred"); + if (image.isEmpty()) { + return QSize(); + } + + ImageSizeFinder *finder = new ImageSizeFinder(image); + connect(finder, &ImageSizeFinder::sizeFound, this, &BackgroundListModel::sizeFound); + QThreadPool::globalInstance()->start(finder); + + QSize size(-1, -1); + const_cast(this)->m_sizeCache.insert(package.path(), size); + return size; +} + +void BackgroundListModel::sizeFound(const QString &path, const QSize &s) +{ + if (!m_wallpaper) { + return; + } + + int idx = indexOf(path); + if (idx >= 0) { + KPackage::Package package = m_packages.at(idx); + m_sizeCache.insert(package.path(), s); + Q_EMIT dataChanged(index(idx, 0), index(idx, 0)); + } +} + +QVariant BackgroundListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() >= m_packages.size()) { + return QVariant(); + } + + KPackage::Package b = package(index.row()); + if (!b.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: { + QString title = b.metadata().isValid() ? b.metadata().name() : QString(); + + if (title.isEmpty()) { + return QFileInfo(b.filePath("preferred")).completeBaseName(); + } + + return title; + } + + case ScreenshotRole: { + const QString path = b.filePath("preferred"); + + QPixmap *cachedPreview = m_imageCache.object(path); + if (cachedPreview) { + return *cachedPreview; + } + + const QUrl url = QUrl::fromLocalFile(path); + const QPersistentModelIndex persistentIndex(index); + if (!m_previewJobsUrls.contains(persistentIndex) && url.isValid()) { + KFileItemList list; + list.append(KFileItem(url, QString(), 0)); + QStringList availablePlugins = KIO::PreviewJob::availablePlugins(); + KIO::PreviewJob *job = KIO::filePreview(list, QSize(m_screenshotSize * 1.6, m_screenshotSize), &availablePlugins); + job->setIgnoreMaximumSize(true); + connect(job, &KIO::PreviewJob::gotPreview, this, &BackgroundListModel::showPreview); + connect(job, &KIO::PreviewJob::failed, this, &BackgroundListModel::previewFailed); + const_cast(this)->m_previewJobsUrls.insert(persistentIndex, url); + } + + return QVariant(); + } + + case AuthorRole: + if (b.metadata().isValid() && !b.metadata().authors().isEmpty()) { + return b.metadata().authors().first().name(); + } else { + return QString(); + } + + case ResolutionRole: { + QSize size = bestSize(b); + + if (size.isValid()) { + return QString::fromLatin1("%1x%2").arg(size.width()).arg(size.height()); + } + + return QString(); + } + + case PathRole: + return QUrl::fromLocalFile(b.filePath("preferred")); + + case PackageNameRole: + return !b.metadata().isValid() ? b.filePath("preferred") : b.path(); + + case RemovableRole: { + QString localWallpapers = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/wallpapers/"; + QString path = b.filePath("preferred"); + return path.startsWith(localWallpapers) || m_removableWallpapers.contains(path); + } + + case PendingDeletionRole: { + QUrl wallpaperUrl = QUrl::fromLocalFile(b.filePath("preferred")); + return m_pendingDeletion.contains(wallpaperUrl.toLocalFile()) ? m_pendingDeletion[wallpaperUrl.toLocalFile()] : false; + } + + default: + return QVariant(); + } + + Q_UNREACHABLE(); +} + +bool BackgroundListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + if (role == PendingDeletionRole) { + KPackage::Package b = package(index.row()); + if (!b.isValid()) { + return false; + } + + const QUrl wallpaperUrl = QUrl::fromLocalFile(b.filePath("preferred")); + m_pendingDeletion[wallpaperUrl.toLocalFile()] = value.toBool(); + + Q_EMIT dataChanged(index, index); + return true; + } + + return false; +} + +void BackgroundListModel::showPreview(const KFileItem &item, const QPixmap &preview) +{ + if (!m_wallpaper) { + return; + } + + QPersistentModelIndex index = m_previewJobsUrls.key(item.url()); + m_previewJobsUrls.remove(index); + + if (!index.isValid()) { + return; + } + + KPackage::Package b = package(index.row()); + if (!b.isValid()) { + return; + } + + const int cost = preview.width() * preview.height() * preview.depth() / 8; + m_imageCache.insert(b.filePath("preferred"), new QPixmap(preview), cost); + + // qCDebug(IMAGEWALLPAPER) << "WP preview size:" << preview.size(); + Q_EMIT dataChanged(index, index); +} + +void BackgroundListModel::previewFailed(const KFileItem &item) +{ + m_previewJobsUrls.remove(m_previewJobsUrls.key(item.url())); +} + +KPackage::Package BackgroundListModel::package(int index) const +{ + return m_packages.at(index); +} + +void BackgroundListModel::openContainingFolder(int rowIndex) +{ + KIO::highlightInFileManager({index(rowIndex, 0).data(PathRole).toUrl()}); +} + +void BackgroundListModel::setPendingDeletion(int rowIndex, bool pendingDeletion) +{ + setData(index(rowIndex, 0), pendingDeletion, PendingDeletionRole); +} + +const QStringList BackgroundListModel::wallpapersAwaitingDeletion() +{ + QStringList candidates; + for (const KPackage::Package &b : qAsConst(m_packages)) { + const QUrl wallpaperUrl = QUrl::fromLocalFile(b.filePath("preferred")); + if (m_pendingDeletion.contains(wallpaperUrl.toLocalFile()) && m_pendingDeletion[wallpaperUrl.toLocalFile()]) { + candidates << wallpaperUrl.toLocalFile(); + } + } + + return candidates; +} + +BackgroundFinder::BackgroundFinder(Image *wallpaper, const QStringList &paths) + : QThread(wallpaper) + , m_paths(paths) + , m_token(QUuid::createUuid().toString()) +{ +} + +BackgroundFinder::~BackgroundFinder() +{ + wait(); +} + +QString BackgroundFinder::token() const +{ + return m_token; +} + +QStringList BackgroundFinder::suffixes() +{ + QMutexLocker lock(&s_suffixMutex); + if (s_suffixes.isEmpty()) { + QSet suffixes; + + QMimeDatabase db; + const auto supportedMimeTypes = QImageReader::supportedMimeTypes(); + for (const QByteArray &mimeType : supportedMimeTypes) { + QMimeType mime(db.mimeTypeForName(mimeType)); + const QStringList globPatterns = mime.globPatterns(); + for (const QString &pattern : globPatterns) { + suffixes.insert(pattern); + } + } + + s_suffixes = suffixes.values(); + } + + return s_suffixes; +} + +bool BackgroundFinder::isAcceptableSuffix(const QString &suffix) +{ + // Despite its name, suffixes() returns a list of glob patterns. + // Therefore the file suffix check needs to include the "*." prefix. + const QStringList &globPatterns = suffixes(); + return globPatterns.contains(QLatin1String("*.") + suffix.toLower()); +} + +void BackgroundFinder::run() +{ + QElapsedTimer t; + t.start(); + + QStringList papersFound; + + QDir dir; + dir.setFilter(QDir::AllDirs | QDir::Files | QDir::Readable); + dir.setNameFilters(suffixes()); + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + + int i; + for (i = 0; i < m_paths.count(); ++i) { + const QString path = m_paths.at(i); + dir.setPath(path); + const QFileInfoList files = dir.entryInfoList(); + for (const QFileInfo &wp : files) { + if (wp.isDir()) { + // qCDebug(IMAGEWALLPAPER) << "scanning directory" << wp.fileName(); + + const QString name = wp.fileName(); + if (name == QString::fromLatin1(".") || name == QString::fromLatin1("..")) { + // do nothing + continue; + } + + const QString filePath = wp.filePath(); + if (QFile::exists(filePath + QString::fromLatin1("/metadata.desktop")) || QFile::exists(filePath + QString::fromLatin1("/metadata.json"))) { + package.setPath(filePath); + if (package.isValid()) { + if (!package.filePath("images").isEmpty()) { + papersFound << package.path(); + } + // qCDebug(IMAGEWALLPAPER) << "adding package" << wp.filePath(); + continue; + } + } + + // add this to the directories we should be looking at + m_paths.append(filePath); + } else { + // qCDebug(IMAGEWALLPAPER) << "adding image file" << wp.filePath(); + papersFound << wp.filePath(); + } + } + } + + // qCDebug(IMAGEWALLPAPER) << "WP background found!" << papersFound.size() << "in" << i << "dirs, taking" << t.elapsed() << "ms"; + Q_EMIT backgroundsFound(papersFound, m_token); + deleteLater(); +} + +#endif // BACKGROUNDLISTMODEL_CPP diff --git a/plasma/workspace/wallpapers/image/backgroundlistmodel.h b/plasma/workspace/wallpapers/image/backgroundlistmodel.h new file mode 100644 index 0000000000..f10f938b13 --- /dev/null +++ b/plasma/workspace/wallpapers/image/backgroundlistmodel.h @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2007 Paolo Capriotti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "image.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +class Image; + +class ImageSizeFinder : public QObject, public QRunnable +{ + Q_OBJECT +public: + explicit ImageSizeFinder(const QString &path, QObject *parent = nullptr); + void run() override; + +Q_SIGNALS: + void sizeFound(const QString &path, const QSize &size); + +private: + QString m_path; +}; + +class BackgroundListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + enum { + AuthorRole = Qt::UserRole, + ScreenshotRole, + ResolutionRole, + PathRole, + PackageNameRole, + RemovableRole, + PendingDeletionRole, + ToggleRole, + }; + + static const int BLUR_INCREMENT = 9; + static const int MARGIN = 6; + + BackgroundListModel(Image *listener, QObject *parent); + ~BackgroundListModel() override; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + KPackage::Package package(int index) const; + + void reload(); + void reload(const QStringList &selected); + void addBackground(const QString &path); + void removeBackground(const QString &path); + Q_INVOKABLE int indexOf(const QString &path) const; + virtual bool contains(const QString &bg) const; + + int count() const + { + return m_packages.size(); + } + + Q_INVOKABLE void openContainingFolder(int rowIndex); + Q_INVOKABLE void setPendingDeletion(int rowIndex, bool pendingDeletion); + const QStringList wallpapersAwaitingDeletion(); + +Q_SIGNALS: + void countChanged(); + +protected Q_SLOTS: + void showPreview(const KFileItem &item, const QPixmap &preview); + void previewFailed(const KFileItem &item); + void sizeFound(const QString &path, const QSize &s); + void processPaths(const QStringList &paths); + +protected: + QPointer m_wallpaper; + QString m_findToken; + QList m_packages; + +private: + QSize bestSize(const KPackage::Package &package) const; + + QSet m_removableWallpapers; + QHash m_sizeCache; + QHash m_previewJobsUrls; + KDirWatch m_dirwatch; + QCache m_imageCache; + + int m_screenshotSize; + QHash m_pendingDeletion; +}; + +class BackgroundFinder : public QThread +{ + Q_OBJECT + +public: + BackgroundFinder(Image *wallpaper, const QStringList &p); + ~BackgroundFinder() override; + + QString token() const; + + static QStringList suffixes(); + static bool isAcceptableSuffix(const QString &suffix); + +Q_SIGNALS: + void backgroundsFound(const QStringList &paths, const QString &token); + +protected: + void run() override; + +private: + QStringList m_paths; + QString m_token; + + static QMutex s_suffixMutex; + static QStringList s_suffixes; +}; diff --git a/plasma/workspace/wallpapers/image/image.cpp b/plasma/workspace/wallpapers/image/image.cpp new file mode 100644 index 0000000000..620c2e10c4 --- /dev/null +++ b/plasma/workspace/wallpapers/image/image.cpp @@ -0,0 +1,917 @@ +/* + SPDX-FileCopyrightText: 2007 Paolo Capriotti + SPDX-FileCopyrightText: 2007 Aaron Seigo + SPDX-FileCopyrightText: 2008 Petri Damsten + SPDX-FileCopyrightText: 2008 Alexis Ménard + SPDX-FileCopyrightText: 2014 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "image.h" +#include "debug.h" + +#include // FLT_MAX +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backgroundlistmodel.h" +#include "slidefiltermodel.h" +#include "slidemodel.h" +#include +#include +#include + +#include + +Image::Image(QObject *parent) + : QObject(parent) + , m_ready(false) + , m_delay(10) + , m_dirWatch(new KDirWatch(this)) + , m_mode(SingleImage) + , m_slideshowMode(Random) + , m_slideshowFoldersFirst(false) + , m_currentSlide(-1) + , m_model(nullptr) + , m_slideshowModel(new SlideModel(this, this)) + , m_slideFilterModel(new SlideFilterModel(this)) + , m_dialog(nullptr) +{ + m_wallpaperPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + + connect(&m_timer, &QTimer::timeout, this, &Image::nextSlide); + + connect(m_dirWatch, &KDirWatch::created, this, &Image::pathCreated); + connect(m_dirWatch, &KDirWatch::dirty, this, &Image::pathDirty); + connect(m_dirWatch, &KDirWatch::deleted, this, &Image::pathDeleted); + m_dirWatch->startScan(); + + m_slideFilterModel->setSourceModel(m_slideshowModel); + connect(this, &Image::uncheckedSlidesChanged, m_slideFilterModel, &SlideFilterModel::invalidateFilter); + + useSingleImageDefaults(); +} + +Image::~Image() +{ + delete m_dialog; +} + +void Image::classBegin() +{ +} + +void Image::componentComplete() +{ + // don't bother loading single image until all properties have settled + // otherwise we would load a too small image (initial view size) just + // to load the proper one afterwards etc etc + m_ready = true; + if (m_mode == SingleImage) { + setSingleImage(); + } else if (m_mode == SlideShow) { + // show the last image shown the last time + m_wallpaperPath = m_wallpaper; + Q_EMIT wallpaperPathChanged(); + startSlideshow(); + } +} + +QString Image::photosPath() const +{ + return QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); +} + +QUrl Image::wallpaperPath() const +{ + return QUrl::fromLocalFile(m_wallpaperPath); +} + +void Image::addUrl(const QString &url) +{ + addUrl(QUrl(url), true); +} + +void Image::addUrls(const QStringList &urls) +{ + bool first = true; + for (const QString &url : urls) { + // set the first drop as the current paper, just add the rest to the roll + addUrl(QUrl(url), first); + first = false; + } +} + +Image::RenderingMode Image::renderingMode() const +{ + return m_mode; +} + +void Image::setRenderingMode(RenderingMode mode) +{ + if (mode == m_mode) { + return; + } + + m_mode = mode; + + if (m_mode == SlideShow) { + startSlideshow(); + + updateDirWatch(m_slidePaths); + updateDirWatch(m_slidePaths); + } else { + // we need to reset the preferred image + setSingleImage(); + } +} + +Image::SlideshowMode Image::slideshowMode() const +{ + return m_slideshowMode; +} + +void Image::setSlideshowMode(Image::SlideshowMode slideshowMode) +{ + if (slideshowMode == m_slideshowMode) { + return; + } + m_slideshowMode = slideshowMode; + m_slideFilterModel->setSortingMode(m_slideshowMode, m_slideshowFoldersFirst); + m_slideFilterModel->sort(0); + if (m_mode == SlideShow) { + startSlideshow(); + } + Q_EMIT slideshowModeChanged(); +} + +bool Image::slideshowFoldersFirst() const +{ + return m_slideshowFoldersFirst; +} + +void Image::setSlideshowFoldersFirst(bool slideshowFoldersFirst) +{ + if (slideshowFoldersFirst == m_slideshowFoldersFirst) { + return; + } + m_slideshowFoldersFirst = slideshowFoldersFirst; + m_slideFilterModel->setSortingMode(m_slideshowMode, m_slideshowFoldersFirst); + m_slideFilterModel->sort(0); + if (m_mode == SlideShow) { + startSlideshow(); + } + Q_EMIT slideshowFoldersFirstChanged(); +} + +float distance(const QSize &size, const QSize &desired) +{ + // compute difference of areas + float desiredAspectRatio = (desired.height() > 0) ? desired.width() / (float)desired.height() : 0; + float candidateAspectRatio = (size.height() > 0) ? size.width() / (float)size.height() : FLT_MAX; + + float delta = size.width() - desired.width(); + delta = (delta >= 0.0 ? delta : -delta * 2); // Penalize for scaling up + + return qAbs(candidateAspectRatio - desiredAspectRatio) * 25000 + delta; +} + +QSize resSize(const QString &str) +{ + int index = str.indexOf('x'); + if (index != -1) { + return QSize(str.leftRef(index).toInt(), str.midRef(index + 1).toInt()); + } + + return QSize(); +} + +QString Image::findPreferedImage(const QStringList &images) +{ + if (images.empty()) { + return QString(); + } + + // float targetAspectRatio = (m_targetSize.height() > 0 ) ? m_targetSize.width() / (float)m_targetSize.height() : 0; + // qCDebug(IMAGEWALLPAPER) << "wanted" << m_targetSize << "options" << images << "aspect ratio" << targetAspectRatio; + float best = FLT_MAX; + + QString bestImage; + for (const QString &entry : images) { + QSize candidate = resSize(QFileInfo(entry).baseName()); + if (candidate == QSize()) { + continue; + } + // float candidateAspectRatio = (candidate.height() > 0 ) ? candidate.width() / (float)candidate.height() : FLT_MAX; + + float dist = distance(candidate, m_targetSize); + // qCDebug(IMAGEWALLPAPER) << "candidate" << candidate << "distance" << dist << "aspect ratio" << candidateAspectRatio; + + if (bestImage.isEmpty() || dist < best) { + bestImage = entry; + best = dist; + // qCDebug(IMAGEWALLPAPER) << "best" << bestImage; + } + } + + // qCDebug(IMAGEWALLPAPER) << "best image" << bestImage; + return bestImage; +} + +void Image::findPreferedImageInPackage(KPackage::Package &package) +{ + if (!package.isValid() || !package.filePath("preferred").isEmpty()) { + return; + } + + QString preferred = findPreferedImage(package.entryList("images")); + + package.removeDefinition("preferred"); + package.addFileDefinition("preferred", QStringLiteral("images/") + preferred, i18n("Recommended wallpaper file")); +} + +QSize Image::targetSize() const +{ + return m_targetSize; +} + +void Image::setTargetSize(const QSize &size) +{ + bool sizeChanged = m_targetSize != size; + m_targetSize = size; + + if (m_mode == SingleImage) { + if (sizeChanged) { + // If screen size was changed, we may want to select a new preferred image + // which has correct aspect ratio for the new screen size. + m_wallpaperPackage.removeDefinition("preferred"); + } + setSingleImage(); + } + + if (sizeChanged) { + Q_EMIT targetSizeChanged(); + } +} + +KPackage::Package *Image::package() +{ + return &m_wallpaperPackage; +} + +void Image::useSingleImageDefaults() +{ + m_wallpaper = QString(); + + // Try from the look and feel package first, then from the plasma theme + KPackage::Package lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); + KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); + const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); + // If empty, it will be the default (currently Breeze) + if (!packageName.isEmpty()) { + lookAndFeelPackage.setPath(packageName); + } + + KConfigGroup lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(lookAndFeelPackage.filePath("defaults")), "Wallpaper"); + + const QString image = lnfDefaultsConfig.readEntry("Image", ""); + if (!image.isEmpty()) { + KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + package.setPath(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/") + image, QStandardPaths::LocateDirectory)); + + if (package.isValid()) { + m_wallpaper = package.path(); + } else { + m_wallpaper = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/") + image); + } + } + + // Try to get a default from the plasma theme + if (m_wallpaper.isEmpty()) { + Plasma::Theme theme; + m_wallpaper = theme.wallpaperPath(); + int index = m_wallpaper.indexOf(QString::fromLatin1("/contents/images/")); + if (index > -1) { // We have file from package -> get path to package + m_wallpaper = m_wallpaper.left(index); + } + } +} + +QAbstractItemModel *Image::wallpaperModel() +{ + if (!m_model) { + KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers")); + m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers); + + m_model = new BackgroundListModel(this, this); + m_model->reload(m_usersWallpapers); + } + + return m_model; +} + +QAbstractItemModel *Image::slideFilterModel() +{ + return m_slideFilterModel; +} +int Image::slideTimer() const +{ + return m_delay; +} + +void Image::setSlideTimer(int time) +{ + if (time == m_delay) { + return; + } + + m_delay = time; + + if (m_mode == SlideShow) { + updateDirWatch(m_slidePaths); + startSlideshow(); + } + + Q_EMIT slideTimerChanged(); +} + +QStringList Image::usersWallpapers() const +{ + return m_usersWallpapers; +} + +void Image::setUsersWallpapers(const QStringList &usersWallpapers) +{ + if (usersWallpapers == m_usersWallpapers) { + return; + } + + m_usersWallpapers = usersWallpapers; + + Q_EMIT usersWallpapersChanged(); +} + +QStringList Image::slidePaths() const +{ + return m_slidePaths; +} + +void Image::setSlidePaths(const QStringList &slidePaths) +{ + if (slidePaths == m_slidePaths) { + return; + } + + m_slidePaths = slidePaths; + m_slidePaths.removeAll(QString()); + + if (!m_slidePaths.isEmpty()) { + // Replace 'preferred://wallpaperlocations' with real paths + const QStringList preProcessedPaths = m_slidePaths; + for (const QString &path : preProcessedPaths) { + if (path == QLatin1String("preferred://wallpaperlocations")) { + m_slidePaths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers"), QStandardPaths::LocateDirectory); + m_slidePaths.removeAll(path); + } + } + } + + if (m_mode == SlideShow) { + updateDirWatch(m_slidePaths); + startSlideshow(); + } + if (m_slideshowModel) { + m_slideshowModel->reload(m_slidePaths); + } + Q_EMIT slidePathsChanged(); +} + +void Image::showAddSlidePathsDialog() +{ + QFileDialog *dialog = new QFileDialog(nullptr, i18n("Directory with the wallpaper to show slides from"), QString()); + dialog->setAttribute(Qt::WA_DeleteOnClose, true); + dialog->setOptions(QFileDialog::ShowDirsOnly); + dialog->setAcceptMode(QFileDialog::AcceptOpen); + connect(dialog, &QDialog::accepted, this, &Image::addDirFromSelectionDialog); + dialog->show(); +} + +void Image::addSlidePath(const QString &path) +{ + if (!path.isEmpty() && !m_slidePaths.contains(path)) { + m_slidePaths.append(path); + if (m_mode == SlideShow) { + updateDirWatch(m_slidePaths); + } + if (m_slideshowModel) { + m_slideshowModel->addDirs({m_slidePaths}); + } + Q_EMIT slidePathsChanged(); + startSlideshow(); + } +} + +void Image::removeSlidePath(const QString &path) +{ + if (m_slidePaths.contains(path)) { + m_slidePaths.removeAll(path); + if (m_mode == SlideShow) { + updateDirWatch(m_slidePaths); + } + if (m_slideshowModel) { + bool haveParent = false; + QStringList children; + for (const QString &slidePath : qAsConst(m_slidePaths)) { + if (path.startsWith(slidePath)) { + haveParent = true; + } + if (slidePath.startsWith(path)) { + children.append(slidePath); + } + } + /*If we have the parent directory do nothing since the directories are recursively searched. + * If we have child directories just reload since removing the parent and then readding the children would + * induce a race.*/ + if (!haveParent) { + if (children.size() > 0) { + m_slideshowModel->reload(m_slidePaths); + } else { + m_slideshowModel->removeDir(path); + } + } + } + + Q_EMIT slidePathsChanged(); + startSlideshow(); + } +} + +void Image::pathDirty(const QString &path) +{ + updateDirWatch(QStringList(path)); +} + +void Image::updateDirWatch(const QStringList &newDirs) +{ + Q_FOREACH (const QString &oldDir, m_dirs) { + if (!newDirs.contains(oldDir)) { + m_dirWatch->removeDir(oldDir); + } + } + + Q_FOREACH (const QString &newDir, newDirs) { + if (!m_dirWatch->contains(newDir)) { + m_dirWatch->addDir(newDir, KDirWatch::WatchSubDirs | KDirWatch::WatchFiles); + } + } + + m_dirs = newDirs; +} + +void Image::addDirFromSelectionDialog() +{ + QFileDialog *dialog = qobject_cast(sender()); + if (dialog) { + addSlidePath(dialog->directoryUrl().toLocalFile()); + } +} + +void Image::syncWallpaperPackage() +{ + m_wallpaperPackage.setPath(m_wallpaper); + findPreferedImageInPackage(m_wallpaperPackage); + m_wallpaperPath = m_wallpaperPackage.filePath("preferred"); +} + +void Image::setSingleImage() +{ + if (!m_ready) { + return; + } + + // supposedly QSize::isEmpty() is true if "either width or height are >= 0" + if (!m_targetSize.width() || !m_targetSize.height()) { + return; + } + + const QString oldPath = m_wallpaperPath; + if (m_wallpaper.isEmpty()) { + useSingleImageDefaults(); + } + + QString img; + if (QDir::isAbsolutePath(m_wallpaper)) { + syncWallpaperPackage(); + + if (QFile::exists(m_wallpaperPath)) { + img = m_wallpaperPath; + } + } else { + // if it's not an absolute path, check if it's just a wallpaper name + QString path = + QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("wallpapers/") + m_wallpaper + QString::fromLatin1("/metadata.json")); + if (path.isEmpty()) + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QLatin1String("wallpapers/") + m_wallpaper + QString::fromLatin1("/metadata.desktop")); + + if (!path.isEmpty()) { + QDir dir(path); + dir.cdUp(); + + syncWallpaperPackage(); + img = m_wallpaperPath; + } + } + + if (img.isEmpty()) { + // ok, so the package we have failed to work out; let's try the default + useSingleImageDefaults(); + syncWallpaperPackage(); + } + + if (m_wallpaperPath != oldPath) { + Q_EMIT wallpaperPathChanged(); + } +} + +void Image::addUrls(const QList &urls) +{ + bool first = true; + for (const QUrl &url : urls) { + // set the first drop as the current paper, just add the rest to the roll + addUrl(url, first); + first = false; + } +} + +void Image::addUrl(const QUrl &url, bool setAsCurrent) +{ + QString path; + if (url.isLocalFile()) { + path = url.toLocalFile(); + } else if (url.scheme().isEmpty()) { + if (QDir::isAbsolutePath(url.path())) { + path = url.path(); + } else { + path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("wallpapers/") + url.path(), QStandardPaths::LocateDirectory); + } + + if (path.isEmpty()) { + return; + } + } else { + QDir wallpaperDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/wallpapers/"); + const QString wallpaperPath = wallpaperDir.absoluteFilePath(url.fileName()); + + if (wallpaperDir.mkpath(wallpaperDir.absolutePath()) && !url.fileName().isEmpty()) { + KIO::CopyJob *job = KIO::copy(url, QUrl::fromLocalFile(wallpaperPath), KIO::HideProgressInfo); + + if (setAsCurrent) { + connect(job, &KJob::result, this, &Image::setWallpaperRetrieved); + } else { + connect(job, &KJob::result, this, &Image::addWallpaperRetrieved); + } + } + + return; + } + + if (setAsCurrent) { + setWallpaper(path); + } else { + if (m_mode != SingleImage) { + // it's a slide show, add it to the slide show + m_slideshowModel->addBackground(path); + } + // always add it to the user papers, though + addUsersWallpaper(path); + } +} + +void Image::setWallpaperRetrieved(KJob *job) +{ + KIO::CopyJob *copyJob = qobject_cast(job); + if (copyJob && !copyJob->error()) { + setWallpaper(copyJob->destUrl().toLocalFile()); + } +} + +void Image::addWallpaperRetrieved(KJob *job) +{ + KIO::CopyJob *copyJob = qobject_cast(job); + if (copyJob && !copyJob->error()) { + addUrl(copyJob->destUrl(), false); + } +} + +void Image::setWallpaper(const QString &path) +{ + if (m_mode == SingleImage) { + m_wallpaper = path; + setSingleImage(); + } else { + m_wallpaper = path; + m_slideshowModel->addBackground(path); + m_currentSlide = m_slideFilterModel->indexOf(path) - 1; + nextSlide(); + } + // addUsersWallpaper(path); +} + +void Image::startSlideshow() +{ + if (!m_ready || m_slideFilterModel->property("usedInConfig").toBool()) { + return; + } + // populate background list + m_timer.stop(); + m_slideshowModel->reload(m_slidePaths); + connect(m_slideshowModel, &SlideModel::done, this, &Image::backgroundsFound); + // TODO: what would be cool: paint on the wallpaper itself a busy widget and perhaps some text + // about loading wallpaper slideshow while the thread runs +} + +void Image::backgroundsFound() +{ + disconnect(m_slideshowModel, &SlideModel::done, this, 0); + + if (m_scanDirty) { + m_scanDirty = false; + startSlideshow(); + return; + } + + // start slideshow + if (m_slideFilterModel->rowCount() == 0) { + // no image has been found, which is quite weird... try again later (this is useful for events which + // are not detected by KDirWatch, like a NFS directory being mounted) + QTimer::singleShot(1000, this, &Image::startSlideshow); + } else { + if (m_currentSlide == -1) { + m_currentSlide = m_slideFilterModel->indexOf(m_wallpaper) - 1; + } else { + m_currentSlide = -1; + } + m_slideFilterModel->sort(0); + nextSlide(); + m_timer.start(m_delay * 1000); + } +} + +void Image::newStuffFinished() +{ + if (m_model) { + m_model->reload(m_usersWallpapers); + } +} + +void Image::showFileDialog() +{ + if (!m_dialog) { + QString path; + const QStringList &locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); + + if (!locations.isEmpty()) { + path = locations.at(0); + } else { + // HomeLocation is guaranteed not to be empty. + path = QStandardPaths::standardLocations(QStandardPaths::HomeLocation).at(0); + } + + QMimeDatabase db; + QStringList imageGlobPatterns; + foreach (const QByteArray &mimeType, QImageReader::supportedMimeTypes()) { + QMimeType mime(db.mimeTypeForName(mimeType)); + imageGlobPatterns << mime.globPatterns(); + } + + m_dialog = new QFileDialog(nullptr, i18n("Open Image"), path, i18n("Image Files") + " (" + imageGlobPatterns.join(' ') + ')'); + // i18n people, this isn't a "word puzzle". there is a specific string format for QFileDialog::setNameFilters + + m_dialog->setFileMode(QFileDialog::ExistingFiles); + connect(m_dialog, &QDialog::accepted, this, &Image::wallpaperBrowseCompleted); + } + + m_dialog->show(); + m_dialog->raise(); + m_dialog->activateWindow(); +} + +void Image::fileDialogFinished() +{ + m_dialog = nullptr; +} + +void Image::wallpaperBrowseCompleted() +{ + Q_ASSERT(m_model); + if (m_dialog && m_dialog->selectedFiles().count() > 0) { + const QStringList selectedFiles = m_dialog->selectedFiles(); + for (const QString &image : selectedFiles) { + addUsersWallpaper(image); + } + Q_EMIT customWallpaperPicked(m_dialog->selectedFiles().first()); + } +} + +void Image::addUsersWallpaper(const QString &file) +{ + QString f = file; + f.remove(QLatin1String("file:/")); + const QFileInfo info(f); // FIXME + + // the full file path, so it isn't broken when dealing with symlinks + const QString wallpaper = info.canonicalFilePath(); + + if (wallpaper.isEmpty()) { + return; + } + if (m_model) { + if (m_model->contains(wallpaper)) { + return; + } + // add background to the model + m_model->addBackground(wallpaper); + } + // save it + KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers")); + m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers); + + if (!m_usersWallpapers.contains(wallpaper)) { + m_usersWallpapers.prepend(wallpaper); + cfg.writeEntry("usersWallpapers", m_usersWallpapers); + cfg.sync(); + Q_EMIT usersWallpapersChanged(); + } +} + +void Image::nextSlide() +{ + if (!m_ready || m_slideFilterModel->rowCount() == 0) { + return; + } + int previousSlide = m_currentSlide; + QUrl previousPath = m_slideFilterModel->index(m_currentSlide, 0).data(BackgroundListModel::PathRole).toUrl(); + if (m_currentSlide == m_slideFilterModel->rowCount() - 1 || m_currentSlide < 0) { + m_currentSlide = 0; + } else { + m_currentSlide += 1; + } + // We are starting again - avoid having the same random order when we restart the slideshow + if (m_slideshowMode == Random && m_currentSlide == 0) { + m_slideFilterModel->invalidate(); + } + QUrl next = m_slideFilterModel->index(m_currentSlide, 0).data(BackgroundListModel::PathRole).toUrl(); + // And avoid showing the same picture twice + if (previousSlide == m_slideFilterModel->rowCount() - 1 && previousPath == next && m_slideFilterModel->rowCount() > 1) { + m_currentSlide += 1; + next = m_slideFilterModel->index(m_currentSlide, 0).data(BackgroundListModel::PathRole).toUrl(); + } + m_timer.stop(); + m_timer.start(m_delay * 1000); + if (next.isEmpty()) { + m_wallpaperPath = previousPath.toLocalFile(); + } else { + m_wallpaperPath = next.toLocalFile(); + } + Q_EMIT wallpaperPathChanged(); +} + +void Image::pathCreated(const QString &path) +{ + if (m_slideshowModel->indexOf(path) == -1) { + QFileInfo fileInfo(path); + if (fileInfo.isFile() && BackgroundFinder::isAcceptableSuffix(fileInfo.suffix())) { + m_slideshowModel->addBackground(path); + if (m_slideFilterModel->rowCount() == 1) { + nextSlide(); + } + } + } +} + +void Image::pathDeleted(const QString &path) +{ + if (m_slideshowModel->indexOf(path) != -1) { + m_slideshowModel->removeBackground(path); + if (path == m_img) { + nextSlide(); + } + } +} + +// FIXME: we have to save the configuration also when the dialog cancel button is clicked. +void Image::removeWallpaper(QString name) +{ + QString localWallpapers = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/wallpapers/"; + QUrl nameUrl(name); + + // Package plugin name + if (!name.contains('/')) { + KPackage::Package p = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images")); + KJob *j = p.uninstall(name, localWallpapers); + connect(j, &KJob::finished, [=]() { + m_model->reload(m_usersWallpapers); + }); + // absolute path in the home + } else if (nameUrl.path().startsWith(localWallpapers)) { + QFile f(nameUrl.path()); + if (f.exists()) { + f.remove(); + } + m_model->reload(m_usersWallpapers); + } else { + // save it + KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers")); + m_usersWallpapers = cfg.readEntry("usersWallpapers", m_usersWallpapers); + + int wallpaperIndex = -1; + // passed as a path or as a file:// url? + if (nameUrl.isValid()) { + wallpaperIndex = m_usersWallpapers.indexOf(nameUrl.path()); + } else { + wallpaperIndex = m_usersWallpapers.indexOf(name); + } + if (wallpaperIndex >= 0) { + m_usersWallpapers.removeAt(wallpaperIndex); + m_model->reload(m_usersWallpapers); + cfg.writeEntry("usersWallpapers", m_usersWallpapers); + cfg.sync(); + Q_EMIT usersWallpapersChanged(); + Q_EMIT settingsChanged(true); + } + } +} + +void Image::commitDeletion() +{ + // This is invokable from qml, so at any moment + // we can't be sure the model exists + if (!m_model) { + return; + } + + for (const QString &wallpaperCandidate : m_model->wallpapersAwaitingDeletion()) { + removeWallpaper(wallpaperCandidate); + } +} + +void Image::openFolder(const QString &path) +{ + auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(path)); + auto *delegate = new KNotificationJobUiDelegate; + delegate->setAutoErrorHandlingEnabled(true); + job->setUiDelegate(delegate); + job->start(); +} + +void Image::toggleSlide(const QString &path, bool checked) +{ + if (checked && m_uncheckedSlides.contains(path)) { + m_uncheckedSlides.removeAll(path); + Q_EMIT uncheckedSlidesChanged(); + startSlideshow(); + } else if (!checked && !m_uncheckedSlides.contains(path)) { + m_uncheckedSlides.append(path); + Q_EMIT uncheckedSlidesChanged(); + startSlideshow(); + } +} + +QStringList Image::uncheckedSlides() const +{ + return m_uncheckedSlides; +} + +void Image::setUncheckedSlides(const QStringList &uncheckedSlides) +{ + if (uncheckedSlides == m_uncheckedSlides) { + return; + } + m_uncheckedSlides = uncheckedSlides; + Q_EMIT uncheckedSlidesChanged(); + startSlideshow(); +} diff --git a/plasma/workspace/wallpapers/image/image.h b/plasma/workspace/wallpapers/image/image.h new file mode 100644 index 0000000000..bcbb2ddfe3 --- /dev/null +++ b/plasma/workspace/wallpapers/image/image.h @@ -0,0 +1,192 @@ +/* + SPDX-FileCopyrightText: 2007 Paolo Capriotti + SPDX-FileCopyrightText: 2008 Petri Damsten + SPDX-FileCopyrightText: 2014 Sebastian Kügler + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class QFileDialog; +class QQuickItem; + +class KDirWatch; +class KJob; +class BackgroundListModel; +class SlideModel; +class SlideFilterModel; + +class Image : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(RenderingMode renderingMode READ renderingMode WRITE setRenderingMode NOTIFY renderingModeChanged) + Q_PROPERTY(SlideshowMode slideshowMode READ slideshowMode WRITE setSlideshowMode NOTIFY slideshowModeChanged) + Q_PROPERTY(bool slideshowFoldersFirst READ slideshowFoldersFirst WRITE setSlideshowFoldersFirst NOTIFY slideshowFoldersFirstChanged) + Q_PROPERTY(QUrl wallpaperPath READ wallpaperPath NOTIFY wallpaperPathChanged) + Q_PROPERTY(QAbstractItemModel *wallpaperModel READ wallpaperModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *slideFilterModel READ slideFilterModel CONSTANT) + Q_PROPERTY(int slideTimer READ slideTimer WRITE setSlideTimer NOTIFY slideTimerChanged) + Q_PROPERTY(QStringList usersWallpapers READ usersWallpapers WRITE setUsersWallpapers NOTIFY usersWallpapersChanged) + Q_PROPERTY(QStringList slidePaths READ slidePaths WRITE setSlidePaths NOTIFY slidePathsChanged) + Q_PROPERTY(QSize targetSize READ targetSize WRITE setTargetSize NOTIFY targetSizeChanged) + Q_PROPERTY(QString photosPath READ photosPath CONSTANT) + Q_PROPERTY(QStringList uncheckedSlides READ uncheckedSlides WRITE setUncheckedSlides NOTIFY uncheckedSlidesChanged) + +public: + enum RenderingMode { + SingleImage, + SlideShow, + }; + Q_ENUM(RenderingMode) + + enum SlideshowMode { + Random, + Alphabetical, + AlphabeticalReversed, + Modified, + ModifiedReversed, + }; + Q_ENUM(SlideshowMode) + + explicit Image(QObject* parent = nullptr); + ~Image() override; + + QUrl wallpaperPath() const; + + // this is for QML use + Q_INVOKABLE void addUrl(const QString &url); + Q_INVOKABLE void addUrls(const QStringList &urls); + + Q_INVOKABLE void addSlidePath(const QString &path); + Q_INVOKABLE void removeSlidePath(const QString &path); + Q_INVOKABLE void openFolder(const QString &path); + + Q_INVOKABLE void showFileDialog(); + + Q_INVOKABLE void addUsersWallpaper(const QString &file); + Q_INVOKABLE void commitDeletion(); + + Q_INVOKABLE void toggleSlide(const QString &path, bool checked); + + RenderingMode renderingMode() const; + void setRenderingMode(RenderingMode mode); + + SlideshowMode slideshowMode() const; + void setSlideshowMode(SlideshowMode slideshowMode); + + bool slideshowFoldersFirst() const; + void setSlideshowFoldersFirst(bool slideshowFoldersFirst); + + QSize targetSize() const; + void setTargetSize(const QSize &size); + + KPackage::Package *package(); + + QAbstractItemModel *wallpaperModel(); + QAbstractItemModel *slideFilterModel(); + + int slideTimer() const; + void setSlideTimer(int time); + + QStringList usersWallpapers() const; + void setUsersWallpapers(const QStringList &usersWallpapers); + + QStringList slidePaths() const; + void setSlidePaths(const QStringList &slidePaths); + + void findPreferedImageInPackage(KPackage::Package &package); + QString findPreferedImage(const QStringList &images); + + void classBegin() override; + void componentComplete() override; + + QString photosPath() const; + + QStringList uncheckedSlides() const; + void setUncheckedSlides(const QStringList &uncheckedSlides); + +public Q_SLOTS: + void nextSlide(); + void removeWallpaper(QString name); + +Q_SIGNALS: + void settingsChanged(bool); + void wallpaperPathChanged(); + void renderingModeChanged(); + void slideshowModeChanged(); + void slideshowFoldersFirstChanged(); + void targetSizeChanged(); + void slideTimerChanged(); + void usersWallpapersChanged(); + void slidePathsChanged(); + void resizeMethodChanged(); + void customWallpaperPicked(const QString &path); + void uncheckedSlidesChanged(); + +protected Q_SLOTS: + void showAddSlidePathsDialog(); + void wallpaperBrowseCompleted(); + void startSlideshow(); + void fileDialogFinished(); + void addUrl(const QUrl &url, bool setAsCurrent); + void addUrls(const QList &urls); + void setWallpaper(const QString &path); + void setWallpaperRetrieved(KJob *job); + void addWallpaperRetrieved(KJob *job); + void newStuffFinished(); + void updateDirWatch(const QStringList &newDirs); + void addDirFromSelectionDialog(); + void pathCreated(const QString &path); + void pathDeleted(const QString &path); + void pathDirty(const QString &path); + void backgroundsFound(); + +protected: + void syncWallpaperPackage(); + void setSingleImage(); + void useSingleImageDefaults(); + +private: + bool m_ready; + int m_delay; + QStringList m_dirs; + QString m_wallpaper; + QString m_wallpaperPath; + QStringList m_usersWallpapers; + KDirWatch *m_dirWatch; + bool m_scanDirty; + QSize m_targetSize; + + RenderingMode m_mode; + SlideshowMode m_slideshowMode; + bool m_slideshowFoldersFirst; + + KPackage::Package m_wallpaperPackage; + QStringList m_slidePaths; + QStringList m_uncheckedSlides; + QTimer m_timer; + int m_currentSlide; + BackgroundListModel *m_model; + SlideModel *m_slideshowModel; + SlideFilterModel *m_slideFilterModel; + QFileDialog *m_dialog; + QString m_img; + QDateTime m_previousModified; + QString m_findToken; +}; diff --git a/plasma/workspace/wallpapers/image/imagepackage/contents/config/main.xml b/plasma/workspace/wallpapers/image/imagepackage/contents/config/main.xml new file mode 100644 index 0000000000..9c8251d937 --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/contents/config/main.xml @@ -0,0 +1,50 @@ + + + + + + + + false + + + + false + + + + #000000 + + + + + + + + 2 + + + + preferred://wallpaperlocations + + + + 900 + + + + 1000 + + + + + + + + 0 + + + diff --git a/plasma/workspace/wallpapers/image/imagepackage/contents/ui/WallpaperDelegate.qml b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/WallpaperDelegate.qml new file mode 100644 index 0000000000..b52ba5dc46 --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/WallpaperDelegate.qml @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls.Private 1.0 +import QtQuick.Controls 2.3 as QtControls2 +import QtGraphicalEffects 1.0 + +import org.kde.plasma.core 2.0 as PlasmaCore + +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kcm 1.1 as KCM + +KCM.GridDelegate { + id: wallpaperDelegate + + property alias color: backgroundRect.color + readonly property bool selected: (GridView.currentIndex === index) + opacity: model.pendingDeletion ? 0.5 : 1 + + text: model.display + subtitle: model.author + + hoverEnabled: true + + actions: [ + Kirigami.Action { + icon.name: "document-open-folder" + tooltip: i18nd("plasma_wallpaper_org.kde.image", "Open Containing Folder") + onTriggered: imageModel.openContainingFolder(index) + }, + Kirigami.Action { + icon.name: "edit-undo" + visible: model.pendingDeletion + tooltip: i18nd("plasma_wallpaper_org.kde.image", "Restore wallpaper") + onTriggered: imageModel.setPendingDeletion(index, !model.pendingDeletion) + }, + Kirigami.Action { + icon.name: "edit-delete" + tooltip: i18nd("plasma_wallpaper_org.kde.image", "Remove Wallpaper") + visible: model.removable && !model.pendingDeletion && configDialog.currentWallpaper == "org.kde.image" + onTriggered: { + imageModel.setPendingDeletion(index, true); + if (wallpapersGrid.currentIndex === index) { + wallpapersGrid.currentIndex = (index + 1) % wallpapersGrid.rowCount(); + } + } + } + ] + + thumbnail: Rectangle { + id: backgroundRect + color: cfg_Color + anchors.fill: parent + + QIconItem { + anchors.centerIn: parent + width: PlasmaCore.Units.iconSizes.large + height: width + icon: "view-preview" + visible: !walliePreview.visible + } + + QPixmapItem { + id: blurBackgroundSource + visible: cfg_Blur + anchors.fill: parent + smooth: true + pixmap: model.screenshot + fillMode: QPixmapItem.PreserveAspectCrop + } + + FastBlur { + visible: cfg_Blur + anchors.fill: parent + source: blurBackgroundSource + radius: 4 + } + + QPixmapItem { + id: walliePreview + anchors.fill: parent + visible: model.screenshot !== null + smooth: true + pixmap: model.screenshot + fillMode: { + if (cfg_FillMode == Image.Stretch) { + return QPixmapItem.Stretch; + } else if (cfg_FillMode == Image.PreserveAspectFit) { + return QPixmapItem.PreserveAspectFit; + } else if (cfg_FillMode == Image.PreserveAspectCrop) { + return QPixmapItem.PreserveAspectCrop; + } else if (cfg_FillMode == Image.Tile) { + return QPixmapItem.Tile; + } else if (cfg_FillMode == Image.TileVertically) { + return QPixmapItem.TileVertically; + } else if (cfg_FillMode == Image.TileHorizontally) { + return QPixmapItem.TileHorizontally; + } + return QPixmapItem.PreserveAspectFit; + } + } + QtControls2.CheckBox { + visible: configDialog.currentWallpaper == "org.kde.slideshow" + anchors.right: parent.right + anchors.top: parent.top + checked: visible ? model.checked : false + onToggled: imageWallpaper.toggleSlide(model.path, checked) + } + } + + onClicked: { + if (configDialog.currentWallpaper == "org.kde.image") { + cfg_Image = model.packageName || model.path; + } + GridView.currentIndex = index; + } +} diff --git a/plasma/workspace/wallpapers/image/imagepackage/contents/ui/config.qml b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/config.qml new file mode 100644 index 0000000000..bbd43eac3c --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/config.qml @@ -0,0 +1,500 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.5 as QtControls2 +import QtQuick.Layouts 1.0 +import QtQuick.Window 2.0 // for Screen +import org.kde.plasma.wallpapers.image 2.0 as Wallpaper +import org.kde.kquickcontrols 2.0 as KQuickControls +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.newstuff 1.62 as NewStuff +import org.kde.kcm 1.5 as KCM +import org.kde.kirigami 2.12 as Kirigami + +ColumnLayout { + id: root + property alias cfg_Color: colorButton.color + property color cfg_ColorDefault + property string cfg_Image + property string cfg_ImageDefault + property int cfg_FillMode + property int cfg_FillModeDefault + property int cfg_SlideshowMode + property int cfg_SlideshowModeDefault + property bool cfg_SlideshowFoldersFirst + property bool cfg_SlideshowFoldersFirstDefault: false + property alias cfg_Blur: blurRadioButton.checked + property bool cfg_BlurDefault + property var cfg_SlidePaths: [] + property var cfg_SlidePathsDefault: [] + property int cfg_SlideInterval: 0 + property int cfg_SlideIntervalDefault: 0 + property var cfg_UncheckedSlides: [] + property var cfg_UncheckedSlidesDefault: [] + + function saveConfig() { + imageWallpaper.commitDeletion(); + } + + SystemPalette { + id: syspal + } + + Wallpaper.Image { + id: imageWallpaper + targetSize: { + if (typeof plasmoid !== "undefined") { + return Qt.size(plasmoid.width, plasmoid.height) + } + // Lock screen configuration case + return Qt.size(Screen.width, Screen.height) + } + onSlidePathsChanged: cfg_SlidePaths = slidePaths + onUncheckedSlidesChanged: cfg_UncheckedSlides = uncheckedSlides + onSlideshowModeChanged: cfg_SlideshowMode = slideshowMode + onSlideshowFoldersFirstChanged: cfg_SlideshowFoldersFirst = slideshowFoldersFirst + } + + onCfg_FillModeChanged: { + resizeComboBox.setMethod() + } + + onCfg_SlidePathsChanged: { + imageWallpaper.slidePaths = cfg_SlidePaths + } + onCfg_UncheckedSlidesChanged: { + imageWallpaper.uncheckedSlides = cfg_UncheckedSlides + } + + onCfg_SlideshowModeChanged: { + imageWallpaper.slideshowMode = cfg_SlideshowMode + } + + onCfg_SlideshowFoldersFirstChanged: { + imageWallpaper.slideshowFoldersFirst = cfg_SlideshowFoldersFirst + } + + property int hoursIntervalValue: Math.floor(cfg_SlideInterval / 3600) + property int minutesIntervalValue: Math.floor(cfg_SlideInterval % 3600) / 60 + property int secondsIntervalValue: cfg_SlideInterval % 3600 % 60 + + property int hoursIntervalValueDefault: Math.floor(cfg_SlideIntervalDefault / 3600) + property int minutesIntervalValueDefault: Math.floor(cfg_SlideIntervalDefault % 3600) / 60 + property int secondsIntervalValueDefault: cfg_SlideIntervalDefault % 3600 % 60 + + //Rectangle { color: "orange"; x: formAlignment; width: formAlignment; height: 20 } + + Kirigami.FormLayout { + twinFormLayouts: parentLayout + QtControls2.ComboBox { + id: resizeComboBox + Layout.preferredWidth: Math.max(implicitWidth, wallpaperComboBox.implicitWidth) + Kirigami.FormData.label: i18nd("plasma_wallpaper_org.kde.image", "Positioning:") + model: [ + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Scaled and Cropped"), + 'fillMode': Image.PreserveAspectCrop + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Scaled"), + 'fillMode': Image.Stretch + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Scaled, Keep Proportions"), + 'fillMode': Image.PreserveAspectFit + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Centered"), + 'fillMode': Image.Pad + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Tiled"), + 'fillMode': Image.Tile + } + ] + + textRole: "label" + onCurrentIndexChanged: cfg_FillMode = model[currentIndex]["fillMode"] + Component.onCompleted: setMethod(); + + KCM.SettingHighlighter { + highlight: cfg_FillModeDefault != cfg_FillMode + } + + function setMethod() { + for (var i = 0; i < model.length; i++) { + if (model[i]["fillMode"] === root.cfg_FillMode) { + resizeComboBox.currentIndex = i; + var tl = model[i]["label"].length; + //resizeComboBox.textLength = Math.max(resizeComboBox.textLength, tl+5); + } + } + } + } + + QtControls2.ButtonGroup { id: backgroundGroup } + + QtControls2.RadioButton { + id: blurRadioButton + visible: cfg_FillMode === Image.PreserveAspectFit || cfg_FillMode === Image.Pad + Kirigami.FormData.label: i18nd("plasma_wallpaper_org.kde.image", "Background:") + text: i18nd("plasma_wallpaper_org.kde.image", "Blur") + QtControls2.ButtonGroup.group: backgroundGroup + } + + RowLayout { + id: colorRow + visible: cfg_FillMode === Image.PreserveAspectFit || cfg_FillMode === Image.Pad + QtControls2.RadioButton { + id: colorRadioButton + text: i18nd("plasma_wallpaper_org.kde.image", "Solid color") + checked: !cfg_Blur + QtControls2.ButtonGroup.group: backgroundGroup + + KCM.SettingHighlighter { + highlight: cfg_Blur != cfg_BlurDefault + } + } + KQuickControls.ColorButton { + id: colorButton + dialogTitle: i18nd("plasma_wallpaper_org.kde.image", "Select Background Color") + + KCM.SettingHighlighter { + highlight: cfg_Color != cfg_ColorDefault + } + } + } + } + + Component { + id: slideshowComponent + ColumnLayout { + Connections { + target: root + function onHoursIntervalValueChanged() {hoursInterval.value = root.hoursIntervalValue} + function onMinutesIntervalValueChanged() {minutesInterval.value = root.minutesIntervalValue} + function onSecondsIntervalValueChanged() {secondsInterval.value = root.secondsIntervalValue} + } + + Kirigami.FormLayout { + twinFormLayouts: parentLayout + + RowLayout { + id: slideshowModeRow + Kirigami.FormData.label: i18nd("plasma_wallpaper_org.kde.image", "Order:") + + QtControls2.ComboBox { + id: slideshowModeComboBox + + model: [ + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Random"), + 'slideshowMode': Wallpaper.Image.Random + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "A to Z"), + 'slideshowMode': Wallpaper.Image.Alphabetical + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Z to A"), + 'slideshowMode': Wallpaper.Image.AlphabeticalReversed + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Date modified (newest first)"), + 'slideshowMode': Wallpaper.Image.ModifiedReversed + }, + { + 'label': i18nd("plasma_wallpaper_org.kde.image", "Date modified (oldest first)"), + 'slideshowMode': Wallpaper.Image.Modified + } + ] + textRole: "label" + onCurrentIndexChanged: { + cfg_SlideshowMode = model[currentIndex]["slideshowMode"]; + } + Component.onCompleted: setMethod(); + function setMethod() { + for (var i = 0; i < model.length; i++) { + if (model[i]["slideshowMode"] === wallpaper.configuration.SlideshowMode) { + slideshowModeComboBox.currentIndex = i; + } + } + } + + KCM.SettingHighlighter { + highlight: cfg_SlideshowMode != cfg_SlideshowModeDefault + } + } + + QtControls2.CheckBox { + id: slideshowFoldersFirstCheckBox + text: i18nd("plasma_wallpaper_org.kde.image", "Group by folders") + checked: root.cfg_SlideshowFoldersFirst + onToggled: cfg_SlideshowFoldersFirst = slideshowFoldersFirstCheckBox.checked + + KCM.SettingHighlighter { + highlight: root.cfg_SlideshowFoldersFirst !== cfg_SlideshowFoldersFirstDefault + } + } + } + + // FIXME: there should be only one spinbox: QtControls spinboxes are still too limited for it tough + RowLayout { + Kirigami.FormData.label: i18nd("plasma_wallpaper_org.kde.image", "Change every:") + QtControls2.SpinBox { + id: hoursInterval + value: root.hoursIntervalValue + from: 0 + to: 24 + editable: true + onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value + + textFromValue: function(value, locale) { + return i18ndp("plasma_wallpaper_org.kde.image","%1 hour", "%1 hours", value) + } + valueFromText: function(text, locale) { + return parseInt(text); + } + + KCM.SettingHighlighter { + highlight: root.hoursIntervalValue != root.hoursIntervalValueDefault + } + } + QtControls2.SpinBox { + id: minutesInterval + value: root.minutesIntervalValue + from: 0 + to: 60 + editable: true + onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value + + textFromValue: function(value, locale) { + return i18ndp("plasma_wallpaper_org.kde.image","%1 minute", "%1 minutes", value) + } + valueFromText: function(text, locale) { + return parseInt(text); + } + + KCM.SettingHighlighter { + highlight: root.minutesIntervalValue != root.minutesIntervalValueDefault + } + } + QtControls2.SpinBox { + id: secondsInterval + value: root.secondsIntervalValue + from: root.hoursIntervalValue === 0 && root.minutesIntervalValue === 0 ? 1 : 0 + to: 60 + editable: true + onValueChanged: cfg_SlideInterval = hoursInterval.value * 3600 + minutesInterval.value * 60 + secondsInterval.value + + textFromValue: function(value, locale) { + return i18ndp("plasma_wallpaper_org.kde.image","%1 second", "%1 seconds", value) + } + valueFromText: function(text, locale) { + return parseInt(text); + } + + KCM.SettingHighlighter { + highlight: root.secondsIntervalValue != root.secondsIntervalValueDefault + } + } + } + } + Kirigami.Heading { + text: i18nd("plasma_wallpaper_org.kde.image", "Folders") + level: 2 + } + GridLayout { + columns: 2 + Layout.fillWidth: true + Layout.fillHeight: true + columnSpacing: Kirigami.Units.largeSpacing + QtControls2.ScrollView { + id: foldersScroll + Layout.fillHeight: true + Layout.preferredWidth: 0.35 * parent.width + Layout.maximumWidth: Kirigami.Units.gridUnit * 16 + Component.onCompleted: foldersScroll.background.visible = true; + ListView { + id: slidePathsView + model: imageWallpaper.slidePaths + delegate: Kirigami.SwipeListItem { + width: slidePathsView.width + // content item includes its own padding + padding: 0 + // Don't need a highlight or hover effects + hoverEnabled: false + contentItem: Kirigami.BasicListItem { + // Don't need a highlight or hover effects + hoverEnabled: false + separatorVisible: false + + // Header: the folder + label: { + var strippedPath = modelData.replace(/\/+$/, ""); + return strippedPath.split('/').pop() + } + // Subtitle: the path to the folder + subtitle: { + var strippedPath = modelData.replace(/\/+$/, ""); + return strippedPath.replace(/\/[^\/]*$/, '');; + } + + QtControls2.ToolTip.text: modelData + QtControls2.ToolTip.visible: hovered + QtControls2.ToolTip.delay: 1000 + QtControls2.ToolTip.timeout: 5000 + } + + actions: [ + Kirigami.Action { + iconName: "list-remove" + tooltip: i18nd("plasma_wallpaper_org.kde.image", "Remove Folder") + onTriggered: imageWallpaper.removeSlidePath(modelData) + }, + Kirigami.Action { + icon.name: "document-open-folder" + tooltip: i18nd("plasma_wallpaper_org.kde.image", "Open Folder") + onTriggered: imageWallpaper.openFolder(modelData) + } + ] + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + visible: slidePathsView.count === 0 + text: i18nd("plasma_wallpaper_org.kde.image", "There are no wallpaper locations configured") + } + } + } + Loader { + sourceComponent: thumbnailsComponent + Layout.fillWidth: true + Layout.fillHeight: true + anchors.fill: undefined + } + QtControls2.Button { + Layout.alignment: Qt.AlignRight + icon.name: "list-add" + text: i18nd("plasma_wallpaper_org.kde.image","Add Folder…") + onClicked: imageWallpaper.showAddSlidePathsDialog() + } + NewStuff.Button { + Layout.alignment: Qt.AlignRight + configFile: Kirigami.Settings.isMobile ? "wallpaper-mobile.knsrc" : "wallpaper.knsrc" + text: i18nd("plasma_wallpaper_org.kde.image", "Get New Wallpapers…") + viewMode: NewStuff.Page.ViewMode.Preview + onEntryEvent: function(entry, event) { + if (event == 1) { // StatusChangedEvent + imageWallpaper.newStuffFinished() + } + } + } + } + } + } + + Component { + id: thumbnailsComponent + + Item { + property var imageModel: (configDialog.currentWallpaper === "org.kde.image") ? imageWallpaper.wallpaperModel : imageWallpaper.slideFilterModel + + KCM.GridView { + id: wallpapersGrid + anchors.fill: parent + + function resetCurrentIndex() { + //that min is needed as the module will be populated in an async way + //and only on demand so we can't ensure it already exists + view.currentIndex = Qt.binding(function() { return Math.min(imageModel.indexOf(cfg_Image), imageModel.count - 1) }); + } + + //kill the space for label under thumbnails + view.model: imageModel + Component.onCompleted: { + imageModel.usedInConfig = true; + resetCurrentIndex() + } + + //set the size of the cell, depending on Screen resolution to respect the aspect ratio + view.implicitCellWidth: Screen.width / 10 + Kirigami.Units.smallSpacing * 2 + view.implicitCellHeight: Screen.height / 10 + Kirigami.Units.smallSpacing * 2 + Kirigami.Units.gridUnit * 3 + + view.delegate: WallpaperDelegate { + color: cfg_Color + } + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + visible: wallpapersGrid.view.count === 0 + text: i18nd("plasma_wallpaper_org.kde.image", "There are no wallpapers in this slideshow") + } + + KCM.SettingHighlighter { + target: wallpapersGrid + highlight: configDialog.currentWallpaper === "org.kde.image" && cfg_Image != cfg_ImageDefault + } + } + } + + DropArea { + Layout.fillWidth: true + Layout.fillHeight: true + + onEntered: { + if (drag.hasUrls) { + event.accept(); + } + } + onDropped: { + drop.urls.forEach(function (url) { + if (url.indexOf("file://") === 0) { + var path = url.substr(7); // 7 is length of "file://" + if (configDialog.currentWallpaper === "org.kde.image") { + imageWallpaper.addUsersWallpaper(path); + } else { + imageWallpaper.addSlidePath(path); + } + } + }); + } + + Loader { + anchors.fill: parent + sourceComponent: (configDialog.currentWallpaper == "org.kde.image") ? thumbnailsComponent : + ((configDialog.currentWallpaper == "org.kde.slideshow") ? slideshowComponent : undefined) + } + } + + RowLayout { + id: buttonsRow + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + visible: configDialog.currentWallpaper == "org.kde.image" + QtControls2.Button { + icon.name: "list-add" + text: i18nd("plasma_wallpaper_org.kde.image","Add Image…") + onClicked: imageWallpaper.showFileDialog(); + } + NewStuff.Button { + Layout.alignment: Qt.AlignRight + configFile: Kirigami.Settings.isMobile ? "wallpaper-mobile.knsrc" : "wallpaper.knsrc" + text: i18nd("plasma_wallpaper_org.kde.image", "Get New Wallpapers…") + viewMode: NewStuff.Page.ViewMode.Preview + onEntryEvent: function(entry, event) { + if (event == 1) { // StatusChangedEvent + imageWallpaper.newStuffFinished() + } + } + } + } +} diff --git a/plasma/workspace/wallpapers/image/imagepackage/contents/ui/main.qml b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/main.qml new file mode 100644 index 0000000000..3f9935bab6 --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/contents/ui/main.qml @@ -0,0 +1,172 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + SPDX-FileCopyrightText: 2014 Sebastian Kügler + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 as QQC2 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.0 +import org.kde.plasma.wallpapers.image 2.0 as Wallpaper +import org.kde.plasma.core 2.0 as PlasmaCore + +QQC2.StackView { + id: root + + readonly property string modelImage: imageWallpaper.wallpaperPath + readonly property string configuredImage: wallpaper.configuration.Image + readonly property int fillMode: wallpaper.configuration.FillMode + readonly property string configColor: wallpaper.configuration.Color + readonly property bool blur: wallpaper.configuration.Blur + readonly property size sourceSize: Qt.size(root.width * Screen.devicePixelRatio, root.height * Screen.devicePixelRatio) + + // Ppublic API functions accessible from C++: + + // e.g. used by WallpaperInterface for drag and drop + function setUrl(url) { + wallpaper.configuration.Image = url + imageWallpaper.addUsersWallpaper(url); + } + + // e.g. used by slideshow wallpaper plugin + function action_next() { + imageWallpaper.nextSlide(); + } + + // e.g. used by slideshow wallpaper plugin + function action_open() { + Qt.openUrlExternally(modelImage) + } + + //private + + onConfiguredImageChanged: { + if (modelImage != configuredImage && configuredImage != "") { + imageWallpaper.addUrl(configuredImage); + } + } + Component.onCompleted: { + wallpaper.loading = true; // delays ksplash until the wallpaper has been loaded + + if (wallpaper.pluginName === "org.kde.slideshow") { + wallpaper.setAction("open", i18nd("plasma_wallpaper_org.kde.image", "Open Wallpaper Image"), "document-open"); + wallpaper.setAction("next", i18nd("plasma_wallpaper_org.kde.image", "Next Wallpaper Image"), "user-desktop"); + } + } + + Wallpaper.Image { + id: imageWallpaper + //the oneliner of difference between image and slideshow wallpapers + renderingMode: (wallpaper.pluginName === "org.kde.image") ? Wallpaper.Image.SingleImage : Wallpaper.Image.SlideShow + targetSize: root.sourceSize + slidePaths: wallpaper.configuration.SlidePaths + slideTimer: wallpaper.configuration.SlideInterval + slideshowMode: wallpaper.configuration.SlideshowMode + slideshowFoldersFirst: wallpaper.configuration.SlideshowFoldersFirst + uncheckedSlides: wallpaper.configuration.UncheckedSlides + } + + onFillModeChanged: Qt.callLater(loadImage); + onModelImageChanged:{ + Qt.callLater(loadImage); + wallpaper.configuration.Image = modelImage; + } + onConfigColorChanged: Qt.callLater(loadImage); + onBlurChanged: Qt.callLater(loadImage); + onWidthChanged: Qt.callLater(loadImage); + onHeightChanged: Qt.callLater(loadImage); + + function loadImage() { + var isFirst = (root.currentItem == undefined); + var pendingImage = baseImage.createObject(root, { "source": root.modelImage, + "fillMode": root.fillMode, + "sourceSize": root.sourceSize, + "color": root.configColor, + "blur": root.blur, + "opacity": isFirst ? 1: 0}); + + function replaceWhenLoaded() { + if (pendingImage.status !== Image.Loading) { + root.replace(pendingImage, {}, + isFirst ? QQC2.StackView.Immediate : QQC2.StackView.Transition);//dont' animate first show + pendingImage.statusChanged.disconnect(replaceWhenLoaded); + + wallpaper.loading = false; + } + } + pendingImage.statusChanged.connect(replaceWhenLoaded); + replaceWhenLoaded(); + } + + Component { + id: baseImage + + Image { + id: mainImage + + property alias color: backgroundColor.color + property bool blur: false + + asynchronous: true + cache: false + autoTransform: true + z: -1 + + QQC2.StackView.onRemoved: destroy() + + Rectangle { + id: backgroundColor + anchors.fill: parent + visible: mainImage.status === Image.Ready && !blurLoader.active + z: -2 + } + + Loader { + id: blurLoader + anchors.fill: parent + z: -3 + active: mainImage.blur && (mainImage.fillMode === Image.PreserveAspectFit || mainImage.fillMode === Image.Pad) + sourceComponent: Item { + Image { + id: blurSource + anchors.fill: parent + asynchronous: true + cache: false + autoTransform: true + fillMode: Image.PreserveAspectCrop + source: mainImage.source + sourceSize: mainImage.sourceSize + visible: false // will be rendered by the blur + } + + GaussianBlur { + id: blurEffect + anchors.fill: parent + source: blurSource + radius: 32 + samples: 65 + visible: blurSource.status === Image.Ready + } + } + } + } + } + + replaceEnter: Transition { + OpacityAnimator { + from: 0 + to: 1 + duration: wallpaper.configuration.TransitionAnimationDuration + } + } + // Keep the old image around till the new one is fully faded in + // If we fade both at the same time you can see the background behind glimpse through + replaceExit: Transition{ + PauseAnimation { + duration: wallpaper.configuration.TransitionAnimationDuration + } + } +} diff --git a/plasma/workspace/wallpapers/image/imagepackage/metadata.json b/plasma/workspace/wallpapers/image/imagepackage/metadata.json new file mode 100644 index 0000000000..f8f33fed52 --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/metadata.json @@ -0,0 +1,182 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "", + "Description": "Wallpaper view for images", + "Description[ar]": "عرض خلفيات للصور", + "Description[az]": "Şəkillərin divar kağızı kimi görünüşü", + "Description[ca]": "Vista del fons de pantalla per a les imatges", + "Description[de]": "Ansicht für Hintergrundbilder", + "Description[en_GB]": "Wallpaper view for images", + "Description[es]": "Visor de imágenes para el fondo del escritorio", + "Description[eu]": "Irudientzako horma-paper ikuspegia", + "Description[fi]": "Kuvien taustakuvanäkymä", + "Description[fr]": "Affichage d'images en fond d'écran", + "Description[hu]": "Háttérkép nézet képekhez", + "Description[ia]": "Vista de tapete de papiro per images", + "Description[it]": "Vista delle immagini di sfondo", + "Description[ko]": "마음에 드는 그림을 배경으로 사용", + "Description[lt]": "Darbalaukio fono rodinys su paveikslais", + "Description[nl]": "Achtergrondweergave voor afbeeldingen", + "Description[nn]": "Bakgrunnsbiletevising for bilete", + "Description[pa]": "ਚਿੱਤਰਾਂ ਲਈ ਵਾਲਪੇਪਰ ਝਲਕ", + "Description[pl]": "Widok tapety dla obrazów", + "Description[pt_BR]": "Visualização do papel de parede para imagens", + "Description[ro]": "Vizualizare de tapet pentru imagini", + "Description[ru]": "Изображение в качестве обоев рабочего стола", + "Description[sk]": "Pohľad tapety pre obrázky", + "Description[sl]": "Pogled slike ozadja za slike", + "Description[sv]": "Skrivbordsunderlägg med bilder", + "Description[ta]": "படங்களை பயன்படுத்தக் கூடிய ஒரு பின்புலம்", + "Description[tr]": "Resimler için duvar kağıdı görünümü", + "Description[uk]": "Тло із зображень", + "Description[vi]": "Khung xem phông nền cho các ảnh", + "Description[x-test]": "xxWallpaper view for imagesxx", + "Description[zh_CN]": "图像壁纸视图", + "Icon": "preferences-desktop-wallpaper", + "Id": "org.kde.image", + "License": "GPLv2+", + "MimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/svg+xml-compressed", + "image/bmp", + "image/webp", + "image/tiff" + ], + "Name": "Image", + "Name[ar]": "صورة", + "Name[ast]": "Imaxe", + "Name[az]": "Şəkil", + "Name[be@latin]": "Vyjava", + "Name[bg]": "Изображение", + "Name[bn]": "ছবি", + "Name[bn_IN]": "ছবি", + "Name[bs]": "Slika", + "Name[ca@valencia]": "Imatge", + "Name[ca]": "Imatge", + "Name[cs]": "Obrázek", + "Name[csb]": "Òbrôzk", + "Name[da]": "Billede", + "Name[de]": "Bild", + "Name[el]": "Εικόνα", + "Name[en_GB]": "Image", + "Name[eo]": "Bildo", + "Name[es]": "Imagen", + "Name[et]": "Pilt", + "Name[eu]": "Irudia", + "Name[fa]": "تصویر", + "Name[fi]": "Kuva", + "Name[fr]": "Image", + "Name[fy]": "Ofbylding", + "Name[ga]": "Íomhá", + "Name[gl]": "Imaxe", + "Name[gu]": "ચિત્ર", + "Name[he]": "תמונה", + "Name[hi]": "छवि", + "Name[hne]": "फोटो", + "Name[hr]": "Slika", + "Name[hsb]": "Wobraz", + "Name[hu]": "Kép", + "Name[ia]": "Image", + "Name[id]": "Citra", + "Name[is]": "Mynd", + "Name[it]": "Immagine", + "Name[ja]": "画像", + "Name[ka]": "გამოსახულება", + "Name[kk]": "Кескіні", + "Name[km]": "រូបភាព", + "Name[kn]": "ಬಿಂಬ (ಇಮೇಜ್)", + "Name[ko]": "그림", + "Name[ku]": "Wêne", + "Name[lt]": "Paveikslas", + "Name[lv]": "Attēls", + "Name[mai]": "चित्र", + "Name[mk]": "Слика", + "Name[ml]": "ചിത്രം", + "Name[mr]": "प्रतिमा", + "Name[nb]": "Bilde", + "Name[nds]": "Bild", + "Name[nl]": "Afbeelding", + "Name[nn]": "Bilete", + "Name[or]": "ପ୍ରତିଛବି", + "Name[pa]": "ਚਿੱਤਰ", + "Name[pl]": "Obraz", + "Name[pt]": "Imagem", + "Name[pt_BR]": "Imagem", + "Name[ro]": "Imagine", + "Name[ru]": "Изображение", + "Name[si]": "පිංතූරය", + "Name[sk]": "Obrázok", + "Name[sl]": "Slika", + "Name[sr@ijekavian]": "слика", + "Name[sr@ijekavianlatin]": "slika", + "Name[sr@latin]": "slika", + "Name[sr]": "слика", + "Name[sv]": "Bild", + "Name[ta]": "படம்", + "Name[tg]": "Тасвир", + "Name[th]": "ภาพ", + "Name[tr]": "Resim", + "Name[ug]": "سۈرەت", + "Name[uk]": "Зображення", + "Name[vi]": "Ảnh", + "Name[wa]": "Imådje", + "Name[x-test]": "xxImagexx", + "Name[zh_CN]": "图像", + "Name[zh_TW]": "影像", + "ServiceTypes": [ + "Plasma/Wallpaper" + ], + "Website": "https://kde.org/plasma-desktop" + }, + "Keywords": "", + "MimeType": "image/jpeg;image/png;image/svg+xml;image/svg+xml-compressed;image/bmp;image/webp;image/tiff;", + "X-KDE-ParentApp": "org.kde.plasmashell", + "X-Plasma-DropMimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/svg+xml-compressed", + "image/bmp", + "image/webp", + "image/tiff" + ] +} diff --git a/plasma/workspace/wallpapers/image/imagepackage/setaswallpaper.desktop.in b/plasma/workspace/wallpapers/image/imagepackage/setaswallpaper.desktop.in new file mode 100644 index 0000000000..ea5ab0b003 --- /dev/null +++ b/plasma/workspace/wallpapers/image/imagepackage/setaswallpaper.desktop.in @@ -0,0 +1,39 @@ +[Desktop Entry] +ServiceTypes=KonqPopupMenu/Plugin +Actions=setAsWallpaper; +Type=Service +MimeType=image/jpeg;image/png;image/svg+xml;image/svg+xml-compressed;image/bmp;image/webp;image/tiff; +X-KDE-Priority=TopLevel + +[Desktop Action setAsWallpaper] +Name=Set as Wallpaper +Name[ar]=عين كخلفية +Name[az]=Divar Kağızı təyin edin +Name[ca]=Estableix com a fons de pantalla +Name[cs]=Nastavit jako tapetu +Name[de]=Als Hintergrundbild festlegen +Name[en_GB]=Set as Wallpaper +Name[es]=Definir como fondo del escritorio +Name[eu]=Ezarri horma-paper gisa +Name[fi]=Aseta taustakuvaksi +Name[fr]=Définir comme fond d'écran +Name[hu]=Beállítás háttérképként +Name[ia]=Fixa como Tapete de papiro +Name[it]=Imposta come sfondo +Name[ko]=배경 그림으로 설정 +Name[nl]=Als achtergrondafbeelding instellen +Name[nn]=Bruk som bakgrunns­bilete +Name[pl]=Ustaw jako tapetę +Name[pt_BR]=Definir como papel de parede +Name[ro]=Stabilește ca tapet +Name[ru]=Сделать фоном рабочего стола +Name[sl]=Postavi kot sliko ozadja +Name[sv]=Använd som skrivbordsunderlägg +Name[ta]=பின்புல படமாக அமை +Name[tr]=Duvar Kağıdı Olarak Ayarla +Name[uk]=Встановити зображенням тла +Name[vi]=Đặt làm phông nền +Name[x-test]=xxSet as Wallpaperxx +Name[zh_CN]=设为壁纸 +Icon=viewimage +Exec=@QtBinariesDir@/qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript 'const allDesktops = desktopsForActivity(currentActivity()); for (i=0; i + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imageplugin.h" +#include "image.h" +#include + +void ImagePlugin::registerTypes(const char *uri) +{ + Q_ASSERT(uri == QLatin1String("org.kde.plasma.wallpapers.image")); + + qmlRegisterType(uri, 2, 0, "Image"); + qmlRegisterAnonymousType("QAbstractItemModel",1); +} diff --git a/plasma/workspace/wallpapers/image/imageplugin.h b/plasma/workspace/wallpapers/image/imageplugin.h new file mode 100644 index 0000000000..ddd6d0218d --- /dev/null +++ b/plasma/workspace/wallpapers/image/imageplugin.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class ImagePlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/plasma/workspace/wallpapers/image/plasma-apply-wallpaperimage.cpp b/plasma/workspace/wallpapers/image/plasma-apply-wallpaperimage.cpp new file mode 100644 index 0000000000..65035be545 --- /dev/null +++ b/plasma/workspace/wallpapers/image/plasma-apply-wallpaperimage.cpp @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "config-X11.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("plasma-apply-wallpaperimage")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + KLocalizedString::setApplicationDomain("plasma-apply-wallpaperimage"); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->setApplicationDescription(i18n("This tool allows you to set an image as the wallpaper for the Plasma session.")); + parser->addPositionalArgument(QStringLiteral("imagefile"), + i18n("An image file or an installed wallpaper kpackage that you wish to set as the wallpaper for your Plasma session")); + parser->process(app); + + int errorCode{0}; + QTextStream ts(stdout); + if (!parser->positionalArguments().isEmpty()) { + QString wallpaperFile{parser->positionalArguments().first()}; + QFileInfo wallpaperInfo{wallpaperFile}; + bool isWallpaper{false}; + bool isKPackage{false}; + if (wallpaperFile.contains(QStringLiteral("\'"))) { + // If this happens, we might very well assume that there is some kind of funny business going on + // even if technically it could just be a possessive. But, security first, so... + ts << i18n( + "There is a stray single quote in the filename of this wallpaper (') - please contact the author of the wallpaper to fix this, or rename the " + "file yourself: %1", + wallpaperFile) + << Qt::endl; + errorCode = -1; + } else { + if (wallpaperInfo.exists()) { + if (wallpaperInfo.isFile()) { + // then we assume it's an image file... we could check with QImage, but + // that makes the operation much heavier than it needs to be + isWallpaper = true; + } else { + if (QFileInfo::exists(QStringLiteral("%1/metadata.desktop").arg(wallpaperFile)) + || QFileInfo::exists(QStringLiteral("%1/metadata.json").arg(wallpaperFile))) { + isWallpaper = true; + isKPackage = true; + // Similarly to above, we could read all the information out of the kpackage, but + // that also is not hugely important, so we just deduce that this reasonably should + // be an installed kpackage + } + } + } + } + if (isWallpaper) { + QString script; + QTextStream out(&script); + out << "for (var key in desktops()) {" + << "var d = desktops()[key];" + << "d.wallpaperPlugin = 'org.kde.image';" + << "d.currentConfigGroup = ['Wallpaper', 'org.kde.image', 'General'];" + << "d.writeConfig('Image', 'file://" + wallpaperFile + "');" + << "}"; + auto message = QDBusMessage::createMethodCall("org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell", "evaluateScript"); + message.setArguments(QVariantList() << QVariant(script)); + auto reply = QDBusConnection::sessionBus().call(message); + + if (reply.type() == QDBusMessage::ErrorMessage) { + ts << i18n("An error occurred while attempting to set the Plasma wallpaper:\n") << reply.errorMessage() << Qt::endl; + errorCode = -1; + } else { + if (isKPackage) { + ts << i18n("Successfully set the wallpaper for all desktops to the KPackage based %1", wallpaperFile) << Qt::endl; + } else { + ts << i18n("Successfully set the wallpaper for all desktops to the image %1", wallpaperFile) << Qt::endl; + } + } + + } else if (errorCode == 0) { + // Just to avoid spitting out multiple errors + ts << i18n("The file passed to be set as wallpaper does not exist, or we cannot identify it as a wallpaper: %1", wallpaperFile) << Qt::endl; + errorCode = -1; + } + } else { + parser->showHelp(); + } + QTimer::singleShot(0, &app, [&app, &errorCode]() { + app.exit(errorCode); + }); + + return app.exec(); +} diff --git a/plasma/workspace/wallpapers/image/qmldir b/plasma/workspace/wallpapers/image/qmldir new file mode 100644 index 0000000000..563f9119af --- /dev/null +++ b/plasma/workspace/wallpapers/image/qmldir @@ -0,0 +1,5 @@ +module org.kde.plasma.wallpapers.image +plugin plasma_wallpaper_imageplugin + + + diff --git a/plasma/workspace/wallpapers/image/slidefiltermodel.cpp b/plasma/workspace/wallpapers/image/slidefiltermodel.cpp new file mode 100644 index 0000000000..c115b3bc18 --- /dev/null +++ b/plasma/workspace/wallpapers/image/slidefiltermodel.cpp @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slidefiltermodel.h" + +#include "backgroundlistmodel.h" +#include "slidemodel.h" + +#include +#include +#include + +#include + +SlideFilterModel::SlideFilterModel(QObject *parent) + : QSortFilterProxyModel{parent} + , m_SortingMode{Image::Random} + , m_SortingFoldersFirst{false} + , m_usedInConfig{false} + , m_random(m_randomDevice()) +{ + srand(time(nullptr)); + setSortCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + connect(this, &SlideFilterModel::usedInConfigChanged, this, &SlideFilterModel::invalidateFilter); +} + +bool SlideFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + auto index = sourceModel()->index(source_row, 0, source_parent); + return m_usedInConfig || index.data(BackgroundListModel::ToggleRole).toBool(); +} + +void SlideFilterModel::setSourceModel(QAbstractItemModel *sourceModel) +{ + if (this->sourceModel()) { + disconnect(this->sourceModel(), nullptr, this, nullptr); + } + QSortFilterProxyModel::setSourceModel(sourceModel); + if (m_SortingMode == Image::Random && !m_usedInConfig) { + buildRandomOrder(); + } + if (sourceModel) { + connect(sourceModel, &QAbstractItemModel::modelReset, this, &SlideFilterModel::buildRandomOrder); + connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this] { + if (m_SortingMode != Image::Random || m_usedInConfig) { + return; + } + const int old_count = m_randomOrder.size(); + m_randomOrder.resize(this->sourceModel()->rowCount()); + std::iota(m_randomOrder.begin() + old_count, m_randomOrder.end(), old_count); + std::shuffle(m_randomOrder.begin() + old_count, m_randomOrder.end(), m_random); + }); + connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this] { + if (m_SortingMode != Image::Random || m_usedInConfig) { + return; + } + m_randomOrder.erase(std::remove_if(m_randomOrder.begin(), + m_randomOrder.end(), + [this](const int v) { + return v >= this->sourceModel()->rowCount(); + }), + m_randomOrder.end()); + }); + } +} + +bool SlideFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + Qt::CaseSensitivity cs = Qt::CaseInsensitive; + + switch (m_SortingMode) { + case Image::Random: + if (m_usedInConfig) { + return source_left.row() < source_right.row(); + } + return m_randomOrder.indexOf(source_left.row()) < m_randomOrder.indexOf(source_right.row()); + case Image::Alphabetical: + if (m_SortingFoldersFirst) { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + QString leftFilePath = getFilePathWithDir(leftFile); + QString rightFilePath = getFilePathWithDir(rightFile); + + if (leftFilePath == rightFilePath) { + return QString::compare(leftFile.fileName(), rightFile.fileName(), cs) < 0; + } else if (leftFilePath.startsWith(rightFilePath, cs)) { + return true; + } else if (rightFilePath.startsWith(leftFilePath, cs)) { + return false; + } else { + return QString::compare(leftFilePath, rightFilePath, cs) < 0; + } + } else { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + return QString::compare(leftFile.fileName(), rightFile.fileName(), cs) < 0; + } + case Image::AlphabeticalReversed: + if (m_SortingFoldersFirst) { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + QString leftFilePath = getFilePathWithDir(leftFile); + QString rightFilePath = getFilePathWithDir(rightFile); + + if (leftFilePath == rightFilePath) { + return QString::compare(leftFile.fileName(), rightFile.fileName(), cs) > 0; + } else if (leftFilePath.startsWith(rightFilePath, cs)) { + return true; + } else if (rightFilePath.startsWith(leftFilePath, cs)) { + return false; + } else { + return QString::compare(leftFilePath, rightFilePath, cs) > 0; + } + } else { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + return QString::compare(leftFile.fileName(), rightFile.fileName(), cs) > 0; + } + case Image::Modified: // oldest first + { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + return leftFile.lastModified() < rightFile.lastModified(); + } + case Image::ModifiedReversed: // newest first + { + QFileInfo leftFile(getLocalFilePath(source_left)); + QFileInfo rightFile(getLocalFilePath(source_right)); + return !(leftFile.lastModified() < rightFile.lastModified()); + } + } + Q_UNREACHABLE(); +} + +void SlideFilterModel::setSortingMode(Image::SlideshowMode slideshowMode, bool slideshowFoldersFirst) +{ + m_SortingMode = slideshowMode; + m_SortingFoldersFirst = slideshowFoldersFirst; + if (m_SortingMode == Image::Random && !m_usedInConfig) { + buildRandomOrder(); + } + QSortFilterProxyModel::invalidate(); +} + +void SlideFilterModel::invalidate() +{ + if (m_SortingMode == Image::Random && !m_usedInConfig) { + std::shuffle(m_randomOrder.begin(), m_randomOrder.end(), m_random); + } + QSortFilterProxyModel::invalidate(); +} + +void SlideFilterModel::invalidateFilter() +{ + QSortFilterProxyModel::invalidateFilter(); +} + +int SlideFilterModel::indexOf(const QString &path) +{ + auto sourceIndex = sourceModel()->index(static_cast(sourceModel())->indexOf(path), 0); + return mapFromSource(sourceIndex).row(); +} + +void SlideFilterModel::openContainingFolder(int rowIndex) +{ + auto sourceIndex = mapToSource(index(rowIndex, 0)); + static_cast(sourceModel())->openContainingFolder(sourceIndex.row()); +} + +void SlideFilterModel::buildRandomOrder() +{ + if (sourceModel()) { + m_randomOrder.resize(sourceModel()->rowCount()); + std::iota(m_randomOrder.begin(), m_randomOrder.end(), 0); + std::shuffle(m_randomOrder.begin(), m_randomOrder.end(), m_random); + } +} + +QString SlideFilterModel::getLocalFilePath(const QModelIndex& modelIndex) const +{ + return modelIndex.data(BackgroundListModel::PathRole).toUrl().toLocalFile(); +} + +QString SlideFilterModel::getFilePathWithDir(const QFileInfo& fileInfo) const +{ + return fileInfo.canonicalPath().append(QDir::separator()); +} diff --git a/plasma/workspace/wallpapers/image/slidefiltermodel.h b/plasma/workspace/wallpapers/image/slidefiltermodel.h new file mode 100644 index 0000000000..39855b6c5c --- /dev/null +++ b/plasma/workspace/wallpapers/image/slidefiltermodel.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include +#include + +#include + +class SlideFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(bool usedInConfig MEMBER m_usedInConfig NOTIFY usedInConfigChanged); + +public: + explicit SlideFilterModel(QObject *parent); + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + void setSourceModel(QAbstractItemModel *sourceModel) override; + void setSortingMode(Image::SlideshowMode slideshowMode, bool slideshowFoldersFirst); + void invalidate(); + void invalidateFilter(); + + Q_INVOKABLE int indexOf(const QString &path); + Q_INVOKABLE void openContainingFolder(int rowIndex); + +Q_SIGNALS: + void usedInConfigChanged(); + +private: + void buildRandomOrder(); + + QString getLocalFilePath(const QModelIndex& modelIndex) const; + QString getFilePathWithDir(const QFileInfo& fileInfo) const; + + QVector m_randomOrder; + Image::SlideshowMode m_SortingMode; + bool m_SortingFoldersFirst; + bool m_usedInConfig; + std::random_device m_randomDevice; + std::mt19937 m_random; +}; diff --git a/plasma/workspace/wallpapers/image/slidemodel.cpp b/plasma/workspace/wallpapers/image/slidemodel.cpp new file mode 100644 index 0000000000..ea1982623c --- /dev/null +++ b/plasma/workspace/wallpapers/image/slidemodel.cpp @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slidemodel.h" + +void SlideModel::reload(const QStringList &selected) +{ + if (!m_packages.isEmpty()) { + beginRemoveRows(QModelIndex(), 0, m_packages.count() - 1); + m_packages.clear(); + endRemoveRows(); + Q_EMIT countChanged(); + } + addDirs(selected); +} + +void SlideModel::addDirs(const QStringList &selected) +{ + BackgroundFinder *finder = new BackgroundFinder(m_wallpaper.data(), selected); + connect(finder, &BackgroundFinder::backgroundsFound, this, &SlideModel::backgroundsFound); + m_findToken = finder->token(); + finder->start(); +} + +void SlideModel::backgroundsFound(const QStringList &paths, const QString &token) +{ + if (token != m_findToken) { + return; + } + processPaths(paths); + Q_EMIT done(); +} + +void SlideModel::removeDir(const QString &path) +{ + BackgroundFinder *finder = new BackgroundFinder(m_wallpaper.data(), QStringList{path}); + connect(finder, &BackgroundFinder::backgroundsFound, this, &SlideModel::removeBackgrounds); + finder->start(); +} + +void SlideModel::removeBackgrounds(const QStringList &paths, const QString &token) +{ + for (const QString &file : paths) { + removeBackground(file); + } +} + +QVariant SlideModel::data(const QModelIndex &index, int role) const +{ + if (role == ToggleRole) { + return !m_wallpaper.data()->uncheckedSlides().contains(data(index, PathRole).toString()); + } + return BackgroundListModel::data(index, role); +} + +QHash SlideModel::roleNames() const +{ + QHash roleNames = BackgroundListModel::roleNames(); + roleNames.insert(ToggleRole, "checked"); + return roleNames; +} diff --git a/plasma/workspace/wallpapers/image/slidemodel.h b/plasma/workspace/wallpapers/image/slidemodel.h new file mode 100644 index 0000000000..778e88d889 --- /dev/null +++ b/plasma/workspace/wallpapers/image/slidemodel.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "backgroundlistmodel.h" + +class SlideModel : public BackgroundListModel +{ + Q_OBJECT +public: + using BackgroundListModel::BackgroundListModel; + void reload(const QStringList &selected); + void addDirs(const QStringList &selected); + void removeDir(const QString &selected); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + +Q_SIGNALS: + void done(); + +private Q_SLOTS: + void removeBackgrounds(const QStringList &paths, const QString &token); + void backgroundsFound(const QStringList &paths, const QString &token); +}; diff --git a/plasma/workspace/wallpapers/image/slideshowpackage/contents/config/main.xml b/plasma/workspace/wallpapers/image/slideshowpackage/contents/config/main.xml new file mode 100644 index 0000000000..92d6181710 --- /dev/null +++ b/plasma/workspace/wallpapers/image/slideshowpackage/contents/config/main.xml @@ -0,0 +1,50 @@ + + + + + + + + false + + + + #000000 + + + + + + + + 2 + + + + preferred://wallpaperlocations + + + + 900 + + + + 1000 + + + + + + + + 0 + + + + false + + + diff --git a/plasma/workspace/wallpapers/image/slideshowpackage/metadata.json b/plasma/workspace/wallpapers/image/slideshowpackage/metadata.json new file mode 100644 index 0000000000..8f4265ecb3 --- /dev/null +++ b/plasma/workspace/wallpapers/image/slideshowpackage/metadata.json @@ -0,0 +1,178 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "mart@kde.org", + "Name": "Marco Martin", + "Name[ar]": "Marco Martin", + "Name[az]": "Marco Martin", + "Name[ca]": "Marco Martin", + "Name[cs]": "Marco Martin", + "Name[de]": "Marco Martin", + "Name[en_GB]": "Marco Martin", + "Name[es]": "Marco Martin", + "Name[eu]": "Marco Martin", + "Name[fi]": "Marco Martin", + "Name[fr]": "Marco Martin", + "Name[hu]": "Marco Martin", + "Name[ia]": "Marco Martin", + "Name[it]": "Marco Martin", + "Name[ko]": "Marco Martin", + "Name[lt]": "Marco Martin", + "Name[nl]": "Marco Martin", + "Name[nn]": "Marco Martin", + "Name[pa]": "ਮਾਰਕੋ ਮਾਰਟਿਨ", + "Name[pl]": "Marco Martin", + "Name[pt_BR]": "Marco Martin", + "Name[ro]": "Marco Martin", + "Name[ru]": "Marco Martin", + "Name[sk]": "Marco Martin", + "Name[sl]": "Marco Martin", + "Name[sv]": "Marco Martin", + "Name[ta]": "மார்க்கோ மார்ட்டின்", + "Name[tr]": "Marco Martin", + "Name[uk]": "Marco Martin", + "Name[vi]": "Marco Martin", + "Name[x-test]": "xxMarco Martinxx", + "Name[zh_CN]": "Marco Martin" + } + ], + "Category": "", + "Description": "Slideshow wallpaper", + "Description[ar]": "خلفية عرض شرائح", + "Description[az]": "Dəyişən divar kağızları", + "Description[ca]": "Passi de diapositives al fons de pantalla", + "Description[cs]": "Tapeta s promítáním", + "Description[de]": "Diaschau-Hintergrundbild", + "Description[en_GB]": "Slideshow wallpaper", + "Description[es]": "Fondo de escritorio de presentación", + "Description[eu]": "Diapositiba emanaldi horma-papera", + "Description[fi]": "Diaesitystausta", + "Description[fr]": "Diaporama en fond d'écran ", + "Description[hu]": "Diabemutató háttérkép", + "Description[ia]": "Tapete de papiro de Slideshow (Sequentia de diapositivas)", + "Description[it]": "Sfondo con presentazione", + "Description[ko]": "슬라이드 쇼를 배경 그림으로 사용", + "Description[lt]": "Skaidrių rodymo darbalaukio fonas", + "Description[nl]": "Achtergrond met diavoorstelling", + "Description[nn]": "Lysbiletvising-bakgrunn", + "Description[pa]": "ਸਲਾਈਡ-ਸ਼ੋ ਵਾਲਪੇਪਰ", + "Description[pl]": "Tapeta pokazu slajdów", + "Description[pt_BR]": "Papel de parede com apresentação de slides", + "Description[ro]": "Tapet cu diaporamă", + "Description[ru]": "Слайд-шоу на месте обоев", + "Description[sk]": "Tapeta prezentácia", + "Description[sl]": "Slike ozadja v predstavitvi", + "Description[sv]": "Skrivbordsunderlägg med bildspel", + "Description[ta]": "வில்லைக்காட்சி (slideshow) பின்புலம்", + "Description[tr]": "Slayt gösterisi duvar kağıdı", + "Description[uk]": "Тло у форматі показу слайдів", + "Description[vi]": "Phông nền dạng chuỗi trình chiếu", + "Description[x-test]": "xxSlideshow wallpaperxx", + "Description[zh_CN]": "幻灯片壁纸", + "Icon": "folder-image", + "Id": "org.kde.slideshow", + "License": "GPLv2+", + "MimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/svg+xml-compressed", + "image/bmp", + "image/webp", + "image/tiff" + ], + "Name": "Slideshow", + "Name[ar]": "عرض شرائح", + "Name[az]": "Dəyişən Şəkillər", + "Name[be@latin]": "Słajdy", + "Name[bg]": "Прожекция", + "Name[bn]": "স্লাইড-শো", + "Name[bn_IN]": "স্লাইড-শো", + "Name[bs]": "Slajd‑šou", + "Name[ca@valencia]": "Passe de diapositives", + "Name[ca]": "Passi de diapositives", + "Name[cs]": "Promítání", + "Name[csb]": "Pòkôz slajdów", + "Name[da]": "Diasshow", + "Name[de]": "Diaschau", + "Name[el]": "Προβολή σλάιντ", + "Name[en_GB]": "Slideshow", + "Name[eo]": "Bildoserio", + "Name[es]": "Presentación", + "Name[et]": "Slaidiseanss", + "Name[eu]": "Filmina erakusketa", + "Name[fi]": "Diaesitys", + "Name[fr]": "Diaporama", + "Name[fy]": "Diafoarstelling", + "Name[ga]": "Taispeántas Sleamhnán", + "Name[gl]": "Presentación", + "Name[gu]": "સ્લાઇડશો", + "Name[he]": "מצגת", + "Name[hi]": "स्लाइड-शो", + "Name[hne]": "स्लाइडसो", + "Name[hr]": "Prezentacija", + "Name[hu]": "Diabemutató", + "Name[ia]": "Slideshow (Sequentia de diapositivas)", + "Name[id]": "Slideshow", + "Name[is]": "Skyggnusýning", + "Name[it]": "Presentazione", + "Name[ja]": "スライドショー", + "Name[kk]": "Слайд көрсетілімі", + "Name[km]": "បញ្ចាំង​ស្លាយ", + "Name[kn]": "ಚಿತ್ರಫಲಕ ಪ್ರದರ್ಶನ (ಸ್ಲೈಡ್ ಶೋ)", + "Name[ko]": "슬라이드 쇼", + "Name[ku]": "NîşandanaSlaydê", + "Name[lt]": "Skaidrių rodymas", + "Name[lv]": "Slīdrāde", + "Name[mai]": "स्लाइडशो", + "Name[mk]": "Слајдшоу", + "Name[ml]": "മാറുന്ന ചിത്രങ്ങള്‍", + "Name[mr]": "स्लाइडशो", + "Name[nb]": "Lysbildevisning", + "Name[nds]": "Diaschau", + "Name[nl]": "Diavoorstelling", + "Name[nn]": "Lysbiletvising", + "Name[or]": "ସ୍ଲାଇଡ଼ ଦୃଶ୍ୟ", + "Name[pa]": "ਸਲਾਈਡ-ਸ਼ੋ", + "Name[pl]": "Pokaz slajdów", + "Name[pt]": "Apresentação", + "Name[pt_BR]": "Apresentação de slides", + "Name[ro]": "Diaporamă", + "Name[ru]": "Слайд-шоу", + "Name[si]": "ස්ලයිඩ් දසුන", + "Name[sk]": "Prezentácia", + "Name[sl]": "Predstavitev", + "Name[sr@ijekavian]": "слајд‑шоу", + "Name[sr@ijekavianlatin]": "slajd‑šou", + "Name[sr@latin]": "slajd‑šou", + "Name[sr]": "слајд‑шоу", + "Name[sv]": "Bildspel", + "Name[ta]": "வில்லைக்காட்சி", + "Name[th]": "นำเสนอภาพนิ่ง", + "Name[tr]": "Slayt Gösterisi", + "Name[ug]": "تام تەسۋىرى", + "Name[uk]": "Показ слайдів", + "Name[vi]": "Chuỗi trình chiếu", + "Name[wa]": "Diaporama", + "Name[x-test]": "xxSlideshowxx", + "Name[zh_CN]": "幻灯片", + "Name[zh_TW]": "投影播放", + "ServiceTypes": [ + "Plasma/Wallpaper" + ], + "Website": "https://kde.org/plasma-desktop" + }, + "Keywords": "", + "MimeType": "image/jpeg;image/png;image/svg+xml;image/svg+xml-compressed;image/bmp;image/webp;image/tiff;", + "X-KDE-ParentApp": "org.kde.plasmashell", + "X-Plasma-DropMimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/svg+xml-compressed", + "image/bmp", + "image/webp", + "image/tiff" + ] +} diff --git a/plasma/workspace/wallpapers/image/wallpaper-mobile.knsrc b/plasma/workspace/wallpapers/image/wallpaper-mobile.knsrc new file mode 100644 index 0000000000..258ee948d3 --- /dev/null +++ b/plasma/workspace/wallpapers/image/wallpaper-mobile.knsrc @@ -0,0 +1,53 @@ +[KNewStuff3] +Name=Wallpapers +Name[ar]=الخلفيات +Name[ast]=Fondos de pantalla +Name[az]=Divar Kağızları +Name[ca]=Fons de pantalla +Name[ca@valencia]=Fons de pantalla +Name[cs]=Tapety +Name[da]=Baggrundsbilleder +Name[de]=Hintergrundbilder +Name[el]=Ταπετσαρίες +Name[en_GB]=Wallpapers +Name[es]=Fondos del escritorio +Name[et]=Taustapildid +Name[eu]=Horma-paperak +Name[fi]=Taustakuvat +Name[fr]=Fonds d'écran +Name[gl]=Fondos de escritorio +Name[he]=רקעים +Name[hi]=वालपेपर +Name[hu]=Háttérképek +Name[ia]=Tapetes de papiro +Name[id]=Wallpaper +Name[it]=Sfondi +Name[ko]=배경 그림 +Name[lt]=Darbalaukio fonai +Name[lv]=Tapetes +Name[ml]=പശ്ചാത്തലചിത്രങ്ങൾ +Name[nl]=Achtergrondafbeeldingen +Name[nn]=Bakgrunnsbilete +Name[pa]=ਵਾਲਪੇਪਰ +Name[pl]=Tapety +Name[pt]=Papéis de Parede +Name[pt_BR]=Papéis de parede +Name[ro]=Tapete +Name[ru]=Обои +Name[sk]=Tapety +Name[sl]=Slike ozadij +Name[sv]=Skrivbordsunderlägg +Name[ta]=பின்புல படங்கள் +Name[tr]=Duvar Kağıtları +Name[uk]=Зображення тла +Name[vi]=Phông nền +Name[x-test]=xxWallpapersxx +Name[zh_CN]=壁纸 +Name[zh_TW]=桌布 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=Plamo Wallpapers +TargetDir=wallpapers +Uncompress=subdir-archive + +AdoptionCommand=plasma-apply-wallpaperimage %f diff --git a/plasma/workspace/wallpapers/image/wallpaper.knsrc b/plasma/workspace/wallpapers/image/wallpaper.knsrc new file mode 100644 index 0000000000..6d89928568 --- /dev/null +++ b/plasma/workspace/wallpapers/image/wallpaper.knsrc @@ -0,0 +1,53 @@ +[KNewStuff3] +Name=Wallpapers +Name[ar]=الخلفيات +Name[ast]=Fondos de pantalla +Name[az]=Divar Kağızları +Name[ca]=Fons de pantalla +Name[ca@valencia]=Fons de pantalla +Name[cs]=Tapety +Name[da]=Baggrundsbilleder +Name[de]=Hintergrundbilder +Name[el]=Ταπετσαρίες +Name[en_GB]=Wallpapers +Name[es]=Fondos del escritorio +Name[et]=Taustapildid +Name[eu]=Horma-paperak +Name[fi]=Taustakuvat +Name[fr]=Fonds d'écran +Name[gl]=Fondos de escritorio +Name[he]=רקעים +Name[hi]=वालपेपर +Name[hu]=Háttérképek +Name[ia]=Tapetes de papiro +Name[id]=Wallpaper +Name[it]=Sfondi +Name[ko]=배경 그림 +Name[lt]=Darbalaukio fonai +Name[lv]=Tapetes +Name[ml]=പശ്ചാത്തലചിത്രങ്ങൾ +Name[nl]=Achtergrondafbeeldingen +Name[nn]=Bakgrunnsbilete +Name[pa]=ਵਾਲਪੇਪਰ +Name[pl]=Tapety +Name[pt]=Papéis de Parede +Name[pt_BR]=Papéis de parede +Name[ro]=Tapete +Name[ru]=Обои +Name[sk]=Tapety +Name[sl]=Slike ozadij +Name[sv]=Skrivbordsunderlägg +Name[ta]=பின்புல படங்கள் +Name[tr]=Duvar Kağıtları +Name[uk]=Зображення тла +Name[vi]=Phông nền +Name[x-test]=xxWallpapersxx +Name[zh_CN]=壁纸 +Name[zh_TW]=桌布 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +Categories=KDE Wallpaper 800x600,KDE Wallpaper 1024x768,KDE Wallpaper 1280x1024,KDE Wallpaper 1440x900,KDE Wallpaper 1600x1200,KDE Wallpaper (other) +TargetDir=wallpapers +Uncompress=subdir-archive + +AdoptionCommand=plasma-apply-wallpaperimage %f diff --git a/plasma/workspace/xembed-sni-proxy/CMakeLists.txt b/plasma/workspace/xembed-sni-proxy/CMakeLists.txt new file mode 100644 index 0000000000..bcc583bfa0 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/CMakeLists.txt @@ -0,0 +1,66 @@ +add_definitions(-DQT_NO_CAST_TO_ASCII +-DQT_NO_CAST_FROM_ASCII +-DQT_NO_CAST_FROM_BYTEARRAY) + +find_package(XCB + REQUIRED COMPONENTS + XCB + XFIXES + DAMAGE + COMPOSITE + RANDR + SHM + UTIL + IMAGE +) + +set(XCB_LIBS + XCB::XCB + XCB::XFIXES + XCB::DAMAGE + XCB::COMPOSITE + XCB::RANDR + XCB::SHM + XCB::UTIL + XCB::IMAGE +) + +set(XEMBED_SNI_PROXY_SOURCES + main.cpp + fdoselectionmanager.cpp + snidbus.cpp + sniproxy.cpp + xtestsender.cpp + ) + +qt_add_dbus_adaptor(XEMBED_SNI_PROXY_SOURCES org.kde.StatusNotifierItem.xml + sniproxy.h SNIProxy) + +set(statusnotifierwatcher_xml org.kde.StatusNotifierWatcher.xml) +qt_add_dbus_interface(XEMBED_SNI_PROXY_SOURCES ${statusnotifierwatcher_xml} statusnotifierwatcher_interface) + +ecm_qt_declare_logging_category(XEMBED_SNI_PROXY_SOURCES HEADER debug.h + IDENTIFIER SNIPROXY + CATEGORY_NAME kde.xembedsniproxy + DEFAULT_SEVERITY Info) + +add_executable(xembedsniproxy ${XEMBED_SNI_PROXY_SOURCES}) + + + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + + +target_link_libraries(xembedsniproxy + Qt::Core + Qt::X11Extras + Qt::DBus + KF5::WindowSystem + ${XCB_LIBS} + X11::Xtst +) + +install(TARGETS xembedsniproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES xembedsniproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) + +ecm_install_configured_files(INPUT plasma-xembedsniproxy.service.in @ONLY DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) diff --git a/plasma/workspace/xembed-sni-proxy/Readme.md b/plasma/workspace/xembed-sni-proxy/Readme.md new file mode 100644 index 0000000000..3bfb77f514 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/Readme.md @@ -0,0 +1,30 @@ +##XEmbed SNI Proxy + +The goal of this project is to make xembed system trays available in Plasma. + +This is to allow legacy apps (xchat, pidgin, tuxguitar) etc. system trays[1] available in Plasma which only supports StatusNotifierItem [2]. + +Ideally we also want this to work in an xwayland session, making X system tray icons available even when plasmashell only has a wayland connection. + +This project should be portable onto all other DEs that speak SNI. + +##How it works (in theory) + +* We register a window as a system tray container +* We render embedded windows composited offscreen +* We render contents into an image and send this over DBus via the SNI protocol +* XDamage events trigger a repaint +* Activate and context menu events are replyed via X send event into the embedded container as left and right clicks + +There are a few extra hacks in the real code to deal with some toolkits being awkward. + +##Build instructions + + cmake . + make + sudo make install + +After building, run `xembedsniproxy`. + +[1] http://standards.freedesktop.org/systemtray-spec/systemtray-spec-latest.html +[2] http://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ diff --git a/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.cpp b/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.cpp new file mode 100644 index 0000000000..495b88014a --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.cpp @@ -0,0 +1,235 @@ +/* + Registers as a embed container + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#include "fdoselectionmanager.h" + +#include "debug.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "sniproxy.h" +#include "xcbutils.h" + +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 + +FdoSelectionManager::FdoSelectionManager() + : QObject() + , m_selectionOwner(new KSelectionOwner(Xcb::atoms->selectionAtom, -1, this)) +{ + qCDebug(SNIPROXY) << "starting"; + + // we may end up calling QCoreApplication::quit() in this method, at which point we need the event loop running + QTimer::singleShot(0, this, &FdoSelectionManager::init); +} + +FdoSelectionManager::~FdoSelectionManager() +{ + qCDebug(SNIPROXY) << "closing"; + m_selectionOwner->release(); +} + +void FdoSelectionManager::init() +{ + // load damage extension + xcb_connection_t *c = QX11Info::connection(); + xcb_prefetch_extension_data(c, &xcb_damage_id); + const auto *reply = xcb_get_extension_data(c, &xcb_damage_id); + if (reply && reply->present) { + m_damageEventBase = reply->first_event; + xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION); + } else { + // no XDamage means + qCCritical(SNIPROXY) << "could not load damage extension. Quitting"; + qApp->exit(-1); + } + + qApp->installNativeEventFilter(this); + + connect(m_selectionOwner, &KSelectionOwner::claimedOwnership, this, &FdoSelectionManager::onClaimedOwnership); + connect(m_selectionOwner, &KSelectionOwner::failedToClaimOwnership, this, &FdoSelectionManager::onFailedToClaimOwnership); + connect(m_selectionOwner, &KSelectionOwner::lostOwnership, this, &FdoSelectionManager::onLostOwnership); + m_selectionOwner->claim(false); +} + +bool FdoSelectionManager::addDamageWatch(xcb_window_t client) +{ + qCDebug(SNIPROXY) << "adding damage watch for " << client; + + xcb_connection_t *c = QX11Info::connection(); + const auto attribsCookie = xcb_get_window_attributes_unchecked(c, client); + + const auto damageId = xcb_generate_id(c); + m_damageWatches[client] = damageId; + xcb_damage_create(c, damageId, client, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); + + xcb_generic_error_t *error = nullptr; + QScopedPointer attr(xcb_get_window_attributes_reply(c, attribsCookie, &error)); + QScopedPointer getAttrError(error); + uint32_t events = XCB_EVENT_MASK_STRUCTURE_NOTIFY; + if (!attr.isNull()) { + events = events | attr->your_event_mask; + } + // if window is already gone, there is no need to handle it. + if (getAttrError && getAttrError->error_code == XCB_WINDOW) { + return false; + } + // the event mask will not be removed again. We cannot track whether another component also needs STRUCTURE_NOTIFY (e.g. KWindowSystem). + // if we would remove the event mask again, other areas will break. + const auto changeAttrCookie = xcb_change_window_attributes_checked(c, client, XCB_CW_EVENT_MASK, &events); + QScopedPointer changeAttrError(xcb_request_check(c, changeAttrCookie)); + // if window is gone by this point, it will be caught by eventFilter, so no need to check later errors. + if (changeAttrError && changeAttrError->error_code == XCB_WINDOW) { + return false; + } + + return true; +} + +bool FdoSelectionManager::nativeEventFilter(const QByteArray &eventType, void *message, long int *result) +{ + Q_UNUSED(result) + + if (eventType != "xcb_generic_event_t") { + return false; + } + + xcb_generic_event_t *ev = static_cast(message); + + const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); + if (responseType == XCB_CLIENT_MESSAGE) { + const auto ce = reinterpret_cast(ev); + if (ce->type == Xcb::atoms->opcodeAtom) { + switch (ce->data.data32[1]) { + case SYSTEM_TRAY_REQUEST_DOCK: + dock(ce->data.data32[2]); + return true; + } + } + } else if (responseType == XCB_UNMAP_NOTIFY) { + const auto unmappedWId = reinterpret_cast(ev)->window; + if (m_proxies.contains(unmappedWId)) { + undock(unmappedWId); + } + } else if (responseType == XCB_DESTROY_NOTIFY) { + const auto destroyedWId = reinterpret_cast(ev)->window; + if (m_proxies.contains(destroyedWId)) { + undock(destroyedWId); + } + } else if (responseType == m_damageEventBase + XCB_DAMAGE_NOTIFY) { + const auto damagedWId = reinterpret_cast(ev)->drawable; + const auto sniProxy = m_proxies.value(damagedWId); + if (sniProxy) { + sniProxy->update(); + xcb_damage_subtract(QX11Info::connection(), m_damageWatches[damagedWId], XCB_NONE, XCB_NONE); + } + } else if (responseType == XCB_CONFIGURE_REQUEST) { + const auto event = reinterpret_cast(ev); + const auto sniProxy = m_proxies.value(event->window); + if (sniProxy) { + // The embedded window tries to move or resize. Ignore move, handle resize only. + if ((event->value_mask & XCB_CONFIG_WINDOW_WIDTH) || (event->value_mask & XCB_CONFIG_WINDOW_HEIGHT)) { + sniProxy->resizeWindow(event->width, event->height); + } + } + } else if (responseType == XCB_VISIBILITY_NOTIFY) { + const auto event = reinterpret_cast(ev); + // it's possible that something showed our container window, we have to hide it + // workaround for BUG 357443: when KWin is restarted, container window is shown on top + if (event->state == XCB_VISIBILITY_UNOBSCURED) { + for (auto sniProxy : m_proxies.values()) { + sniProxy->hideContainerWindow(event->window); + } + } + } + + return false; +} + +void FdoSelectionManager::dock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to dock window " << winId; + + if (m_proxies.contains(winId)) { + return; + } + + if (addDamageWatch(winId)) { + m_proxies[winId] = new SNIProxy(winId, this); + } +} + +void FdoSelectionManager::undock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to undock window " << winId; + + if (!m_proxies.contains(winId)) { + return; + } + m_proxies[winId]->deleteLater(); + m_proxies.remove(winId); +} + +void FdoSelectionManager::onClaimedOwnership() +{ + qCDebug(SNIPROXY) << "Manager selection claimed"; + + setSystemTrayVisual(); +} + +void FdoSelectionManager::onFailedToClaimOwnership() +{ + qCWarning(SNIPROXY) << "failed to claim ownership of Systray Manager"; + qApp->exit(-1); +} + +void FdoSelectionManager::onLostOwnership() +{ + qCWarning(SNIPROXY) << "lost ownership of Systray Manager"; + qApp->exit(-1); +} + +void FdoSelectionManager::setSystemTrayVisual() +{ + xcb_connection_t *c = QX11Info::connection(); + auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + auto trayVisual = screen->root_visual; + xcb_depth_iterator_t depth_iterator = xcb_screen_allowed_depths_iterator(screen); + xcb_depth_t *depth = nullptr; + + while (depth_iterator.rem) { + if (depth_iterator.data->depth == 32) { + depth = depth_iterator.data; + break; + } + xcb_depth_next(&depth_iterator); + } + + if (depth) { + xcb_visualtype_iterator_t visualtype_iterator = xcb_depth_visuals_iterator(depth); + while (visualtype_iterator.rem) { + xcb_visualtype_t *visualtype = visualtype_iterator.data; + if (visualtype->_class == XCB_VISUAL_CLASS_TRUE_COLOR) { + trayVisual = visualtype->visual_id; + break; + } + xcb_visualtype_next(&visualtype_iterator); + } + } + + xcb_change_property(c, XCB_PROP_MODE_REPLACE, m_selectionOwner->ownerWindow(), Xcb::atoms->visualAtom, XCB_ATOM_VISUALID, 32, 1, &trayVisual); +} diff --git a/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.h b/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.h new file mode 100644 index 0000000000..16695e2d35 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/fdoselectionmanager.h @@ -0,0 +1,47 @@ +/* + Registers as a embed container + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include + +#include + +class KSelectionOwner; +class SNIProxy; + +class FdoSelectionManager : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT + +public: + FdoSelectionManager(); + ~FdoSelectionManager() override; + +protected: + bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override; + +private Q_SLOTS: + void onClaimedOwnership(); + void onFailedToClaimOwnership(); + void onLostOwnership(); + +private: + void init(); + bool addDamageWatch(xcb_window_t client); + void dock(xcb_window_t embed_win); + void undock(xcb_window_t client); + void setSystemTrayVisual(); + + uint8_t m_damageEventBase; + + QHash m_damageWatches; + QHash m_proxies; + KSelectionOwner *m_selectionOwner; +}; diff --git a/plasma/workspace/xembed-sni-proxy/main.cpp b/plasma/workspace/xembed-sni-proxy/main.cpp new file mode 100644 index 0000000000..a95ec36a7e --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/main.cpp @@ -0,0 +1,60 @@ +/* + Main + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include +#include + +#include "fdoselectionmanager.h" + +#include "debug.h" +#include "snidbus.h" +#include "xcbutils.h" + +#include + +#include + +namespace Xcb +{ +Xcb::Atoms *atoms; +} + +int main(int argc, char **argv) +{ + // the whole point of this is to interact with X, if we are in any other session, force trying to connect to X + // if the QPA can't load xcb, this app is useless anyway. + qputenv("QT_QPA_PLATFORM", "xcb"); + + QGuiApplication::setDesktopSettingsAware(false); + + QGuiApplication app(argc, argv); + + if (!KWindowSystem::isPlatformX11()) { + qFatal("xembed-sni-proxy is only useful XCB. Aborting"); + } + + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement); + + app.setQuitOnLastWindowClosed(false); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + Xcb::atoms = new Xcb::Atoms(); + + FdoSelectionManager manager; + + auto rc = app.exec(); + + delete Xcb::atoms; + return rc; +} diff --git a/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierItem.xml b/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierItem.xml new file mode 100644 index 0000000000..0cd7edb1a3 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierItem.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml b/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml new file mode 100644 index 0000000000..2eb1a7a0b8 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma/workspace/xembed-sni-proxy/plasma-xembedsniproxy.service.in b/plasma/workspace/xembed-sni-proxy/plasma-xembedsniproxy.service.in new file mode 100644 index 0000000000..61090fd006 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/plasma-xembedsniproxy.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=Handle legacy xembed system tray icons +PartOf=graphical-session.target +After=plasma-core.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/xembedsniproxy +Restart=on-failure +Type=simple +Slice=background.slice +TimeoutSec=5sec diff --git a/plasma/workspace/xembed-sni-proxy/snidbus.cpp b/plasma/workspace/xembed-sni-proxy/snidbus.cpp new file mode 100644 index 0000000000..12caec540e --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/snidbus.cpp @@ -0,0 +1,133 @@ +/* + SNI DBus Serialisers + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "snidbus.h" + +#include +#include + +// mostly copied from KStatusNotiferItemDbus.cpps from knotification + +KDbusImageStruct::KDbusImageStruct() +{ +} + +KDbusImageStruct::KDbusImageStruct(const QImage &image) +{ + width = image.size().width(); + height = image.size().height(); + if (image.format() == QImage::Format_ARGB32) { + data = QByteArray((char *)image.bits(), image.sizeInBytes()); + } else { + QImage image32 = image.convertToFormat(QImage::Format_ARGB32); + data = QByteArray((char *)image32.bits(), image32.sizeInBytes()); + } + + // swap to network byte order if we are little endian + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { + quint32 *uintBuf = (quint32 *)data.data(); + for (uint i = 0; i < data.size() / sizeof(quint32); ++i) { + *uintBuf = qToBigEndian(*uintBuf); + ++uintBuf; + } + } +} + +// Marshall the ImageStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.data; + argument.endStructure(); + return argument; +} + +// Retrieve the ImageStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon) +{ + qint32 width; + qint32 height; + QByteArray data; + + argument.beginStructure(); + argument >> width; + argument >> height; + argument >> data; + argument.endStructure(); + + icon.width = width; + icon.height = height; + icon.data = data; + + return argument; +} + +// Marshall the ImageVector data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector) +{ + argument.beginArray(qMetaTypeId()); + for (int i = 0; i < iconVector.size(); ++i) { + argument << iconVector[i]; + } + argument.endArray(); + return argument; +} + +// Retrieve the ImageVector data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector) +{ + argument.beginArray(); + iconVector.clear(); + + while (!argument.atEnd()) { + KDbusImageStruct element; + argument >> element; + iconVector.append(element); + } + + argument.endArray(); + + return argument; +} + +// Marshall the ToolTipStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip) +{ + argument.beginStructure(); + argument << toolTip.icon; + argument << toolTip.image; + argument << toolTip.title; + argument << toolTip.subTitle; + argument.endStructure(); + return argument; +} + +// Retrieve the ToolTipStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip) +{ + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; + + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + + toolTip.icon = icon; + toolTip.image = image; + toolTip.title = title; + toolTip.subTitle = subTitle; + + return argument; +} diff --git a/plasma/workspace/xembed-sni-proxy/snidbus.h b/plasma/workspace/xembed-sni-proxy/snidbus.h new file mode 100644 index 0000000000..87ba1ed5af --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/snidbus.h @@ -0,0 +1,47 @@ +/* + SNI Dbus serialisers + Copyright 2015 David Edmundson + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include +#include + +// Custom message type for DBus +struct KDbusImageStruct { + KDbusImageStruct(); + KDbusImageStruct(const QImage &image); + int width; + int height; + QByteArray data; +}; + +typedef QVector KDbusImageVector; + +struct KDbusToolTipStruct { + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; +}; + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon); + +Q_DECLARE_METATYPE(KDbusImageStruct) + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector); + +Q_DECLARE_METATYPE(KDbusImageVector) + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip); + +Q_DECLARE_METATYPE(KDbusToolTipStruct) diff --git a/plasma/workspace/xembed-sni-proxy/sniproxy.cpp b/plasma/workspace/xembed-sni-proxy/sniproxy.cpp new file mode 100644 index 0000000000..08b750e4ee --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/sniproxy.cpp @@ -0,0 +1,596 @@ +/* + Holds one embedded window, registers as DBus entry + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "sniproxy.h" + +#include +#include +#include + +#include "debug.h" +#include "xcbutils.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); + } + Q_EMIT NewIcon(); + Q_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 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; +} diff --git a/plasma/workspace/xembed-sni-proxy/sniproxy.h b/plasma/workspace/xembed-sni-proxy/sniproxy.h new file mode 100644 index 0000000000..1fd69f2b5c --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/sniproxy.h @@ -0,0 +1,153 @@ +/* + Holds one embedded window, registers as DBus entry + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "snidbus.h" + +class SNIProxy : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString Category READ Category) + Q_PROPERTY(QString Id READ Id) + Q_PROPERTY(QString Title READ Title) + Q_PROPERTY(QString Status READ Status) + Q_PROPERTY(int WindowId READ WindowId) + Q_PROPERTY(bool ItemIsMenu READ ItemIsMenu) + Q_PROPERTY(KDbusImageVector IconPixmap READ IconPixmap) + +public: + explicit SNIProxy(xcb_window_t wid, QObject *parent = nullptr); + ~SNIProxy() override; + + void update(); + void resizeWindow(const uint16_t width, const uint16_t height) const; + void hideContainerWindow(xcb_window_t windowId) const; + + /** + * @return the category of the application associated to this item + * @see Category + */ + QString Category() const; + + /** + * @return the id of this item + */ + QString Id() const; + + /** + * @return the title of this item + */ + QString Title() const; + + /** + * @return The status of this item + * @see Status + */ + QString Status() const; + + /** + * @return The id of the main window of the application that controls the item + */ + int WindowId() const; + + /** + * @return The item only support the context menu, the visualization should prefer sending ContextMenu() instead of Activate() + */ + bool ItemIsMenu() const; + + /** + * @return a serialization of the icon data + */ + KDbusImageVector IconPixmap() const; + +public Q_SLOTS: + // interaction + /** + * Shows the context menu associated to this item + * at the desired screen position + */ + void ContextMenu(int x, int y); + + /** + * Shows the main widget and try to position it on top + * of the other windows, if the widget is already visible, hide it. + */ + void Activate(int x, int y); + + /** + * The user activated the item in an alternate way (for instance with middle mouse button, this depends from the systray implementation) + */ + void SecondaryActivate(int x, int y); + + /** + * Inform this item that the mouse wheel was used on its representation + */ + void Scroll(int delta, const QString &orientation); + +Q_SIGNALS: + /** + * Inform the systemtray that the own main icon has been changed, + * so should be reloaded + */ + void NewIcon(); + + /** + * Inform the systemtray that there is a new icon to be used as overlay + */ + void NewOverlayIcon(); + + /** + * Inform the systemtray that the requesting attention icon + * has been changed, so should be reloaded + */ + void NewAttentionIcon(); + + /** + * Inform the systemtray that something in the tooltip has been changed + */ + void NewToolTip(); + + /** + * Signal the new status when it has been changed + * @see Status + */ + void NewStatus(const QString &status); + +private: + enum InjectMode { + Direct, + XTest, + }; + + QSize calculateClientWindowSize() const; + void sendClick(uint8_t mouseButton, int x, int y); + QImage getImageNonComposite() const; + bool isTransparentImage(const QImage &image) const; + QImage convertFromNative(xcb_image_t *xcbImage) const; + QPoint calculateClickPoint() const; + void stackContainerWindow(const uint32_t stackMode) const; + + QDBusConnection m_dbus; + xcb_window_t m_windowId; + xcb_window_t m_containerWid; + static int s_serviceCount; + QPixmap m_pixmap; + bool sendingClickEvent; + InjectMode m_injectMode; +}; diff --git a/plasma/workspace/xembed-sni-proxy/xcbutils.h b/plasma/workspace/xembed-sni-proxy/xcbutils.h new file mode 100644 index 0000000000..b3288ab723 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/xcbutils.h @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2012, 2013 Martin Graesslin + SPDX-FileCopyrightText: 2015 David Edmudson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/** XEMBED messages */ +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_WINDOW_ACTIVATE 1 +#define XEMBED_WINDOW_DEACTIVATE 2 +#define XEMBED_REQUEST_FOCUS 3 +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 +#define XEMBED_FOCUS_NEXT 6 +#define XEMBED_FOCUS_PREV 7 + +namespace Xcb +{ +typedef xcb_window_t WindowId; + +template +using ScopedCPointer = QScopedPointer; + +class Atom +{ +public: + explicit Atom(const QByteArray &name, bool onlyIfExists = false, xcb_connection_t *c = QX11Info::connection()) + : m_connection(c) + , m_retrieved(false) + , m_cookie(xcb_intern_atom_unchecked(m_connection, onlyIfExists, name.length(), name.constData())) + , m_atom(XCB_ATOM_NONE) + , m_name(name) + { + } + Atom() = delete; + Atom(const Atom &) = delete; + + ~Atom() + { + if (!m_retrieved && m_cookie.sequence) { + xcb_discard_reply(m_connection, m_cookie.sequence); + } + } + + operator xcb_atom_t() const + { + (const_cast(this))->getReply(); + return m_atom; + } + bool isValid() + { + getReply(); + return m_atom != XCB_ATOM_NONE; + } + bool isValid() const + { + (const_cast(this))->getReply(); + return m_atom != XCB_ATOM_NONE; + } + + inline const QByteArray &name() const + { + return m_name; + } + +private: + void getReply() + { + if (m_retrieved || !m_cookie.sequence) { + return; + } + ScopedCPointer reply(xcb_intern_atom_reply(m_connection, m_cookie, nullptr)); + if (!reply.isNull()) { + m_atom = reply->atom; + } + m_retrieved = true; + } + xcb_connection_t *m_connection; + bool m_retrieved; + xcb_intern_atom_cookie_t m_cookie; + xcb_atom_t m_atom; + QByteArray m_name; +}; + +class Atoms +{ +public: + Atoms() + : xembedAtom("_XEMBED") + , selectionAtom(xcb_atom_name_by_screen("_NET_SYSTEM_TRAY", QX11Info::appScreen())) + , opcodeAtom("_NET_SYSTEM_TRAY_OPCODE") + , messageData("_NET_SYSTEM_TRAY_MESSAGE_DATA") + , visualAtom("_NET_SYSTEM_TRAY_VISUAL") + { + } + + Atom xembedAtom; + Atom selectionAtom; + Atom opcodeAtom; + Atom messageData; + Atom visualAtom; +}; + +extern Atoms *atoms; + +} // namespace Xcb diff --git a/plasma/workspace/xembed-sni-proxy/xembedsniproxy.desktop b/plasma/workspace/xembed-sni-proxy/xembedsniproxy.desktop new file mode 100644 index 0000000000..0fb9ecff43 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/xembedsniproxy.desktop @@ -0,0 +1,55 @@ +[Desktop Entry] +Exec=xembedsniproxy +Name=XembedSniProxy +Name[ar]=ميفاق sni مضمن +Name[ast]=XembedSniProxy +Name[az]=XembedSniProxy +Name[ca]=XembedSniProxy +Name[ca@valencia]=XembedSniProxy +Name[cs]=XembedSniProxy +Name[da]=XembedSniProxy +Name[de]=XembedSniProxy +Name[el]=XembedSniProxy +Name[en_GB]=XembedSniProxy +Name[es]=XembedSniProxy +Name[et]=XembedSniProxy +Name[eu]=XembedSniProxy +Name[fi]=XembedSniProxy +Name[fr]=XembedSniProxy +Name[gl]=XembedSniProxy +Name[hi]=एक्सएमबेड-एसएनआइ-प्रॉक्सी +Name[hu]=XembedSniProxy +Name[ia]=XembedSniProxy +Name[id]=XembedSniProxy +Name[is]=XembedSniProxy +Name[it]=XembedSniProxy +Name[ko]=XembedSniProxy +Name[lt]=XembedSniProxy +Name[ml]=എക്സ്എമ്പെഡ്സ്നിപ്രോക്സി +Name[nl]=XembedSniProxy +Name[nn]=XembedSniProxy +Name[pa]=XembedSniProxy +Name[pl]=XembedSniProxy +Name[pt]=XembedSniProxy +Name[pt_BR]=XembedSniProxy +Name[ro]=XembedSniProxy +Name[ru]=XembedSniProxy +Name[sk]=XembedSniProxy +Name[sl]=XembedSniProxy +Name[sr]=Иксембед‑сни‑прокси +Name[sr@ijekavian]=Иксембед‑сни‑прокси +Name[sr@ijekavianlatin]=XembedSniProxy +Name[sr@latin]=XembedSniProxy +Name[sv]=XembedSniProxy +Name[tr]=XembedSniProxy +Name[uk]=XembedSniProxy +Name[vi]=XembedSniProxy +Name[x-test]=xxXembedSniProxyxx +Name[zh_CN]=XembedSniProxy +Name[zh_TW]=XembedSniProxy +Type=Application +X-KDE-StartupNotify=false +NoDisplay=true +OnlyShowIn=KDE; +X-KDE-autostart-phase=0 +X-systemd-skip=true diff --git a/plasma/workspace/xembed-sni-proxy/xtestsender.cpp b/plasma/workspace/xembed-sni-proxy/xtestsender.cpp new file mode 100644 index 0000000000..69833d4c54 --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/xtestsender.cpp @@ -0,0 +1,19 @@ +/* Wrap XLIB code in a new file as it defines keywords that conflict with Qt + + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "xtestsender.h" +#include + +void sendXTestPressed(Display *display, int button) +{ + XTestFakeButtonEvent(display, button, true, 0); +} + +void sendXTestReleased(Display *display, int button) +{ + XTestFakeButtonEvent(display, button, false, 0); +} diff --git a/plasma/workspace/xembed-sni-proxy/xtestsender.h b/plasma/workspace/xembed-sni-proxy/xtestsender.h new file mode 100644 index 0000000000..50176bdc9e --- /dev/null +++ b/plasma/workspace/xembed-sni-proxy/xtestsender.h @@ -0,0 +1,12 @@ +/* Wrap XLIB code in a new file as it defines keywords that conflict with Qt + + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +typedef struct _XDisplay Display; + +void sendXTestPressed(Display *display, int button); +void sendXTestReleased(Display *display, int button);