├── .gitignore ├── Changelog.md ├── ReadMe.md ├── Theming.md ├── build ├── install ├── kpac ├── package ├── contents │ ├── config │ │ ├── config.qml │ │ └── main.xml │ ├── icons │ │ ├── view-list-alphabetically.svg │ │ ├── view-list-categorically.svg │ │ └── view-tilesonly.svg │ └── ui │ │ ├── AppContextMenu.qml │ │ ├── AppObject.qml │ │ ├── AppToolButton.qml │ │ ├── AppToolButtonStyle.qml │ │ ├── AppletConfig.qml │ │ ├── AppsModel.qml │ │ ├── AppsView.qml │ │ ├── Base64JsonString.qml │ │ ├── ButtonShadow.qml │ │ ├── FlatButton.qml │ │ ├── Hierarchy.md │ │ ├── HoverOutlineButtonEffect.qml │ │ ├── HoverOutlineButtonStyle.qml │ │ ├── HoverOutlineEffect.qml │ │ ├── JumpToLetterView.qml │ │ ├── JumpToSectionButton.qml │ │ ├── JumpToSectionView.qml │ │ ├── KickerAppModel.qml │ │ ├── KickerListModel.qml │ │ ├── KickerListView.qml │ │ ├── KickerSectionHeader.qml │ │ ├── LauncherIcon.qml │ │ ├── MenuListItem.qml │ │ ├── Popup.qml │ │ ├── RoundShadow.qml │ │ ├── SearchField.qml │ │ ├── SearchFiltersView.qml │ │ ├── SearchFiltersViewItem.qml │ │ ├── SearchModel.qml │ │ ├── SearchResultsList.qml │ │ ├── SearchResultsModel.qml │ │ ├── SearchResultsView.qml │ │ ├── SearchStackView.qml │ │ ├── SearchView.qml │ │ ├── SidebarContextMenu.qml │ │ ├── SidebarFavouritesView.qml │ │ ├── SidebarItem.qml │ │ ├── SidebarItemRepeater.qml │ │ ├── SidebarMenu.qml │ │ ├── SidebarMenuShadows.qml │ │ ├── SidebarView.qml │ │ ├── SidebarViewButton.qml │ │ ├── TileEditorColorField.qml │ │ ├── TileEditorColorGroup.qml │ │ ├── TileEditorField.qml │ │ ├── TileEditorFileField.qml │ │ ├── TileEditorGroupBox.qml │ │ ├── TileEditorPresetTileButton.qml │ │ ├── TileEditorPresetTiles.qml │ │ ├── TileEditorRectField.qml │ │ ├── TileEditorSpinBox.qml │ │ ├── TileEditorView.qml │ │ ├── TileGrid.qml │ │ ├── TileGridPresets.qml │ │ ├── TileGridSplash.qml │ │ ├── TileItem.qml │ │ ├── TileItemView.qml │ │ ├── Utils.js │ │ ├── config │ │ ├── ConfigExportLayout.qml │ │ ├── ConfigGeneral.qml │ │ ├── ConfigurationShortcuts.qml │ │ └── TextAreaBase64JsonString.qml │ │ ├── lib │ │ ├── ConfigAdvanced.qml │ │ ├── Logger.qml │ │ ├── Requests.js │ │ └── XdgUserDir.qml │ │ ├── libconfig │ │ ├── CheckBox.qml │ │ ├── ColorField.qml │ │ ├── ComboBox.qml │ │ ├── FormKCM.qml │ │ ├── Heading.qml │ │ ├── IconField.qml │ │ ├── RadioButtonGroup.qml │ │ ├── SpinBox.qml │ │ ├── TextArea.qml │ │ └── TextAreaStringList.qml │ │ └── main.qml ├── metadata.json └── translate │ ├── ReadMe.md │ ├── de.po │ ├── es.po │ ├── fa.po │ ├── fi.po │ ├── fr.po │ ├── he.po │ ├── hr.po │ ├── id.po │ ├── ja.po │ ├── ko.po │ ├── nl.po │ ├── pl.po │ ├── pt.po │ ├── pt_BR.po │ ├── ro.po │ ├── ru.po │ ├── sl.po │ ├── template.pot │ ├── tr.po │ ├── zh_CN.po │ └── zh_TW.po └── uninstall /.gitignore: -------------------------------------------------------------------------------- 1 | *.plasmoid 2 | *.qmlc 3 | *.jsc 4 | *.mo 5 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Tiled Menu 2 | 3 | https://store.kde.org/p/2142716/ 4 | 5 | A menu based on Windows 10's Start Menu. 6 | 7 | * Supports: 8 | * Pin/Favourite apps/files through the context menu (or by dragging them from dolphin). 9 | * Resizing (permanently) the size of the menu by Meta + Right Clicking and dragging. 10 | * Any size tile 1x1, 2x2, 4x4, 4x2, 1x3, etc. 11 | * Easily edit the background image of a tile. 12 | * Customizable sidebar shortcuts. 13 | * Jump to Letter/Category (can also default to this view) 14 | * Defaulting to only showing the tiles. 15 | * Labeling Groups of Tiles + Move Groups of Tiles + Sorting items in the group 16 | * Does not support (Win10): 17 | * Tile Groups ("Folders") 18 | 19 | ## Screenshots 20 | 21 | ![](https://i.imgur.com/rf6dI9Q.png) 22 | 23 | ## Theming 24 | 25 | Read the [theming guide](Theming.md) to develop Desktop/Icon Themes for this widget. 26 | 27 | ## Translating 28 | 29 | See the [package/translate](package/translate) folder for instructions on translating. 30 | -------------------------------------------------------------------------------- /Theming.md: -------------------------------------------------------------------------------- 1 | Fair warning, I might look for `widgets/tilemenu.svg` in the future for a the tile background (normal/hover) + sidebar (closed/opened) + sidebar buttons (normal/hover/pressed). So don't get toooo comfortable since I'm still developing this widget. 2 | 3 | ## Desktop Theme .svgs 4 | 5 | https://techbase.kde.org/Development/Tutorials/Plasma5/ThemeDetails 6 | 7 | * Sidebar / Power Menu 8 | * Background 9 | * Defaults to drawing `theme.backgroundColor` at 50% transparency when closed, and 0% transparency when open. It used to be black (`#000`) in older versions. 10 | * `widgets/frame.svg` prefix: `raised` Background 11 | * Note that `theme.backgroundColor` is drawn undrneath the svg when the sidebar is open since 90% of themes have a transparent image. 12 | * App List 13 | * Items 14 | * `widgets/button.svg` 15 | * Scrollbar 16 | * `widgets/scrollbar.svg` 17 | * Search Box 18 | * `widgets/lineedit.svg` 19 | 20 | 21 | ## Color Theme 22 | 23 | * `theme.backgroundColor` 24 | * Drawn under the sidebar background svg when using the desktop theme. 25 | * `theme.buttonBackgroundColor` 26 | * The default tile background color. 27 | 28 | 29 | ## Icons 30 | 31 | * Sidebar 32 | * `open-menu-symbolic` Menu 33 | * `view-sort-ascending-symbolic` Apps 34 | * `system-search-symbolic` Search 35 | * `open-menu-symbolic` Menu 36 | * ... 37 | * `folder-open-symbolic` File Manager 38 | * `configure` Settings 39 | * `system-shutdown-symbolic` Power 40 | * `system-lock-screen` Lock 41 | * `system-log-out` Logout 42 | * `system-save-session` Save Session 43 | * `system-switch-user` Switch User 44 | * `system-suspend` Suspend 45 | * `system-suspend-hibernate` Hibernate 46 | * `system-reboot` Reboot 47 | * `system-shutdown` Shutdown 48 | * Search View 49 | * Filter Bar 50 | * `system-search-symbolic` All/Default Filter 51 | * `window` Apps Filter 52 | * `document-new` File Filter 53 | * `globe` Bookmarks Filter 54 | 55 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 22 3 | 4 | packageDir="package" 5 | i18nDir="package/translate" 6 | qtMinVer="6.6" 7 | kfMinVer="6.0" 8 | plasmaMinVer="6.0" 9 | filenameTag="plasma${plasmaMinVer}" 10 | filenameTag=`echo "$filenameTag" | sed 's/\./\-/'` 11 | 12 | function printHelp() { 13 | echo "sh ./build For building a .zip for KDE Store" 14 | echo "sh ./build --i18n-only To package for distros like Arch we only need to convert" 15 | echo " translation files .po => .mo with gettext's msgfmt." 16 | } 17 | 18 | # Builds .zip or .plasmoid for uploading to https://store.kde.org 19 | function buildZip() { 20 | python3 ./kpac --dir "$packageDir" --i18ndir "$i18nDir" \ 21 | build --tag "$filenameTag" 22 | } 23 | 24 | # For distributing with distro packaging (like AUR) we only need to convert 25 | # the translation *.po files to *.mo files with gettext's msgfmt command. 26 | # Eg: "package/translate/fr.po" => "package/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.widgetname.mo" 27 | function buildI18nOnly() { 28 | python3 ./kpac --dir "$packageDir" --i18ndir "$i18nDir" \ 29 | i18n --no-merge 30 | } 31 | 32 | showHelp=false 33 | i18nOnly=false 34 | for arg in "$@"; do 35 | case "$arg" in 36 | --i18n-only) i18nOnly=true;; 37 | -h|--help) showHelp=true;; 38 | *) ;; 39 | esac 40 | done 41 | 42 | if $showHelp; then 43 | # sh ./build --help 44 | printHelp 45 | elif $i18nOnly; then 46 | # sh ./build --i18n-only 47 | buildI18nOnly 48 | else 49 | # sh ./build 50 | buildZip 51 | fi 52 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 8 3 | 4 | # This script detects if the widget is already installed. 5 | # If it is, it will use --upgrade instead and restart plasmashell. 6 | # Eg: kpackagetool6 --type "Plasma/Applet" --install package 7 | # Eg: kpackagetool6 --type "Plasma/Applet" --upgrade package 8 | # Eg: killall plasmashell ; kstart plasmashell 9 | 10 | if [ -f "$PWD/package/metadata.json" ]; then # Plasma6 (and later versions of Plasma5) 11 | packageNamespace=`python3 -c 'import sys, json; print(json.load(sys.stdin).get("KPlugin", {}).get("Id", ""))' < "$PWD/package/metadata.json"` 12 | packageServiceType=`python3 -c 'import sys, json; print(json.load(sys.stdin).get("KPackageStructure",""))' < "$PWD/package/metadata.json"` 13 | if [ -z "$packageServiceType" ]; then # desktoptojson will set KPlugin.ServiceTypes[0] instead of KPackageStructure 14 | packageServiceType=`python3 -c 'import sys, json; print((json.load(sys.stdin).get("KPlugin", {}).get("ServiceTypes", [])+[""])[0])' < "$PWD/package/metadata.json"` 15 | echo "[warning] metadata.json needs KPackageStructure set in Plasma6" 16 | fi 17 | elif [ -f "$PWD/package/metadata.desktop" ]; then # Plasma5 18 | packageNamespace=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 19 | packageServiceType=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-ServiceTypes"` 20 | else 21 | echo "[error] Could not find 'package/metadata.json' or 'package/metadata.desktop'" 22 | exit 1 23 | fi 24 | echo "Namespace: ${packageNamespace}" 25 | echo "Type: ${packageServiceType}" 26 | if [ -z "$packageServiceType" ]; then 27 | echo "[error] Could not parse metadata" 28 | exit 1 29 | fi 30 | 31 | 32 | if command -v kpackagetool6 &> /dev/null ; then kpackagetool="kpackagetool6" # Plasma6 33 | elif command -v kpackagetool5 &> /dev/null ; then kpackagetool="kpackagetool5" # Plasma5 34 | else 35 | echo "[error] Could not find 'kpackagetool6'" 36 | exit 1 37 | fi 38 | if command -v kstart &> /dev/null ; then kstart="kstart" # Plasma6 39 | elif command -v kstart5 &> /dev/null ; then kstart="kstart5" # Plasma5 40 | else 41 | echo "[error] Could not find 'kstart'" 42 | exit 1 43 | fi 44 | restartPlasmashell=false 45 | 46 | for arg in "$@"; do 47 | case "$arg" in 48 | -r) restartPlasmashell=true;; 49 | --restart) restartPlasmashell=true;; 50 | *) ;; 51 | esac 52 | done 53 | 54 | isAlreadyInstalled=false 55 | "$kpackagetool" --type="${packageServiceType}" --show="$packageNamespace" &> /dev/null 56 | if [ $? == 0 ]; then 57 | isAlreadyInstalled=true 58 | fi 59 | 60 | if $isAlreadyInstalled; then 61 | # Eg: kpackagetool6 --type "Plasma/Applet" --upgrade package 62 | "$kpackagetool" -t "${packageServiceType}" -u package 63 | restartPlasmashell=true 64 | else 65 | # Eg: kpackagetool6 --type "Plasma/Applet" --install package 66 | "$kpackagetool" -t "${packageServiceType}" -i package 67 | fi 68 | 69 | if $restartPlasmashell; then 70 | killall plasmashell 71 | "$kstart" plasmashell 72 | fi 73 | -------------------------------------------------------------------------------- /package/contents/config/config.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.configuration 3 | 4 | ConfigModel { 5 | ConfigCategory { 6 | name: i18n("General") 7 | icon: "configure" 8 | source: "config/ConfigGeneral.qml" 9 | } 10 | ConfigCategory { 11 | name: i18n("Import/Export Layout") 12 | icon: "grid-rectangular" 13 | source: "config/ConfigExportLayout.qml" 14 | } 15 | ConfigCategory { 16 | name: i18n("Advanced") 17 | icon: "applications-development" 18 | source: "lib/ConfigAdvanced.qml" 19 | visible: false 20 | } 21 | ConfigCategory { 22 | name: i18nd("plasma_shell_org.kde.plasma.desktop", "Keyboard Shortcuts") 23 | icon: "preferences-desktop-keyboard" 24 | source: "config/ConfigurationShortcuts.qml" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package/contents/config/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | false 8 | 9 | 10 | 11 | start-here-kde-symbolic 12 | 13 | 14 | true 15 | 16 | 17 | true 18 | 19 | 20 | false 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | krunner_systemsettings,Dictionary,services,calculator,shell,org.kde.windowedwidgets,org.kde.datetime,baloosearch,locations,unitconverter 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 1 37 | 38 | 39 | 5 40 | 41 | 42 | 43 | xdg:DOCUMENTS,xdg:PICTURES,org.kde.dolphin.desktop,systemsettings.desktop 44 | 45 | 46 | 47 | Alphabetical 48 | 49 | 50 | 51 | 52 | org.kde.konsole.desktop 53 | 54 | 55 | org.kde.plasma-systemmonitor.desktop 56 | 57 | 58 | org.kde.dolphin.desktop 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0.8 69 | 70 | 71 | 5 72 | 73 | 74 | false 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | false 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | false 92 | 93 | 94 | false 95 | 96 | 97 | false 98 | 99 | 100 | 101 | left 102 | 103 | 104 | 105 | left 106 | 107 | 108 | 109 | after 110 | 111 | 112 | 36 113 | 114 | 115 | 48 116 | 117 | 118 | 350 119 | 120 | 121 | 6 122 | 123 | 124 | 620 125 | 126 | 127 | 48 128 | 129 | 130 | 30 131 | 132 | 133 | 36 134 | 135 | 136 | 146 | 147 | 153 | 154 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /package/contents/icons/view-list-alphabetically.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /package/contents/icons/view-tilesonly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 22 | 27 | 32 | 33 | -------------------------------------------------------------------------------- /package/contents/ui/AppContextMenu.qml: -------------------------------------------------------------------------------- 1 | // Based off kicker's ActionMenu 2 | import QtQuick 3 | import org.kde.plasma.extras as PlasmaExtras 4 | 5 | Item { 6 | id: root 7 | 8 | property QtObject menu 9 | property Item visualParent 10 | property bool opened: menu ? (menu.status != PlasmaExtras.Menu.Closed) : false 11 | property int tileIndex: -1 12 | 13 | signal closed 14 | signal populateMenu(var menu) 15 | 16 | onOpenedChanged: { 17 | if (!opened) { 18 | closed() 19 | } 20 | } 21 | 22 | onClosed: destroyMenu() 23 | 24 | function open(x, y) { 25 | refreshMenu() 26 | 27 | if (menu.content.length === 0) { 28 | return 29 | } 30 | 31 | if (x && y) { 32 | menu.open(x, y) 33 | } else { 34 | menu.open() 35 | } 36 | } 37 | 38 | function destroyMenu() { 39 | if (menu) { 40 | menu.destroy() 41 | // menu = null // Don't null here. Binding loop: onOpended=false => closed() => destroyMenu() => menu=null => opened=false 42 | logger.debug('AppContextMenu.destroyMenu', menu) 43 | } 44 | } 45 | 46 | function refreshMenu() { 47 | destroyMenu() 48 | menu = contextMenuComponent.createObject(root) 49 | populateMenu(menu) 50 | } 51 | 52 | Component { 53 | id: contextMenuComponent 54 | 55 | PlasmaExtras.Menu { 56 | id: contextMenu 57 | visualParent: root.visualParent 58 | 59 | function newSeperator() { 60 | return Qt.createQmlObject("import org.kde.plasma.extras as PlasmaExtras; PlasmaExtras.MenuItem { separator: true }", contextMenu) 61 | } 62 | function newMenuItem() { 63 | return Qt.createQmlObject("import org.kde.plasma.extras as PlasmaExtras; PlasmaExtras.MenuItem {}", contextMenu) 64 | } 65 | 66 | function addPinToMenuAction(favoriteId) { 67 | var menuItem = menu.newMenuItem() 68 | if (tileGrid.hasAppTile(favoriteId)) { 69 | menuItem.text = i18n("Unpin from Menu") 70 | menuItem.icon = "list-remove" 71 | menuItem.clicked.connect(function() { 72 | if (root.tileIndex >= 0) { 73 | tileGrid.removeIndex(root.tileIndex) 74 | } else { 75 | tileGrid.removeApp(favoriteId) 76 | } 77 | }) 78 | } else { 79 | menuItem.text = i18n("Pin to Menu") 80 | menuItem.icon = "bookmark-new" 81 | menuItem.clicked.connect(function() { 82 | tileGrid.addApp(favoriteId) 83 | }) 84 | } 85 | menu.addMenuItem(menuItem) 86 | } 87 | 88 | // https://invent.kde.org/plasma/plasma-desktop/-/blob/Plasma/5.8/applets/taskmanager/package/contents/ui/ContextMenu.qml#L75 89 | // https://invent.kde.org/plasma/plasma-desktop/-/blob/Plasma/5.27/applets/taskmanager/package/contents/ui/ContextMenu.qml#L75 90 | // https://invent.kde.org/plasma/plasma-desktop/-/blob/master/applets/taskmanager/package/contents/ui/ContextMenu.qml 91 | function addActionList(actionList, listModel, index) { 92 | // .desktop file Exec actions 93 | // ------ 94 | // Pin to Taskbar / Desktop / Panel 95 | // ------ 96 | // Recent Documents 97 | // ------ 98 | // ... 99 | // ------ 100 | // Edit Application 101 | actionList.forEach(function(actionItem) { 102 | // console.log(index, actionItem.actionId, actionItem.actionArgument, actionItem.text) 103 | var menuItem = menu.newMenuItem() 104 | menuItem.text = actionItem.text ? actionItem.text : "" 105 | menuItem.enabled = actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) 106 | menuItem.separator = actionItem.type == "separator" 107 | menuItem.section = actionItem.type == "title" 108 | menuItem.icon = actionItem.icon ? actionItem.icon : null 109 | menuItem.clicked.connect(function() { 110 | listModel.triggerIndexAction(index, actionItem.actionId, actionItem.actionArgument) 111 | }) 112 | 113 | //--- Overrides 114 | if (actionItem.actionId == 'addToDesktop') { 115 | // Remove (user should just drag it) 116 | menu.removeMenuItem(menuItem) 117 | } else if (actionItem.actionId == 'addToPanel') { 118 | // Remove (user should just drag it) 119 | // User usually means to add it to taskmanager anyways. 120 | menu.removeMenuItem(menuItem) 121 | } else if (actionItem.actionId == 'addToTaskManager') { 122 | menuItem.text = i18n("Pin to Taskbar") 123 | menuItem.icon = "bookmark-new" 124 | } else if (actionItem.actionId == 'editApplication') { 125 | // menuItem.text = i18n("Properties") 126 | } 127 | 128 | }) 129 | } 130 | } 131 | } 132 | 133 | Component { 134 | id: contextMenuItemComponent 135 | 136 | PlasmaExtras.MenuItem { 137 | property variant actionItem 138 | 139 | text: actionItem.text ? actionItem.text : "" 140 | enabled: actionItem.type != "title" && ("enabled" in actionItem ? actionItem.enabled : true) 141 | separator: actionItem.type == "separator" 142 | section: actionItem.type == "title" 143 | icon: actionItem.icon ? actionItem.icon : null 144 | 145 | onClicked: { 146 | actionClicked(actionItem.actionId, actionItem.actionArgument) 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /package/contents/ui/AppObject.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | QtObject { 4 | id: appObj 5 | 6 | property var tile: null 7 | 8 | readonly property bool isGroup: tile && tile.tileType == "group" 9 | readonly property bool isLauncher: !isGroup 10 | 11 | readonly property color defaultBackgroundColor: isGroup ? "transparent" : config.defaultTileColor 12 | readonly property bool defaultShowIcon: isGroup ? false : true 13 | readonly property int defaultTileW: isGroup ? 6 : 2 14 | readonly property int defaultTileH: isGroup ? 1 : 2 15 | 16 | readonly property string favoriteId: tile && tile.url || '' 17 | readonly property var app: favoriteId ? appsModel.tileGridModel.getApp(favoriteId) : null 18 | readonly property string appLabel: app ? app.display : "" 19 | readonly property string appUrl: app ? app.url : "" 20 | readonly property var appIcon: app ? app.decoration : null 21 | readonly property string labelText: tile && tile.label || appLabel || appUrl || "" 22 | readonly property var iconSource: tile && tile.icon || appIcon 23 | readonly property bool iconFill: tile && typeof tile.iconFill !== "undefined" ? tile.iconFill : false 24 | readonly property bool showIcon: tile && typeof tile.showIcon !== "undefined" ? tile.showIcon : defaultShowIcon 25 | readonly property bool showLabel: tile && typeof tile.showLabel !== "undefined" ? tile.showLabel : true 26 | readonly property color backgroundColor: tile && typeof tile.backgroundColor !== "undefined" ? tile.backgroundColor : defaultBackgroundColor 27 | readonly property string backgroundImage: tile && typeof tile.backgroundImage !== "undefined" ? tile.backgroundImage : "" 28 | readonly property bool backgroundGradient: tile && typeof tile.gradient !== "undefined" ? tile.gradient : config.defaultTileGradient 29 | 30 | readonly property int tileX: tile && typeof tile.x !== "undefined" ? tile.x : 0 31 | readonly property int tileY: tile && typeof tile.y !== "undefined" ? tile.y : 0 32 | readonly property int tileW: tile && typeof tile.w !== "undefined" ? tile.w : defaultTileW 33 | readonly property int tileH: tile && typeof tile.h !== "undefined" ? tile.h : defaultTileH 34 | 35 | 36 | // onTileChanged: console.log('onTileChanged', JSON.stringify(tile)) 37 | // onAppLabelChanged: console.log('onAppLabelChanged', appLabel) 38 | 39 | function hasActionList() { 40 | return app ? appsModel.tileGridModel.indexHasActionList(app.indexInModel) : false 41 | } 42 | 43 | function getActionList() { 44 | return app ? appsModel.tileGridModel.getActionListAtIndex(app.indexInModel) : [] 45 | } 46 | 47 | function addActionList(menu) { 48 | if (hasActionList()) { 49 | var actionList = getActionList() 50 | menu.addActionList(actionList, appsModel.tileGridModel, appObj.app.indexInModel) 51 | } 52 | } 53 | 54 | readonly property var groupRect: { 55 | if (isGroup) { 56 | return tileGrid.getGroupAreaRect(tile) 57 | } else { 58 | return null 59 | } 60 | } 61 | property Connections tileGridConnection: Connections { 62 | target: tileGrid 63 | function onTileModelChanged() { 64 | if (appObj.isGroup) { 65 | appObj.groupRectChanged() 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /package/contents/ui/AppToolButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.kirigami as Kirigami 3 | 4 | MouseArea { 5 | id: control 6 | hoverEnabled: true 7 | 8 | property alias hovered: control.containsMouse 9 | property string iconName: "" 10 | property var iconSource: null 11 | property string text: "" 12 | 13 | Kirigami.MnemonicData.enabled: control.enabled && control.visible 14 | Kirigami.MnemonicData.label: control.text 15 | 16 | property font font: Kirigami.Theme.defaultFont 17 | property real minimumWidth: 0 18 | property real minimumHeight: 0 19 | property bool flat: true 20 | 21 | property int paddingTop: styleLoader.item ? styleLoader.item.paddingTop : 0 22 | property int paddingLeft: styleLoader.item ? styleLoader.item.paddingLeft : 0 23 | property int paddingRight: styleLoader.item ? styleLoader.item.paddingRight : 0 24 | property int paddingBottom: styleLoader.item ? styleLoader.item.paddingBottom : 0 25 | 26 | Loader { 27 | id: styleLoader 28 | anchors.fill: parent 29 | asynchronous: true 30 | // source: "AppToolButtonStyle.qml" 31 | source: "HoverOutlineButtonStyle.qml" 32 | property var mouseArea: control 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package/contents/ui/AppToolButtonStyle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.ksvg as KSvg 3 | 4 | Item { 5 | id: style 6 | 7 | property int paddingTop: surfaceNormal.margins.top 8 | property int paddingLeft: surfaceNormal.margins.left 9 | property int paddingRight: surfaceNormal.margins.right 10 | property int paddingBottom: surfaceNormal.margins.bottom 11 | 12 | ButtonShadow { 13 | id: shadow 14 | visible: control.activeFocus 15 | anchors.fill: parent 16 | enabledBorders: surfaceNormal.enabledBorders 17 | state: { 18 | if (control.pressed) { 19 | return "hidden" 20 | } else if (control.containsMouse) { 21 | return "hover" 22 | } else if (control.activeFocus) { 23 | return "focus" 24 | } else { 25 | return "shadow" 26 | } 27 | } 28 | } 29 | KSvg.FrameSvgItem { 30 | id: surfaceNormal 31 | anchors.fill: parent 32 | imagePath: "widgets/button" 33 | prefix: "normal" 34 | enabledBorders: "AllBorders" 35 | } 36 | KSvg.FrameSvgItem { 37 | id: surfacePressed 38 | anchors.fill: parent 39 | imagePath: "widgets/button" 40 | prefix: "pressed" 41 | enabledBorders: surfaceNormal.enabledBorders 42 | opacity: 0 43 | } 44 | 45 | state: (control.pressed || control.checked ? "pressed" : (control.containsMouse ? "hover" : "normal")) 46 | 47 | states: [ 48 | State { name: "normal" 49 | PropertyChanges { 50 | target: surfaceNormal 51 | opacity: 0 52 | } 53 | PropertyChanges { 54 | target: surfacePressed 55 | opacity: 0 56 | } 57 | }, 58 | State { name: "hover" 59 | PropertyChanges { 60 | target: surfaceNormal 61 | opacity: 1 62 | } 63 | PropertyChanges { 64 | target: surfacePressed 65 | opacity: 0 66 | } 67 | }, 68 | State { name: "pressed" 69 | PropertyChanges { 70 | target: surfaceNormal 71 | opacity: 0 72 | } 73 | PropertyChanges { 74 | target: surfacePressed 75 | opacity: 1 76 | } 77 | } 78 | ] 79 | 80 | transitions: [ 81 | Transition { 82 | //Cross fade from pressed to normal 83 | ParallelAnimation { 84 | NumberAnimation { target: surfaceNormal; property: "opacity"; duration: 100 } 85 | NumberAnimation { target: surfacePressed; property: "opacity"; duration: 100 } 86 | } 87 | } 88 | ] 89 | 90 | } 91 | -------------------------------------------------------------------------------- /package/contents/ui/AppletConfig.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Window 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.core as PlasmaCore 5 | 6 | Item { 7 | function setAlpha(c, a) { 8 | var c2 = Qt.darker(c, 1) 9 | c2.a = a 10 | return c2 11 | } 12 | 13 | //--- Sizes 14 | readonly property int panelIconSize: 24 * Screen.devicePixelRatio 15 | readonly property int flatButtonSize: plasmoid.configuration.sidebarButtonSize * Screen.devicePixelRatio 16 | readonly property int flatButtonIconSize: plasmoid.configuration.sidebarIconSize * Screen.devicePixelRatio 17 | readonly property int sidebarWidth: flatButtonSize 18 | readonly property int sidebarMinOpenWidth: 200 * Screen.devicePixelRatio 19 | readonly property int sidebarRightMargin: 4 * Screen.devicePixelRatio 20 | readonly property int sidebarPopupButtonSize: plasmoid.configuration.sidebarPopupButtonSize * Screen.devicePixelRatio 21 | readonly property int appListWidth: plasmoid.configuration.appListWidth * Screen.devicePixelRatio 22 | readonly property int tileEditorMinWidth: Math.max(350, 350 * Screen.devicePixelRatio) 23 | readonly property int minimumHeight: flatButtonSize * 5 // Issue #125 24 | 25 | property bool showSearch: false 26 | property bool isEditingTile: false 27 | readonly property int appAreaWidth: { 28 | if (isEditingTile) { 29 | return tileEditorMinWidth 30 | } else if (showSearch) { 31 | return appListWidth 32 | } else { 33 | return 0 34 | } 35 | } 36 | readonly property bool hideSearchField: plasmoid.configuration.hideSearchField 37 | readonly property int leftSectionWidth: sidebarWidth + sidebarRightMargin + appAreaWidth 38 | 39 | readonly property real tileScale: plasmoid.configuration.tileScale 40 | readonly property int cellBoxUnits: 80 41 | readonly property int cellMarginUnits: plasmoid.configuration.tileMargin 42 | readonly property int cellSizeUnits: cellBoxUnits - cellMarginUnits*2 43 | readonly property int cellSize: cellSizeUnits * tileScale * Screen.devicePixelRatio 44 | readonly property real cellMargin: cellMarginUnits * tileScale * Screen.devicePixelRatio 45 | readonly property real cellPushedMargin: cellMargin * 2 46 | readonly property int cellBoxSize: cellMargin + cellSize + cellMargin 47 | readonly property int tileGridWidth: plasmoid.configuration.favGridCols * cellBoxSize 48 | 49 | readonly property int favCellWidth: 60 * Screen.devicePixelRatio 50 | readonly property int favCellPushedMargin: 5 * Screen.devicePixelRatio 51 | readonly property int favCellPadding: 3 * Screen.devicePixelRatio 52 | readonly property int favColWidth: ((favCellWidth + favCellPadding * 2) * 2) // = 132 (Medium Size) 53 | readonly property int favViewDefaultWidth: (favColWidth * 3) * Screen.devicePixelRatio 54 | readonly property int favSmallIconSize: 32 * Screen.devicePixelRatio 55 | readonly property int favMediumIconSize: 72 * Screen.devicePixelRatio 56 | readonly property int favGridWidth: (plasmoid.configuration.favGridCols/2) * favColWidth 57 | 58 | readonly property int searchFieldHeight: plasmoid.configuration.searchFieldHeight * Screen.devicePixelRatio 59 | 60 | readonly property int popupWidth: { 61 | if (plasmoid.configuration.fullscreen) { 62 | return Screen.desktopAvailableWidth 63 | } else { 64 | return leftSectionWidth + tileGridWidth 65 | } 66 | } 67 | readonly property int popupHeight: { 68 | if (plasmoid.configuration.fullscreen) { 69 | return Screen.desktopAvailableHeight 70 | } else { 71 | // implicit Math.floor() when cast as int 72 | var dPR = Screen.devicePixelRatio 73 | var pH3 = plasmoid.configuration.popupHeight 74 | var pH4 = pH3 * dPR 75 | var pH5 = Math.floor(pH4) 76 | // console.log('pH.get', 'dPR='+dPR, 'pH3='+pH3, 'pH4='+pH4, 'pH5='+pH5) 77 | return pH5 78 | } 79 | } 80 | 81 | readonly property int menuItemHeight: plasmoid.configuration.menuItemHeight * Screen.devicePixelRatio 82 | 83 | readonly property int searchFilterRowHeight: { 84 | if (plasmoid.configuration.appListWidth >= 310) { 85 | return flatButtonSize // 60px 86 | } else if (plasmoid.configuration.appListWidth >= 250) { 87 | return flatButtonSize*3/4 // 45px 88 | } else { 89 | return flatButtonSize/2 // 30px 90 | } 91 | } 92 | 93 | //--- Colors 94 | readonly property color themeButtonBgColor: { 95 | if (PlasmaCore.Theme.themeName == "oxygen") { 96 | return "#20FFFFFF" 97 | } else { 98 | return Kirigami.Theme.backgroundColor 99 | } 100 | } 101 | readonly property color defaultTileColor: plasmoid.configuration.defaultTileColor || themeButtonBgColor 102 | readonly property bool defaultTileGradient: plasmoid.configuration.defaultTileGradient 103 | readonly property color sidebarBackgroundColor: plasmoid.configuration.sidebarBackgroundColor || Kirigami.Theme.backgroundColor 104 | readonly property color menuItemTextColor2: setAlpha(Kirigami.Theme.textColor, 0.6) 105 | readonly property color favHoverOutlineColor: setAlpha(Kirigami.Theme.textColor, 0.8) 106 | readonly property color flatButtonBgHoverColor: themeButtonBgColor 107 | readonly property color flatButtonBgColor: Qt.rgba(flatButtonBgHoverColor.r, flatButtonBgHoverColor.g, flatButtonBgHoverColor.b, 0) 108 | readonly property color flatButtonBgPressedColor: Kirigami.Theme.highlightColor 109 | readonly property color flatButtonCheckedColor: Kirigami.Theme.highlightColor 110 | 111 | //--- Style 112 | // Tiles 113 | readonly property int tileLabelAlignment: { 114 | var val = plasmoid.configuration.tileLabelAlignment 115 | if (val === 'center') { 116 | return Text.AlignHCenter 117 | } else if (val === 'right') { 118 | return Text.AlignRight 119 | } else { // left 120 | return Text.AlignLeft 121 | } 122 | } 123 | readonly property int groupLabelAlignment: { 124 | var val = plasmoid.configuration.groupLabelAlignment 125 | if (val === 'center') { 126 | return Text.AlignHCenter 127 | } else if (val === 'right') { 128 | return Text.AlignRight 129 | } else { // left 130 | return Text.AlignLeft 131 | } 132 | } 133 | 134 | // App Description Enum (hidden, after, below) 135 | readonly property bool appDescriptionVisible: plasmoid.configuration.appDescription !== 'hidden' 136 | readonly property bool appDescriptionBelow: plasmoid.configuration.appDescription == 'below' 137 | 138 | //--- Settings 139 | // Search 140 | readonly property bool searchResultsMerged: plasmoid.configuration.searchResultsMerged 141 | readonly property bool searchResultsCustomSort: plasmoid.configuration.searchResultsCustomSort 142 | readonly property int searchResultsDirection: plasmoid.configuration.searchResultsReversed ? ListView.BottomToTop : ListView.TopToBottom 143 | 144 | //--- Tile Data 145 | property var tileModel: Base64JsonString { 146 | configKey: 'tileModel' 147 | defaultValue: [] 148 | 149 | // defaultValue: [ 150 | // { 151 | // "x": 0, 152 | // "y": 0, 153 | // "w": 2, 154 | // "h": 2, 155 | // "url": "org.kde.dolphin.desktop", 156 | // "label": "Files", 157 | // }, 158 | // { 159 | // "x": 2, 160 | // "y": 1, 161 | // "w": 1, 162 | // "h": 1, 163 | // "url": "virtualbox.desktop", 164 | // "iconFill": true, 165 | // }, 166 | // { 167 | // "x": 2, 168 | // "y": 0, 169 | // "w": 1, 170 | // "h": 1, 171 | // "url": "org.kde.ark.desktop", 172 | // }, 173 | // ] 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /package/contents/ui/AppsView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | 4 | QQC2.ScrollView { 5 | id: appsView 6 | property alias listView: appsListView 7 | 8 | // The horizontal ScrollBar always appears in QQC2 for some reason. 9 | // The PC3 is drawn as if it thinks the scrollWidth is 0, which is 10 | // possible since it inits at width=350px, then changes to 0px until 11 | // the popup is opened before it returns to 350px. 12 | QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff 13 | 14 | KickerListView { 15 | id: appsListView 16 | 17 | section.property: 'sectionKey' 18 | // section.criteria: ViewSection.FirstCharacter 19 | 20 | model: appsModel.allAppsModel // Should be populated by the time this is created 21 | 22 | section.delegate: KickerSectionHeader { 23 | enableJumpToSection: true 24 | } 25 | 26 | delegate: MenuListItem { 27 | secondRowVisible: config.appDescriptionBelow 28 | description: config.appDescriptionVisible ? modelDescription : '' 29 | } 30 | 31 | iconSize: config.menuItemHeight 32 | showItemUrl: false 33 | } 34 | 35 | function scrollToTop() { 36 | appsListView.positionViewAtBeginning() 37 | } 38 | 39 | function jumpToSection(section) { 40 | for (var i = 0; i < appsListView.model.count; i++) { 41 | var app = appsListView.model.get(i) 42 | if (section == app.sectionKey) { 43 | appsListView.currentIndex = i 44 | appsListView.positionViewAtIndex(i, ListView.Beginning) 45 | break 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package/contents/ui/Base64JsonString.qml: -------------------------------------------------------------------------------- 1 | // Version 4 2 | 3 | import QtQuick 4 | 5 | QtObject { 6 | property string configKey 7 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" 8 | property variant value: { return {} } 9 | property variant defaultValue: { return {} } 10 | property bool writing: false 11 | property bool loadOnConfigChange: true 12 | signal loaded() 13 | 14 | Component.onCompleted: { 15 | load() 16 | } 17 | 18 | onConfigValueChanged: { 19 | if (loadOnConfigChange && !writing) { 20 | load() 21 | } 22 | } 23 | 24 | onDefaultValueChanged: { 25 | if (configValue === '') { // Optimization 26 | load() 27 | } 28 | } 29 | 30 | function getBase64Json(key, defaultValue) { 31 | if (configValue === '') { 32 | return defaultValue 33 | } 34 | var val = Qt.atob(configValue) // decode base64 35 | val = JSON.parse(val) 36 | return val 37 | } 38 | 39 | function setBase64Json(key, data) { 40 | var val = JSON.stringify(data) 41 | val = Qt.btoa(val) 42 | writing = true 43 | plasmoid.configuration[key] = val 44 | writing = false 45 | } 46 | 47 | function set(obj) { 48 | setBase64Json(configKey, obj) 49 | } 50 | 51 | function setItemProperty(key1, key2, val) { 52 | var item = value[key1] || {} 53 | item[key2] = val 54 | value[key1] = item 55 | set(value) 56 | valueChanged() 57 | } 58 | 59 | function getItemProperty(key1, key2, def) { 60 | var item = value[key1] || {} 61 | return typeof item[key2] !== "undefined" ? item[key2] : def 62 | } 63 | 64 | function load() { 65 | // console.log('load') 66 | // console.log('configKey', configKey) 67 | // console.log('plasmoid.configuration[key]', plasmoid.configuration[configKey]) 68 | value = getBase64Json(configKey, defaultValue) 69 | loaded() 70 | } 71 | 72 | function save() { 73 | // console.log('save') 74 | // console.log('configKey', configKey) 75 | // console.log('plasmoid.configuration[key]', plasmoid.configuration[configKey]) 76 | setBase64Json(configKey, value || defaultValue) 77 | } 78 | 79 | onValueChanged: { 80 | // console.log('onValueChanged', configKey, value) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package/contents/ui/ButtonShadow.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 by Daker Fernandes Pinheiro 3 | * Copyright (C) 2011 by Marco Martin 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU Library General Public License as 7 | * published by the Free Software Foundation; either version 2, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Library General Public License for more details 14 | * 15 | * You should have received a copy of the GNU Library General Public 16 | * License along with this program; if not, write to the 17 | * Free Software Foundation, Inc., 18 | * 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. 19 | */ 20 | 21 | /**Documented API 22 | Inherits: 23 | Item 24 | 25 | Imports: 26 | QtQuick 2.1 27 | org.kde.plasma.core 28 | 29 | Description: 30 | TODO i need more info here 31 | 32 | 33 | Properties: 34 | **/ 35 | 36 | import QtQuick 37 | import org.kde.kirigami as Kirigami 38 | import org.kde.ksvg as KSvg 39 | 40 | Item { 41 | id: main 42 | state: parent.state 43 | //used to tell apart this implementation with the touch components one 44 | property bool hasOverState: true 45 | property alias enabledBorders: shadow.enabledBorders 46 | 47 | KSvg.FrameSvgItem { 48 | id: hover 49 | 50 | anchors { 51 | fill: parent 52 | leftMargin: -margins.left 53 | topMargin: -margins.top 54 | rightMargin: -margins.right 55 | bottomMargin: -margins.bottom 56 | } 57 | opacity: 0 58 | imagePath: "widgets/button" 59 | prefix: "hover" 60 | } 61 | 62 | KSvg.FrameSvgItem { 63 | id: shadow 64 | 65 | anchors { 66 | fill: parent 67 | leftMargin: -margins.left 68 | topMargin: -margins.top 69 | rightMargin: -margins.right 70 | bottomMargin: -margins.bottom 71 | } 72 | imagePath: "widgets/button" 73 | prefix: "shadow" 74 | } 75 | 76 | states: [ 77 | State { 78 | name: "shadow" 79 | PropertyChanges { 80 | target: shadow 81 | opacity: 1 82 | } 83 | PropertyChanges { 84 | target: hover 85 | opacity: 0 86 | prefix: "hover" 87 | } 88 | }, 89 | State { 90 | name: "hover" 91 | PropertyChanges { 92 | target: shadow 93 | opacity: 0 94 | } 95 | PropertyChanges { 96 | target: hover 97 | opacity: 1 98 | prefix: "hover" 99 | } 100 | }, 101 | State { 102 | name: "focus" 103 | PropertyChanges { 104 | target: shadow 105 | opacity: 0 106 | } 107 | PropertyChanges { 108 | target: hover 109 | opacity: 1 110 | prefix: "focus" 111 | } 112 | }, 113 | State { 114 | name: "hidden" 115 | PropertyChanges { 116 | target: shadow 117 | opacity: 0 118 | } 119 | PropertyChanges { 120 | target: hover 121 | opacity: 0 122 | prefix: "hover" 123 | } 124 | } 125 | ] 126 | 127 | transitions: [ 128 | Transition { 129 | PropertyAnimation { 130 | properties: "opacity" 131 | duration: Kirigami.Units.longDuration 132 | easing.type: Easing.OutQuad 133 | } 134 | } 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /package/contents/ui/FlatButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | import QtQuick.Layouts 4 | import org.kde.kirigami as Kirigami 5 | import org.kde.plasma.components as PlasmaComponents3 6 | 7 | QQC2.ToolButton { 8 | id: flatButton 9 | 10 | icon.name: "" 11 | property bool expanded: true 12 | text: "" 13 | display: expanded ? QQC2.AbstractButton.TextBesideIcon : QQC2.AbstractButton.IconOnly 14 | property string label: expanded ? text : "" 15 | property bool labelVisible: text != "" 16 | property color backgroundColor: config.flatButtonBgColor 17 | property color backgroundHoverColor: config.flatButtonBgHoverColor 18 | property color backgroundPressedColor: config.flatButtonBgPressedColor 19 | property color checkedColor: config.flatButtonCheckedColor 20 | property bool zoomOnPush: true 21 | 22 | // http://doc.qt.io/qt-5/qt.html#Edge-enum 23 | property int checkedEdge: 0 // 0 = all edges 24 | property int checkedEdgeWidth: 2 * Screen.devicePixelRatio 25 | 26 | property int buttonHeight: config.flatButtonSize 27 | property int iconSize: config.flatButtonIconSize 28 | readonly property int _iconSize: Math.min(buttonHeight, iconSize) 29 | implicitHeight: buttonHeight 30 | 31 | // contentItem: RowLayout { 32 | // id: labelRowLayout 33 | // // spacing: Kirigami.Units.smallSpacing 34 | // spacing: 0 35 | // scale: control.zoomOnPush && control.pressed ? (height-5) / height : 1 36 | // Behavior on scale { NumberAnimation { duration: 200 } } 37 | 38 | // Item { 39 | // id: iconContainer 40 | // Layout.fillHeight: true 41 | // implicitWidth: height 42 | // visible: !!icon.source 43 | 44 | // Kirigami.Icon { 45 | // id: icon 46 | // source: control.icon.name 47 | // implicitWidth: control._iconSize 48 | // implicitHeight: control._iconSize 49 | // anchors.centerIn: parent 50 | // // colorGroup: Kirigami.Theme.Button 51 | // } 52 | 53 | // // Rectangle { border.color: "#f00"; anchors.fill: parent; border.width: 1; color: "transparent"; } 54 | // } 55 | 56 | // Item { 57 | // id: spacingItem 58 | // Layout.fillHeight: true 59 | // implicitWidth: 4 * Screen.devicePixelRatio 60 | // visible: control.labelVisible 61 | 62 | // // Rectangle { border.color: "#f00"; anchors.fill: parent; border.width: 1; color: "transparent"; } 63 | // } 64 | 65 | // PlasmaComponents3.Label { 66 | // id: label 67 | // text: QtQuickControlsPrivate.StyleHelpers.stylizeMnemonics(control.text) 68 | // font: control.font || Kirigami.Theme.defaultFont 69 | // visible: control.labelVisible 70 | // horizontalAlignment: Text.AlignLeft 71 | // verticalAlignment: Text.AlignVCenter 72 | // Layout.fillWidth: true 73 | 74 | // // Rectangle { border.color: "#f00"; anchors.fill: parent; border.width: 1; color: "transparent"; } 75 | // } 76 | 77 | // Item { 78 | // id: rightPaddingItem 79 | // Layout.fillHeight: true 80 | // property int iconMargin: (iconContainer.width - icon.width)/2 81 | // property int iconPadding: icon.width * (16-12)/16 82 | // implicitWidth: iconMargin + iconPadding 83 | // visible: control.labelVisible 84 | 85 | // // Rectangle { border.color: "#f00"; anchors.fill: parent; border.width: 1; color: "transparent"; } 86 | // } 87 | // } 88 | 89 | // background: Item { 90 | // Rectangle { 91 | // id: background 92 | // anchors.fill: parent 93 | // color: flatButton.backgroundColor 94 | // } 95 | 96 | // Rectangle { 97 | // id: checkedOutline 98 | // color: flatButton.checkedColor 99 | // visible: control.checked 100 | // anchors.left: parent.left 101 | // anchors.top: parent.top 102 | // anchors.right: parent.right 103 | // anchors.bottom: parent.bottom 104 | 105 | // states: [ 106 | // State { 107 | // when: control.checkedEdge === 0 108 | // PropertyChanges { 109 | // target: checkedOutline 110 | // anchors.fill: checkedOutline.parent 111 | // color: "transparent" 112 | // border.color: flatButton.checkedColor 113 | // } 114 | // }, 115 | // State { 116 | // when: control.checkedEdge == Qt.TopEdge 117 | // PropertyChanges { 118 | // target: checkedOutline 119 | // anchors.bottom: undefined 120 | // height: control.checkedEdgeWidth 121 | // } 122 | // }, 123 | // State { 124 | // when: control.checkedEdge == Qt.LeftEdge 125 | // PropertyChanges { 126 | // target: checkedOutline 127 | // anchors.right: undefined 128 | // width: control.checkedEdgeWidth 129 | // } 130 | // }, 131 | // State { 132 | // when: control.checkedEdge == Qt.RightEdge 133 | // PropertyChanges { 134 | // target: checkedOutline 135 | // anchors.left: undefined 136 | // width: control.checkedEdgeWidth 137 | // } 138 | // }, 139 | // State { 140 | // when: control.checkedEdge == Qt.BottomEdge 141 | // PropertyChanges { 142 | // target: checkedOutline 143 | // anchors.top: undefined 144 | // height: control.checkedEdgeWidth 145 | // } 146 | // } 147 | // ] 148 | // } 149 | 150 | // states: [ 151 | // State { 152 | // name: "hovering" 153 | // when: !control.pressed && control.hovered 154 | // PropertyChanges { 155 | // target: background 156 | // color: flatButton.backgroundHoverColor 157 | // } 158 | // }, 159 | // State { 160 | // name: "pressed" 161 | // when: control.pressed 162 | // PropertyChanges { 163 | // target: background 164 | // color: flatButton.backgroundPressedColor 165 | // } 166 | // } 167 | // ] 168 | 169 | // transitions: [ 170 | // Transition { 171 | // to: "hovering" 172 | // ColorAnimation { duration: 200 } 173 | // }, 174 | // Transition { 175 | // to: "pressed" 176 | // ColorAnimation { duration: 100 } 177 | // } 178 | // ] 179 | // } 180 | } 181 | -------------------------------------------------------------------------------- /package/contents/ui/Hierarchy.md: -------------------------------------------------------------------------------- 1 | Main 2 | SearchModel 3 | SearchResultsModel 4 | LauncherIcon 5 | Popup 6 | SearchView 7 | SidebarView 8 | SearchResultsView 9 | SearchResultsList 10 | SearchField 11 | TileGrid 12 | 13 | 14 | krunner_id (filename) 15 | 16 | org.kde.activities (activityrunner) 17 | Audio Player Control Runner (audioplayercontrol) 18 | baloosearch (baloosearch) 19 | bookmarks (bookmarks) 20 | calculator (calculator) 21 | unitconverter (converter) 22 | org.kde.datetime (datetime) 23 | Dictionary (dictionary) 24 | Kill Runner (kill) 25 | kwin (kwin) 26 | locations (locations) 27 | places (places) 28 | Name=plasma-desktop (plasma) 29 | PowerDevil (powerdevil) 30 | services (services) 31 | desktopsessions (sessions) 32 | shell (shell) 33 | Spell Checker (spellchecker) 34 | webshortcuts (webshortcuts) 35 | org.kde.windowedwidgets (windowedwidgets) 36 | windows (windows) 37 | -------------------------------------------------------------------------------- /package/contents/ui/HoverOutlineButtonEffect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | HoverOutlineEffect { 4 | id: hoverOutlineButtonEffect 5 | anchors.fill: parent 6 | hoverRadius: Math.max(width/2, height) 7 | pressedRadius: width 8 | mouseArea: __mouseArea 9 | } 10 | -------------------------------------------------------------------------------- /package/contents/ui/HoverOutlineButtonStyle.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Item { 4 | id: style 5 | property int paddingTop: 0 6 | property int paddingRight: 0 7 | property int paddingBottom: 0 8 | property int paddingLeft: 0 9 | 10 | Loader { 11 | id: hoverOutlineEffectLoader 12 | anchors.fill: parent 13 | active: mouseArea.containsMouse 14 | visible: active 15 | source: "HoverOutlineButtonEffect.qml" 16 | 17 | property var __mouseArea: mouseArea 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /package/contents/ui/HoverOutlineEffect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.kirigami as Kirigami 3 | 4 | 5 | // https://doc.qt.io/qt-5/graphicaleffects.html 6 | // https://doc.qt.io/qt-6/qtgraphicaleffects5-index.html 7 | // import QtGraphicalEffects 1.0 // TODO Deprecated in Qt6 8 | import Qt5Compat.GraphicalEffects as QtGraphicalEffects 9 | 10 | Item { 11 | id: hoverOutlineEffect 12 | property int hoverOutlineSize: 1 * Screen.devicePixelRatio 13 | property int hoverRadius: 40 14 | property int pressedRadius: hoverRadius 15 | property bool useOutlineMask: true 16 | 17 | property var mouseArea 18 | property bool hovered: mouseArea ? mouseArea.containsMouse : false 19 | property bool pressed: mouseArea ? mouseArea.pressed : false 20 | property int mouseX: mouseArea ? mouseArea.mouseX : width/2 21 | property int mouseY: mouseArea ? mouseArea.mouseY : height/2 22 | 23 | property int effectRadius: hoverOutlineEffect.pressed ? pressedRadius : hoverRadius 24 | Behavior on effectRadius { 25 | NumberAnimation { 26 | duration: Kirigami.Units.longDuration 27 | } 28 | } 29 | 30 | visible: hoverOutlineEffect.hovered 31 | 32 | function alpha(c, a) { 33 | return Qt.rgba(c.r, c.g, c.b, a) 34 | } 35 | property color effectColor: Kirigami.Theme.textColor 36 | property color fillColor: alpha(effectColor, 1/16) 37 | property color pressedFillColor: alpha(effectColor, 4/16) 38 | property color borderColor: alpha(effectColor, 8/16) 39 | 40 | Rectangle { 41 | id: hoverSolidFill 42 | anchors.fill: parent 43 | anchors.margins: hoverOutlineSize 44 | color: fillColor 45 | } 46 | 47 | Rectangle { 48 | id: hoverOutline 49 | visible: !hoverOutlineEffect.useOutlineMask 50 | anchors.fill: parent 51 | // color: "transparent" 52 | color: hoverOutlineEffect.pressed ? pressedFillColor : fillColor 53 | border.color: borderColor 54 | border.width: hoverOutlineSize 55 | 56 | Behavior on color { 57 | ColorAnimation { 58 | duration: Kirigami.Units.longDuration 59 | } 60 | } 61 | } 62 | 63 | QtGraphicalEffects.RadialGradient { 64 | id: hoverOutlineMask 65 | visible: false 66 | anchors.fill: parent 67 | horizontalOffset: hoverOutlineEffect.visible ? hoverOutlineEffect.mouseX - width/2 : 0 68 | verticalOffset: hoverOutlineEffect.visible ? hoverOutlineEffect.mouseY - height/2 : 0 69 | horizontalRadius: effectRadius 70 | verticalRadius: effectRadius 71 | gradient: Gradient { 72 | GradientStop { position: 0.0; color: "#FFFFFFFF" } 73 | GradientStop { position: 1; color: "#00FFFFFF" } 74 | } 75 | } 76 | 77 | QtGraphicalEffects.OpacityMask { 78 | anchors.fill: parent 79 | visible: hoverOutlineEffect.useOutlineMask 80 | source: hoverOutline 81 | maskSource: hoverOutlineMask 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /package/contents/ui/JumpToLetterView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | // import org.kde.plasma.components as PlasmaComponents3 3 | 4 | JumpToSectionView { 5 | id: jumpToLetterView 6 | 7 | squareView: appsModel.order == "alphabetical" 8 | 9 | onUpdate: { 10 | // console.log('jumpToLetterView.update()') 11 | var sections = [] 12 | for (var i = 0; i < appsModel.allAppsModel.count; i++) { 13 | var app = appsModel.allAppsModel.get(i) 14 | var section = app.sectionKey 15 | if (sections.indexOf(section) == -1) { 16 | sections.push(section) 17 | } 18 | } 19 | availableSections = sections 20 | // console.log('jumpToLetterView.update.availableSections', sections) 21 | 22 | if (appsModel.order == "alphabetical") { 23 | sections = presetSections.slice() // shallow copy 24 | for (var i = 0; i < availableSections.length; i++) { 25 | var section = availableSections[i] 26 | if (sections.indexOf(section) == -1) { 27 | sections.push(section) 28 | } 29 | } 30 | allSections = sections 31 | } else { 32 | allSections = availableSections 33 | } 34 | // console.log('jumpToLetterView.update.allSections', allSections) 35 | } 36 | 37 | presetSections: [ 38 | appsModel.recentAppsSectionKey, 39 | '&', 40 | '0-9', 41 | 'A', 'B', 'C', 'D', 'E', 'F', 42 | 'G', 'H', 'I', 'J', 'K', 'L', 43 | 'M', 'N', 'O', 'P', 'Q', 'R', 44 | 'S', 'T', 'U', 'V', 'W', 'X', 45 | 'Y', 'Z', 46 | ] 47 | 48 | // delegate: PlasmaComponents3.ToolButton { 49 | // width: jumpToLetterView.cellWidth 50 | // height: jumpToLetterView.cellHeight 51 | 52 | // readonly property string section: modelData || '' 53 | // readonly property bool isRecentApps: section == i18n("Recent Apps") 54 | 55 | // enabled: availableSections.indexOf(section) >= 0 56 | 57 | // font.pixelSize: height * 0.6 58 | 59 | // icon.name: { 60 | // if (jumpToLetterView.squareView) { 61 | // if (isRecentApps) { 62 | // return 'view-history' 63 | // } else { 64 | // return '' 65 | // } 66 | // } else { 67 | // return 'view-list-tree' 68 | // } 69 | // } 70 | // text: { 71 | // if (isRecentApps) { 72 | // return '' // Use '◷' icon 73 | // } else if (section == '0-9') { 74 | // return '#' 75 | // } else { 76 | // return section 77 | // } 78 | // } 79 | 80 | // onClicked: { 81 | // appsView.show() 82 | // appsView.jumpToSection(section) 83 | // } 84 | // } 85 | } 86 | -------------------------------------------------------------------------------- /package/contents/ui/JumpToSectionButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.components as PlasmaComponents3 5 | 6 | AppToolButton { 7 | id: control 8 | 9 | RowLayout { 10 | id: buttonContent 11 | anchors.fill: parent 12 | anchors.topMargin: control.paddingTop 13 | anchors.leftMargin: control.paddingLeft 14 | anchors.rightMargin: control.paddingRight 15 | anchors.bottomMargin: control.paddingBottom 16 | 17 | opacity: control.enabled ? 1 : 0.5 18 | spacing: Kirigami.Units.smallSpacing 19 | 20 | Layout.preferredHeight: Math.max(Kirigami.Units.iconSizes.small, label.implicitHeight) 21 | 22 | Kirigami.Icon { 23 | id: icon 24 | source: control.iconName || control.iconSource 25 | 26 | implicitHeight: label.implicitHeight 27 | implicitWidth: implicitHeight 28 | 29 | Layout.minimumWidth: valid ? parent.height: 0 30 | Layout.maximumWidth: Layout.minimumWidth 31 | visible: valid 32 | Layout.minimumHeight: Layout.minimumWidth 33 | Layout.maximumHeight: Layout.minimumWidth 34 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 35 | active: control.containsMouse 36 | } 37 | 38 | PlasmaComponents3.Label { 39 | id: label 40 | Layout.minimumWidth: implicitWidth 41 | text: control.Kirigami.MnemonicData.richTextLabel 42 | font: control.font || Kirigami.Theme.defaultFont 43 | visible: control.text != "" 44 | Layout.fillWidth: true 45 | height: parent.height 46 | color: control.containsMouse ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor 47 | horizontalAlignment: icon.valid ? Text.AlignLeft : Text.AlignHCenter 48 | verticalAlignment: Text.AlignVCenter 49 | elide: Text.ElideRight 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package/contents/ui/JumpToSectionView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.core as PlasmaCore 4 | 5 | GridView { 6 | id: jumpToSectionView 7 | 8 | Layout.fillWidth: true 9 | Layout.fillHeight: true 10 | 11 | clip: true 12 | 13 | property bool squareView: false 14 | 15 | Connections { 16 | target: appsModel.allAppsModel 17 | function onRefreshed() { jumpToLetterView.update() } 18 | } 19 | 20 | signal update() 21 | 22 | property var availableSections: [] 23 | property var presetSections: [] 24 | property var allSections: [] 25 | model: allSections 26 | 27 | property int buttonSize: { 28 | if (squareView) { 29 | return 70 * Screen.devicePixelRatio 30 | } else { 31 | return 36 * Screen.devicePixelRatio 32 | } 33 | } 34 | 35 | cellWidth: { 36 | if (squareView) { 37 | return buttonSize 38 | } else { 39 | return width 40 | } 41 | } 42 | cellHeight: buttonSize 43 | 44 | delegate: JumpToSectionButton { 45 | width: jumpToLetterView.cellWidth 46 | height: jumpToLetterView.cellHeight 47 | 48 | readonly property string section: modelData || '' 49 | readonly property bool isRecentApps: section == appsModel.recentAppsSectionKey 50 | readonly property var sectionIcon: appsModel.allAppsModel.sectionIcons[section] || null 51 | 52 | enabled: availableSections.indexOf(section) >= 0 53 | 54 | font.pixelSize: height * 0.6 55 | 56 | iconSource: { 57 | if (isRecentApps) { 58 | return 'view-history' 59 | } else if (jumpToLetterView.squareView) { 60 | return '' 61 | } else { 62 | return sectionIcon 63 | } 64 | } 65 | text: { 66 | if (isRecentApps) { 67 | if (jumpToLetterView.squareView) { 68 | return '' // Use '◷' icon 69 | } else { 70 | return appsModel.recentAppsSectionLabel 71 | } 72 | } else if (jumpToLetterView.squareView && section == '0-9') { 73 | return '#' 74 | } else { 75 | return section 76 | } 77 | } 78 | 79 | onClicked: { 80 | appsView.show() // appsView.show(stackView.zoomIn) 81 | appsView.jumpToSection(section) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /package/contents/ui/KickerAppModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.private.kicker as Kicker 3 | 4 | Kicker.FavoritesModel { 5 | // Kicker.FavoritesModel must be a child object of RootModel. 6 | // appEntry.actions() looks at the parent object for parent.appletInterface and will crash plasma if it can't find it. 7 | // https://github.com/KDE/plasma-desktop/blob/master/applets/kicker/plugin/appentry.cpp#L151 8 | id: kickerAppModel 9 | 10 | signal triggerIndex(int index) 11 | onTriggerIndex: { 12 | var closeRequested = kickerAppModel.trigger(index, "", null) 13 | if (closeRequested) { 14 | plasmoid.expanded = false 15 | } 16 | } 17 | 18 | signal triggerIndexAction(int index, string actionId, string actionArgument) 19 | onTriggerIndexAction: { 20 | var closeRequested = kickerAppModel.trigger(index, actionId, actionArgument) 21 | if (closeRequested) { 22 | plasmoid.expanded = false 23 | } 24 | } 25 | 26 | // https://invent.kde.org/plasma/plasma-workspace/-/blame/master/applets/kicker/plugin/actionlist.h#L18 27 | // DescriptionRole Qt.UserRole + 1 28 | // GroupRole Qt.UserRole + 2 29 | // FavoriteIdRole Qt.UserRole + 3 30 | // IsSeparatorRole Qt.UserRole + 4 31 | // IsDropPlaceholderRole Qt.UserRole + 5 32 | // IsParentRole Qt.UserRole + 6 33 | // HasChildrenRole Qt.UserRole + 7 34 | // HasActionListRole Qt.UserRole + 8 35 | // ActionListRole Qt.UserRole + 9 36 | // UrlRole Qt.UserRole + 10 37 | // DisabledRole Qt.UserRole + 11 @since: Plasma 5.20 38 | // IsMultilineTextRole Qt.UserRole + 12 @since: Plasma 5.24 39 | // DisplayWrappedRole Qt.UserRole + 13 @since: Plasma 6.0 40 | function getApp(url) { 41 | for (var i = 0; i < count; i++) { 42 | var modelIndex = kickerAppModel.index(i, 0) 43 | var favoriteId = kickerAppModel.data(modelIndex, Qt.UserRole + 3) 44 | if (favoriteId == url) { 45 | var app = {} 46 | app.indexInModel = i 47 | app.favoriteId = favoriteId 48 | app.display = kickerAppModel.data(modelIndex, Qt.DisplayRole) 49 | app.decoration = kickerAppModel.data(modelIndex, Qt.DecorationRole) 50 | app.description = kickerAppModel.data(modelIndex, Qt.UserRole + 1) 51 | app.group = kickerAppModel.data(modelIndex, Qt.UserRole + 2) 52 | app.url = kickerAppModel.data(modelIndex, Qt.UserRole + 10) 53 | 54 | // console.log(app, app.display, app.decoration, app.description, app.group, app.favoriteId) 55 | 56 | return app 57 | } 58 | } 59 | console.log('getApp', url, 'no index') 60 | return null 61 | } 62 | function runApp(url) { 63 | for (var i = 0; i < count; i++) { 64 | var modelIndex = kickerAppModel.index(i, 0) 65 | var favoriteId = kickerAppModel.data(modelIndex, Qt.UserRole + 3) 66 | if (favoriteId == url) { 67 | kickerAppModel.triggerIndex(i) 68 | return 69 | } 70 | } 71 | console.log('runApp', url, 'no index') 72 | } 73 | 74 | function indexHasActionList(i) { 75 | var modelIndex = kickerAppModel.index(i, 0) 76 | var hasActionList = kickerAppModel.data(modelIndex, Qt.UserRole + 8) 77 | return hasActionList 78 | } 79 | 80 | function getActionListAtIndex(i) { 81 | var modelIndex = kickerAppModel.index(i, 0) 82 | var actionList = kickerAppModel.data(modelIndex, Qt.UserRole + 9) 83 | return actionList 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package/contents/ui/KickerListModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | ListModel { 4 | id: listModel 5 | 6 | property var list: [] 7 | property var sectionIcons: { return {} } 8 | 9 | signal refreshing() 10 | signal refreshed() 11 | 12 | onListChanged: { 13 | clear() 14 | for (var i = 0; i < list.length; i++) { 15 | append(list[i]) 16 | } 17 | } 18 | 19 | 20 | function parseAppsModelItem(model, i) { 21 | // https://github.com/KDE/plasma-desktop/blob/master/applets/kicker/plugin/actionlist.h#L30 22 | var DescriptionRole = Qt.UserRole + 1 23 | var GroupRole = Qt.UserRole + 2 24 | var FavoriteIdRole = Qt.UserRole + 3 25 | var IsSeparatorRole = Qt.UserRole + 4 26 | var IsDropPlaceholderRole = Qt.UserRole + 5 27 | var IsParentRole = Qt.UserRole + 6 28 | var HasChildrenRole = Qt.UserRole + 7 29 | var HasActionListRole = Qt.UserRole + 8 30 | var ActionListRole = Qt.UserRole + 9 31 | var UrlRole = Qt.UserRole + 10 32 | var DisabledRole = Qt.UserRole + 11 // @since: Plasma 5.20 33 | var IsMultilineTextRole = Qt.UserRole + 12 // @since: Plasma 5.24 34 | var DisplayWrappedRole = Qt.UserRole + 13 // @since: Plasma 6.0 35 | 36 | var modelIndex = model.index(i, 0) 37 | 38 | var item = { 39 | parentModel: model, 40 | indexInParent: i, 41 | name: model.data(modelIndex, Qt.DisplayRole), 42 | description: model.data(modelIndex, DescriptionRole), 43 | favoriteId: model.data(modelIndex, FavoriteIdRole), 44 | disabled: false, // for SidebarContextMenu 45 | largeIcon: false, // for KickerListView 46 | } 47 | 48 | if (typeof model.name === 'string') { 49 | item.parentName = model.name 50 | } 51 | 52 | // ListView.append() doesn't like it when we have { key: [object] }. 53 | var url = model.data(modelIndex, UrlRole) 54 | if (typeof url === 'object') { 55 | url = url.toString() 56 | } 57 | if (typeof url === 'string') { 58 | item.url = url 59 | } 60 | 61 | var icon = model.data(modelIndex, Qt.DecorationRole) 62 | if (typeof icon === 'object') { 63 | item.icon = icon 64 | } else if (typeof icon === 'string') { 65 | item.iconName = icon 66 | } 67 | 68 | var isDisabled = model.data(modelIndex, DisabledRole) 69 | if (typeof isDisabled !== 'undefined') { 70 | item.disabled = isDisabled 71 | } 72 | 73 | return item 74 | } 75 | 76 | function parseModel(appList, model, path) { 77 | // console.log(path, model, model.description, model.count) 78 | for (var i = 0; i < model.count; i++) { 79 | var item = model.modelForRow(i) 80 | if (!item) { 81 | item = parseAppsModelItem(model, i) 82 | } 83 | var itemPath = (path || []).concat(i) 84 | if (item && item.hasChildren) { 85 | // console.log(item) 86 | parseModel(appList, item, itemPath) 87 | } else { 88 | // console.log(itemPath, item, item.description) 89 | appList.push(item) 90 | } 91 | } 92 | } 93 | 94 | 95 | function refresh() { 96 | refreshing() 97 | 98 | refreshed() 99 | } 100 | 101 | function log() { 102 | for (var i = 0; i < list.length; i++) { 103 | var item = list[i] 104 | console.log(JSON.stringify({ 105 | name: item.name, 106 | description: item.description, 107 | }, null, '\t')) 108 | } 109 | } 110 | 111 | function triggerIndex(index) { 112 | var item = list[index] 113 | item.parentModel.trigger(item.indexInParent, "", null) 114 | itemTriggered() 115 | } 116 | 117 | signal itemTriggered() 118 | 119 | function hasActionList(index) { 120 | var DescriptionRole = Qt.UserRole + 1 121 | var HasActionListRole = Qt.UserRole + 8 122 | 123 | var item = list[index] 124 | var modelIndex = item.parentModel.index(item.indexInParent, 0) 125 | return item.parentModel.data(modelIndex, HasActionListRole) 126 | } 127 | 128 | function getActionList(index) { 129 | var DescriptionRole = Qt.UserRole + 1 130 | var ActionListRole = Qt.UserRole + 9 131 | 132 | var item = list[index] 133 | var modelIndex = item.parentModel.index(item.indexInParent, 0) 134 | return item.parentModel.data(modelIndex, ActionListRole) 135 | } 136 | 137 | function triggerIndexAction(index, actionId, actionArgument) { 138 | // kicker/code/tools.js triggerAction() 139 | 140 | var item = list[index] 141 | item.parentModel.trigger(item.indexInParent, actionId, actionArgument) 142 | itemTriggered() 143 | } 144 | 145 | function getByValue(key, value) { 146 | for (var i = 0; i < count; i++) { 147 | var item = get(i) 148 | if (item[key] == value) { 149 | return item 150 | } 151 | } 152 | return null 153 | } 154 | 155 | function hasApp(favoriteId) { 156 | for (var i = 0; i < count; i++) { 157 | var item = get(i) 158 | if (item.favoriteId == favoriteId) { 159 | return true 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /package/contents/ui/KickerListView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.extras as PlasmaExtras 3 | 4 | ListView { 5 | id: listView 6 | clip: true 7 | cacheBuffer: 200 // Don't unload when scrolling (prevent stutter) 8 | 9 | // snapMode: ListView.SnapToItem 10 | keyNavigationWraps: true 11 | highlightMoveDuration: 0 12 | highlightResizeDuration: 0 13 | 14 | property bool showItemUrl: true 15 | property bool showDesktopFileUrl: false 16 | property int iconSize: 36 * Screen.devicePixelRatio 17 | 18 | section.delegate: KickerSectionHeader {} 19 | 20 | delegate: MenuListItem {} 21 | 22 | property var modelList: model ? model.list : [] 23 | 24 | // currentIndex: 0 25 | // Connections { 26 | // target: appsModel.allAppsModel 27 | // function onRefreshing() { 28 | // console.log('appsList.onRefreshing') 29 | // appsList.model = [] 30 | // // console.log('search.results.onRefreshed') 31 | // appsList.currentIndex = 0 32 | // } 33 | // function onRefreshed() { 34 | // console.log('appsList.onRefreshed') 35 | // // appsList.model = appsModel.allAppsList 36 | // appsList.model = appsModel.allAppsList 37 | // appsList.modelList = appsModel.allAppsList.list 38 | // appsList.currentIndex = 0 39 | // } 40 | // } 41 | 42 | highlight: PlasmaExtras.Highlight { 43 | visible: listView.currentItem && !listView.currentItem.isSeparator 44 | } 45 | 46 | // function triggerIndex(index) { 47 | // model.triggerIndex(index) 48 | // } 49 | 50 | function goUp() { 51 | if (verticalLayoutDirection == ListView.TopToBottom) { 52 | decrementCurrentIndex() 53 | } else { // ListView.BottomToTop 54 | incrementCurrentIndex() 55 | } 56 | } 57 | 58 | function goDown() { 59 | if (verticalLayoutDirection == ListView.TopToBottom) { 60 | incrementCurrentIndex() 61 | } else { // ListView.BottomToTop 62 | decrementCurrentIndex() 63 | } 64 | } 65 | 66 | function skipToMin() { 67 | currentIndex = Math.max(0, currentIndex - 10) 68 | } 69 | 70 | function skipToMax() { 71 | currentIndex = Math.min(currentIndex + 10, count-1) 72 | } 73 | 74 | function pageUp() { 75 | if (verticalLayoutDirection == ListView.TopToBottom) { 76 | skipToMin() 77 | } else { // ListView.BottomToTop 78 | skipToMax() 79 | } 80 | } 81 | 82 | function pageDown() { 83 | if (verticalLayoutDirection == ListView.TopToBottom) { 84 | skipToMax() 85 | } else { // ListView.BottomToTop 86 | skipToMin() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package/contents/ui/KickerSectionHeader.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.kirigami as Kirigami 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | MouseArea { 6 | id: sectionDelegate 7 | 8 | width: ListView.view.width 9 | // height: childrenRect.height 10 | implicitHeight: listView.iconSize 11 | 12 | property bool enableJumpToSection: false 13 | 14 | PlasmaComponents3.Label { 15 | id: sectionHeading 16 | anchors { 17 | left: parent.left 18 | leftMargin: Kirigami.Units.smallSpacing 19 | verticalCenter: parent.verticalCenter 20 | } 21 | text: { 22 | if (section == appsModel.recentAppsSectionKey) { 23 | return appsModel.recentAppsSectionLabel 24 | } else { 25 | return section 26 | } 27 | } 28 | 29 | // Add 4pt to font. Default 10pt => 14pt 30 | font.pointSize: Kirigami.Theme.defaultFont.pointSize + 4 31 | 32 | property bool centerOverIcon: sectionHeading.contentWidth <= listView.iconSize 33 | width: centerOverIcon ? listView.iconSize : parent.width 34 | horizontalAlignment: centerOverIcon ? Text.AlignHCenter : Text.AlignLeft 35 | } 36 | 37 | HoverOutlineEffect { 38 | id: hoverOutlineEffect 39 | anchors.fill: parent 40 | visible: enableJumpToSection && mouseArea.containsMouse 41 | hoverRadius: width/2 42 | pressedRadius: width 43 | mouseArea: sectionDelegate 44 | } 45 | 46 | hoverEnabled: true 47 | onClicked: { 48 | if (enableJumpToSection) { 49 | if (appsModel.order == "alphabetical") { 50 | jumpToLetterView.show() 51 | } else { // appsModel.order = "categories" 52 | jumpToLetterView.show() 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package/contents/ui/LauncherIcon.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.core as PlasmaCore 4 | import org.kde.draganddrop as DragAndDrop 5 | import org.kde.kirigami as Kirigami 6 | import org.kde.plasma.plasmoid 7 | 8 | MouseArea { 9 | id: launcherIcon 10 | 11 | readonly property bool inPanel: (plasmoid.location == PlasmaCore.Types.TopEdge 12 | || plasmoid.location == PlasmaCore.Types.RightEdge 13 | || plasmoid.location == PlasmaCore.Types.BottomEdge 14 | || plasmoid.location == PlasmaCore.Types.LeftEdge) 15 | 16 | Layout.minimumWidth: { 17 | switch (plasmoid.formFactor) { 18 | case PlasmaCore.Types.Vertical: 19 | return 0 20 | case PlasmaCore.Types.Horizontal: 21 | return height 22 | default: 23 | return Kirigami.Units.gridUnit * 3 24 | } 25 | } 26 | 27 | Layout.minimumHeight: { 28 | switch (plasmoid.formFactor) { 29 | case PlasmaCore.Types.Vertical: 30 | return width 31 | case PlasmaCore.Types.Horizontal: 32 | return 0 33 | default: 34 | return Kirigami.Units.gridUnit * 3 35 | } 36 | } 37 | 38 | readonly property int maxSize: Math.max(width, height) 39 | property int size: { 40 | if (inPanel) { 41 | if (plasmoid.configuration.fixedPanelIcon) { 42 | // Was PlasmaCore.Units.iconSizeHints.panel in Plasma5 43 | // In Plasma6 https://invent.kde.org/plasma/plasma-desktop/-/merge_requests/1390/diffs 44 | return 48 // Kickoff uses this hardcoded number 45 | } else { 46 | return maxSize 47 | } 48 | } else { 49 | return -1 50 | } 51 | } 52 | Layout.maximumWidth: size 53 | Layout.maximumHeight: size 54 | 55 | 56 | property int iconSize: Math.min(width, height) 57 | property alias iconSource: icon.source 58 | 59 | Kirigami.Icon { 60 | id: icon 61 | anchors.centerIn: parent 62 | source: "start-here-kde-symbolic" 63 | width: launcherIcon.iconSize 64 | height: launcherIcon.iconSize 65 | active: launcherIcon.containsMouse && !justOpenedTimer.running 66 | smooth: true 67 | } 68 | 69 | // Debugging 70 | // Rectangle { anchors.fill: parent; border.color: "#ff0"; color: "transparent"; border.width: 1; } 71 | // Rectangle { anchors.fill: icon; border.color: "#f00"; color: "transparent"; border.width: 1; } 72 | 73 | Accessible.name: Plasmoid.title 74 | Accessible.role: Accessible.Button 75 | 76 | hoverEnabled: true 77 | // cursorShape: Qt.PointingHandCursor 78 | 79 | property bool wasExpanded 80 | onPressed: wasExpanded = widget.expanded 81 | onClicked: widget.expanded = !wasExpanded 82 | 83 | property alias activateOnDrag: dropArea.enabled 84 | DragAndDrop.DropArea { 85 | id: dropArea 86 | anchors.fill: parent 87 | } 88 | 89 | onContainsMouseChanged: { 90 | if (!containsMouse) { 91 | dragHoverTimer.stop() 92 | } 93 | } 94 | 95 | Timer { 96 | id: dragHoverTimer 97 | interval: 250 // Same as taskmanager's activationTimer in MouseHandler.qml 98 | running: dropArea.containsDrag 99 | onTriggered: widget.expanded = true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /package/contents/ui/MenuListItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.components as PlasmaComponents3 5 | import org.kde.draganddrop as DragAndDrop 6 | 7 | AppToolButton { 8 | id: itemDelegate 9 | 10 | width: ListView.view.width 11 | implicitHeight: row.implicitHeight 12 | 13 | property var parentModel: typeof modelList !== "undefined" && modelList[index] ? modelList[index].parentModel : undefined 14 | property string modelDescription: model.name == model.description ? '' : model.description // Ignore the Comment if it's the same as the Name. 15 | property string description: model.url ? modelDescription : '' // 16 | property bool isDesktopFile: !!(model.url && endsWith(model.url, '.desktop')) 17 | property bool showItemUrl: listView.showItemUrl && (!isDesktopFile || listView.showDesktopFileUrl) 18 | property string secondRowText: showItemUrl && model.url ? model.url : modelDescription 19 | property bool secondRowVisible: secondRowText 20 | property string launcherUrl: model.favoriteId || model.url 21 | property string iconName: model.iconName || '' 22 | property alias iconSource: itemIcon.source 23 | property int iconSize: model.largeIcon ? listView.iconSize * 2 : listView.iconSize 24 | 25 | function endsWith(s, substr) { 26 | return s.indexOf(substr) == s.length - substr.length 27 | } 28 | 29 | // We need to look at the js list since ListModel doesn't support item's with non primitive propeties (like an Image). 30 | property bool modelListPopulated: !!listView.model.list && listView.model.list.length - 1 >= index 31 | property var iconInstance: modelListPopulated && listView.model.list[index] ? listView.model.list[index].icon : "" 32 | Connections { 33 | target: listView.model 34 | function onRefreshed() { 35 | // We need to manually trigger an update when we update the model without replacing the list. 36 | // Otherwise the icon won't be in sync. 37 | itemDelegate.iconInstance = listView.model.list[index] ? listView.model.list[index].icon : "" 38 | } 39 | } 40 | 41 | // Drag (based on kicker) 42 | // https://github.com/KDE/plasma-desktop/blob/4aad3fdf16bc5fd25035d3d59bb6968e06f86ec6/applets/kicker/package/contents/ui/ItemListDelegate.qml#L96 43 | // https://github.com/KDE/plasma-desktop/blob/master/applets/kicker/plugin/draghelper.cpp 44 | property int pressX: -1 45 | property int pressY: -1 46 | property bool dragEnabled: launcherUrl 47 | function initDrag(mouse) { 48 | pressX = mouse.x 49 | pressY = mouse.y 50 | } 51 | function shouldStartDrag(mouse) { 52 | return dragEnabled 53 | && pressX != -1 // Drag initialized? 54 | && dragHelper.isDrag(pressX, pressY, mouse.x, mouse.y) // Mouse moved far enough? 55 | } 56 | function startDrag() { 57 | // Note that we fallback from url to favoriteId for "Most Used" apps. 58 | var dragIcon = iconInstance 59 | if (typeof dragIcon === "string") { 60 | // startDrag must use QIcon. See Issue #75. 61 | // dragIcon = dragHelper.defaultIcon 62 | dragIcon = null 63 | } 64 | // console.log('startDrag', widget, model.url, "favoriteId", model.favoriteId) 65 | // console.log(' iconInstance', iconInstance) 66 | // console.log(' dragIcon', dragIcon) 67 | if (dragIcon) { 68 | dragHelper.startDrag(widget, model.url || model.favoriteId, dragIcon, "favoriteId", model.favoriteId) 69 | } 70 | 71 | resetDragState() 72 | } 73 | function resetDragState() { 74 | pressX = -1 75 | pressY = -1 76 | } 77 | onPressed: function(mouse) { 78 | if (mouse.buttons & Qt.LeftButton) { 79 | initDrag(mouse) 80 | } 81 | } 82 | onContainsMouseChanged: function(containsMouse) { 83 | if (!containsMouse) { 84 | resetDragState() 85 | } 86 | } 87 | onPositionChanged: function(mouse) { 88 | if (shouldStartDrag(mouse)) { 89 | startDrag() 90 | } 91 | } 92 | 93 | RowLayout { // ItemListDelegate 94 | id: row 95 | anchors.left: parent.left 96 | anchors.leftMargin: Kirigami.Units.smallSpacing 97 | anchors.right: parent.right 98 | anchors.rightMargin: Kirigami.Units.smallSpacing 99 | 100 | Item { 101 | Layout.fillHeight: true 102 | implicitHeight: itemIcon.implicitHeight 103 | implicitWidth: itemIcon.implicitWidth 104 | 105 | Kirigami.Icon { 106 | id: itemIcon 107 | anchors.centerIn: parent 108 | implicitHeight: itemDelegate.iconSize 109 | implicitWidth: implicitHeight 110 | 111 | // visible: iconsEnabled 112 | 113 | animated: false 114 | // usesPlasmaTheme: false 115 | source: itemDelegate.iconName || itemDelegate.iconInstance 116 | } 117 | } 118 | 119 | ColumnLayout { 120 | Layout.fillWidth: true 121 | // Layout.fillHeight: true 122 | Layout.alignment: Qt.AlignVCenter 123 | spacing: 0 124 | 125 | RowLayout { 126 | Layout.fillWidth: true 127 | // height: itemLabel.height 128 | 129 | PlasmaComponents3.Label { 130 | id: itemLabel 131 | text: model.name 132 | maximumLineCount: 1 133 | // elide: Text.ElideMiddle 134 | height: implicitHeight 135 | } 136 | 137 | PlasmaComponents3.Label { 138 | Layout.fillWidth: true 139 | text: !itemDelegate.secondRowVisible ? itemDelegate.description : '' 140 | color: config.menuItemTextColor2 141 | maximumLineCount: 1 142 | elide: Text.ElideRight 143 | height: implicitHeight // ElideRight causes some top padding for some reason 144 | } 145 | } 146 | 147 | PlasmaComponents3.Label { 148 | visible: itemDelegate.secondRowVisible 149 | Layout.fillWidth: true 150 | // Layout.fillHeight: true 151 | text: itemDelegate.secondRowText 152 | color: config.menuItemTextColor2 153 | maximumLineCount: 1 154 | elide: Text.ElideMiddle 155 | height: implicitHeight 156 | } 157 | } 158 | 159 | } 160 | 161 | acceptedButtons: Qt.LeftButton | Qt.RightButton 162 | onClicked: function(mouse) { 163 | mouse.accepted = true 164 | resetDragState() 165 | logger.debug('MenuListItem.onClicked', mouse.button, Qt.LeftButton, Qt.RightButton) 166 | if (mouse.button == Qt.LeftButton) { 167 | trigger() 168 | } else if (mouse.button == Qt.RightButton) { 169 | contextMenu.open(mouse.x, mouse.y) 170 | } 171 | } 172 | 173 | function trigger() { 174 | listView.model.triggerIndex(index) 175 | } 176 | 177 | // property bool hasActionList: listView.model.hasActionList(index) 178 | // property var actionList: hasActionList ? listView.model.getActionList(index) : [] 179 | AppContextMenu { 180 | id: contextMenu 181 | onPopulateMenu: function(menu) { 182 | if (launcherUrl && !plasmoid.configuration.tilesLocked) { 183 | menu.addPinToMenuAction(launcherUrl) 184 | } 185 | if (listView.model.hasActionList(index)) { 186 | var actionList = listView.model.getActionList(index) 187 | menu.addActionList(actionList, listView.model, index) 188 | } 189 | } 190 | } 191 | 192 | } // delegate: AppToolButton 193 | -------------------------------------------------------------------------------- /package/contents/ui/Popup.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.core as PlasmaCore 5 | 6 | MouseArea { 7 | id: popup 8 | property alias searchView: searchView 9 | property alias appsView: searchView.appsView 10 | property alias tileEditorView: searchView.tileEditorView 11 | property alias tileEditorViewLoader: searchView.tileEditorViewLoader 12 | property alias tileGrid: tileGrid 13 | 14 | RowLayout { 15 | anchors.fill: parent 16 | spacing: 0 17 | 18 | Item { 19 | id: sidebarPlaceholder 20 | implicitWidth: config.sidebarWidth + config.sidebarRightMargin 21 | Layout.fillHeight: true 22 | } 23 | 24 | SearchView { 25 | id: searchView 26 | Layout.fillHeight: true 27 | } 28 | 29 | TileGrid { 30 | id: tileGrid 31 | Layout.fillWidth: true 32 | Layout.fillHeight: true 33 | 34 | cellSize: config.cellSize 35 | cellMargin: config.cellMargin 36 | cellPushedMargin: config.cellPushedMargin 37 | 38 | tileModel: config.tileModel.value 39 | 40 | onEditTile: function(tile) { tileEditorViewLoader.open(tile) } 41 | 42 | onTileModelChanged: saveTileModel.restart() 43 | Timer { 44 | id: saveTileModel 45 | interval: 2000 46 | onTriggered: config.tileModel.save() 47 | } 48 | } 49 | 50 | } 51 | 52 | SidebarView { 53 | id: sidebarView 54 | } 55 | 56 | MouseArea { 57 | visible: !plasmoid.configuration.tilesLocked && !(plasmoid.location == PlasmaCore.Types.TopEdge || plasmoid.location == PlasmaCore.Types.RightEdge) 58 | anchors.top: parent.top 59 | anchors.right: parent.right 60 | width: Kirigami.Units.largeSpacing 61 | height: Kirigami.Units.largeSpacing 62 | cursorShape: Qt.WhatsThisCursor 63 | 64 | PlasmaCore.ToolTipArea { 65 | anchors.fill: parent 66 | icon: "help-hint" 67 | mainText: i18n("Resize?") 68 | subText: i18n("Meta + Right Click to resize the menu.") 69 | } 70 | } 71 | 72 | MouseArea { 73 | visible: !plasmoid.configuration.tilesLocked && !(plasmoid.location == PlasmaCore.Types.BottomEdge || plasmoid.location == PlasmaCore.Types.RightEdge) 74 | anchors.bottom: parent.bottom 75 | anchors.right: parent.right 76 | width: Kirigami.Units.largeSpacing 77 | height: Kirigami.Units.largeSpacing 78 | cursorShape: Qt.WhatsThisCursor 79 | 80 | PlasmaCore.ToolTipArea { 81 | anchors.fill: parent 82 | icon: "help-hint" 83 | mainText: i18n("Resize?") 84 | subText: i18n("Meta + Right Click to resize the menu.") 85 | } 86 | } 87 | 88 | onClicked: searchView.searchField.forceActiveFocus() 89 | } 90 | -------------------------------------------------------------------------------- /package/contents/ui/RoundShadow.qml: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 by Daker Fernandes Pinheiro 3 | * Copyright (C) 2011 by Marco Martin 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU Library General Public License as 7 | * published by the Free Software Foundation; either version 2, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Library General Public License for more details 14 | * 15 | * You should have received a copy of the GNU Library General Public 16 | * License along with this program; if not, write to the 17 | * Free Software Foundation, Inc., 18 | * 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. 19 | */ 20 | 21 | /**Documented API 22 | Inherits: 23 | Item 24 | 25 | Imports: 26 | QtQuick 2.1 27 | org.kde.plasma.core 28 | 29 | Description: 30 | It is a simple Radio button which is using the plasma theme. 31 | TODO Do we need more info? 32 | 33 | Properties: 34 | TODO needs more info?? 35 | **/ 36 | 37 | import QtQuick 38 | import org.kde.kirigami as Kirigami 39 | import org.kde.ksvg as KSvg 40 | 41 | Item { 42 | id: main 43 | state: parent.state 44 | property alias imagePath: shadowSvg.imagePath 45 | property string hoverElement: "hover" 46 | property string focusElement: "focus" 47 | property alias shadowElement: shadow.elementId 48 | 49 | //used to tell apart this implementation with the touch components one 50 | property bool hasOverState: true 51 | 52 | KSvg.Svg { 53 | id: shadowSvg 54 | imagePath: "widgets/actionbutton" 55 | } 56 | 57 | KSvg.SvgItem { 58 | id: hover 59 | svg: shadowSvg 60 | elementId: "hover" 61 | 62 | anchors.fill: parent 63 | 64 | opacity: 0 65 | } 66 | 67 | KSvg.SvgItem { 68 | id: shadow 69 | svg: shadowSvg 70 | elementId: "shadow" 71 | 72 | anchors.fill: parent 73 | } 74 | 75 | states: [ 76 | State { 77 | name: "shadow" 78 | PropertyChanges { 79 | target: shadow 80 | opacity: 1 81 | } 82 | PropertyChanges { 83 | target: hover 84 | opacity: 0 85 | elementId: hoverElement 86 | } 87 | }, 88 | State { 89 | name: "hover" 90 | PropertyChanges { 91 | target: shadow 92 | opacity: 0 93 | } 94 | PropertyChanges { 95 | target: hover 96 | opacity: 1 97 | elementId: hoverElement 98 | } 99 | }, 100 | State { 101 | name: "focus" 102 | PropertyChanges { 103 | target: shadow 104 | opacity: 0 105 | } 106 | PropertyChanges { 107 | target: hover 108 | opacity: 1 109 | elementId: focusElement 110 | } 111 | }, 112 | State { 113 | name: "hidden" 114 | PropertyChanges { 115 | target: shadow 116 | opacity: 0 117 | } 118 | PropertyChanges { 119 | target: hover 120 | opacity: 0 121 | elementId: hoverElement 122 | } 123 | } 124 | ] 125 | 126 | transitions: [ 127 | Transition { 128 | PropertyAnimation { 129 | properties: "opacity" 130 | duration: Kirigami.Units.longDuration 131 | easing.type: Easing.OutQuad 132 | } 133 | } 134 | ] 135 | } 136 | -------------------------------------------------------------------------------- /package/contents/ui/SearchField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | // import QtQuick.Controls as QQC2 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.components as PlasmaComponents3 5 | 6 | // import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 7 | 8 | // QQC2.TextField { 9 | PlasmaComponents3.TextField { 10 | id: searchField 11 | placeholderText: { 12 | if (search.isDefaultFilter) { 13 | return i18n("Search") 14 | } else if (search.isAppsFilter) { 15 | return i18n("Search Apps") 16 | } else if (search.isFileFilter) { 17 | return i18n("Search Files") 18 | } else if (search.isBookmarksFilter) { 19 | return i18n("Search Bookmarks") 20 | } else { 21 | return i18nc("Search [krunnerName, krunnerName, ...], ", "Search %1", search.filters.toString()) 22 | } 23 | } 24 | property int topMargin: 0 25 | property int bottomMargin: 0 26 | property int defaultFontSize: 16 * Screen.devicePixelRatio // Not the same as pointSize=16 27 | property int styleMaxFontSize: height - topMargin - bottomMargin 28 | font.pixelSize: Math.min(defaultFontSize, styleMaxFontSize) 29 | 30 | // style: plasmoid.configuration.searchFieldFollowsTheme ? plasmaStyle : redmondStyle 31 | // Component { 32 | // id: plasmaStyle 33 | // // Creates the following warning when not in use: 34 | // // file:///usr/lib/x86_64-linux-gnu/qt5/qml/QtQuick/Controls/Styles/Plasma/TextFieldStyle.qml:74: ReferenceError: textField is not defined 35 | // // Caused by: 36 | // // var actionIconSize = Math.max(textField.height * 0.8, Kirigami.Units.iconSizes.small); 37 | // PlasmaStyles.TextFieldStyle { 38 | // id: style 39 | // Component.onCompleted: { 40 | // searchField.topMargin = Qt.binding(function() { 41 | // return style.padding.top 42 | // }) 43 | // searchField.bottomMargin = Qt.binding(function() { 44 | // return style.padding.bottom 45 | // }) 46 | // } 47 | // } 48 | // } 49 | // Component { 50 | // id: redmondStyle 51 | 52 | // // https://github.com/qt/qtquickcontrols/blob/dev/src/controls/Styles/Base/TextFieldStyle.qml 53 | // // https://github.com/qt/qtquickcontrols/blob/dev/src/controls/Styles/Desktop/TextFieldStyle.qml 54 | // QtStyles.TextFieldStyle { 55 | // id: style 56 | 57 | // background: Rectangle { 58 | // color: "#eee" 59 | // } 60 | // textColor: "#111" 61 | // placeholderTextColor: "#777" 62 | 63 | // Component.onCompleted: { 64 | // searchField.topMargin = Qt.binding(function() { 65 | // return style.padding.top 66 | // }) 67 | // searchField.bottomMargin = Qt.binding(function() { 68 | // return style.padding.bottom 69 | // }) 70 | // } 71 | // } 72 | // } 73 | 74 | onTextChanged: { 75 | search.query = text 76 | } 77 | Connections { 78 | target: search 79 | function onQueryChanged() { 80 | searchField.text = search.query 81 | } 82 | } 83 | 84 | property var listView: searchResultsView.listView 85 | Keys.onPressed: function(event) { 86 | if (event.key == Qt.Key_Up) { 87 | event.accepted = true; listView.goUp() 88 | } else if (event.key == Qt.Key_Down) { 89 | event.accepted = true; listView.goDown() 90 | } else if (event.key == Qt.Key_PageUp) { 91 | event.accepted = true; listView.pageUp() 92 | } else if (event.key == Qt.Key_PageDown) { 93 | event.accepted = true; listView.pageDown() 94 | } else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) { 95 | event.accepted = true; listView.currentItem.trigger() 96 | } else if (event.modifiers & Qt.MetaModifier && event.key == Qt.Key_R) { 97 | event.accepted = true; search.filters = ['shell'] 98 | } else if (event.key == Qt.Key_Escape) { 99 | plasmoid.expanded = false 100 | } 101 | } 102 | 103 | Component.onCompleted: { 104 | forceActiveFocus() 105 | } 106 | } -------------------------------------------------------------------------------- /package/contents/ui/SearchFiltersView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | ColumnLayout { 5 | id: searchFiltersView 6 | // width: parent.width 7 | // Layout.fillHeight: true 8 | 9 | SearchFiltersViewItem { 10 | visible: false 11 | runnerId: "" 12 | indentLevel: 0 13 | iconSource: "applications-other" 14 | text: i18n("All") + ' (Not working)' 15 | subText: i18n("Search with all KRunner plugins") 16 | checkBox.visible: false 17 | onApplyButtonClicked: search.filters = [] 18 | enabled: false 19 | } 20 | 21 | SearchFiltersViewItem { 22 | runnerId: "" 23 | indentLevel: 0 24 | iconSource: "applications-other" 25 | text: i18n("Default") 26 | subText: i18n("Search with user selected defaults") 27 | checkBox.visible: false 28 | onApplyButtonClicked: search.applyDefaultFilters() 29 | } 30 | 31 | // Installed runners are listed at: /usr/share/kservices5/plasma-runner-*.desktop 32 | 33 | SearchFiltersViewItem { 34 | runnerId: "services" 35 | indentLevel: 1 36 | iconSource: "window" 37 | text: i18n("Applications") 38 | } 39 | 40 | SearchFiltersViewItem { 41 | runnerId: "baloosearch" 42 | indentLevel: 1 43 | iconSource: "document-new" 44 | text: i18n("Files") 45 | } 46 | 47 | //--- baloosearch filters 48 | // https://github.com/KDE/baloo/blob/master/docs/user/searching.md#advanced-searches 49 | // Use `type:Audio` or `type:Document` to filter specific filetypes. 50 | SearchFiltersViewItem { 51 | runnerId: "baloosearch" 52 | indentLevel: 2 53 | iconSource: "folder-music-symbolic" 54 | text: i18n("Music") 55 | checkBox.visible: false 56 | onApplyButtonClicked: search.setQueryPrefix('type:Audio ') 57 | } 58 | SearchFiltersViewItem { 59 | runnerId: "baloosearch" 60 | indentLevel: 2 61 | iconSource: "folder-videos-symbolic" 62 | text: i18n("Videos") 63 | checkBox.visible: false 64 | onApplyButtonClicked: search.setQueryPrefix('type:Video ') 65 | } 66 | //--- end baloosearch filters 67 | 68 | SearchFiltersViewItem { 69 | runnerId: "krunner_systemsettings" 70 | indentLevel: 1 71 | iconSource: "preferences-system" 72 | text: i18n("System Settings") 73 | } 74 | 75 | SearchFiltersViewItem { 76 | runnerId: "bookmarks" 77 | indentLevel: 1 78 | iconSource: "globe" 79 | text: i18n("Bookmarks") 80 | } 81 | 82 | SearchFiltersViewItem { 83 | runnerId: "locations" 84 | indentLevel: 1 85 | iconSource: "system-file-manager" 86 | text: i18n("Locations") 87 | } 88 | 89 | SearchFiltersViewItem { 90 | runnerId: "Dictionary" 91 | indentLevel: 1 92 | iconSource: "accessories-dictionary" 93 | text: i18n("Dictionary") 94 | onApplyButtonClicked: search.setQueryPrefix('define ') 95 | } 96 | 97 | SearchFiltersViewItem { 98 | runnerId: "shell" 99 | indentLevel: 1 100 | iconSource: "system-run" 101 | text: i18n("Shell") 102 | } 103 | 104 | SearchFiltersViewItem { 105 | runnerId: "calculator" 106 | indentLevel: 1 107 | iconSource: "accessories-calculator" 108 | text: i18n("Calculator") 109 | } 110 | 111 | SearchFiltersViewItem { 112 | runnerId: "org.kde.windowedwidgets" 113 | indentLevel: 1 114 | iconSource: "plasma" 115 | text: i18n("Windowed Widgets") 116 | } 117 | 118 | SearchFiltersViewItem { 119 | runnerId: "org.kde.datetime" 120 | indentLevel: 1 121 | iconSource: "clock" 122 | text: i18n("Date/Time") 123 | } 124 | 125 | SearchFiltersViewItem { 126 | runnerId: "unitconverter" 127 | indentLevel: 1 128 | iconSource: "accessories-calculator" 129 | text: i18n("Unit Converter") 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /package/contents/ui/SearchFiltersViewItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Window 4 | import org.kde.kirigami as Kirigami 5 | import org.kde.plasma.components as PlasmaComponents3 6 | import org.kde.ksvg as KSvg 7 | 8 | // import QtQuick.Controls.Styles 1.1 as QtQuickControlStyle 9 | // import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 10 | 11 | RowLayout { 12 | id: searchFiltersViewItem 13 | Layout.fillWidth: true 14 | spacing: 0 15 | 16 | property string runnerId: '' 17 | property int indentLevel: 0 18 | 19 | property alias iconSource: applyFilterButton.icon.source 20 | property alias text: applyFilterButton.text 21 | property alias subText: applyFilterButton.subText 22 | 23 | property alias checkBox: isDefaultFilter 24 | property alias applyButton: applyFilterButton 25 | 26 | signal applyButtonClicked() 27 | 28 | property var surfaceNormal: KSvg.FrameSvgItem { 29 | anchors.fill: parent 30 | imagePath: "widgets/button" 31 | prefix: "normal" 32 | // prefix: style.flat ? ["toolbutton-hover", "normal"] : "normal" 33 | } 34 | 35 | Item { // Align CheckBoxes buttons to "All" 36 | Layout.minimumWidth: surfaceNormal.margins.left + (config.flatButtonIconSize + surfaceNormal.margins.left) * searchFiltersViewItem.indentLevel 37 | Layout.maximumWidth: Layout.minimumWidth 38 | Layout.fillHeight: true 39 | } 40 | 41 | PlasmaComponents3.ToolButton { 42 | id: applyFilterButton 43 | Layout.fillWidth: true 44 | property string subText: "" 45 | 46 | // style: PlasmaStyles.ToolButtonStyle { 47 | // id: style 48 | // readonly property bool smallIcon: !control.subText 49 | 50 | // label: RowLayout { 51 | // Kirigami.Icon { 52 | // source: control.iconSource 53 | // Layout.preferredHeight: style.smallIcon ? config.flatButtonIconSize : -1 54 | // Layout.preferredWidth: style.smallIcon ? config.flatButtonIconSize : -1 55 | // } 56 | // ColumnLayout { 57 | // id: textColumn 58 | // Layout.fillWidth: true 59 | // Layout.fillHeight: true 60 | // spacing: 0 61 | // PlasmaComponents3.Label { 62 | // Layout.fillWidth: true 63 | // text: control.text 64 | // horizontalAlignment: Text.AlignLeft 65 | // maximumLineCount: 1 66 | // elide: Text.ElideRight 67 | // } 68 | // PlasmaComponents3.Label { 69 | // Layout.fillWidth: true 70 | // text: control.subText 71 | // horizontalAlignment: Text.AlignLeft 72 | // visible: control.subText 73 | // color: config.menuItemTextColor2 74 | // maximumLineCount: 1 75 | // elide: Text.ElideRight 76 | // } 77 | // } 78 | // } 79 | // } 80 | onClicked: { 81 | if (searchFiltersViewItem.runnerId) { 82 | search.filters = [searchFiltersViewItem.runnerId] 83 | } 84 | searchFiltersViewItem.applyButtonClicked() 85 | searchResultsView.filterViewOpen = false 86 | } 87 | } 88 | 89 | PlasmaComponents3.CheckBox { 90 | id: isDefaultFilter 91 | checked: search.defaultFiltersContains(searchFiltersViewItem.runnerId) 92 | onCheckedChanged: { 93 | if (checked) { 94 | search.addDefaultFilter(searchFiltersViewItem.runnerId) 95 | } else { 96 | search.removeDefaultFilter(searchFiltersViewItem.runnerId) 97 | } 98 | } 99 | Layout.fillHeight: true 100 | text: i18n("Default") 101 | } 102 | 103 | Item { // Align CheckBoxes buttons to "All" 104 | Layout.minimumWidth: surfaceNormal.margins.right 105 | Layout.maximumWidth: Layout.minimumWidth 106 | Layout.fillHeight: true 107 | // visible: isDefaultFilter.visible && searchFiltersViewItem.indentLevel > 0 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /package/contents/ui/SearchModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.private.kicker as Kicker 3 | 4 | Item { 5 | id: search 6 | property alias results: resultModel 7 | property alias runnerModel: runnerModel 8 | 9 | property string query: "" 10 | property bool isSearching: query.length > 0 11 | onQueryChanged: { 12 | runnerModel.query = search.query 13 | } 14 | 15 | // KRunner runners are defined in /usr/share/kservices5/plasma-runner-*.desktop 16 | // To list the runner ids, use: 17 | // find /usr/share/kservices5/ -iname "plasma-runner-*.desktop" -print0 | xargs -0 grep "PluginInfo-Name" | sort 18 | property var filters: [] 19 | onFiltersChanged: { 20 | // runnerModel.deleteWhenEmpty = !runnerModel.deleteWhenEmpty // runnerModel.clear() 21 | // runnerModel.runners = filters 22 | clearQueryPrefix() 23 | runnerModel.query = search.query 24 | } 25 | 26 | Kicker.RunnerModel { 27 | id: runnerModel 28 | 29 | appletInterface: plasmoid 30 | favoritesModel: rootModel.favoritesModel 31 | mergeResults: config.searchResultsMerged 32 | 33 | // runners: [] // Empty = All runners. 34 | 35 | // deleteWhenEmpty: isDash 36 | // deleteWhenEmpty: false 37 | 38 | onRunnersChanged: debouncedRefresh.restart() 39 | onDataChanged: debouncedRefresh.restart() 40 | onCountChanged: debouncedRefresh.restart() 41 | } 42 | 43 | Timer { 44 | id: debouncedRefresh 45 | interval: 100 46 | onTriggered: resultModel.refresh() 47 | 48 | function logAndRestart() { 49 | // console.log('debouncedRefresh') 50 | restart() 51 | } 52 | } 53 | 54 | SearchResultsModel { 55 | id: resultModel 56 | } 57 | 58 | readonly property var defaultFilters: plasmoid.configuration.searchDefaultFilters 59 | function defaultFiltersContains(runnerId) { 60 | return defaultFilters.indexOf(runnerId) != -1 61 | } 62 | function addDefaultFilter(runnerId) { 63 | if (!defaultFiltersContains(runnerId)) { 64 | var l = plasmoid.configuration.searchDefaultFilters 65 | l.push(runnerId) 66 | plasmoid.configuration.searchDefaultFilters = l 67 | } 68 | } 69 | function removeDefaultFilter(runnerId) { 70 | console.log(JSON.stringify(plasmoid.configuration.searchDefaultFilters)) 71 | var i = defaultFilters.indexOf(runnerId) 72 | if (i >= 0) { 73 | var l = plasmoid.configuration.searchDefaultFilters 74 | l.splice(i, 1) // Remove 1 item at index 75 | plasmoid.configuration.searchDefaultFilters = l 76 | } 77 | } 78 | 79 | function isFilter(runnerId) { 80 | return filters.length == 1 && filters[0] == runnerId 81 | } 82 | property bool isDefaultFilter: filters == defaultFilters 83 | property bool isAppsFilter: isFilter('services') 84 | property bool isFileFilter: isFilter('baloosearch') 85 | property bool isBookmarksFilter: isFilter('bookmarks') 86 | 87 | function hasFilter(runnerId) { 88 | return filters.indexOf(runnerId) >= 0 89 | } 90 | 91 | function applyDefaultFilters() { 92 | filters = defaultFilters 93 | } 94 | 95 | function setQueryPrefix(prefix) { 96 | // First check to see if there's already a prefix we need to replace. 97 | var firstSpaceIndex = query.indexOf(' ') 98 | if (firstSpaceIndex > 0) { 99 | var firstToken = query.substring(0, firstSpaceIndex) 100 | 101 | if (/^type:\w+$/.exec(firstToken) // baloosearch 102 | || /^define$/.exec(firstToken) // Dictionary 103 | ) { 104 | // replace existing prefix 105 | query = prefix + query.substring(firstSpaceIndex + 1, query.length) 106 | return 107 | } 108 | } 109 | 110 | // If not, just prepend the prefix 111 | var newQuery = prefix + query 112 | if (newQuery != query) { 113 | query = prefix + query 114 | } 115 | } 116 | 117 | function clearQueryPrefix() { 118 | setQueryPrefix('') 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /package/contents/ui/SearchResultsList.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | KickerListView { // RunnerResultsList 4 | id: searchResultsList 5 | 6 | model: [] 7 | delegate: MenuListItem { 8 | property var runner: search.runnerModel.modelForRow(model.runnerIndex) 9 | iconSource: runner && runner.data(runner.index(model.runnerItemIndex, 0), Qt.DecorationRole) 10 | } 11 | 12 | section.property: 'runnerName' 13 | section.criteria: ViewSection.FullString 14 | // verticalLayoutDirection: config.searchResultsDirection 15 | 16 | Connections { 17 | target: search.results 18 | function onRefreshing() { 19 | searchResultsList.model = [] 20 | // console.log('search.results.onRefreshed') 21 | searchResultsList.currentIndex = 0 22 | } 23 | function onRefreshed() { 24 | // console.log('search.results.onRefreshed') 25 | searchResultsList.model = search.results 26 | // if (searchResultsList.verticalLayoutDirection == Qt.BottomToTop) { 27 | if (plasmoid.configuration.searchResultsReversed) { 28 | searchResultsList.currentIndex = searchResultsList.model.count - 1 29 | } else { // TopToBottom (normal) 30 | searchResultsList.currentIndex = 0 31 | } 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /package/contents/ui/SearchResultsView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | import QtQuick.Layouts 4 | import org.kde.plasma.components as PlasmaComponents3 5 | import org.kde.kirigami as Kirigami 6 | 7 | GridLayout { 8 | id: searchResultsView 9 | rowSpacing: 0 10 | property alias listView: searchResultsList 11 | property bool filterViewOpen: false 12 | 13 | RowLayout { 14 | id: searchFiltersRow 15 | Layout.row: searchView.searchOnTop ? 2 : 0 16 | Layout.preferredHeight: config.searchFilterRowHeight - 1 // -1px is for the underline seperator 17 | Layout.fillWidth: true 18 | 19 | FlatButton { 20 | icon.name: "system-search-symbolic" 21 | Layout.preferredHeight: parent.Layout.preferredHeight 22 | Layout.preferredWidth: parent.Layout.preferredHeight 23 | onClicked: search.applyDefaultFilters() 24 | checked: search.isDefaultFilter 25 | checkedEdge: searchView.searchOnTop ? Qt.TopEdge : Qt.BottomEdge 26 | } 27 | FlatButton { 28 | icon.name: "window" 29 | Layout.preferredHeight: parent.Layout.preferredHeight 30 | Layout.preferredWidth: parent.Layout.preferredHeight 31 | onClicked: search.filters = ['services'] 32 | checked: search.isAppsFilter 33 | checkedEdge: searchView.searchOnTop ? Qt.TopEdge : Qt.BottomEdge 34 | } 35 | FlatButton { 36 | icon.name: "document-new" 37 | Layout.preferredHeight: parent.Layout.preferredHeight 38 | Layout.preferredWidth: parent.Layout.preferredHeight 39 | onClicked: search.filters = ['baloosearch'] 40 | checked: search.isFileFilter 41 | checkedEdge: searchView.searchOnTop ? Qt.TopEdge : Qt.BottomEdge 42 | } 43 | // FlatButton { 44 | // icon.name: "globe" 45 | // Layout.preferredHeight: parent.Layout.preferredHeight 46 | // Layout.preferredWidth: parent.Layout.preferredHeight 47 | // onClicked: search.filters = ['bookmarks'] 48 | // checked: search.isBookmarksFilter 49 | // checkedEdge: searchView.searchOnTop ? Qt.TopEdge : Qt.BottomEdge 50 | // } 51 | 52 | Item { Layout.fillWidth: true } 53 | 54 | FlatButton { 55 | id: moreFiltersButton 56 | Layout.preferredHeight: parent.Layout.preferredHeight 57 | Layout.preferredWidth: moreFiltersButtonRow.implicitWidth + padding*2 58 | // property int padding: (config.searchFilterRowHeight - config.flatButtonIconSize) / 2 59 | padding: (config.searchFilterRowHeight - config.flatButtonIconSize) / 2 60 | // enabled: false 61 | 62 | RowLayout { 63 | id: moreFiltersButtonRow 64 | anchors.centerIn: parent 65 | anchors.margins: parent.padding 66 | 67 | PlasmaComponents3.Label { 68 | id: moreFiltersButtonLabel 69 | text: i18n("Filters") 70 | } 71 | Kirigami.Icon { 72 | source: "usermenu-down" 73 | rotation: searchResultsView.filterViewOpen ? 180 : 0 74 | Layout.preferredHeight: config.flatButtonIconSize 75 | Layout.preferredWidth: config.flatButtonIconSize 76 | 77 | Behavior on rotation { 78 | NumberAnimation { duration: Kirigami.Units.longDuration } 79 | } 80 | } 81 | } 82 | 83 | onClicked: searchResultsView.filterViewOpen = !searchResultsView.filterViewOpen 84 | } 85 | } 86 | 87 | Rectangle { 88 | color: "#111" 89 | height: 1 90 | width: parent.width 91 | // anchors.bottom: searchFiltersRow.bottom - 1 92 | } 93 | 94 | QQC2.StackView { 95 | id: searchResultsViewStackView 96 | Layout.row: searchView.searchOnTop ? 0 : 2 97 | Layout.fillWidth: true 98 | Layout.fillHeight: true 99 | clip: true 100 | initialItem: searchResultsListScrollView 101 | 102 | Connections { 103 | target: searchResultsView 104 | function onFilterViewOpenChanged() { 105 | if (searchResultsView.filterViewOpen) { 106 | searchResultsViewStackView.push(searchFiltersViewScrollView) 107 | } else { 108 | searchResultsViewStackView.pop() 109 | } 110 | } 111 | } 112 | 113 | QQC2.ScrollView { 114 | id: searchResultsListScrollView 115 | visible: false 116 | 117 | SearchResultsList { 118 | id: searchResultsList 119 | } 120 | } 121 | 122 | QQC2.ScrollView { 123 | id: searchFiltersViewScrollView 124 | visible: false 125 | 126 | SearchFiltersView { 127 | id: searchFiltersView 128 | } 129 | } 130 | 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /package/contents/ui/SearchStackView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | 4 | QQC2.StackView { 5 | id: stackView 6 | clip: true 7 | 8 | // delegate: panUp 9 | 10 | property int zoomDuration: 250 11 | property int zoomDelta: 100 12 | property real zoomedInRatio: Math.max(1, stackView.width + zoomDelta * Screen.devicePixelRatio) / stackView.width 13 | property real zoomedOutRatio: Math.max(1, stackView.width - zoomDelta * Screen.devicePixelRatio) / stackView.width 14 | 15 | // readonly property var noTransition: StackViewDelegate {} 16 | 17 | // readonly property var panUp: StackViewDelegate { 18 | // pushTransition: StackViewTransition { 19 | // PropertyAnimation { 20 | // target: enterItem 21 | // property: "y" 22 | // from: stackView.height * (searchView.searchOnTop ? -1 : 1) 23 | // to: 0 24 | // } 25 | // PropertyAnimation { 26 | // target: exitItem 27 | // property: "opacity" 28 | // from: 1 29 | // to: 0 30 | // } 31 | // } 32 | 33 | // function transitionFinished(properties) { 34 | // properties.exitItem.opacity = 1 35 | // } 36 | // } 37 | // readonly property var zoomOut: StackViewDelegate { 38 | // function transitionFinished(properties) { 39 | // properties.exitItem.opacity = 1 40 | // properties.exitItem.scale = 1 41 | // } 42 | 43 | // pushTransition: StackViewTransition { 44 | // PropertyAnimation { 45 | // target: enterItem 46 | // property: "opacity" 47 | // easing.type: Easing.InQuad 48 | // from: 0 49 | // to: 1 50 | // duration: stackView.zoomDuration 51 | // } 52 | // PropertyAnimation { 53 | // target: exitItem 54 | // property: "opacity" 55 | // easing.type: Easing.InQuad 56 | // from: 1 57 | // to: 0 58 | // duration: stackView.zoomDuration 59 | // } 60 | // PropertyAnimation { 61 | // target: enterItem 62 | // property: "scale" 63 | // easing.type: Easing.Linear 64 | // from: stackView.zoomedInRatio 65 | // to: 1 66 | // duration: stackView.zoomDuration 67 | // } 68 | // PropertyAnimation { 69 | // target: exitItem 70 | // property: "scale" 71 | // easing.type: Easing.Linear 72 | // from: 1 73 | // to: stackView.zoomedOutRatio 74 | // duration: stackView.zoomDuration 75 | // } 76 | // } 77 | // } 78 | // readonly property var zoomIn: StackViewDelegate { 79 | // function transitionFinished(properties) { 80 | // properties.exitItem.opacity = 1 81 | // properties.exitItem.scale = 1 82 | // } 83 | 84 | // pushTransition: StackViewTransition { 85 | // PropertyAnimation { 86 | // target: enterItem 87 | // property: "opacity" 88 | // easing.type: Easing.InQuad 89 | // from: 0 90 | // to: 1 91 | // duration: stackView.zoomDuration 92 | // } 93 | // PropertyAnimation { 94 | // target: exitItem 95 | // property: "opacity" 96 | // easing.type: Easing.InQuad 97 | // from: 1 98 | // to: 0 99 | // duration: stackView.zoomDuration 100 | // } 101 | // PropertyAnimation { 102 | // target: enterItem 103 | // property: "scale" 104 | // easing.type: Easing.Linear 105 | // from: stackView.zoomedOutRatio 106 | // to: 1 107 | // duration: stackView.zoomDuration 108 | // } 109 | // PropertyAnimation { 110 | // target: exitItem 111 | // property: "scale" 112 | // easing.type: Easing.Linear 113 | // from: 1 114 | // to: stackView.zoomedInRatio 115 | // duration: stackView.zoomDuration 116 | // } 117 | // } 118 | // popTransition: pushTransition 119 | // } 120 | } 121 | -------------------------------------------------------------------------------- /package/contents/ui/SearchView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Item { 4 | id: searchView 5 | implicitWidth: config.appAreaWidth 6 | // Behavior on implicitWidth { 7 | // NumberAnimation { duration: 400 } 8 | // } 9 | 10 | visible: opacity > 0 11 | opacity: config.showSearch ? 1 : 0 12 | // Behavior on opacity { 13 | // NumberAnimation { duration: 400 } 14 | // } 15 | 16 | Connections { 17 | target: search 18 | function onIsSearchingChanged() { 19 | if (search.isSearching) { 20 | searchView.showSearchView() 21 | } 22 | } 23 | } 24 | clip: true 25 | 26 | property alias searchResultsView: searchResultsView 27 | property alias appsView: appsView 28 | property alias tileEditorView: tileEditorViewLoader.item 29 | property alias tileEditorViewLoader: tileEditorViewLoader 30 | property alias searchField: searchField 31 | property alias jumpToLetterView: jumpToLetterView 32 | 33 | readonly property bool showingOnlyTiles: !config.showSearch 34 | readonly property bool showingAppList: stackView.currentItem == appsView || stackView.currentItem == jumpToLetterView 35 | readonly property bool showingAppsAlphabetically: config.showSearch && appsModel.order == "alphabetical" && showingAppList 36 | readonly property bool showingAppsCategorically: config.showSearch && appsModel.order == "categories" && showingAppList 37 | readonly property bool showSearchField: config.hideSearchField ? !!searchField.text : true 38 | 39 | property bool searchOnTop: false 40 | 41 | function showDefaultView() { 42 | var defView = plasmoid.configuration.defaultAppListView 43 | if (defView == 'Alphabetical') { 44 | appsView.showAppsAlphabetically() 45 | config.showSearch = true 46 | } else if (defView == 'Categories') { 47 | appsView.showAppsCategorically() 48 | config.showSearch = true 49 | } else if (defView == 'JumpToLetter') { 50 | jumpToLetterView.showLetters() 51 | config.showSearch = true 52 | } else if (defView == 'JumpToCategory') { 53 | jumpToLetterView.showCategories() 54 | config.showSearch = true 55 | } else if (defView == 'TilesOnly') { 56 | searchView.showTilesOnly() 57 | } 58 | } 59 | 60 | function showTilesOnly() { 61 | if (!showingAppList) { 62 | // appsView.show(stackView.noTransition) 63 | appsView.show() 64 | } 65 | config.showSearch = false 66 | } 67 | 68 | function showSearchView() { 69 | config.showSearch = true 70 | } 71 | 72 | states: [ 73 | State { 74 | name: "searchOnTop" 75 | when: searchOnTop 76 | PropertyChanges { 77 | target: stackViewContainer 78 | anchors.topMargin: searchField.visible ? searchField.height : 0 79 | } 80 | PropertyChanges { 81 | target: searchField 82 | anchors.top: searchField.parent.top 83 | } 84 | }, 85 | State { 86 | name: "searchOnBottom" 87 | when: !searchOnTop 88 | PropertyChanges { 89 | target: stackViewContainer 90 | anchors.bottomMargin: searchField.visible ? searchField.height : 0 91 | } 92 | PropertyChanges { 93 | target: searchField 94 | anchors.bottom: searchField.parent.bottom 95 | } 96 | } 97 | ] 98 | 99 | 100 | Item { 101 | id: stackViewContainer 102 | anchors.fill: parent 103 | 104 | SearchResultsView { 105 | id: searchResultsView 106 | visible: false 107 | 108 | Connections { 109 | target: search 110 | function onQueryChanged() { 111 | if (search.query.length > 0 && stackView.currentItem != searchResultsView) { 112 | stackView.replace(searchResultsView) 113 | } 114 | searchResultsView.filterViewOpen = false 115 | } 116 | } 117 | 118 | onVisibleChanged: { 119 | if (!visible) { // !stackView.currentItem 120 | search.query = "" 121 | } 122 | } 123 | 124 | function showDefaultSearch() { 125 | if (stackView.currentItem != searchResultsView) { 126 | stackView.replace(searchResultsView) 127 | } 128 | search.applyDefaultFilters() 129 | } 130 | } 131 | 132 | AppsView { 133 | id: appsView 134 | visible: false 135 | 136 | function showAppsAlphabetically() { 137 | appsModel.order = "alphabetical" 138 | show() 139 | } 140 | 141 | function showAppsCategorically() { 142 | appsModel.order = "categories" 143 | show() 144 | } 145 | 146 | function show(animation) { 147 | config.showSearch = true 148 | if (stackView.currentItem != appsView) { 149 | // stackView.delegate = animation || stackView.panUp 150 | stackView.replace(appsView) 151 | } 152 | appsView.scrollToTop() 153 | } 154 | } 155 | 156 | JumpToLetterView { 157 | id: jumpToLetterView 158 | visible: false 159 | 160 | function showLetters() { 161 | appsModel.order = "alphabetical" 162 | show() 163 | } 164 | 165 | function showCategories() { 166 | appsModel.order = "categories" 167 | show() 168 | } 169 | 170 | function show() { 171 | config.showSearch = true 172 | if (stackView.currentItem != jumpToLetterView) { 173 | // stackView.delegate = stackView.zoomOut 174 | stackView.replace(jumpToLetterView) 175 | } 176 | } 177 | } 178 | 179 | Loader { 180 | id: tileEditorViewLoader 181 | source: "TileEditorView.qml" 182 | visible: false 183 | active: false 184 | // asynchronous: true 185 | function open(tile) { 186 | config.showSearch = true 187 | active = true 188 | item.open(tile) 189 | } 190 | readonly property bool isCurrentView: stackView.currentItem == tileEditorView 191 | onIsCurrentViewChanged: { 192 | config.isEditingTile = isCurrentView 193 | } 194 | } 195 | 196 | SearchStackView { 197 | id: stackView 198 | anchors.fill: parent 199 | initialItem: appsView 200 | } 201 | } 202 | 203 | 204 | SearchField { 205 | id: searchField 206 | visible: !config.isEditingTile && searchView.showSearchField 207 | height: config.searchFieldHeight 208 | anchors.left: parent.left 209 | anchors.right: parent.right 210 | 211 | listView: stackView.currentItem && stackView.currentItem.listView ? stackView.currentItem.listView : [] 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarContextMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQml.Models as QtModels 3 | import org.kde.plasma.extras as PlasmaExtras 4 | 5 | // https://invent.kde.org/plasma/plasma-framework/-/blame/master/src/declarativeimports/plasmaextracomponents/qmenu.h 6 | PlasmaExtras.Menu { 7 | id: kickerContextMenu 8 | required property var model 9 | 10 | function toggleOpen() { 11 | if (kickerContextMenu.status == PlasmaExtras.Menu.Open) { 12 | kickerContextMenu.close() 13 | } else if (kickerContextMenu.status == PlasmaExtras.Menu.Closed) { 14 | kickerContextMenu.openRelative() 15 | } 16 | } 17 | 18 | // https://invent.kde.org/plasma/plasma-desktop/-/blame/master/applets/kickoff/package/contents/ui/LeaveButtons.qml 19 | // https://invent.kde.org/plasma/plasma-desktop/-/blame/master/applets/kickoff/package/contents/ui/ActionMenu.qml 20 | // https://doc.qt.io/qt-6/qml-qtqml-models-instantiator.html 21 | property Instantiator _instantiator: QtModels.Instantiator { 22 | model: kickerContextMenu.model 23 | delegate: PlasmaExtras.MenuItem { 24 | icon: model.iconName || model.decoration 25 | text: model.name || model.display 26 | visible: !model.disabled 27 | onClicked: { 28 | kickerContextMenu.model.triggerIndex(index) 29 | } 30 | } 31 | onObjectAdded: (index, object) => kickerContextMenu.addMenuItem(object) 32 | onObjectRemoved: (index, object) => kickerContextMenu.removeMenuItem(object) 33 | } 34 | placement: { 35 | if (searchView.searchOnTop) { 36 | return PlasmaExtras.Menu.BottomPosedRightAlignedPopup 37 | } else { 38 | return PlasmaExtras.Menu.TopPosedRightAlignedPopup 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarFavouritesView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import "./lib/" as Lib 3 | 4 | Repeater { 5 | id: repeater 6 | property int maxHeight: 1000000 7 | property int numAvailable: maxHeight / config.flatButtonSize 8 | property int minVisibleIndex: count - numAvailable // Hide items with an index smaller than this 9 | 10 | property QtObject xdgUserDir: Lib.XdgUserDir {} 11 | 12 | delegate: SidebarItem { 13 | icon.name: symbolicIconName || model.iconName || model.decoration 14 | text: xdgDisplayName || model.name || model.display 15 | sidebarMenu: repeater.parent.parent // SidebarContextMenu { Column { Repeater{} } } 16 | onClicked: { 17 | repeater.parent.parent.open = false // SidebarContextMenu { Column { Repeater{} } } 18 | var xdgFolder = isLocalizedFolder() 19 | if (xdgFolder === 'DOCUMENTS') { 20 | Qt.openUrlExternally(xdgUserDir.documents) 21 | } else if (xdgFolder === 'DOWNLOAD') { 22 | Qt.openUrlExternally(xdgUserDir.download) 23 | } else if (xdgFolder === 'MUSIC') { 24 | Qt.openUrlExternally(xdgUserDir.music) 25 | } else if (xdgFolder === 'PICTURES') { 26 | Qt.openUrlExternally(xdgUserDir.pictures) 27 | } else if (xdgFolder === 'VIDEOS') { 28 | Qt.openUrlExternally(xdgUserDir.videos) 29 | } else { 30 | repeater.model.triggerIndex(index) 31 | } 32 | } 33 | visible: index >= minVisibleIndex 34 | 35 | // These files are localize, so open them via commandline 36 | // since Qt 5.7 doesn't expose the localized paths anywhere. 37 | function isLocalizedFolder() { 38 | var s = model.url.toString() 39 | if (startsWith(s, 'xdg:')) { 40 | s = s.substring('xdg:'.length, s.length) 41 | if (s == 'DOCUMENTS' 42 | || s == 'DOWNLOAD' 43 | || s == 'MUSIC' 44 | || s == 'PICTURES' 45 | || s == 'VIDEOS' 46 | ) { 47 | return s 48 | } 49 | } 50 | return '' 51 | } 52 | 53 | function startsWith(s, sub) { 54 | return s.indexOf(sub) === 0 55 | } 56 | function endsWith(s, sub) { 57 | return s.indexOf(sub) === s.length - sub.length 58 | } 59 | 60 | property string xdgDisplayName: { 61 | var xdgFolder = isLocalizedFolder() 62 | if (xdgFolder) { 63 | // https://translationproject.org/domain/xdg-user-dirs.html 64 | // https://translationproject.org/PO-files/fr/xdg-user-dirs-0.17.fr.po 65 | if (xdgFolder === 'DOCUMENTS') { 66 | return i18nd("xdg-user-dirs", "Documents") 67 | } else if (xdgFolder === 'DOWNLOAD') { 68 | return i18nd("xdg-user-dirs", "Download") 69 | } else if (xdgFolder === 'MUSIC') { 70 | return i18nd("xdg-user-dirs", "Music") 71 | } else if (xdgFolder === 'PICTURES') { 72 | return i18nd("xdg-user-dirs", "Pictures") 73 | } else if (xdgFolder === 'VIDEOS') { 74 | return i18nd("xdg-user-dirs", "Videos") 75 | } else { 76 | return '' 77 | } 78 | } else { 79 | return '' 80 | } 81 | } 82 | property string symbolicIconName: { 83 | if (model.url) { 84 | var s = model.url.toString() 85 | if (endsWith(s, '.desktop')) { 86 | if (endsWith(s, '/org.kde.dolphin.desktop')) { 87 | return 'folder-open-symbolic' 88 | } else if (endsWith(s, '/systemsettings.desktop')) { 89 | return 'configure' 90 | } 91 | } else if (startsWith(s, 'file:///home/')) { 92 | s = s.substring('file:///home/'.length, s.length) 93 | // console.log(model.url, s) 94 | 95 | var trimIndex = s.indexOf('/') 96 | if (trimIndex == -1) { // file:///home/username 97 | s = '' 98 | } else { 99 | s = s.substring(trimIndex, s.length) 100 | } 101 | // console.log(model.url, s) 102 | 103 | if (s === '') { // Home Directory 104 | return 'user-home-symbolic' 105 | } 106 | } else if (startsWith(s, 'xdg:')) { 107 | s = s.substring('xdg:'.length, s.length) 108 | if (s === 'DOCUMENTS') { 109 | return 'folder-documents-symbolic' 110 | } else if (s === 'DOWNLOAD') { 111 | return 'folder-download-symbolic' 112 | } else if (s === 'MUSIC') { 113 | return 'folder-music-symbolic' 114 | } else if (s === 'PICTURES') { 115 | return 'folder-pictures-symbolic' 116 | } else if (s === 'VIDEOS') { 117 | return 'folder-videos-symbolic' 118 | } 119 | } 120 | } 121 | return "" 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | import QtQuick.Layouts 4 | 5 | FlatButton { 6 | id: sidebarItem 7 | Layout.fillWidth: true 8 | Layout.minimumWidth: expanded ? config.sidebarMinOpenWidth : implicitWidth 9 | property var sidebarMenu: parent.parent // Column.SidebarMenu 10 | expanded: sidebarMenu ? sidebarMenu.open : false 11 | labelVisible: expanded 12 | property bool closeOnClick: true 13 | 14 | QQC2.ToolTip { 15 | id: control 16 | visible: sidebarItem.hovered && !sidebarItem.expanded 17 | text: sidebarItem.text 18 | delay: 0 19 | x: parent.width + rightPadding 20 | y: (parent.height - height) / 2 21 | } 22 | 23 | Loader { 24 | id: hoverOutlineEffectLoader 25 | anchors.fill: parent 26 | source: "HoverOutlineButtonEffect.qml" 27 | asynchronous: true 28 | property var mouseArea: sidebarItem.__behavior 29 | active: !!mouseArea && mouseArea.containsMouse 30 | visible: active 31 | property var __mouseArea: mouseArea 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarItemRepeater.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Repeater { 4 | id: repeater 5 | property int buttonHeight: config.flatButtonSize 6 | property int iconSize: config.flatButtonIconSize 7 | 8 | delegate: SidebarItem { 9 | buttonHeight: repeater.buttonHeight 10 | iconSize: repeater.iconSize 11 | icon.name: model.iconName || model.decoration 12 | text: model.name || model.display 13 | sidebarMenu: repeater.parent.parent // SidebarContextMenu { Column { Repeater{} } } 14 | onClicked: { 15 | repeater.parent.parent.open = false // SidebarContextMenu { Column { Repeater{} } } 16 | repeater.model.triggerIndex(index) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.kirigami as Kirigami 3 | import org.kde.ksvg as KSvg 4 | 5 | MouseArea { 6 | id: sidebarMenu 7 | hoverEnabled: true 8 | z: 1 9 | // clip: true 10 | implicitWidth: config.sidebarWidth 11 | property bool open: false 12 | 13 | onOpenChanged: { 14 | if (open) { 15 | forceActiveFocus() 16 | } else { 17 | searchView.searchField.forceActiveFocus() 18 | } 19 | } 20 | 21 | Rectangle { 22 | anchors.fill: parent 23 | visible: !plasmoid.configuration.sidebarFollowsTheme 24 | color: config.sidebarBackgroundColor 25 | opacity: parent.open ? 1 : 0 26 | } 27 | 28 | Rectangle { 29 | anchors.fill: parent 30 | visible: plasmoid.configuration.sidebarFollowsTheme 31 | color: Kirigami.Theme.backgroundColor 32 | opacity: parent.open ? 1 : 0 33 | } 34 | KSvg.FrameSvgItem { 35 | anchors.fill: parent 36 | visible: plasmoid.configuration.sidebarFollowsTheme 37 | imagePath: "widgets/frame" 38 | prefix: "raised" 39 | } 40 | 41 | property alias showDropShadow: sidebarMenuShadows.visible 42 | SidebarMenuShadows { 43 | id: sidebarMenuShadows 44 | anchors.fill: parent 45 | visible: !plasmoid.configuration.sidebarFollowsTheme && sidebarMenu.open 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarMenuShadows.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Item { 4 | property int dropShadowSize: 4 * Screen.devicePixelRatio 5 | property int roundShadowHack: dropShadowSize/2 // "dropShadowSize/2" draws enough to fool the eye. 6 | Rectangle { 7 | id: topShadow 8 | anchors.left: parent.left 9 | anchors.bottom: parent.top 10 | width: parent.width + roundShadowHack 11 | height: dropShadowSize 12 | gradient: Gradient { 13 | GradientStop { position: 0.0; color: "#00000000" } 14 | GradientStop { position: 1.0; color: "#60000000" } 15 | } 16 | } 17 | Rectangle { 18 | id: rightShadow 19 | anchors.bottom: parent.top 20 | anchors.left: parent.right 21 | height: dropShadowSize 22 | width: parent.height 23 | 24 | transformOrigin: Item.BottomLeft 25 | rotation: 90 26 | 27 | gradient: Gradient { 28 | GradientStop { position: 0.0; color: "#00000000" } 29 | GradientStop { position: 1.0; color: "#60000000" } 30 | } 31 | } 32 | Rectangle { 33 | id: bottomShadow 34 | anchors.left: parent.left 35 | anchors.top: parent.bottom 36 | width: parent.width + roundShadowHack 37 | height: dropShadowSize 38 | gradient: Gradient { 39 | GradientStop { position: 0.0; color: "#90000000" } 40 | GradientStop { position: 1.0; color: "#00000000" } 41 | } 42 | } 43 | Rectangle { 44 | id: leftShadow 45 | anchors.bottom: parent.top 46 | anchors.left: parent.left 47 | height: dropShadowSize 48 | width: parent.height 49 | 50 | transformOrigin: Item.BottomLeft 51 | rotation: 90 52 | 53 | gradient: Gradient { 54 | GradientStop { position: 0.0; color: "#00000000" } 55 | GradientStop { position: 1.0; color: "#20000000" } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.core as PlasmaCore 4 | import org.kde.plasma.extras as PlasmaExtras 5 | import org.kde.config as KConfig 6 | import org.kde.draganddrop as DragAndDrop 7 | import org.kde.kcmutils as KCM // KCMLauncher 8 | import "Utils.js" as Utils 9 | 10 | Item { 11 | id: sidebarView 12 | anchors.left: parent.left 13 | anchors.top: parent.top 14 | anchors.bottom: parent.bottom 15 | z: 1 16 | 17 | width: sidebarMenu.width 18 | Behavior on width { NumberAnimation { duration: 100 } } 19 | 20 | DragAndDrop.DropArea { 21 | anchors.fill: sidebarMenu 22 | 23 | onDrop: { 24 | if (event && event.mimeData && event.mimeData.url) { 25 | var url = event.mimeData.url.toString() 26 | url = Utils.parseDropUrl(url) 27 | appsModel.sidebarModel.addFavorite(url, 0) 28 | } 29 | } 30 | } 31 | 32 | SidebarMenu { 33 | id: sidebarMenu 34 | anchors.left: parent.left 35 | anchors.top: parent.top 36 | anchors.bottom: parent.bottom 37 | 38 | 39 | ColumnLayout { 40 | id: sidebarMenuTop 41 | spacing: 0 42 | 43 | // SidebarItem { 44 | // icon.name: 'open-menu-symbolic' 45 | // text: i18n("Menu") 46 | // closeOnClick: false 47 | // onClicked: sidebarMenu.open = !sidebarMenu.open 48 | // zoomOnPush: expanded 49 | // } 50 | 51 | SidebarViewButton { 52 | appletIconName: "view-tilesonly" 53 | text: i18n("Tiles Only") 54 | onClicked: searchView.showTilesOnly() 55 | checked: searchView.showingOnlyTiles 56 | visible: checked || plasmoid.configuration.defaultAppListView == 'TilesOnly' 57 | } 58 | SidebarViewButton { 59 | appletIconName: "view-list-alphabetically" 60 | text: i18n("Alphabetical") 61 | onClicked: appsView.showAppsAlphabetically() 62 | checked: searchView.showingAppsAlphabetically 63 | } 64 | SidebarViewButton { 65 | appletIconName: 'view-list-categorically' 66 | text: i18n("Categories") 67 | onClicked: appsView.showAppsCategorically() 68 | checked: searchView.showingAppsCategorically 69 | } 70 | // SidebarItem { 71 | // icon.name: 'system-search-symbolic' 72 | // text: i18n("Search") 73 | // onClicked: searchResultsView.showDefaultSearch() 74 | // // checked: stackView.currentItem == searchResultsView 75 | // // checkedEdge: Qt.RightEdge 76 | // // checkedEdgeWidth: 4 * Screen.devicePixelRatio // Twice as thick as normal 77 | // } 78 | } 79 | ColumnLayout { 80 | anchors.bottom: parent.bottom 81 | spacing: 0 82 | 83 | SidebarItem { 84 | id: userMenuButton 85 | icon.name: kuser.hasFaceIcon ? kuser.faceIconUrl : 'user-identity' 86 | text: kuser.fullName 87 | onClicked: { 88 | userMenu.toggleOpen() 89 | } 90 | SidebarContextMenu { 91 | id: userMenu 92 | visualParent: userMenuButton 93 | model: appsModel.sessionActionsModel 94 | 95 | PlasmaExtras.MenuItem { 96 | icon: 'system-users' 97 | text: i18n("User Manager") 98 | onClicked: KCM.KCMLauncher.open('kcm_users') 99 | visible: KConfig.KAuthorized.authorizeControlModule('kcm_users') 100 | } 101 | 102 | // ... appsModel.sessionActionsModel 103 | } 104 | } 105 | 106 | SidebarFavouritesView { 107 | model: appsModel.sidebarModel 108 | maxHeight: sidebarMenu.height - sidebarMenuTop.height - 2 * config.flatButtonSize 109 | } 110 | 111 | SidebarItem { 112 | id: powerMenuButton 113 | icon.name: 'system-shutdown-symbolic' 114 | text: i18n("Power") 115 | onClicked: { 116 | powerMenu.toggleOpen() 117 | } 118 | SidebarContextMenu { 119 | id: powerMenu 120 | visualParent: powerMenuButton 121 | model: appsModel.powerActionsModel 122 | } 123 | } 124 | } 125 | 126 | onFocusChanged: { 127 | logger.debug('searchView.onFocusChanged', focus) 128 | if (!focus) { 129 | open = false 130 | } 131 | } 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /package/contents/ui/SidebarViewButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.kirigami as Kirigami 3 | import org.kde.ksvg as KSvg 4 | 5 | SidebarItem { 6 | id: control 7 | 8 | implicitWidth: config.flatButtonSize 9 | 10 | property string appletIconName: "" 11 | readonly property string appletIconFilename: appletIconName ? Qt.resolvedUrl("../icons/" + appletIconName + ".svg") : "" 12 | 13 | checkedEdge: Qt.LeftEdge 14 | checkedEdgeWidth: 4 * Screen.devicePixelRatio // Twice as thick as normal 15 | 16 | Kirigami.Icon { 17 | id: icon 18 | source: control.appletIconFilename 19 | 20 | isMask: true // Force color 21 | color: control.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor 22 | 23 | // From FlatButton.qml, modifed so icon is also 16px 24 | property int iconSize: Kirigami.Units.iconSizes.roundedIconSize(config.flatButtonIconSize) 25 | width: iconSize 26 | height: iconSize 27 | anchors.centerIn: parent 28 | 29 | // Note: Disabled this seems it seems to create a blurry icon after release. 30 | // From FlatButton.qml 31 | // scale: control.zoomOnPush && control.pressed ? (control.height-5) / control.height : 1 32 | // Behavior on scale { NumberAnimation { duration: 200 } } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorColorField.qml: -------------------------------------------------------------------------------- 1 | // Based on LibConfig.ColorField v8 2 | // QQC2.TextField => PlasmaComponents3.TextField 3 | 4 | import QtQuick 5 | import QtQuick.Controls as QQC2 6 | import QtQuick.Dialogs as QtDialogs 7 | import QtQuick.Window 8 | import org.kde.kirigami as Kirigami 9 | 10 | // https://doc.qt.io/qt-6/qtgraphicaleffects5-index.html 11 | import Qt5Compat.GraphicalEffects as QtGraphicalEffects // TODO Deprecated in Qt6 12 | 13 | import org.kde.plasma.components as PlasmaComponents3 14 | 15 | PlasmaComponents3.TextField { 16 | id: colorField 17 | font.family: "monospace" 18 | readonly property string defaultText: "#AARRGGBB" 19 | placeholderText: defaultColor ? defaultColor : defaultText 20 | 21 | onTextChanged: { 22 | // Make sure the text is: 23 | // Empty (use default) 24 | // or #123 or #112233 or #11223344 before applying the color. 25 | if (text.length === 0 26 | || (text.indexOf('#') === 0 && (text.length == 4 || text.length == 7 || text.length == 9)) 27 | ) { 28 | colorField.value = text 29 | } 30 | } 31 | 32 | property bool showAlphaChannel: true 33 | property bool showPreviewBg: true 34 | 35 | property string configKey: '' 36 | property string defaultColor: '' 37 | property string value: { 38 | if (configKey) { 39 | return plasmoid.configuration[configKey] 40 | } else { 41 | return "#000" 42 | } 43 | } 44 | 45 | readonly property color defaultColorValue: defaultColor 46 | readonly property color valueColor: { 47 | if (value == '' && defaultColor) { 48 | return defaultColor 49 | } else { 50 | return value 51 | } 52 | } 53 | 54 | onValueChanged: { 55 | if (!activeFocus) { 56 | text = colorField.value 57 | } 58 | if (configKey) { 59 | if (value == defaultColorValue) { 60 | plasmoid.configuration[configKey] = "" 61 | } else { 62 | plasmoid.configuration[configKey] = value 63 | } 64 | } 65 | } 66 | 67 | leftPadding: rightPadding + mouseArea.height + rightPadding 68 | 69 | FontMetrics { 70 | id: fontMetrics 71 | font.family: colorField.font.family 72 | font.italic: colorField.font.italic 73 | font.pointSize: colorField.font.pointSize 74 | font.pixelSize: colorField.font.pixelSize 75 | font.weight: colorField.font.weight 76 | } 77 | readonly property int defaultWidth: Math.ceil(fontMetrics.advanceWidth(defaultText)) 78 | implicitWidth: rightPadding + Math.max(defaultWidth, contentWidth) + leftPadding 79 | 80 | MouseArea { 81 | id: mouseArea 82 | anchors.leftMargin: parent.rightPadding 83 | anchors.topMargin: parent.topPadding 84 | anchors.bottomMargin: parent.bottomPadding 85 | anchors.left: parent.left 86 | anchors.top: parent.top 87 | anchors.bottom: parent.bottom 88 | width: height 89 | hoverEnabled: true 90 | cursorShape: Qt.PointingHandCursor 91 | 92 | onClicked: dialogLoader.active = true 93 | 94 | // Color Preview Circle 95 | Rectangle { 96 | id: previewBgMask 97 | visible: false 98 | anchors.fill: parent 99 | border.width: 1 * Screen.devicePixelRatio 100 | border.color: "transparent" 101 | radius: width / 2 102 | } 103 | QtGraphicalEffects.ConicalGradient { 104 | id: previewBgGradient 105 | visible: colorField.showPreviewBg 106 | anchors.fill: parent 107 | angle: 0.0 108 | gradient: Gradient { 109 | GradientStop { position: 0.00; color: "white" } 110 | GradientStop { position: 0.24; color: "white" } 111 | GradientStop { position: 0.25; color: "#cccccc" } 112 | GradientStop { position: 0.49; color: "#cccccc" } 113 | GradientStop { position: 0.50; color: "white" } 114 | GradientStop { position: 0.74; color: "white" } 115 | GradientStop { position: 0.75; color: "#cccccc" } 116 | GradientStop { position: 1.00; color: "#cccccc" } 117 | } 118 | source: previewBgMask 119 | } 120 | Rectangle { 121 | id: previewFill 122 | anchors.fill: parent 123 | color: colorField.valueColor 124 | border.width: 1 * Kirigami.Units.devicePixelRatio 125 | border.color: Kirigami.ColorUtils.linearInterpolation(color, Kirigami.Theme.textColor, 0.5) 126 | radius: width / 2 127 | } 128 | } 129 | 130 | Loader { 131 | id: dialogLoader 132 | active: false 133 | sourceComponent: QtDialogs.ColorDialog { 134 | id: dialog 135 | visible: true 136 | modality: Qt.WindowModal 137 | options: colorField.showAlphaChannel ? QtDialogs.ColorDialog.ShowAlphaChannel : 0 138 | selectedColor: colorField.valueColor 139 | onSelectedColorChanged: { 140 | if (visible) { 141 | colorField.text = selectedColor 142 | } 143 | } 144 | onAccepted: { 145 | colorField.text = selectedColor 146 | dialogLoader.active = false 147 | } 148 | onRejected: { 149 | // This event is also triggered when the user clicks outside the popup modal. 150 | // TODO Find a way to only trigger when Cancel is clicked. 151 | colorField.text = initColor 152 | dialogLoader.active = false 153 | } 154 | 155 | property color initColor 156 | Component.onCompleted: { 157 | initColor = colorField.valueColor 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorColorGroup.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | TileEditorGroupBox { 5 | id: tileEditorColorField 6 | title: "Label" 7 | implicitWidth: parent.implicitWidth 8 | Layout.fillWidth: true 9 | property alias placeholderText: colorField.placeholderText 10 | property alias enabled: colorField.enabled 11 | property string key: '' 12 | 13 | TileEditorColorField { 14 | id: colorField 15 | showPreviewBg: false 16 | anchors.left: parent.left 17 | anchors.right: parent.right 18 | text: key && appObj.tile && appObj.tile[key] ? appObj.tile[key] : '' 19 | property bool updateOnChange: false 20 | onTextChanged: { 21 | if (key && updateOnChange) { 22 | if (text) { 23 | appObj.tile[key] = text 24 | } else { 25 | delete appObj.tile[key] 26 | } 27 | appObj.tileChanged() 28 | tileGrid.tileModelChanged() 29 | } 30 | } 31 | } 32 | 33 | Connections { 34 | target: appObj 35 | 36 | function onTileChanged() { 37 | if (key && tile) { 38 | colorField.updateOnChange = false 39 | colorField.text = appObj.tile[key] || '' 40 | colorField.updateOnChange = true 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | TileEditorGroupBox { 6 | id: tileEditorField 7 | title: "Label" 8 | Layout.fillWidth: true 9 | property alias text: textField.text 10 | property alias placeholderText: textField.placeholderText 11 | property alias enabled: textField.enabled 12 | property string key: '' 13 | property string checkedKey: '' 14 | checkable: checkedKey 15 | property bool checkedDefault: true 16 | 17 | property bool updateOnChange: false 18 | onCheckedChanged: { 19 | if (checkedKey && tileEditorField.updateOnChange) { 20 | appObj.tile[checkedKey] = checked 21 | appObj.tileChanged() 22 | tileGrid.tileModelChanged() 23 | } 24 | } 25 | 26 | default property alias _contentChildren: content.data 27 | 28 | Connections { 29 | target: appObj 30 | 31 | function onTileChanged() { 32 | if (checkedKey && tile) { 33 | tileEditorField.updateOnChange = false 34 | tileEditorField.checked = typeof appObj.tile[checkedKey] !== "undefined" ? appObj.tile[checkedKey] : checkedDefault 35 | tileEditorField.updateOnChange = true 36 | } 37 | } 38 | } 39 | 40 | RowLayout { 41 | id: content 42 | anchors.left: parent.left 43 | anchors.right: parent.right 44 | 45 | PlasmaComponents3.TextField { 46 | id: textField 47 | Layout.fillWidth: true 48 | text: key && appObj.tile && appObj.tile[key] ? appObj.tile[key] : '' 49 | property bool updateOnChange: false 50 | onTextChanged: { 51 | if (key && textField.updateOnChange) { 52 | appObj.tile[key] = text 53 | appObj.tileChanged() 54 | tileGrid.tileModelChanged() 55 | } 56 | } 57 | 58 | Connections { 59 | target: appObj 60 | 61 | function onTileChanged() { 62 | if (key && tile) { 63 | textField.updateOnChange = false 64 | textField.text = appObj.tile[key] || '' 65 | textField.updateOnChange = true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorFileField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Dialogs as QtDialogs 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | TileEditorField { 6 | id: fileField 7 | property string dialogTitle: "" 8 | signal dialogOpen(var dialog) 9 | 10 | PlasmaComponents3.Button { 11 | icon.name: 'document-open' 12 | onClicked: dialogLoader.active = true 13 | 14 | Loader { 15 | id: dialogLoader 16 | active: false 17 | sourceComponent: QtDialogs.FileDialog { 18 | id: dialog 19 | visible: false 20 | modality: Qt.WindowModal 21 | onAccepted: { 22 | fileField.text = selectedFile 23 | dialogLoader.active = false // visible=false is called before onAccepted 24 | } 25 | onRejected: { 26 | dialogLoader.active = false // visible=false is called before onRejected 27 | } 28 | 29 | // nameFilters must be set before opening the dialog. 30 | // If we create the dialog with visible=true, the nameFilters 31 | // will not be set before it opens. 32 | Component.onCompleted: { 33 | fileField.dialogOpen(dialog) 34 | visible = true 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorGroupBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | // https://invent.kde.org/frameworks/plasma-framework/-/blame/master/src/declarativeimports/plasmacomponents3/GroupBox.qml 6 | PlasmaComponents3.GroupBox { 7 | id: control 8 | property bool checkable: false 9 | property bool checked: false 10 | 11 | label: RowLayout { 12 | x: control.leftPadding 13 | y: control.topInset 14 | width: control.availableWidth 15 | 16 | Loader { 17 | id: checkBoxLoader 18 | active: control.checkable 19 | sourceComponent: PlasmaComponents3.CheckBox { 20 | id: checkBox 21 | Layout.fillWidth: true 22 | enabled: control.enabled 23 | text: control.title 24 | checked: control.checked 25 | onCheckedChanged: control.checked = checked 26 | } 27 | } 28 | PlasmaComponents3.Label { 29 | Layout.fillWidth: true 30 | enabled: control.enabled 31 | visible: !control.checkable 32 | 33 | text: control.title 34 | font: control.font 35 | // color: SystemPaletteSingleton.text(control.enabled) // TODO: Fix Label color upstream 36 | elide: Text.ElideRight 37 | horizontalAlignment: Text.AlignLeft 38 | verticalAlignment: Text.AlignVCenter 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorPresetTileButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | Item { 5 | id: presetTileButton 6 | Layout.fillWidth: parent.width 7 | Layout.preferredHeight: image.paintedHeight 8 | 9 | visible: source 10 | property alias source: image.source 11 | property string filename: 'temp.jpg' 12 | property int w: 0 13 | property int h: 0 14 | 15 | Image { 16 | id: image 17 | anchors.centerIn: parent 18 | width: Math.min(parent.width, sourceSize.width) 19 | 20 | fillMode: Image.PreserveAspectFit 21 | } 22 | 23 | HoverOutlineEffect { 24 | id: hoverOutlineEffect 25 | anchors.fill: image 26 | hoverRadius: Math.min(width, height) 27 | property alias control: mouseArea 28 | } 29 | 30 | MouseArea { 31 | id: mouseArea 32 | anchors.fill: image 33 | hoverEnabled: true 34 | acceptedButtons: Qt.LeftButton 35 | cursorShape: Qt.ArrowCursor 36 | 37 | onClicked: presetTileButton.select() 38 | } 39 | 40 | function getDownloadDir() { 41 | // plasmoid.downloadPath() will create this folder. 42 | // ~/Downloads/Plasma/com.github.zren.tiledmenu/ 43 | // I litters the Downloads folder... which isn't ideal. 44 | return plasmoid.downloadPath() 45 | 46 | // TODO: Download to ~/.local/share since it's hidden. 47 | // Note, this folder does not exist! So we need to create it somehow. 48 | // Maybe we could run `mkdir -p /path/to/folder` using the exec dataengine. 49 | 50 | // Requires: import Qt.labs.platform 1.0 51 | // ~/.local/share/ 52 | // var localDownloadDir = StandardPaths.writableLocation(StandardPaths.GenericDataLocation) 53 | // console.log('localDownloadDir', localDownloadDir) 54 | 55 | // Remove file:// URL scheme 56 | // localFilepath = localDownloadDir.substr('file://'.length) 57 | 58 | // ~/.local/share/plasma_com.github.zren.tiledmenu 59 | // var tiledMenuDir = localDownloadDir + '/' + 'plasma_' + plasmoid.pluginName 60 | // console.log('tiledMenuDir', tiledMenuDir) 61 | // return tiledMenuDir 62 | } 63 | 64 | function resizeTile() { 65 | var sizeChanged = false 66 | if (presetTileButton.w > 0) { 67 | appObj.tile.w = presetTileButton.w 68 | sizeChanged = true 69 | } 70 | if (presetTileButton.h > 0) { 71 | appObj.tile.h = presetTileButton.h 72 | sizeChanged = true 73 | } 74 | if (sizeChanged) { 75 | appObj.tileChanged() 76 | tileGrid.tileModelChanged() 77 | } 78 | } 79 | 80 | function setTileBackgroundImage(filepath) { 81 | backgroundImageField.text = filepath 82 | labelField.checked = false 83 | iconField.checked = false 84 | } 85 | 86 | function select() { 87 | logger.debug('select', source) 88 | 89 | var sourceFilepath = '' + source // cast to string 90 | var isLocalFilepath = sourceFilepath.indexOf('file://') == 0 || sourceFilepath.indexOf('/') == 0 91 | if (isLocalFilepath) { 92 | presetTileButton.setTileBackgroundImage(source) 93 | presetTileButton.resizeTile() 94 | } else { 95 | var tiledMenuDir = getDownloadDir() 96 | var localFilepath = tiledMenuDir + filename 97 | logger.debug('localFilepath', localFilepath) 98 | 99 | // Save tile image to file 100 | logger.debug('grabToImage.start') 101 | image.grabToImage(function(result){ 102 | logger.debug('grabToImage.done', result, result.url) 103 | result.saveToFile(localFilepath) 104 | presetTileButton.setTileBackgroundImage(localFilepath) 105 | presetTileButton.resizeTile() 106 | }, image.sourceSize) 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorPresetTiles.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | import "lib/Requests.js" as Requests 5 | 6 | // Note: This references a global KCoreAddons.KUser { id: kuser } 7 | 8 | TileEditorGroupBox { 9 | id: tileEditorPresetTiles 10 | title: "Label" 11 | Layout.fillWidth: true 12 | 13 | visible: false 14 | function checkForPreset() { 15 | var visiblePresets = 0 16 | for (var i = 0; i < content.children.length; i++) { 17 | var item = content.children[i] 18 | var hasImageUrl = item.source && item.source.toString() 19 | if (hasImageUrl) { 20 | visiblePresets += 1 21 | } 22 | } 23 | visible = visiblePresets > 0 24 | } 25 | Component.onCompleted: { 26 | checkIfRecognizedLauncher() 27 | } 28 | 29 | readonly property bool isDesktopFile: endsWith(appObj.appUrl, '.desktop') 30 | property string steamGameId: '' 31 | readonly property bool isSteamGameLauncher: !!steamGameId 32 | property string lutrisGameSlug: '' 33 | readonly property bool isLutrisGameLauncher: !!lutrisGameSlug 34 | 35 | function endsWith(s, substr) { 36 | return s.indexOf(substr) == s.length - substr.length 37 | } 38 | 39 | function resetRecognizedLaunchers() { 40 | tileEditorPresetTiles.steamGameId = '' 41 | tileEditorPresetTiles.lutrisGameSlug = '' 42 | } 43 | 44 | function checkIfRecognizedLauncher() { 45 | // console.log('checkIfRecognizedLauncher', appObj.appUrl) 46 | 47 | resetRecognizedLaunchers() 48 | checkForPreset() 49 | 50 | if (!appObj.appUrl) { 51 | return 52 | } 53 | 54 | if (!isDesktopFile) { 55 | return 56 | } 57 | 58 | // Qt 5.15+ warns that XHR on local file will be removed. 59 | // Requests.getFile(appObj.appUrl, function(err, data) { 60 | // if (err) { 61 | // console.log('[tiledmenu] checkIfRecognizedLauncher.err', err) 62 | // return 63 | // } 64 | 65 | // var desktopFile = Requests.parseMetadata(data) 66 | // checkIfSteamLauncher(desktopFile) 67 | // checkIfLutrisLauncher(desktopFile) 68 | 69 | // tileEditorPresetTiles.checkForPreset() 70 | // }) 71 | 72 | checkIfSteamIcon(appObj.iconSource) 73 | appObj.iconSourceChanged.connect(function(){ 74 | tileEditorPresetTiles.checkIfSteamIcon(appObj.iconSource) 75 | tileEditorPresetTiles.checkIfLutrisIcon(appObj.iconSource) 76 | tileEditorPresetTiles.checkForPreset() 77 | }) 78 | 79 | // Lutris does not use game id in icon name. Eg: lutris_overwatch instead of lutris_game_1 80 | } 81 | 82 | function checkIfSteamIcon(iconSource) { 83 | var steamIconRegex = /steam_icon_(\d+)/ 84 | var m = steamIconRegex.exec(iconSource) 85 | if (m) { 86 | tileEditorPresetTiles.steamGameId = m[1] 87 | } else { 88 | tileEditorPresetTiles.steamGameId = '' // Reset 89 | } 90 | } 91 | 92 | function checkIfLutrisIcon(iconSource) { 93 | var lutrisIconRegex = /lutris_([\w\-]+)/ 94 | var m = lutrisIconRegex.exec(iconSource) 95 | if (m) { 96 | tileEditorPresetTiles.lutrisGameSlug = m[1] 97 | } else { 98 | tileEditorPresetTiles.lutrisGameSlug = '' // Reset 99 | } 100 | } 101 | 102 | function checkIfSteamLauncher(desktopFile) { 103 | var steamCommandRegex = /steam steam:\/\/rungameid\/(\d+)/ 104 | var m = steamCommandRegex.exec(desktopFile['Exec']) 105 | if (m) { 106 | tileEditorPresetTiles.steamGameId = m[1] 107 | } else { 108 | tileEditorPresetTiles.steamGameId = '' // Reset 109 | } 110 | } 111 | 112 | function checkIfLutrisLauncher(desktopFile) { 113 | var lutrisCommandRegex = /lutris lutris:rungameid\/(\d+)/ 114 | var m1 = lutrisCommandRegex.exec(desktopFile['Exec']) 115 | var lutrisIconRegex = /^lutris_(.+)$/ 116 | var m2 = lutrisIconRegex.exec(desktopFile['Icon']) 117 | if (m1 && m2) { 118 | tileEditorPresetTiles.lutrisGameSlug = m2[1] 119 | } else { 120 | tileEditorPresetTiles.lutrisGameSlug = '' // Reset 121 | } 122 | } 123 | 124 | Connections { 125 | target: appObj 126 | 127 | function onAppUrlChanged() { 128 | logger.debug('onAppUrlChanged', appObj.appUrl) 129 | tileEditorPresetTiles.checkIfRecognizedLauncher() 130 | } 131 | } 132 | 133 | GridLayout { 134 | id: content 135 | anchors.left: parent.left 136 | anchors.right: parent.right 137 | columns: 2 138 | 139 | //--- Steam 140 | // 4x2 141 | TileEditorPresetTileButton { 142 | filename: 'steam_' + steamGameId + '_4x2.jpg' 143 | property string tileImageUrl: 'https://steamcdn-a.akamaihd.net/steam/apps/' + steamGameId + '/header.jpg' 144 | source: isSteamGameLauncher ? tileImageUrl : '' 145 | w: 4 146 | h: 2 147 | } 148 | 149 | // 3x1 150 | TileEditorPresetTileButton { 151 | filename: 'steam_' + steamGameId + '_3x1.jpg' 152 | property string tileImageUrl: 'https://steamcdn-a.akamaihd.net/steam/apps/' + steamGameId + '/capsule_184x69.jpg' 153 | source: isSteamGameLauncher ? tileImageUrl : '' 154 | w: 3 155 | h: 1 156 | } 157 | 158 | // 5x3 or 3x2 159 | TileEditorPresetTileButton { 160 | filename: 'steam_' + steamGameId + '_5x3.jpg' 161 | property string tileImageUrl: 'https://steamcdn-a.akamaihd.net/steam/apps/' + steamGameId + '/capsule_616x353.jpg' 162 | source: isSteamGameLauncher ? tileImageUrl : '' 163 | w: 3 164 | h: 2 165 | } 166 | 167 | // 5x2 or 2x1 168 | TileEditorPresetTileButton { 169 | filename: 'lutris_' + lutrisGameSlug + '_2x1.jpg' 170 | // property string tileImageUrl: '/home/' + kuser.loginName + '/.local/share/lutris/banners/' + lutrisGameSlug + '.jpg' 171 | // source: (isLutrisGameLauncher && kuser.loginName) ? tileImageUrl : '' 172 | property string tileImageUrl: 'https://lutris.net/games/banner/' + lutrisGameSlug + '.jpg' 173 | source: (isLutrisGameLauncher) ? tileImageUrl : '' 174 | w: 2 175 | h: 1 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorRectField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | TileEditorGroupBox { 6 | id: tileEditorRectField 7 | title: "Label" 8 | implicitWidth: parent.implicitWidth 9 | Layout.fillWidth: true 10 | 11 | // readonly property int xLeft: tileGrid.columns - (appObj.tileX + appObj.tileW) 12 | 13 | RowLayout { 14 | anchors.fill: parent 15 | 16 | GridLayout { 17 | columns: 2 18 | Layout.fillWidth: true 19 | 20 | PlasmaComponents3.Label { text: "x:" } 21 | TileEditorSpinBox { 22 | key: 'x' 23 | from: 0 24 | // to: tileGrid.columns - (appObj.tile && appObj.tile.w-1 || 0) 25 | // to: appObj.tileX + tileEditorRectField.xLeft 26 | } 27 | PlasmaComponents3.Label { text: "y:" } 28 | TileEditorSpinBox { 29 | key: 'y' 30 | from: 0 31 | } 32 | PlasmaComponents3.Label { text: "w:" } 33 | TileEditorSpinBox { 34 | key: 'w' 35 | from: 1 36 | // to: tileGrid.columns - (appObj.tile && appObj.tile.x || 0) 37 | // to: appObj.tileW + tileEditorRectField.xLeft 38 | } 39 | PlasmaComponents3.Label { text: "h:" } 40 | TileEditorSpinBox { 41 | key: 'h' 42 | from: 1 43 | } 44 | } 45 | 46 | GridLayout { 47 | id: resizeGrid 48 | Layout.fillWidth: true 49 | rows: 4 50 | columns: 4 51 | 52 | Repeater { 53 | model: resizeGrid.rows * resizeGrid.columns 54 | 55 | PlasmaComponents3.Button { 56 | Layout.fillWidth: true 57 | implicitWidth: 20 58 | property int w: (modelData % resizeGrid.columns) + 1 59 | property int h: Math.floor(modelData / resizeGrid.columns) + 1 60 | text: '' + w + 'x' + h 61 | checked: w <= appObj.tileW && h <= appObj.tileH 62 | // enabled: w - appObj.tileW <= tileEditorRectField.xLeft 63 | onClicked: { 64 | appObj.tile.w = w 65 | appObj.tile.h = h 66 | appObj.tileChanged() 67 | tileGrid.tileModelChanged() 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorSpinBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.plasma.components as PlasmaComponents3 4 | 5 | PlasmaComponents3.SpinBox { 6 | id: spinBox 7 | property string key: '' 8 | Layout.fillWidth: true 9 | implicitWidth: 20 10 | value: appObj.tile && appObj.tile[key] || 0 11 | property bool updateOnChange: false 12 | onValueChanged: { 13 | if (key && updateOnChange) { 14 | appObj.tile[key] = value 15 | appObj.tileChanged() 16 | tileGrid.tileModelChanged() 17 | } 18 | } 19 | 20 | Connections { 21 | target: appObj 22 | 23 | function onTileChanged() { 24 | if (key && tile) { 25 | spinBox.updateOnChange = false 26 | spinBox.value = appObj.tile[key] || 0 27 | spinBox.updateOnChange = true 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package/contents/ui/TileEditorView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Dialogs as QtDialogs 4 | import org.kde.plasma.components as PlasmaComponents3 5 | import org.kde.plasma.extras as PlasmaExtras 6 | import org.kde.iconthemes as KIconThemes // IconDialog 7 | 8 | ColumnLayout { 9 | id: tileEditorView 10 | Layout.alignment: Qt.AlignTop 11 | 12 | AppObject { 13 | id: appObj 14 | } 15 | property alias tile: appObj.tile 16 | 17 | function resetView() { 18 | tile = null 19 | } 20 | 21 | function resetTile() { 22 | delete appObj.tile.showIcon 23 | delete appObj.tile.showLabel 24 | delete appObj.tile.label 25 | delete appObj.tile.icon 26 | delete appObj.tile.backgroundColor 27 | delete appObj.tile.backgroundImage 28 | appObj.tileChanged() 29 | tileGrid.tileModelChanged() 30 | } 31 | 32 | 33 | RowLayout { 34 | PlasmaExtras.Heading { 35 | Layout.fillWidth: true 36 | level: 2 37 | text: i18n("Edit Tile") 38 | } 39 | 40 | PlasmaComponents3.Button { 41 | text: i18n("Reset Tile") 42 | onClicked: resetTile() 43 | } 44 | 45 | PlasmaComponents3.Button { 46 | text: i18n("Close") 47 | onClicked: { 48 | tileEditorView.close() 49 | } 50 | } 51 | } 52 | 53 | 54 | PlasmaComponents3.ScrollView { 55 | id: scrollView 56 | Layout.fillHeight: true 57 | Layout.fillWidth: true 58 | 59 | ColumnLayout { 60 | id: scrollContent 61 | Layout.fillWidth: true 62 | width: scrollView.availableWidth 63 | 64 | TileEditorField { 65 | // visible: appObj.isLauncher 66 | title: i18n("Url") 67 | key: 'url' 68 | } 69 | 70 | TileEditorField { 71 | id: labelField 72 | title: i18n("Label") 73 | placeholderText: appObj.appLabel 74 | key: 'label' 75 | checkedKey: 'showLabel' 76 | } 77 | 78 | TileEditorField { 79 | id: iconField 80 | title: i18n("Icon") 81 | // placeholderText: appObj.appIcon ? appObj.appIcon.toString() : '' 82 | key: 'icon' 83 | checkedKey: 'showIcon' 84 | checkedDefault: appObj.defaultShowIcon 85 | 86 | PlasmaComponents3.Button { 87 | icon.name: "document-open" 88 | onClicked: iconDialog.open() 89 | 90 | KIconThemes.IconDialog { 91 | id: iconDialog 92 | onIconNameChanged: iconField.text = iconName 93 | } 94 | } 95 | } 96 | 97 | TileEditorFileField { 98 | id: backgroundImageField 99 | title: i18n("Background Image") 100 | key: 'backgroundImage' 101 | onTextChanged: { 102 | if (text) { 103 | labelField.checked = false 104 | iconField.checked = false 105 | } 106 | } 107 | onDialogOpen: function(dialog) { 108 | dialog.title = i18n("Choose an image") 109 | dialog.nameFilters.unshift(i18n("Image Files (*.png *.jpg *.jpeg *.bmp *.svg *.svgz)")) 110 | } 111 | } 112 | 113 | TileEditorPresetTiles { 114 | title: i18n("Preset Tiles") 115 | } 116 | 117 | TileEditorColorGroup { 118 | title: i18n("Background Color") 119 | placeholderText: config.defaultTileColor 120 | key: 'backgroundColor' 121 | } 122 | 123 | TileEditorRectField { 124 | title: i18n("Position / Size") 125 | } 126 | 127 | Item { // Consume the extra space below 128 | Layout.fillHeight: true 129 | } 130 | } 131 | } 132 | 133 | function show() { 134 | if (stackView.currentItem != tileEditorView) { 135 | stackView.replace(tileEditorView) 136 | } 137 | } 138 | 139 | function open(tile) { 140 | resetView() 141 | tileEditorView.tile = tile 142 | show() 143 | } 144 | 145 | function close() { 146 | searchView.showDefaultView() 147 | } 148 | 149 | 150 | Connections { 151 | target: stackView 152 | 153 | function onCurrentItemChanged() { 154 | if (stackView.currentItem != tileEditorView) { 155 | tileEditorView.resetView() 156 | } 157 | } 158 | } 159 | 160 | 161 | Connections { 162 | target: config.tileModel 163 | 164 | function onLoaded() { 165 | // Base64JsonString.save() will create a new JavaScript array [], 166 | // and our current tile {} reference will be incorrect, which breaks the tile editor. 167 | // We could keep a reference to the tile's index in the array, and make sure 168 | // the tile's url did not change, but there's no guarantee we won't overwrite data 169 | // during an Import, so just close the view. 170 | tileEditorView.close() 171 | } 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /package/contents/ui/TileGridPresets.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.extras as PlasmaExtras 3 | 4 | PlasmaExtras.MenuItem { 5 | id: presetMenuItem 6 | icon: "list-add-symbolic" 7 | text: i18n("Add Preset") 8 | 9 | //--- 10 | function addDefault() { 11 | var pos = tileGrid.findOpenPos(6, 6) 12 | addProductivity(pos.x, pos.y) 13 | addExplore(pos.x, pos.y + 3) 14 | } 15 | 16 | function isAppInstalled(appId) { 17 | return appsModel.allAppsModel.hasApp(appId) 18 | } 19 | 20 | function addTilePreset(x, y, tileData) { 21 | var appId = tileData.url 22 | if (isAppInstalled(appId)) { 23 | return tileGrid.addTile(x, y, tileData) 24 | } else { 25 | return null 26 | } 27 | } 28 | 29 | function addGroupPreset(x, y, groupData, tileFnList) { 30 | var group = tileGrid.addGroup(x, y, groupData) 31 | var tileX = group.x 32 | var tileY = y + group.h 33 | for (var i = 0; i < tileFnList.length; i++) { 34 | var tileFn = tileFnList[i] 35 | var tile = tileFn(tileX, tileY) 36 | if (tile) { 37 | // TODO: Support Wrap 38 | tileX += tile.w 39 | } 40 | } 41 | } 42 | 43 | function addProductivity(x, y) { 44 | addGroupPreset(x, y, { 45 | label: i18n("Productivity"), 46 | }, [ 47 | addWriter, 48 | addCalc, 49 | addMail, 50 | ]) 51 | } 52 | 53 | function addExplore(x, y) { 54 | addGroupPreset(x, y, { 55 | label: i18n("Explore"), 56 | }, [ 57 | addAppCenter, 58 | addWebBrowser, 59 | addSteam, 60 | ]) 61 | } 62 | 63 | 64 | //--- 65 | function addWriter(x, y) { 66 | return addTilePreset(x, y, { 67 | url: 'libreoffice-writer.desktop', 68 | backgroundColor: '#802584b7', 69 | }) 70 | } 71 | function addCalc(x, y) { 72 | return addTilePreset(x, y, { 73 | url: 'libreoffice-calc.desktop', 74 | backgroundColor: '#80289769', 75 | }) 76 | } 77 | function addMail(x, y) { 78 | var tile = addKMail(x, y) 79 | if (!tile) { 80 | tile = addGmail(x, y) 81 | } 82 | return tile 83 | } 84 | function addKMail(x, y) { 85 | return addTilePreset(x, y, { 86 | url: 'org.kde.kmail2.desktop', 87 | }) 88 | } 89 | function addGmail(x, y) { 90 | return tileGrid.addTile(x, y, { 91 | url: 'https://mail.google.com/mail/u/0/#inbox', 92 | label: i18n("Gmail"), 93 | icon: 'mail-message', 94 | backgroundColor: '#80a73325', 95 | }) 96 | } 97 | 98 | function addAppCenter(x, y) { 99 | if (isAppInstalled('octopi.desktop')) { 100 | return tileGrid.addTile(x, y, { 101 | url: 'octopi.desktop', 102 | label: i18n("Software Center"), 103 | }) 104 | } else if (isAppInstalled('org.manjaro.pamac.manager.desktop')) { 105 | return tileGrid.addTile(x, y, { 106 | url: 'org.manjaro.pamac.manager.desktop', 107 | // default label is 'Add/Remove Software' 108 | }) 109 | } else if (isAppInstalled('org.opensuse.yast.Packager.desktop')) { 110 | return tileGrid.addTile(x, y, { 111 | url: 'org.opensuse.yast.Packager.desktop', 112 | label: i18n("Software Center"), 113 | }) 114 | } else if (isAppInstalled('org.kde.discover')) { 115 | return tileGrid.addTile(x, y, { 116 | url: 'org.kde.discover', 117 | label: i18n("Software Center"), 118 | }) 119 | } else { 120 | return null 121 | } 122 | } 123 | 124 | function addWebBrowser(x, y) { 125 | return tileGrid.addTile(x, y, { 126 | url: 'preferred://browser', 127 | }) 128 | } 129 | 130 | function addSteam(x, y) { 131 | return addTilePreset(x, y, { 132 | url: 'steam.desktop', 133 | }) 134 | } 135 | 136 | 137 | //--- 138 | readonly property var presetSubMenu: PlasmaExtras.Menu { 139 | visualParent: presetMenuItem.action 140 | 141 | PlasmaExtras.MenuItem { 142 | icon: "libreoffice-startcenter" 143 | text: i18n("Productivity") 144 | onClicked: { 145 | var pos = tileGrid.findOpenPos(6, 3) 146 | presetMenuItem.addProductivity(pos.x, pos.y) 147 | } 148 | } 149 | 150 | PlasmaExtras.MenuItem { 151 | icon: "internet-web-browser" 152 | text: i18n("Explore") 153 | onClicked: { 154 | var pos = tileGrid.findOpenPos(6, 3) 155 | presetMenuItem.addExplore(pos.x, pos.y) 156 | } 157 | } 158 | 159 | PlasmaExtras.MenuItem { 160 | icon: "mail-message" 161 | text: i18n("Gmail") 162 | onClicked: { 163 | var tile = presetMenuItem.addGmail(cellContextMenu.cellX, cellContextMenu.cellY) 164 | tileGrid.editTile(tile) 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /package/contents/ui/TileGridSplash.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import org.kde.kirigami as Kirigami 4 | import org.kde.plasma.components as PlasmaComponents3 5 | import org.kde.plasma.extras as PlasmaExtras 6 | 7 | ColumnLayout { 8 | // inherits tileGridPresets from Loader 9 | // inherits maxWidth from Loader 10 | 11 | spacing: Kirigami.Units.largeSpacing 12 | 13 | PlasmaExtras.Heading { 14 | Layout.alignment: Qt.AlignHCenter 15 | Layout.maximumWidth: maxWidth 16 | wrapMode: Text.Wrap 17 | horizontalAlignment: Text.AlignHCenter 18 | text: i18n("Getting Started") 19 | } 20 | PlasmaComponents3.Label { 21 | Layout.alignment: Qt.AlignHCenter 22 | Layout.maximumWidth: maxWidth 23 | wrapMode: Text.Wrap 24 | 25 | text: { 26 | var tips = [ 27 | i18n("Drag apps onto the grid."), 28 | i18n("Drag folders from the file manager here."), 29 | i18n("Meta + Right Click to resize the menu."), 30 | ] 31 | var str = '
    ' 32 | for (var i = 0; i < tips.length; i++) { 33 | var tip = tips[i] 34 | str += '
  • ' + tip + '
  • ' 35 | } 36 | str += '
