Plasmoid на чистом QML и JavaScript

в 14:56, , рубрики: javascript, kde, linux, plasma, QML, SDK, widget, Графические оболочки, Песочница, метки: , , , , ,

image

На хабре еще не было ни одного поста про создание плазмоида на чистом QML с использованием JavaScript. Данный пост призван исправить данный недостаток.

Немного теории и истории

Плазмоид — это виджет оболочки Plasma, появившейся в KDE 4. На этапе появления первых версий KDE 4 все плазмоиды писались исключительно на C++, затем появилась поддержка python. На C++ это было, скажем, не очень легко (ввиду трудоемкости, знаний всех основ языка и пр.), на python уже было гораздо легче. Так как KDE написано на C++ с использованием Qt, то предпочтительнее было бы использовать этот самый Qt (по крайней мере в KDE так считают). Когда появился QML — KDE сразу сделали полную поддержку создания плазмоидов на QML + JS + C++, причем теперь они настаивают на создании плазмоидов исключительно на QML.

На хабре уже были посты о создании плазмоидов на Python и C++, теперь пришла очередь и для QML. Но эта серия постов (Я рассчитываю написать не один пост) не просто о создании плазмоида используя связку QML + JS + C++, но и обо всех подводных камнях, с которым может столкнуться начинающий разработчик виджета. Дело в том, что о QML-плазмоидах еще слишком мало информации на techbase.kde.org, ввиду этого я постоянно вел дискуссии с разработчиками на #kde и #plasma @ irc.freenode.net. Кстати, все они помогают абсолютно всегда и во всех случаях без исключения, за этом им большой и жирный плюс в карму.

Инструментарий

Вообще говоря, для написания плазмоида можно использовать любой текстовый редактор, но относительно недавно на свет появился замечательный plasmate. Plasmate — это часть Plasma SDK, это мини-IDE (мини, потому что еще очень много багов, очень много нереализованных фич и вообще это еще альфа версия, тем не менее, для написания плазмоидов она уже вполне годна) для создания всего, что связано с Plasma: плазмоиды, переключатели окон (alt-tab), эффекты kwin, темы plasma и т.д.

На примере Ubuntu все очень ставится просто:

sudo apt-get install kde-sdk

или еще проще:

sudo apt-get install plasmate

Но для правильной установки нужно установить kubuntu-ppa backports, почитать об этом можно здесь.

После установки запускаем plasmate, щелкаем Plasma Widget. Обзываем будущий плазмоид — задаем значение Addon name, щелкаем по кнопке Create и попадаем в окно редактора. По умолчанию включен режим "Preview", это особенно удобно, если у вас два монитора — на одном можно расположить окно с виджетом, на другом оставить редактор кода.

image

Итак, далее самое интересное.

QML-компоненты Plasma

На данный момент компонентов плазмы уже достаточно много для того, чтобы описать любой визуальный интерфейс, и не только визуальный, но и интерфейсы для работы с данными (например DataSource).

Все компоненты можно найти на http://api.kde.org, а именно здесь. Для построения визуального интерфейса обычно используют PlasmaExtraComponents и PlasmaComponents, но чаще всего хватает и одного последнего. Их подробное описание можно найти там же.

При создании плазмоида в plasmate заготовка будущего виджета уже готова к установке на рабочий стол. Сразу хочу обратить ваше внимание на заготовку — в ней уже подключены нужные импорты и даже используется i18n, что очень полезно для локализации. К слову, на данный момент в заготовке указан QtQuick 1.1, хотя у Вас в системе уже может стоять Qt5 с QtQuick 2.0. Если попытаться его подключить — plasmate будет ругаться т.к. плазма в последнем KDE (и до версии 5.0) не имеет поддержки QtQuick 2.

