Mi aplicación de notas para KDE Plasma 5
Mi aplicación de notas para KDE Plasma 5
Holas, después de un buen tiempo estoy de vuelta en el blog y hoy les quiero hablar de una pequeña app para tomar notas hecha con QML y Python3, por lo que funciona como un plasmoide para KDE Plasma 5.
El funcionamiento es practicamente el mismo que tiene Mi aplicación de Notas con Python3 y GTK, la diferencia fundamental es que la aplicación GTK está hecha por completo en Python3 utilizando los bindings para GTK3, mientras que esta nueva aplicación está hecha en 2 archivos, el primero en un lenguaje llamado QML que pertenece a QT Project, es un lenguaje de marcado para interfaces QT y el segundo archivo es un script en Python3 que permite guardar y obtener las notas en cuestión. Los archivos son los siuientes:
main.qml
import QtQuick 2.15
import org.kde.plasma.core 2.0 as PlasmaCore
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
// Objeto padre con un id y un tamaño fijo
Item {
id: root
width: 300
height: 450
// Permite trabajar con scripts como el de Python3
PlasmaCore.DataSource {
id: dataSource
engine: 'executable'
connectedSources:[]
onNewData: loadData(data)
}
//--------------//
// Tab Section //
//-------------//
// Sección de pestañas
TabBar {
id: tabSection
width: parent.width
}
// Componente de pestañas, permite añadir pestañas
// a la barra de pestañas de manera dinamica
Component {
id: tabButton
TabButton {}
}
//---------------//
// Note Section //
//--------------//
// Permite decir como se van a crear las notas para cada pestaña
StackLayout {
id: noteSection
// Permite decir como se llenará el espacio en donde estará la nota
anchors.fill: parent - tabSection.height
width: root.width // El ancho debe ser el del padre
// El alto debe ser el del padre menos las secciones de pestañas y botones
height: root.height - tabSection.height - buttonSection.height
y: tabSection.height // Posición comenzando debajo de los pestañas
currentIndex: tabSection.currentIndex
}
// Componente de la nota como tal,
// este componente se va a crear dinamicamente
Component {
id: notePage
ScrollView {
width: root.width
focus: true
// Las barras de Scroll están desactivadas
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
TextArea {
width: parent.width
height: parent.height
focus: true
// El texto de la nota hace "Wrap" cuando sobrepasa el tamaño de la nota
wrapMode: TextEdit.WordWrap
}
}
}
//-----------------//
// Button Section //
//----------------//
RowLayout {
id: buttonSection
width: parent.width
spacing: 2
// La sección de botones debe estar posicionado
// después de las pestañas y las notas
y: tabSection.height + noteSection.height
Button {
id: addTabButton
icon.name: "add"
flat: true
// Permite abrir un dialogo para crear una nueva pestaña
onClicked: newTabNameDialog.open()
}
Button {
id: removeTabButton
icon.name: "remove"
flat: true
// Permite borrar una pestaña con todo su contenido
onClicked: removeTab()
}
// Rectángulo transparente que actua como "espaciador"
Rectangle{
anchors.fill: parent
color: "transparent"
anchors.leftMargin: addTabButton.width + removeTabButton.width
anchors.rightMargin: saveButton.width * 2
}
Button {
id: saveButton
icon.name: "dialog-ok"
flat: true
x: parent.width
// Permite guardar las notas
onClicked: saveData()
}
}
//----------//
// Dialogs //
//---------//
// Caja de diálogo que se ejecutará si el usuario
// intenta añadir una pestaña con un nombre ya existente
Dialog {
id: errorPopup
x: 0
y: 150
width: root.width
height: 100
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
}
// Caja de diálogo que aparece cuando el usuario intenta añadir una pestaña nueva
Dialog {
id: newTabNameDialog
x: 0
y: 150
width: root.width
height: 100
title: "Insert Tab Name"
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
standardButtons: Dialog.Ok
TextInput {
id: tabName
width: parent.width/2
wrapMode: TextInput.Wrap
focus: true
cursorVisible: true
}
onAccepted: checkTabNameAndAddIfNotExists()
onRejected: {
tabName.text = ""
}
}
//---------------------------------//
// Connection to Python "Backend" //
//--------------------------------//
// Permite ejecutar el script de Python3 para leer las notas
Connections {
target: plasmoid
onExpandedChanged: {
if (plasmoid.expanded) {
var url = Qt.resolvedUrl(".");
var exec = url.substring(7, url.length);
dataSource.connectedSources = ['python3 ' + exec + 'notes.py READ']
}
else {
dataSource.connectedSources = [];
}
}
}
//------------//
// Functions //
//-----------//
// Permite saber si el nombre que el usuario quiere poner a la pestaña
// ya existe entre la notas actuales
// No existe -> Pestaña será añadida
// Si existe -> Lanza un diálogo con el "error"
function checkTabNameAndAddIfNotExists() {
var tabNameStr = tabName.text
var currentTabNames = []
for (var i = 0; i < tabSection.count; i++) {
currentTabNames.push(tabSection.itemAt(i).text.toUpperCase())
}
tabName.text = ""
if (currentTabNames.includes(tabNameStr.toUpperCase())) {
errorPopup.title = 'Tab with name: \n\t' + tabNameStr + '\n\talready exists'
errorPopup.open()
} else {
addTab(tabNameStr)
}
}
// Añade una pestaña recibiendo el titulo y el contenido de la misma
function addTab(tabName, tabData) {
var newTab = tabButton.createObject(tabSection, {text: tabName})
var newView = notePage.createObject(noteSection, {})
getTextAreaAttachedToTab(newView).text = tabData
tabSection.addItem(newTab)
}
// Borra una pestaña con todo su contenido
function removeTab() {
var tabToBeRemoved = tabSection.itemAt(tabSection.currentIndex)
tabSection.removeItem(tabToBeRemoved)
}
// Permite ejecutar el script de Python3 para guardar las notas
function saveData() {
var url = Qt.resolvedUrl(".");
var exec = url.substring(7, url.length);
var notesDataStr = '"' + getTitleAndDataFromAllTabs() + '"'
dataSource.connectedSources = ['python3 ' + exec + 'notes.py WRITE ' + notesDataStr]
}
// Carga las notas y crea una pestaña con su contenido por cada una
function loadData(data) {
var notesDataStr = data['stdout']
if(notesDataStr.length) {
try {
var notesData = JSON.parse(notesDataStr)
if (tabSection.count < 1) {
Object.keys(notesData).forEach(keyName => {
addTab(keyName, notesData[keyName])
})
}
} catch (e) {
print(e)
}
}
}
// Función un poco espantosa que permite obtener
// el area de texto que pertenece a una pestaña
// Si encuentro una mejor forma la cambiaré
function getTextAreaAttachedToTab(tabObject) {
return tabObject.children[0].children[0].children[0]
}
// Permite obtener el titulo de la pestaña y su contenido
// para luego dar el formato deseado al texto previo a
// la operación de guardar
function getTitleAndDataFromAllTabs() {
const TITLE_TAG = '###'
var notesDataStr = ''
for (var i = 0; i < tabSection.count; i++) {
var tab = noteSection.itemAt(i)
notesDataStr += TITLE_TAG
notesDataStr += ' '
notesDataStr += tabSection.itemAt(i).text
notesDataStr += '\n'
var notePage = getTextAreaAttachedToTab(tab)
notesDataStr += notePage.text
if(!notesDataStr.endsWith('\n')) {
notesDataStr += '\n'
}
notesDataStr += '\n'
}
return notesDataStr
}
}
notes.py
El script de Python3 contiene los siguiente:
#!/usr/bin/env python3
# La clase NotesHandler realiza el mismo trabajo
# que en la aplicación GTK, por lo cual no es
# necesaria mayor explicación
class NotesHandler(object):
_TITLE_TAG = '###'
def __init__(self):
from os.path import expanduser
self.data = {}
# El path del las notas es estático
self.file_path = expanduser('~/.local/share/notes')
def read_data(self):
title = ''
paged_data = ''
try:
file = open(self.file_path, 'r+')
except FileNotFoundError:
file = open(self.file_path, 'w+')
with file as opened_file:
for line in opened_file:
data = line.strip().split('\n', 1)[0]
if line == '\n':
continue
if self._TITLE_TAG in line:
paged_data = ''
title = data.split(self._TITLE_TAG, 1)[1].strip()
else:
paged_data += data.strip() + '\n'
self.data[title] = paged_data
file.close()
def get_file_data(self):
return self.data
def write_data(self, data):
file = open(self.file_path, 'w')
file.write(data)
file.close()
# El script recibe dos argumentos
# READ -> Leer notas
# WRITE -> Escribir notas, para este caso tambié recibe las notas como String
if __name__ == '__main__':
from sys import argv, exit
# Si el número de argumentos recibidos es 1
# o es mayor que 3 se lanza un mensaje y termina la ejecución
# Por que 1 y mayor que 3?
# El primer argumento siempre es el path del script
if len(argv) == 1 or len(argv) > 3:
print(
'''This script needs the following arguments:
- action (READ,WRITE)
- notes data'''
)
exit(1)
notes_handler = NotesHandler()
# Si el argumento es "read o READ" se va a leer
# las notas e imprimirlas en consola como un JSON
if argv[1].upper() == 'READ':
import json
notes_handler.read_data()
print(json.dumps(notes_handler.get_file_data(), indent = 2))
# Si recibe "write o WRITE" debe recibir también las notas
# procederá a guardar las notas en el path especificado
elif argv[1].upper() == 'WRITE':
notes_handler.write_data(str(argv[2]))
else:
print('Argument: ' + str(argv[1]) + ' is not valid.\nMust be READ or WRITE!')
exit(1)
La aplicación es totalmente funcional, pero aún le quedan mejoras por hacer, tales como que los colores sean mas acordes a los diferentes tipos de temas de KDE Plasma, permitir el cambio del tamaño de la interfaz o permitir el cambio del path de las notas.
El código de esta aplicación se encuentra en: https://github.com/danesc87/tabbed-notes
Happy Hacking!!