' 37 | return str 38 | } 39 | } 40 | 41 | PlasmaComponents3.Button { 42 | Layout.alignment: Qt.AlignHCenter 43 | text: i18n("Use Default Tile Layout") 44 | onClicked: tileGridPresets.addDefault() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package/contents/ui/TileItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | import org.kde.plasma.core as PlasmaCore 4 | 5 | Item { 6 | id: tileItem 7 | x: modelData.x * cellBoxSize 8 | y: modelData.y * cellBoxSize 9 | width: modelData.w * cellBoxSize 10 | height: modelData.h * cellBoxSize 11 | 12 | function fixCoordinateBindings() { 13 | x = Qt.binding(function(){ return modelData.x * cellBoxSize }) 14 | y = Qt.binding(function(){ return modelData.y * cellBoxSize }) 15 | z = 0 16 | } 17 | 18 | AppObject { 19 | id: appObj 20 | tile: modelData 21 | } 22 | readonly property alias app: appObj.app 23 | 24 | readonly property bool faded: tileGrid.editing || tileMouseArea.isLeftPressed 25 | readonly property int fadedWidth: width - cellPushedMargin 26 | opacity: faded ? 0.75 : 1 27 | scale: faded ? fadedWidth / width : 1 28 | Behavior on opacity { NumberAnimation { duration: 200 } } 29 | Behavior on scale { NumberAnimation { duration: 200 } } 30 | 31 | //--- View Start 32 | TileItemView { 33 | id: tileItemView 34 | anchors.fill: parent 35 | anchors.margins: cellMargin 36 | width: modelData.w * cellBoxSize 37 | height: modelData.h * cellBoxSize 38 | readonly property int minSize: Math.min(width, height) 39 | readonly property int maxSize: Math.max(width, height) 40 | hovered: tileMouseArea.containsMouse 41 | } 42 | 43 | HoverOutlineEffect { 44 | id: hoverOutlineEffect 45 | anchors.fill: parent 46 | anchors.margins: cellMargin 47 | hoverRadius: { 48 | if (appObj.isGroup) { 49 | return tileItemView.maxSize 50 | } else { 51 | return tileItemView.minSize 52 | } 53 | } 54 | hoverOutlineSize: tileGrid.hoverOutlineSize 55 | mouseArea: tileMouseArea 56 | } 57 | //--- View End 58 | 59 | MouseArea { 60 | id: tileMouseArea 61 | anchors.fill: parent 62 | hoverEnabled: true 63 | acceptedButtons: Qt.LeftButton | Qt.RightButton 64 | cursorShape: editing ? Qt.ClosedHandCursor : Qt.ArrowCursor 65 | readonly property bool isLeftPressed: pressedButtons & Qt.LeftButton 66 | 67 | property int pressX: -1 68 | property int pressY: -1 69 | onPressed: function(mouse) { 70 | pressX = mouse.x 71 | pressY = mouse.y 72 | } 73 | 74 | drag.target: plasmoid.configuration.tilesLocked ? undefined : tileItem 75 | // drag.onActiveChanged: console.log('drag.active', drag.active) 76 | 77 | // This MouseArea will spam "QQuickItem::ungrabMouse(): Item is not the mouse grabber." 78 | // but there's no other way of having a clickable drag area. 79 | onClicked: function(mouse) { 80 | mouse.accepted = true 81 | tileGrid.resetDrag() 82 | if (mouse.button == Qt.LeftButton) { 83 | if (tileEditorView && tileEditorView.tile) { 84 | openTileEditor() 85 | } else if (modelData.url) { 86 | appsModel.tileGridModel.runApp(modelData.url) 87 | } 88 | } else if (mouse.button == Qt.RightButton) { 89 | contextMenu.open(mouse.x, mouse.y) 90 | } 91 | } 92 | } 93 | 94 | Drag.dragType: Drag.Automatic 95 | Drag.proposedAction: Qt.MoveAction 96 | 97 | // We use this drag pattern to use the internal drag with events. 98 | // https://stackoverflow.com/a/24729837/947742 99 | readonly property bool dragActive: tileMouseArea.drag.active 100 | onDragActiveChanged: function(dragActive) { 101 | if (dragActive) { 102 | // console.log("drag started") 103 | // console.log('onDragStarted', JSON.stringify(modelData), index, tileModel.length) 104 | tileGrid.startDrag(index) 105 | // tileGrid.dropOffsetX = 0 106 | // tileGrid.dropOffsetY = 0 107 | tileItem.z = 1 108 | Drag.start() 109 | } else { 110 | // console.log("drag finished") 111 | // console.log('DragArea.onDrop', draggedItem) 112 | Qt.callLater(tileGrid.resetDrag) 113 | Qt.callLater(tileItem.fixCoordinateBindings) 114 | Drag.drop() // Breaks QML context. 115 | // We need to use callLater to call functions after Drag.drop(). 116 | } 117 | } 118 | 119 | QQC2.ToolTip { 120 | id: control 121 | visible: tileItemView.hovered && !(dragActive || contextMenu.opened) && appObj.tile.w == 1 && appObj.tile.h == 1 122 | text: appObj.labelText 123 | delay: 0 124 | x: parent.width + rightPadding 125 | y: (parent.height - height) / 2 126 | } 127 | 128 | Loader { 129 | id: groupEffectLoader 130 | visible: tileMouseArea.containsMouse 131 | active: appObj.isGroup && visible 132 | sourceComponent: Rectangle { 133 | id: groupOutline 134 | color: "transparent" 135 | border.width: Math.max(1, Math.round(1 * Screen.devicePixelRatio)) 136 | border.color: "#80ffffff" 137 | y: modelData.h * cellBoxSize 138 | z: 100 139 | width: appObj.groupRect.w * cellBoxSize 140 | height: appObj.groupRect.h * cellBoxSize 141 | } 142 | } 143 | 144 | AppContextMenu { 145 | id: contextMenu 146 | tileIndex: index 147 | onPopulateMenu: function(menu) { 148 | if (!plasmoid.configuration.tilesLocked) { 149 | menu.addPinToMenuAction(modelData.url) 150 | } 151 | 152 | appObj.addActionList(menu) 153 | 154 | if (!plasmoid.configuration.tilesLocked) { 155 | if (modelData.tileType == "group") { 156 | var menuItem = menu.newMenuItem() 157 | menuItem.text = i18n("Sort Tiles") 158 | menuItem.icon = 'sort-name' 159 | menuItem.onClicked.connect(function(){ 160 | tileGrid.sortGroupTiles(modelData) 161 | }) 162 | } 163 | var menuItem = menu.newMenuItem() 164 | menuItem.text = i18n("Edit Tile") 165 | menuItem.icon = 'rectangle-shape' 166 | menuItem.onClicked.connect(function(){ 167 | tileItem.openTileEditor() 168 | }) 169 | } 170 | } 171 | } 172 | 173 | function openTileEditor() { 174 | tileGrid.editTile(tileGrid.tileModel[index]) 175 | } 176 | function closeTileEditor() { 177 | 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /package/contents/ui/TileItemView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import org.kde.plasma.components as PlasmaComponents3 3 | import org.kde.kirigami as Kirigami 4 | 5 | Rectangle { 6 | id: tileItemView 7 | color: appObj.backgroundColor 8 | property color gradientBottomColor: Qt.darker(appObj.backgroundColor, 2.0) 9 | 10 | Component { 11 | id: tileGradient 12 | Gradient { 13 | GradientStop { position: 0.0; color: appObj.backgroundColor } 14 | GradientStop { position: 1.0; color: tileItemView.gradientBottomColor } 15 | } 16 | } 17 | gradient: appObj.backgroundGradient ? tileGradient.createObject(tileItemView) : null 18 | 19 | readonly property int tilePadding: 4 * Screen.devicePixelRatio 20 | readonly property int smallIconSize: 32 * Screen.devicePixelRatio 21 | readonly property int mediumIconSize: 72 * Screen.devicePixelRatio 22 | readonly property int largeIconSize: 96 * Screen.devicePixelRatio 23 | 24 | readonly property int labelAlignment: appObj.isGroup ? config.groupLabelAlignment : config.tileLabelAlignment 25 | 26 | property bool hovered: false 27 | 28 | states: [ 29 | State { 30 | when: modelData.w == 1 && modelData.h >= 1 31 | PropertyChanges { target: icon; size: smallIconSize } 32 | PropertyChanges { target: label; visible: false } 33 | }, 34 | State { 35 | when: modelData.w >= 2 && modelData.h == 1 36 | AnchorChanges { target: icon 37 | anchors.horizontalCenter: undefined 38 | anchors.left: tileItemView.left 39 | } 40 | PropertyChanges { target: icon; anchors.leftMargin: tilePadding } 41 | PropertyChanges { target: label 42 | verticalAlignment: Text.AlignVCenter 43 | } 44 | AnchorChanges { target: label 45 | anchors.left: icon.right 46 | } 47 | }, 48 | State { 49 | when: (modelData.w >= 2 && modelData.h == 2) || (modelData.w == 2 && modelData.h >= 2) 50 | PropertyChanges { target: icon; size: mediumIconSize } 51 | }, 52 | State { 53 | when: modelData.w >= 3 && modelData.h >= 3 54 | PropertyChanges { target: icon; size: largeIconSize } 55 | } 56 | ] 57 | 58 | Image { 59 | id: backgroundImage 60 | anchors.fill: parent 61 | visible: appObj.backgroundImage 62 | source: appObj.backgroundImage 63 | fillMode: Image.PreserveAspectCrop 64 | asynchronous: true 65 | } 66 | 67 | Kirigami.Icon { 68 | id: icon 69 | visible: appObj.showIcon 70 | source: appObj.iconSource 71 | anchors.verticalCenter: parent.verticalCenter 72 | anchors.horizontalCenter: parent.horizontalCenter 73 | // property int size: 72 // Just a default, overriden in State change 74 | property int size: Math.min(parent.width, parent.height) / 2 75 | width: appObj.showIcon ? size : 0 76 | height: appObj.showIcon ? size : 0 77 | anchors.fill: appObj.iconFill ? parent : null 78 | smooth: appObj.iconFill 79 | } 80 | 81 | PlasmaComponents3.Label { 82 | id: label 83 | visible: appObj.showLabel 84 | text: appObj.labelText 85 | anchors.top: parent.top 86 | anchors.bottom: parent.bottom 87 | anchors.leftMargin: tilePadding 88 | anchors.rightMargin: tilePadding 89 | anchors.left: parent.left 90 | anchors.right: parent.right 91 | wrapMode: Text.Wrap 92 | horizontalAlignment: labelAlignment 93 | verticalAlignment: Text.AlignBottom 94 | width: parent.width 95 | renderType: Text.QtRendering // Fix pixelation when scaling. Plasma.Label uses NativeRendering. 96 | style: Text.Outline 97 | styleColor: appObj.backgroundGradient ? tileItemView.gradientBottomColor : appObj.backgroundColor 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package/contents/ui/Utils.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | function parseDropUrl(url) { 4 | var startsWithAppsScheme = url.indexOf('applications:') === 0 // Search Results add this prefix 5 | if (startsWithAppsScheme) { 6 | // console.log('parseDropUrl', 'startsWithAppsScheme', url) 7 | url = url.substr('applications:'.length) 8 | } 9 | 10 | var workingDir = Qt.resolvedUrl('.') 11 | var endsWithDesktop = url.indexOf('.desktop') === url.length - '.desktop'.length 12 | var isRelativeDesktopUrl = endsWithDesktop && ( 13 | url.indexOf(workingDir) === 0 14 | // || url.indexOf('file:///usr/share/applications/') === 0 15 | // || url.indexOf('/.local/share/applications/') >= 0 16 | || url.indexOf('/share/applications/') >= 0 // 99% certain this desktop file should be accessed relatively. 17 | ) 18 | // console.log('parseDropUrl', workingDir, endsWithDesktop, isRelativeDesktopUrl) 19 | // console.log('onUrlDropped', 'url', url) 20 | if (isRelativeDesktopUrl) { 21 | // Remove the path because .favoriteId is just the file name. 22 | // However passing the favoriteId in mimeData.url will prefix the current QML path because it's a QUrl. 23 | var tokens = url.toString().split('/') 24 | var favoriteId = tokens[tokens.length-1] 25 | // console.log('isRelativeDesktopUrl', tokens, favoriteId) 26 | return favoriteId 27 | } else { 28 | return url 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigExportLayout.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls 3 | import QtQuick.Layouts 4 | import org.kde.plasma.core as PlasmaCore 5 | 6 | import ".." as TiledMenu 7 | 8 | ColumnLayout { 9 | id: page 10 | 11 | TextAreaBase64JsonString { 12 | id: exportData 13 | Layout.fillHeight: true 14 | 15 | TiledMenu.Base64JsonString { 16 | id: configTileModel 17 | configKey: 'tileModel' 18 | writing: exportData.base64JsonString.writing 19 | defaultValue: [] 20 | } 21 | 22 | property var ignoredKeys: [ 23 | 'tileScale', 24 | 'searchResultsReversed', 25 | 'searchResultsCustomSort', 26 | ] 27 | 28 | defaultValue: { 29 | var data = {} 30 | var configKeyList = plasmoid.configuration.keys() 31 | for (var i = 0; i < configKeyList.length; i++) { 32 | var configKey = configKeyList[i] 33 | var configValue = plasmoid.configuration[configKey] 34 | if (typeof configValue === "undefined") { 35 | continue 36 | } 37 | if (ignoredKeys.indexOf(configKey) >= 0) { 38 | continue 39 | } 40 | // Filter KF5 5.78 default keys https://invent.kde.org/frameworks/kdeclarative/-/merge_requests/38 41 | if (configKey.endsWith('Default')) { 42 | var key2 = configKey.substr(0, configKey.length - 'Default'.length) 43 | if (typeof plasmoid.configuration[key2] !== 'undefined') { 44 | continue 45 | } 46 | } 47 | if (configKey == 'tileModel') { 48 | data.tileModel = configTileModel.value 49 | } else { 50 | data[configKey] = configValue 51 | } 52 | } 53 | return data 54 | } 55 | 56 | function serialize() { 57 | var newValue = parseText(textArea.text) 58 | var configKeyList = plasmoid.configuration.keys() 59 | for (var i = 0; i < configKeyList.length; i++) { 60 | var configKey = configKeyList[i] 61 | var propValue = newValue[configKey] 62 | if (typeof propValue === "undefined") { 63 | continue 64 | } 65 | if (ignoredKeys.indexOf(configKey) >= 0) { 66 | continue 67 | } 68 | if (configKey == 'tileModel') { 69 | configTileModel.set(propValue) 70 | } else { 71 | if (plasmoid.configuration[configKey] != propValue) { 72 | plasmoid.configuration[configKey] = propValue 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigurationShortcuts.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Controls as QQC2 3 | import QtQuick.Layouts 4 | import org.kde.kquickcontrols as KQuickControls 5 | import org.kde.kirigami as Kirigami 6 | import org.kde.plasma.plasmoid 7 | import org.kde.kcmutils as KCM 8 | 9 | // Based on: 10 | // https://invent.kde.org/plasma/plasma-desktop/blob/master/desktoppackage/contents/configuration/ConfigurationShortcuts.qml 11 | KCM.SimpleKCM { 12 | id: page 13 | 14 | title: i18nd("plasma_shell_org.kde.plasma.desktop", "Shortcuts") 15 | 16 | signal configurationChanged() 17 | function saveConfig() { 18 | Plasmoid.globalShortcut = keySequenceItem.keySequence 19 | } 20 | 21 | ColumnLayout { 22 | QQC2.Label { 23 | Layout.fillWidth: true 24 | text: i18nd("plasma_shell_org.kde.plasma.desktop", "This shortcut will activate the applet as though it had been clicked.") 25 | wrapMode: Text.WordWrap 26 | } 27 | 28 | // https://github.com/KDE/kdeclarative/blob/master/src/qmlcontrols/kquickcontrols/KeySequenceItem.qml 29 | // https://github.com/KDE/kdeclarative/blob/master/src/qmlcontrols/kquickcontrols/private/keysequencehelper.h 30 | KQuickControls.KeySequenceItem { 31 | id: keySequenceItem 32 | keySequence: Plasmoid.globalShortcut 33 | modifierOnlyAllowed: true 34 | onCaptureFinished: { 35 | if (keySequence !== Plasmoid.globalShortcut) { 36 | page.configurationChanged() 37 | } 38 | } 39 | 40 | // Unfortunately, keySequence does not exposed the isEmpty function to QML. 41 | // There's no way to detect if the shortcut is not set. 42 | // readonly property bool isEmpty: keySequence.isEmpty() 43 | 44 | // Luckily, the PlasmaQuick::ConfigView exposes the global shortcut as a String. 45 | // Unfortunately, appletGlobalShortcut only updates when the config tab is loaded. 46 | // It does not even change when we hit apply. It's limited use makes it useless 47 | // for notifying the user when the Meta shortcut is active. 48 | // https://github.com/KDE/plasma-framework/blob/master/src/plasmaquick/configview.cpp#L174 49 | // https://github.com/KDE/plasma-framework/blob/master/src/plasmaquick/configview.h 50 | // readonly property bool isEmpty: configDialog.appletGlobalShortcut == "" 51 | } 52 | 53 | Item { 54 | implicitHeight: Kirigami.Units.largeSpacing 55 | } 56 | 57 | QQC2.Label { 58 | Layout.fillWidth: true 59 | text: i18n("When this widget has a global shortcut set, like 'Alt+F1', Plasma will open this menu with just the ⊞ Windows / Meta key.") 60 | wrapMode: Text.WordWrap 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package/contents/ui/config/TextAreaBase64JsonString.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | import ".." as TiledMenu 5 | import "../libconfig" as LibConfig 6 | 7 | LibConfig.TextArea { 8 | id: textArea 9 | Layout.fillWidth: true 10 | 11 | property var base64JsonString: TiledMenu.Base64JsonString { 12 | id: base64JsonString 13 | } 14 | 15 | property alias jsonKey: base64JsonString.configKey 16 | property alias defaultValue: base64JsonString.defaultValue 17 | 18 | property alias enabled: textArea.enabled 19 | 20 | readonly property var configValue: configKey ? plasmoid.configuration[configKey] : "" 21 | onConfigValueChanged: deserialize() 22 | readonly property var value: base64JsonString.value 23 | 24 | property alias textArea: textArea 25 | property alias textAreaText: textArea.text 26 | 27 | property string indent: ' ' 28 | 29 | function parseValue(value) { 30 | return JSON.stringify(value, null, indent) 31 | } 32 | function parseText(text) { 33 | return JSON.parse(text) 34 | } 35 | 36 | function setValue(val) { 37 | var newText = parseValue(val) 38 | if (textArea.text != newText) { 39 | textArea.text = newText 40 | } 41 | } 42 | 43 | function deserialize() { 44 | if (!textArea.focus) { 45 | setValue(value) 46 | } 47 | } 48 | function serialize() { 49 | var newValue = parseText(textArea.text) 50 | base64JsonString.set(newValue) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Logger.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Item { 4 | id: logger 5 | property string name: 'logger' 6 | property bool showDebug: false 7 | 8 | function prettifyArguments(rawArgs) { 9 | var args = Array.apply(null, rawArgs) 10 | for (var i = 0; i < args.length; i++) { 11 | if (typeof args[i] === "object" || args[i] instanceof Array) { 12 | args[i] = JSON.stringify(args[i], null, '\t') 13 | } 14 | } 15 | return args 16 | } 17 | 18 | function debug() { 19 | if (showDebug) { 20 | var args = Array.apply(null, arguments) 21 | args.unshift('[' + name + ':debug]') 22 | console.log.apply(console, args) 23 | } 24 | } 25 | 26 | function debugJSON() { 27 | if (showDebug) { 28 | var args = prettifyArguments(arguments) 29 | args.unshift('[' + name + ':debug]') 30 | console.log.apply(console, args) 31 | } 32 | } 33 | 34 | function log() { 35 | var args = Array.apply(null, arguments) 36 | args.unshift('[' + name + ']') 37 | console.log.apply(console, args) 38 | } 39 | 40 | function logJSON() { 41 | if (showDebug) { 42 | var args = prettifyArguments(arguments) 43 | args.unshift('[' + name + ']') 44 | console.log.apply(console, args) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Requests.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | // Version 6 3 | 4 | function request(opt, callback) { 5 | if (typeof opt === 'string') { 6 | opt = { url: opt } 7 | } 8 | var req = new XMLHttpRequest() 9 | req.onerror = function(e) { 10 | console.log('XMLHttpRequest.onerror', e) 11 | if (e) { 12 | console.log('\t', e.status, e.statusText, e.message) 13 | callback(e.message) 14 | } else { 15 | callback('XMLHttpRequest.onerror(undefined)') 16 | } 17 | } 18 | req.onreadystatechange = function() { 19 | if (req.readyState === XMLHttpRequest.DONE) { // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-done 20 | if (200 <= req.status && req.status < 400) { 21 | callback(null, req.responseText, req) 22 | } else { 23 | if (req.status === 0) { 24 | console.log('HTTP 0 Headers: \n' + req.getAllResponseHeaders()) 25 | } 26 | var msg = "HTTP Error " + req.status + ": " + req.statusText 27 | callback(msg, req.responseText, req) 28 | } 29 | } 30 | } 31 | req.open(opt.method || "GET", opt.url, true) 32 | if (opt.headers) { 33 | for (var key in opt.headers) { 34 | req.setRequestHeader(key, opt.headers[key]) 35 | } 36 | } 37 | req.send(opt.data) 38 | } 39 | 40 | function encodeFormData(opt) { 41 | opt.headers = opt.headers || {} 42 | opt.headers['Content-Type'] = 'application/x-www-form-urlencoded' 43 | if (opt.data) { 44 | var s = '' 45 | var i = 0 46 | for (var key in opt.data) { 47 | if (i > 0) { 48 | s += '&' 49 | } 50 | var value = opt.data[key] 51 | if (typeof value === "object") { 52 | // TODO: Flatten obj={list: [1, 2]} as 53 | // obj[list][0]=1 54 | // obj[list][1]=2 55 | } 56 | s += encodeURIComponent(key) + '=' + encodeURIComponent(value) 57 | i += 1 58 | } 59 | opt.data = s 60 | } 61 | return opt 62 | } 63 | 64 | function post(opt, callback) { 65 | if (typeof opt === 'string') { 66 | opt = { url: opt } 67 | } 68 | opt.method = 'POST' 69 | encodeFormData(opt) 70 | request(opt, callback) 71 | } 72 | 73 | 74 | function getJSON(opt, callback) { 75 | if (typeof opt === 'string') { 76 | opt = { url: opt } 77 | } 78 | opt.headers = opt.headers || {} 79 | opt.headers['Accept'] = 'application/json' 80 | request(opt, function(err, data, req) { 81 | if (!err && data) { 82 | data = JSON.parse(data) 83 | } 84 | callback(err, data, req) 85 | }) 86 | } 87 | 88 | 89 | function postJSON(opt, callback) { 90 | if (typeof opt === 'string') { 91 | opt = { url: opt } 92 | } 93 | opt.method = opt.method || 'POST' 94 | opt.headers = opt.headers || {} 95 | opt.headers['Content-Type'] = 'application/json' 96 | if (opt.data) { 97 | opt.data = JSON.stringify(opt.data) 98 | } 99 | getJSON(opt, callback) 100 | } 101 | 102 | function getFile(url, callback) { 103 | var req = new XMLHttpRequest() 104 | req.onerror = function(e) { 105 | console.log('XMLHttpRequest.onerror', e) 106 | if (e) { 107 | console.log('\t', e.status, e.statusText, e.message) 108 | callback(e.message) 109 | } else { 110 | callback('XMLHttpRequest.onerror(undefined)') 111 | } 112 | } 113 | req.onreadystatechange = function() { 114 | if (req.readyState === 4) { 115 | // Since the file is local, it will have HTTP 0 Unsent. 116 | callback(null, req.responseText, req) 117 | } 118 | } 119 | req.open("GET", url, true) 120 | req.send() 121 | } 122 | 123 | function parseMetadata(data) { 124 | var lines = data.split('\n') 125 | var d = {} 126 | for (var i = 0; i < lines.length; i++) { 127 | var line = lines[i] 128 | var delimeterIndex = line.indexOf('=') 129 | if (delimeterIndex >= 0) { 130 | var key = line.substr(0, delimeterIndex) 131 | var value = line.substr(delimeterIndex + 1) 132 | d[key] = value 133 | } 134 | } 135 | return d 136 | } 137 | 138 | function getAppletMetadata(callback) { 139 | var url = Qt.resolvedUrl('.') 140 | 141 | var s = '/share/plasma/plasmoids/' 142 | var index = url.indexOf(s) 143 | if (index >= 0) { 144 | var a = index + s.length 145 | var b = url.indexOf('/', a) 146 | // var packageName = url.substr(a, b-a) 147 | var metadataUrl = url.substr(0, b) + '/metadata.desktop' 148 | Requests.getFile(metadataUrl, function(err, data) { 149 | if (err) { 150 | return callback(err) 151 | } 152 | 153 | var metadata = parseMetadata(data) 154 | callback(null, metadata) 155 | }) 156 | } else { 157 | return callback('Could not parse version.') 158 | } 159 | } 160 | 161 | function getAppletVersion(callback) { 162 | getAppletMetadata(function(err, metadata) { 163 | if (err) return callback(err) 164 | 165 | callback(err, metadata['X-KDE-PluginInfo-Version']) 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /package/contents/ui/lib/XdgUserDir.qml: -------------------------------------------------------------------------------- 1 | // Version 1 2 | 3 | import QtQml 4 | import QtCore as QtCore 5 | 6 | QtObject { 7 | id: xdgUserDir 8 | readonly property url home: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.HomeLocation) 9 | readonly property url desktop: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.DesktopLocation) 10 | readonly property url documents: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.DocumentsLocation) 11 | readonly property url download: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.DownloadLocation) 12 | readonly property url music: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.MusicLocation) 13 | readonly property url pictures: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.PicturesLocation) 14 | readonly property url videos: QtCore.StandardPaths.writableLocation(QtCore.StandardPaths.MoviesLocation) 15 | } 16 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/CheckBox.qml: -------------------------------------------------------------------------------- 1 | // Version 5 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | 6 | QQC2.CheckBox { 7 | id: configCheckBox 8 | 9 | property string configKey: '' 10 | checked: plasmoid.configuration[configKey] 11 | onClicked: plasmoid.configuration[configKey] = !plasmoid.configuration[configKey] 12 | } 13 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/ColorField.qml: -------------------------------------------------------------------------------- 1 | // Version 8 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Dialogs as QtDialogs 6 | import QtQuick.Window 7 | import org.kde.kirigami as Kirigami 8 | 9 | // https://doc.qt.io/qt-6/qtgraphicaleffects5-index.html 10 | import Qt5Compat.GraphicalEffects as QtGraphicalEffects // TODO Deprecated in Qt6 11 | 12 | 13 | QQC2.TextField { 14 | id: colorField 15 | font.family: "monospace" 16 | readonly property string defaultText: "#AARRGGBB" 17 | placeholderText: defaultColor ? defaultColor : defaultText 18 | 19 | onTextChanged: { 20 | // Make sure the text is: 21 | // Empty (use default) 22 | // or #123 or #112233 or #11223344 before applying the color. 23 | if (text.length === 0 24 | || (text.indexOf('#') === 0 && (text.length == 4 || text.length == 7 || text.length == 9)) 25 | ) { 26 | colorField.value = text 27 | } 28 | } 29 | 30 | property bool showAlphaChannel: true 31 | property bool showPreviewBg: true 32 | 33 | property string configKey: '' 34 | property string defaultColor: '' 35 | property string value: { 36 | if (configKey) { 37 | return plasmoid.configuration[configKey] 38 | } else { 39 | return "#000" 40 | } 41 | } 42 | 43 | readonly property color defaultColorValue: defaultColor 44 | readonly property color valueColor: { 45 | if (value == '' && defaultColor) { 46 | return defaultColor 47 | } else { 48 | return value 49 | } 50 | } 51 | 52 | onValueChanged: { 53 | if (!activeFocus) { 54 | text = colorField.value 55 | } 56 | if (configKey) { 57 | if (value == defaultColorValue) { 58 | plasmoid.configuration[configKey] = "" 59 | } else { 60 | plasmoid.configuration[configKey] = value 61 | } 62 | } 63 | } 64 | 65 | leftPadding: rightPadding + mouseArea.height + rightPadding 66 | 67 | FontMetrics { 68 | id: fontMetrics 69 | font.family: colorField.font.family 70 | font.italic: colorField.font.italic 71 | font.pointSize: colorField.font.pointSize 72 | font.pixelSize: colorField.font.pixelSize 73 | font.weight: colorField.font.weight 74 | } 75 | readonly property int defaultWidth: Math.ceil(fontMetrics.advanceWidth(defaultText)) 76 | implicitWidth: rightPadding + Math.max(defaultWidth, contentWidth) + leftPadding 77 | 78 | MouseArea { 79 | id: mouseArea 80 | anchors.leftMargin: parent.rightPadding 81 | anchors.topMargin: parent.topPadding 82 | anchors.bottomMargin: parent.bottomPadding 83 | anchors.left: parent.left 84 | anchors.top: parent.top 85 | anchors.bottom: parent.bottom 86 | width: height 87 | hoverEnabled: true 88 | cursorShape: Qt.PointingHandCursor 89 | 90 | onClicked: dialogLoader.active = true 91 | 92 | // Color Preview Circle 93 | Rectangle { 94 | id: previewBgMask 95 | visible: false 96 | anchors.fill: parent 97 | border.width: 1 * Screen.devicePixelRatio 98 | border.color: "transparent" 99 | radius: width / 2 100 | } 101 | QtGraphicalEffects.ConicalGradient { 102 | id: previewBgGradient 103 | visible: colorField.showPreviewBg 104 | anchors.fill: parent 105 | angle: 0.0 106 | gradient: Gradient { 107 | GradientStop { position: 0.00; color: "white" } 108 | GradientStop { position: 0.24; color: "white" } 109 | GradientStop { position: 0.25; color: "#cccccc" } 110 | GradientStop { position: 0.49; color: "#cccccc" } 111 | GradientStop { position: 0.50; color: "white" } 112 | GradientStop { position: 0.74; color: "white" } 113 | GradientStop { position: 0.75; color: "#cccccc" } 114 | GradientStop { position: 1.00; color: "#cccccc" } 115 | } 116 | source: previewBgMask 117 | } 118 | Rectangle { 119 | id: previewFill 120 | anchors.fill: parent 121 | color: colorField.valueColor 122 | border.width: 1 * Screen.devicePixelRatio 123 | border.color: Kirigami.ColorUtils.linearInterpolation(color, Kirigami.Theme.textColor, 0.5) 124 | radius: width / 2 125 | } 126 | } 127 | 128 | Loader { 129 | id: dialogLoader 130 | active: false 131 | sourceComponent: QtDialogs.ColorDialog { 132 | id: dialog 133 | visible: true 134 | modality: Qt.WindowModal 135 | options: colorField.showAlphaChannel ? QtDialogs.ColorDialog.ShowAlphaChannel : 0 136 | selectedColor: colorField.valueColor 137 | onSelectedColorChanged: { 138 | if (visible) { 139 | colorField.text = selectedColor 140 | } 141 | } 142 | onAccepted: { 143 | colorField.text = selectedColor 144 | dialogLoader.active = false 145 | } 146 | onRejected: { 147 | // This event is also triggered when the user clicks outside the popup modal. 148 | // TODO Find a way to only trigger when Cancel is clicked. 149 | colorField.text = initColor 150 | dialogLoader.active = false 151 | } 152 | 153 | property color initColor 154 | Component.onCompleted: { 155 | initColor = colorField.valueColor 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/ComboBox.qml: -------------------------------------------------------------------------------- 1 | // Version 8 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | 6 | /* 7 | ** Example: 8 | ** 9 | import './libconfig' as LibConfig 10 | LibConfig.ComboBox { 11 | configKey: "appDescription" 12 | model: [ 13 | { value: "a", text: i18n("A") }, 14 | { value: "b", text: i18n("B") }, 15 | { value: "c", text: i18n("C") }, 16 | ] 17 | } 18 | LibConfig.ComboBox { 19 | configKey: "appDescription" 20 | populated: false 21 | onPopulate: { 22 | model = [ 23 | { value: "a", text: i18n("A") }, 24 | { value: "b", text: i18n("B") }, 25 | { value: "c", text: i18n("C") }, 26 | ] 27 | populated = true 28 | } 29 | } 30 | */ 31 | QQC2.ComboBox { 32 | id: configComboBox 33 | 34 | property string configKey: '' 35 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" 36 | onConfigValueChanged: { 37 | if (!focus && value != configValue) { 38 | selectValue(configValue) 39 | } 40 | } 41 | 42 | readonly property var currentItem: currentIndex >= 0 ? model[currentIndex] : null 43 | 44 | textRole: "text" // Doesn't autodeduce from model if we manually populate it 45 | 46 | // Note that ComboBox.valueRole and ComboBox.currentValue was introduced in Qt 5.14. 47 | // Ubuntu 20.04 only has Qt 5.12. We cannot define a currentValue property or it will 48 | // break when users upgrade to Qt 5.14. 49 | property string _valueRole: "value" 50 | readonly property var _currentValue: _valueRole && currentIndex >= 0 ? model[currentIndex][_valueRole] : null 51 | readonly property alias value: configComboBox._currentValue 52 | 53 | model: [] 54 | 55 | signal populate() 56 | property bool populated: true 57 | 58 | Component.onCompleted: { 59 | populate() 60 | selectValue(configValue) 61 | } 62 | 63 | onCurrentIndexChanged: { 64 | if (typeof model !== 'number' && 0 <= currentIndex && currentIndex < count) { 65 | var item = model[currentIndex] 66 | if (typeof item !== "undefined") { 67 | var val = item[_valueRole] 68 | if (configKey && (typeof val !== "undefined") && populated) { 69 | plasmoid.configuration[configKey] = val 70 | } 71 | } 72 | } 73 | } 74 | 75 | function size() { 76 | if (typeof model === "number") { 77 | return model 78 | } else if (typeof model.count === "number") { 79 | return model.count 80 | } else if (typeof model.length === "number") { 81 | return model.length 82 | } else { 83 | return 0 84 | } 85 | } 86 | 87 | function findValue(val) { 88 | for (var i = 0; i < size(); i++) { 89 | if (model[i][_valueRole] == val) { 90 | return i 91 | } 92 | } 93 | return -1 94 | } 95 | 96 | function selectValue(val) { 97 | var index = findValue(val) 98 | if (index >= 0) { 99 | currentIndex = index 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/FormKCM.qml: -------------------------------------------------------------------------------- 1 | // Version 3 2 | 3 | import QtQuick 4 | import QtQuick.Window 5 | import org.kde.kirigami as Kirigami 6 | import org.kde.kcmutils as KCM 7 | 8 | KCM.SimpleKCM { 9 | id: simpleKCM 10 | default property alias _formChildren: formLayout.data 11 | 12 | Kirigami.FormLayout { 13 | id: formLayout 14 | } 15 | 16 | // https://invent.kde.org/plasma/plasma-desktop/-/blame/master/desktoppackage/contents/configuration/AppletConfiguration.qml 17 | // AppletConfiguration.implicitWidth: Kirigami.Units.gridUnit * 40 = 720 18 | // AppletConfiguration.Layout.minimumWidth: Kirigami.Units.gridUnit * 30 = 540 19 | // In practice, Window.width = 744px is a typical FormLayout.wideMode switchWidth 20 | // A rough guess is 128+24+180+10+360+24+20 = 746px 21 | // TabSidebar=128x, Padding=24px, Labels=180px, Spacing=10px, Controls=360px, Padding=24px, Scrollbar=20px 22 | // However the default is only 720px. So we'll set it to a 800px minimum to avoid wideMode=false 23 | property int wideModeMinWidth: 800 * Screen.devicePixelRatio 24 | Window.onWindowChanged: { 25 | if (Window.window) { 26 | Window.window.visibleChanged.connect(function(){ 27 | if (Window.window && Window.window.visible && Window.window.width < wideModeMinWidth) { 28 | Window.window.width = wideModeMinWidth 29 | } 30 | }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/Heading.qml: -------------------------------------------------------------------------------- 1 | // Version 6 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Layouts 6 | import org.kde.kirigami as Kirigami 7 | 8 | /* 9 | ** Example: 10 | ** 11 | import './libconfig' as LibConfig 12 | LibConfig.Heading { 13 | text: i18n("SpinBox (Double)") 14 | } 15 | */ 16 | 17 | // While the following Kirigami is very simple: 18 | // Kirigami.Separator { 19 | // Kirigami.FormData.label: "Heading" 20 | // Kirigami.FormData.isSection: true 21 | // } 22 | // 23 | // I want to be able to adjust the label size and make it bold. 24 | // Kirigami's buddy Heading is level=3, which does not stand out 25 | // very well. I also want to center the heading. 26 | // Since we can't access the Heading in the buddy component, we 27 | // need to make sure the Heading has no text, and draw our own. 28 | ColumnLayout { 29 | id: heading 30 | spacing: 0 31 | 32 | property string text: "" 33 | property alias separator: separator 34 | property alias label: label 35 | property bool useThickTopMargin: true 36 | 37 | property Item __formLayout: { 38 | if (parent && typeof parent.wideMode === 'boolean') { 39 | return parent 40 | } else if (typeof formLayout !== 'undefined' && typeof formLayout.wideMode === 'boolean') { 41 | return formLayout 42 | } else if (typeof page !== 'undefined' && typeof page.wideMode === 'boolean') { 43 | return page 44 | } else { 45 | return null 46 | } 47 | } 48 | 49 | Layout.fillWidth: true 50 | // Kirigami.FormData.isSection: true 51 | Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel 52 | 53 | Kirigami.Separator { 54 | id: separator 55 | visible: false 56 | Layout.fillWidth: true 57 | Layout.topMargin: Kirigami.Units.largeSpacing 58 | Layout.bottomMargin: Kirigami.Units.largeSpacing 59 | } 60 | 61 | Kirigami.Heading { 62 | id: label 63 | Layout.topMargin: useThickTopMargin ? Kirigami.Units.largeSpacing * 3 : Kirigami.Units.largeSpacing 64 | Layout.bottomMargin: Kirigami.Units.smallSpacing 65 | Layout.fillWidth: true 66 | text: heading.text 67 | level: 1 68 | font.weight: Font.Bold 69 | // horizontalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignHCenter : Text.AlignLeft 70 | verticalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignVCenter : Text.AlignBottom 71 | } 72 | } 73 | 74 | //--- Test Default Kirigami Heading 75 | // Kirigami.Separator { 76 | // property string text: "" 77 | // Kirigami.FormData.label: text 78 | // Kirigami.FormData.isSection: true 79 | // property alias separator: separator 80 | // Item { 81 | // id: separator 82 | // } 83 | // } 84 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/IconField.qml: -------------------------------------------------------------------------------- 1 | // Version 11 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Layouts 6 | import org.kde.kirigami as Kirigami 7 | import org.kde.ksvg as KSvg 8 | import org.kde.plasma.core as PlasmaCore 9 | import org.kde.iconthemes as KIconThemes // IconDialog 10 | 11 | RowLayout { 12 | id: iconField 13 | 14 | default property alias _contentChildren: content.data 15 | 16 | property string configKey: '' 17 | property alias value: textField.text 18 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" 19 | onConfigValueChanged: { 20 | if (!textField.focus && value != configValue) { 21 | value = configValue 22 | } 23 | } 24 | property int previewIconSize: Kirigami.Units.iconSizes.medium 25 | property string defaultValue: "" 26 | property alias placeholderValue: textField.placeholderText 27 | property var presetValues: [] 28 | property bool showPresetLabel: true 29 | 30 | // Based on org.kde.plasma.kickoff 31 | QQC2.Button { 32 | id: iconButton 33 | padding: Kirigami.Units.smallSpacing 34 | Layout.alignment: Qt.AlignTop 35 | 36 | // KDE QQC2 sets implicitSize to background.implicitSize ignoring padding/inset properties. 37 | implicitWidth: leftPadding + contentItem.implicitWidth + rightPadding 38 | implicitHeight: topPadding + contentItem.implicitHeight + bottomPadding 39 | 40 | onPressed: iconMenu.opened ? iconMenu.close() : iconMenu.open() 41 | 42 | contentItem: KSvg.FrameSvgItem { 43 | id: previewFrame 44 | imagePath: plasmoid.location === PlasmaCore.Types.Vertical || plasmoid.location === PlasmaCore.Types.Horizontal 45 | ? "widgets/panel-background" : "widgets/background" 46 | implicitWidth: fixedMargins.left + previewIconSize + fixedMargins.right 47 | implicitHeight: fixedMargins.top + previewIconSize + fixedMargins.bottom 48 | 49 | Kirigami.Icon { 50 | anchors.fill: parent 51 | anchors.leftMargin: previewFrame.fixedMargins.left 52 | anchors.topMargin: previewFrame.fixedMargins.top 53 | anchors.rightMargin: previewFrame.fixedMargins.right 54 | anchors.bottomMargin: previewFrame.fixedMargins.bottom 55 | source: iconField.value || iconField.placeholderValue 56 | active: iconButton.hovered 57 | } 58 | } 59 | 60 | QQC2.Menu { 61 | id: iconMenu 62 | 63 | // Appear below the button 64 | y: +parent.height 65 | 66 | QQC2.MenuItem { 67 | text: i18ndc("plasma_applet_org.kde.plasma.kickoff", "@item:inmenu Open icon chooser dialog", "Choose...") 68 | icon.name: "document-open" 69 | onClicked: dialogLoader.active = true 70 | } 71 | QQC2.MenuItem { 72 | text: i18ndc("plasma_applet_org.kde.plasma.kickoff", "@item:inmenu Reset icon to default", "Clear Icon") 73 | icon.name: "edit-clear" 74 | onClicked: iconField.value = iconField.defaultValue 75 | } 76 | } 77 | } 78 | 79 | ColumnLayout { 80 | id: content 81 | Layout.fillWidth: true 82 | 83 | RowLayout { 84 | QQC2.TextField { 85 | id: textField 86 | Layout.fillWidth: true 87 | 88 | text: iconField.configValue 89 | onTextChanged: serializeTimer.restart() 90 | 91 | rightPadding: clearButton.width + Kirigami.Units.smallSpacing 92 | 93 | QQC2.ToolButton { 94 | id: clearButton 95 | visible: iconField.configValue != iconField.defaultValue 96 | icon.name: iconField.defaultValue === "" ? "edit-clear" : "edit-undo" 97 | onClicked: iconField.value = iconField.defaultValue 98 | 99 | anchors.top: parent.top 100 | anchors.right: parent.right 101 | anchors.bottom: parent.bottom 102 | 103 | width: height 104 | } 105 | } 106 | 107 | QQC2.Button { 108 | id: browseButton 109 | icon.name: "document-open" 110 | onClicked: dialogLoader.active = true 111 | } 112 | } 113 | 114 | Flow { 115 | Layout.fillWidth: true 116 | Layout.maximumWidth: Kirigami.Units.gridUnit * 30 117 | Repeater { 118 | model: presetValues 119 | QQC2.Button { 120 | icon.name: modelData 121 | text: iconField.showPresetLabel ? modelData : '' 122 | onClicked: iconField.value = modelData 123 | QQC2.ToolTip.text: modelData 124 | QQC2.ToolTip.visible: !iconField.showPresetLabel && hovered 125 | } 126 | } 127 | } 128 | } 129 | 130 | Loader { 131 | id: dialogLoader 132 | active: false 133 | sourceComponent: KIconThemes.IconDialog { 134 | id: dialog 135 | visible: true 136 | modality: Qt.WindowModal 137 | onIconNameChanged: { 138 | iconField.value = iconName 139 | } 140 | onVisibleChanged: { 141 | if (!visible) { 142 | dialogLoader.active = false 143 | } 144 | } 145 | } 146 | } 147 | 148 | Timer { // throttle 149 | id: serializeTimer 150 | interval: 300 151 | onTriggered: { 152 | if (configKey) { 153 | plasmoid.configuration[configKey] = iconField.value 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/RadioButtonGroup.qml: -------------------------------------------------------------------------------- 1 | // Version 7 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Layouts 6 | import org.kde.kirigami as Kirigami 7 | 8 | /* 9 | ** Example: 10 | ** 11 | import './libconfig' as LibConfig 12 | LibConfig.RadioButtonGroup { 13 | configKey: "priority" 14 | model: [ 15 | { value: "a", text: i18n("A") }, 16 | { value: "b", text: i18n("B") }, 17 | { value: "c", text: i18n("C") }, 18 | ] 19 | } 20 | */ 21 | ColumnLayout { 22 | id: radioButtonGroup 23 | 24 | property string configKey: '' 25 | readonly property var configValue: configKey ? plasmoid.configuration[configKey] : "" 26 | 27 | Kirigami.FormData.labelAlignment: Qt.AlignTop 28 | 29 | property alias group: group 30 | QQC2.ButtonGroup { 31 | id: group 32 | } 33 | 34 | property alias model: buttonRepeater.model 35 | 36 | // The main reason we put all the RadioButtons in 37 | // a ColumnLayout is to shrink the spacing between the buttons. 38 | spacing: Kirigami.Units.smallSpacing 39 | 40 | // Assign buddyFor to the first RadioButton so that the Kirigami label aligns with it. 41 | // Repeater is also a visibleChild, so avoid it. 42 | Kirigami.FormData.buddyFor: { 43 | for (var i = 0; i < visibleChildren.length; i++) { 44 | if (!(visibleChildren[i] instanceof Repeater)) { 45 | return visibleChildren[i] 46 | } 47 | } 48 | return null 49 | } 50 | 51 | Repeater { 52 | id: buttonRepeater 53 | QQC2.RadioButton { 54 | visible: typeof modelData.visible !== "undefined" ? modelData.visible : true 55 | enabled: typeof modelData.enabled !== "undefined" ? modelData.enabled : true 56 | text: modelData.text 57 | checked: modelData.value === configValue 58 | QQC2.ButtonGroup.group: radioButtonGroup.group 59 | onClicked: { 60 | focus = true 61 | if (configKey) { 62 | plasmoid.configuration[configKey] = modelData.value 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/TextArea.qml: -------------------------------------------------------------------------------- 1 | // Version 7 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Layouts 6 | import org.kde.kirigami as Kirigami 7 | 8 | QQC2.TextArea { 9 | id: textArea 10 | property string configKey: '' 11 | readonly property var configValue: configKey ? plasmoid.configuration[configKey] : "" 12 | onConfigValueChanged: deserialize() 13 | 14 | onTextChanged: serializeTimer.restart() 15 | 16 | wrapMode: TextArea.Wrap 17 | 18 | Kirigami.FormData.labelAlignment: Qt.AlignTop 19 | 20 | // An empty TextArea adjust to it's empty contents. 21 | // So we need the TextArea to be wide enough. 22 | Layout.fillWidth: true 23 | 24 | // Since QQC2 defaults to implicitWidth=contentWidth, a really long 25 | // line in TextArea will cause a binding loop on FormLayout.width 26 | // when we only set fillWidth=true. 27 | // Setting an implicitWidth fixes this and allows the text to wrap. 28 | implicitWidth: Kirigami.Units.gridUnit * 20 29 | 30 | // Load 31 | function deserialize() { 32 | if (configKey) { 33 | var newText = valueToText(configValue) 34 | setText(newText) 35 | } 36 | } 37 | function valueToText(value) { 38 | return value 39 | } 40 | function setText(newText) { 41 | if (textArea.text != newText) { 42 | if (textArea.focus) { 43 | // TODO: Find cursor in newText and replace text before + after cursor. 44 | } else { 45 | textArea.text = newText 46 | } 47 | } 48 | } 49 | 50 | // Save 51 | function serialize() { 52 | var newValue = textToValue(textArea.text) 53 | setConfigValue(newValue) 54 | } 55 | function textToValue(text) { 56 | return text 57 | } 58 | function setConfigValue(newValue) { 59 | if (configKey) { 60 | var oldValue = plasmoid.configuration[configKey] 61 | if (oldValue != newValue) { 62 | plasmoid.configuration[configKey] = newValue 63 | } 64 | } 65 | } 66 | 67 | Timer { // throttle 68 | id: serializeTimer 69 | interval: 300 70 | onTriggered: serialize() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /package/contents/ui/libconfig/TextAreaStringList.qml: -------------------------------------------------------------------------------- 1 | // Version 6 2 | 3 | import QtQuick 4 | import QtQuick.Controls as QQC2 5 | import QtQuick.Layouts 6 | 7 | import "." as LibConfig 8 | 9 | LibConfig.TextArea { 10 | id: textArea 11 | 12 | // Load 13 | function valueToText(value) { 14 | if (value) { 15 | return value.join("\n") 16 | } else { 17 | return "" 18 | } 19 | } 20 | 21 | // Save 22 | function textToValue(text) { 23 | if (text) { 24 | return text.split("\n") 25 | } else { 26 | return [] 27 | } 28 | } 29 | 30 | // Modify 31 | function prepend(str) { 32 | textArea.focus = true 33 | textArea.select(0, 0) // Make sure the text area has focus or we'll enter a loop. 34 | textArea.text = str + '\n' + textArea.text 35 | } 36 | 37 | function append(str) { 38 | textArea.focus = true 39 | textArea.select(0, 0) // Make sure the text area has focus or we'll enter a loop. 40 | textArea.text += '\n' + str 41 | } 42 | 43 | function hasItem(str) { 44 | var list = textToValue(textArea.text) 45 | for (var i = 0; i < list.length; i++) { 46 | if (list[i].trim() == str) { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | 53 | function selectItem(str) { 54 | var start = textArea.text.indexOf(str) 55 | if (start >= 0) { 56 | textArea.select(start, start + str.length) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "KPackageStructure": "Plasma/Applet", 3 | "KPlugin": { 4 | "Authors": [ 5 | { 6 | "Email": "zrenfire@gmail.com", 7 | "Name": "Chris Holland" 8 | } 9 | ], 10 | "BugReportUrl": "https://github.com/Zren/plasma-applet-tiledmenu/issues", 11 | "Category": "Application Launchers", 12 | "Description": "A menu based on Windows 10's Start Menu.", 13 | "Description[es]": "Menú basado en el menú de inicio de Windows 10", 14 | "Description[fa]": "یک منو بر پایه ویندوز ۱۰", 15 | "Description[fi]": "Vailikko perustuu Windows 10: n käynnistä-valikkoon.", 16 | "Description[fr]": "Un menu basé sur le menu de démarrage de Windows 10.", 17 | "Description[he]": "תפריט שמבוסס על מראה תפריט ההתחלה של Windows 10.", 18 | "Description[ja]": "Windows 10 のスタートメニューに基づいたメニュー。", 19 | "Description[ko]": "Windows 10 시작메뉴를 기반으로 한 실행기", 20 | "Description[nl]": "Een menu a la het Windows 10-startmenu.", 21 | "Description[pt_BR]": "Um menu baseado no Start Menu do Windows 10.", 22 | "Description[ru]": "Меню, основанное на меню Пуск Windows 10.", 23 | "Description[sl]": "Meni, narejen po izgledu zaganjalnika v sistemu Windows 10", 24 | "Description[tr]": "Windows 10'un Başlat Menüsü'nü temel alan bir menü.", 25 | "Description[zh_TW]": "一個基於 Windows 10 開始功能表的選單。", 26 | "Icon": "start-here-kde", 27 | "Id": "com.github.zren.tiledmenu", 28 | "License": "GPL-2.0+", 29 | "Name": "Tiled Menu", 30 | "Name[es]": "Menú con Baldosas", 31 | "Name[fa]": "منو طرح کاشی", 32 | "Name[fi]": "Tiled Menu", 33 | "Name[fr]": "Tiled Menu", 34 | "Name[he]": "תפריט אריחים", 35 | "Name[ja]": "タイルメニュー", 36 | "Name[ko]": "타일 메뉴", 37 | "Name[nl]": "Tegelmenu", 38 | "Name[pt_BR]": "Menu em Ladrilhos", 39 | "Name[ru]": "Плиточное Меню", 40 | "Name[sl]": "Meni s ploščicami", 41 | "Name[tr]": "Döşeli Menü", 42 | "Name[zh_TW]": "方塊磚選單", 43 | "Version": "46", 44 | "Website": "https://github.com/Zren/plasma-applet-tiledmenu" 45 | }, 46 | "X-Plasma-API-Minimum-Version": "6.0", 47 | "X-Plasma-Provides": [ 48 | "org.kde.plasma.launchermenu" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /package/translate/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Translate 2 | 3 | ## Status 4 | 5 | | Locale | Lines | % Done| 6 | |----------|---------|-------| 7 | | Template | 117 | | 8 | | de | 110/117 | 94% | 9 | | es | 113/117 | 96% | 10 | | fa | 117/117 | 100% | 11 | | fi | 113/117 | 96% | 12 | | fr | 116/117 | 99% | 13 | | he | 116/117 | 99% | 14 | | hr | 91/117 | 77% | 15 | | id | 99/117 | 84% | 16 | | ja | 117/117 | 100% | 17 | | ko | 114/117 | 97% | 18 | | nl | 117/117 | 100% | 19 | | pl | 101/117 | 86% | 20 | | pt | 96/117 | 82% | 21 | | pt_BR | 112/117 | 95% | 22 | | ro | 101/117 | 86% | 23 | | ru | 113/117 | 96% | 24 | | sl | 112/117 | 95% | 25 | | tr | 112/117 | 95% | 26 | | zh_CN | 88/117 | 75% | 27 | | zh_TW | 117/117 | 100% | 28 | 29 | 30 | ## New Translations 31 | 32 | * Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/Zren/plasma-applet-tiledmenu/issues/new), name the file `spanish.txt`, attach the txt file to the issue (drag and drop). 33 | 34 | Or if you know how to make a pull request 35 | 36 | * Copy the `template.pot` file and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`. 37 | * Your region's locale code can be found at: https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes/28357857#28357857 38 | 39 | ## Scripts 40 | 41 | Zren's `kpac` script can easily run the `gettext` commands for you, parsing the `metadata.json` and filling out any placeholders for you. `kpac` can be [downloaded here](https://github.com/Zren/plasma-applet-lib/blob/master/kpac) and should be placed at `~/Code/plasmoid-widgetname/kpac` to edit translations at `~/Code/plasmoid-widgetname/package/translate/`. 42 | 43 | 44 | * `python3 ./kpac i18n` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files. Then it converts the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...` which will bundle the translations in the `*.plasmoid` without needing the user to manually install them. 45 | * `python3 ./kpac localetest` will convert the `.po` to the `*.mo` files then run `plasmoidviewer` (part of `plasma-sdk`). 46 | 47 | ## How it works 48 | 49 | Since KDE Frameworks v5.37, translations can be bundled with the zipped `*.plasmoid` file downloaded from the store. 50 | 51 | * `xgettext` extracts the messages from the source code into a `template.pot`. 52 | * Translators copy the `template.pot` to `fr.po` to translate the French language. 53 | * When the source code is updated, we use `msgmerge` to update the `fr.po` based on the updated `template.pot`. 54 | * When testing or releasing the widget, we convert the `.po` files to their binary `.mo` form with `msgfmt`. 55 | 56 | The binary `.mo` translation files are placed in `package/contents/locale/` so you may want to add `*.mo` to your `.gitignore`. 57 | 58 | ``` 59 | package/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.tiledmenu.mo 60 | ``` 61 | 62 | ## Links 63 | 64 | * https://develop.kde.org/docs/plasma/widget/translations-i18n/ 65 | * https://l10n.kde.org/stats/gui/trunk-kf5/team/fr/plasma-desktop/ 66 | * https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems 67 | * https://api.kde.org/frameworks/ki18n/html/prg_guide.html 68 | 69 | > Version 8 of [Zren's i18n scripts](https://github.com/Zren/plasma-applet-lib). 70 | -------------------------------------------------------------------------------- /uninstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 4 3 | 4 | # Eg: kpackagetool6 --type "Plasma/Applet" --remove package 5 | 6 | if [ -f "$PWD/package/metadata.json" ]; then # Plasma6 (and later versions of Plasma5) 7 | packageNamespace=`python3 -c 'import sys, json; print(json.load(sys.stdin).get("KPlugin", {}).get("Id", ""))' < "$PWD/package/metadata.json"` 8 | packageServiceType=`python3 -c 'import sys, json; print(json.load(sys.stdin).get("KPackageStructure",""))' < "$PWD/package/metadata.json"` 9 | if [ -z "$packageServiceType" ]; then # desktoptojson will set KPlugin.ServiceTypes[0] instead of KPackageStructure 10 | packageServiceType=`python3 -c 'import sys, json; print((json.load(sys.stdin).get("KPlugin", {}).get("ServiceTypes", [])+[""])[0])' < "$PWD/package/metadata.json"` 11 | echo "[warning] metadata.json needs KPackageStructure set in Plasma6" 12 | fi 13 | elif [ -f "$PWD/package/metadata.desktop" ]; then # Plasma5 14 | packageNamespace=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 15 | packageServiceType=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-ServiceTypes"` 16 | else 17 | echo "[error] Could not find 'package/metadata.json' or 'package/metadata.desktop'" 18 | exit 1 19 | fi 20 | echo "Namespace: ${packageNamespace}" 21 | echo "Type: ${packageServiceType}" 22 | if [ -z "$packageServiceType" ]; then 23 | echo "[error] Could not parse metadata" 24 | exit 1 25 | fi 26 | 27 | if command -v kpackagetool6 &> /dev/null ; then kpackagetool="kpackagetool6" # Plasma6 28 | elif command -v kpackagetool5 &> /dev/null ; then kpackagetool="kpackagetool5" # Plasma5 29 | else 30 | echo "[error] Could not find 'kpackagetool6'" 31 | exit 1 32 | fi 33 | 34 | # Eg: kpackagetool6 --type "Plasma/Applet" --remove package 35 | "$kpackagetool" -t "${packageServiceType}" -r package 36 | --------------------------------------------------------------------------------