Итак, простенький «Hello world» мы уже видим в заготовке. Но чтобы сделать что-то посложнее необходимо кое-что осознать, а именно: любая информация о системе, компьютере, и обо всем остальном в плазмоиде недоступна, т.к. он просто не имеет доступ ко всему вышеперечисленному. А так, как QML — технология, чаще всего привязанная к C++, то и с плазмоидами так же — все данные предоставляются так называемыми DataSource и DataEngine.

DataSource — что-то вроде «поставщика» данных в QML из C++. Например, текущее время мы узнать в QML не можем, а используя C++ и соответствующие заголовочные файлы — вполне. В таком случае нам нужно написать DataEngine, который будет поставлять эту информацию(время) всем плазмоидам, которые его используют как источник (DataSource), кстати, плазмоиды в этом случае могут быть написано на любом языке.

DataEngine — тот самый C++-код, отправляющий данные в QML. Разумеется, его нужно писать по особым правилам KDE, но в данном посте я вникать в это не стану, может быть в следующем, если кому-то интересно. И кстати, DateEngine тоже может быть написан не только на C++.

Далее, я буду пояснять на примере своего плазмоида — KFilePlaces (https://bugs.kde.org/show_bug.cgi?id=180139). На самом деле он еще не готов, но я выложу alpha-версию, чтобы посмотреть, из чего вообще лепятся плазмоиды и пощупать данное тесто.

Создание плазмоида «Places»

Обзор существующих DataEngine

Для создания плазмоида, показывающего «Точки входа», нам необходимо сначала понять, как все работает. Здесь нужно знать две вещи: если данный функционал уже есть — скорее всего в исходниках (в данном случае в исходниках файлового менеджера Dolphin) можно узнать, каким способам берутся нужные нам данные. Если его нет — то можно поискать соответствующий DataEngine в системе и там уже разобраться (поверьте, это очень просто). Для этого можно вызвать Plasma Engine Explorer:

plasmaengineexplorer

Вылезет небольшое окно, в котором можно будет, как ясно из названия, посмотреть все установленные в системе DataEngine. В моем случае, необходимый DataEngine — «places». Сразу после того, как мы выберем его из списка — появится древовидный список объектов, которые данный «двигатель» поставляет наружу. У меня это выглядит так:

Plasmoid на чистом QML и JavaScript
Здесь от 0 до 11 — источники данных, т.к. DataSources. Данный массив и есть все точки входа. Чтобы использовать этот двигатель в QML нужно написать нечто подобное:

PlasmaCore.DataSource {
    
        id: placesDataSource
        
        engine: "places"
        interval: 1000   
        
        connectedSources: sources
    }

Здесь sources — это все эти DataSource от 0 до 11, т.е. массив. Можно было бы указать и явно:

PlasmaCore.DataSource {
    
        id: placesDataSource
        
        engine: "places"
        interval: 1000   

        connectedSources: ["0", "2", "11"]
    }

Но в моем случае нужно обрабатывать все точки входа, поэтому оставляем «sources».

После создания источника данных пользоваться им сразу нельзя, необходимо создать модель. Для этого существует объект "DataModel", который просто берет данные из DataSource и из которого уже можно выводить данные. Создается DataModel тоже очень просто:

PlasmaCore.DataModel {
    
        id: placesDataModel

        dataSource: placesDataSource
    }    
Сортировка и фильтрация элементов источника данных

Итак, данные мы получили. Но, как можно заметить, они идут не по порядку, да и вполне возможно, что нам их нужно отсортировать по какому-либо признаку(переменной). В моем случае, мне нужно показывать все не скрытые элементы(параметр hidden: false), и отсортировать по параметру isDevice (устройство или место). Для этого воспользуемся средствами PlasmaCore.SortFilterModel:

PlasmaCore.SortFilterModel {
        id: sortedEntriesDataModel
        
        
        filterRole: "isDevice"
        filterRegExp: "false"
            
        sourceModel: PlasmaCore.SortFilterModel {
        
            sourceModel: placesDataModel
            
            filterRole: "hidden"
            filterRegExp: "false"
            
            sortRole: "isDevice"
            sortOrder: "AscendingOrder"
       }
    }

Здесь sortRole — переменная, по которой будет проводиться сортировка, sortOrder — порядок; filterRole — переменная, по которой будет проводиться фильтрация (т.е. отсекутся все неподходящие элементы), и filterRegExp — выражение, проверкой которого будет проводиться отбор. Здесь есть одна загвоздка: если вы захотите отфильтровать еще по какому-то признаку кроме «isDevice» — вам придется обернуть данный элемент SortFilterModel так, как сделано в данном примере в случае с дополнительной фильтрацией по переменной «hidden».

PlasmaCore.SortFilterModel является расширением PlasmaCore.DataModel, поэтому они совместимы.

Создание визуального списка

Для создания списка сперва советую использовать компонент ScrollArea из PlasmaExtras, он нужен практически для всего, где неизвестны размеры дочернего элемента. Он подходит по таким причинам, как:

  1. Flickable-контейнер. Содержимое может скроллиться не только скроллбаром сбоку, который можно отключить, но и жестами мыши или тач-скрина.
  2. Его настоятельно рекомендуют использовать вместо самостоятельного создания велосипеда оберток из других компонентов Plasma runtime, например таких, как создание Item/Page и т.д. с PlasmaComponents.ScrollBar.

Создаем:

PlasmaExtras.ScrollArea {
        id: entriesScrollArea
    
        anchors.top:                    parent.top
        anchors.left:                   parent.left
        anchors.right:                  parent.right
        
        anchors.horizontalCenter:       plasmoidItem.horizontalCenter
        
        height: 40 * (entriesListView.count + 1)
        
        flickableItem: ListView {

Здесь я специально остановился на ListView, чтобы пояснить: элемент flickableItem представляет собой контейнер для любого элемента, который можно скроллить/прокручивать, если он не вмещается в ширину и/или высоту родительского(нашего ScrollArea) элемента. Это полезно для списков, так как их удобно прокручивать и они могут расширяться.

Чтобы не загромождать экран, я специально вынес создание списка в спойлер:

Создание списка

PlasmaExtras.ScrollArea {
        id: entriesScrollArea
    
        anchors.top:                    parent.top
        anchors.left:                   parent.left
        anchors.right:                  parent.right
        
        anchors.horizontalCenter:       plasmoidItem.horizontalCenter
        
        height: 40 * (entriesListView.count + 1)
        
        flickableItem: ListView {
    
            id: entriesListView
        
            highlightRangeMode: ListView.NoHighlightRange
            orientation:        ListView.Vertical
            
            focus:              true
            clip:               true
            
            model: sortedEntriesDataModel
            
            delegate: listViewItemTemplate
            
            header: PlasmaComponents.Switch {
                id: entriesHeaderSwitch
            
                anchors.top:   parent.top
                anchors.left:   parent.left
                anchors.right:  devicesHeaderLabel.left
                    
                text: i18n("Places")
                
                checked: true                
            }  
            
            highlight: Rectangle {
            
                id: highlightListViewItem       
            
                anchors.left: parent.left 

                color: "lightgrey"; 
                radius: 6; 
                opacity: 0.6
            }
        }
    }

Здесь пояснять особо не вижу смысла, т.к. список — обычный ListView-элемент QML, начиная с QtQuick 1.0.
Идем дальше.

Создание делегата для элементов списка

Казалось бы, зачем вообще уделять такому простому шагу внимание, но я с ним провозился довольно долго. Дело в том, что элементом-контейнером для делагата обязательно должен быть PlasmaComponents.ListItem. Если просто заключить элементы в Item или просто контейнер(не ListItem) — все элементы перемешаются в кучу. Увы, нигде про этот ньюанс написано не было.

Пробуем!

Если вы все сделали правильно, у нас будет что-то вроде этого:

Код

import QtQuick 1.1
import org.kde.locale 0.1
import org.kde.plasma.components 0.1 as PlasmaComponents
import org.kde.plasma.core 0.1 as PlasmaCore
import org.kde.plasma.extras 0.1 as PlasmaExtras

Item {
    width: 200
    height: 300

    PlasmaCore.DataSource {
    
        id: placesDataSource
        
        engine: "places"
        interval: 1000   
        
        connectedSources: sources
    }
    
    PlasmaCore.DataModel {
    
        id: placesDataModel

        dataSource: placesDataSource
    }    
    
    PlasmaCore.SortFilterModel {
        id: sortedEntriesDataModel
        
        
        filterRole: "isDevice"
        filterRegExp: "false"
            
        sourceModel: PlasmaCore.SortFilterModel {
        
            sourceModel: placesDataModel
            
            filterRole: "hidden"
            filterRegExp: "false"
            
            sortRole: "isDevice"
            sortOrder: "AscendingOrder"
       }
    }
    
    Component {     
        id: listViewItemTemplate
        
                        
        PlasmaComponents.ListItem {
            id: placeListItem
        
        
            anchors.left: plasmoidItem.left    
            anchors.right: plasmoidItem.right
            x: 20
            
            height: 40
            
            Item {
                id: listItemObject
            
                anchors.left: plasmoidItem.left    
                anchors.right: plasmoidItem.right
                
                    
                PlasmaCore.IconItem {
                    id: placeIconItem
                
                    anchors.left: parent.left 
                    anchors.leftMargin: 5 
                    
                    source: icon
                }
                
                PlasmaComponents.Label {    
                    id: placeNameLabel
                
                    anchors.left: placeIconItem.right 
                    anchors.leftMargin: 10
                    
                    text: name
                    
                    font.pointSize: 12                
                }
                
                PlasmaComponents.ProgressBar {
                    id: placeFreeSizeProgressBar
                    
                    anchors.top:        placeNameLabel.bottom
                    anchors.left:       placeNameLabel.left
                    
                    width:              placeNameLabel.width
                    height:             10
                
                    value:              kBUsed / kBSize
                    
                    opacity:            0
                }
            }
        }
    }

    PlasmaExtras.ScrollArea {
        id: entriesScrollArea
    
        anchors.top:                    parent.top
        anchors.left:                   parent.left
        anchors.right:                  parent.right
        
        anchors.horizontalCenter:       plasmoidItem.horizontalCenter
        
        height: 40 * (entriesListView.count + 1)
        
        flickableItem: ListView {
    
            id: entriesListView
        
            highlightRangeMode: ListView.NoHighlightRange
            orientation:        ListView.Vertical
            
            focus:              true
            clip:               true
            
            model: sortedEntriesDataModel
            
            delegate: listViewItemTemplate
            
            header: PlasmaComponents.Switch {
                id: entriesHeaderSwitch
            
                anchors.top:   parent.top
                anchors.left:   parent.left
                anchors.right:  devicesHeaderLabel.left
                    
                text: i18n("Places")
                
                checked: true
            }  
            
            highlight: Rectangle {
            
                id: highlightListViewItem       
            
                anchors.left: parent.left 

                color: "lightgrey"; 
                radius: 6; 
                opacity: 0.6
            }
        }
    }    
}

Или внешне вот так (в моем случае):
Plasmoid на чистом QML и JavaScript

Код можно скопипастить и вставить в plasmate.

К сожалению, пост получился слишком обширный, поэтому продолжение следует!

Помощь

#plasma, #kde @ irc.freenode.net

P.S. Не могу понять баг с атрибутом «align: 'center'» в — после него текст тоже центрируется и не сбивается обратно после <br clear=«left» />, в результате чего у меня пост выглядит кривовато.

Автор: broken

Источник

  1. Олег:

    Пишу сейчас свой первый плазмоид (мне нужно спарсить страницу и показать всего одно число в плазмоиде). Ваша статья для меня была весьма информативна и полезна. Где же продолжение?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js