Actualización en la App de Notas con Python y Gtk3
Holas, como expliqué en mi post anterior Mi aplicación de Notas con Python3 y GTK decidí crear mi propia aplicación de notas debido a la inconformidad que tengo con la mayoría (o tal vez todas) de las aplicaciones de sticky notes que hay disponibles para GNU/Linux.
El día de hoy les traigo algunas actualizaciones que he realizado, tanto en el script Python como su homónimo en Bash, las actualizaciones que he realizado son las siguientes:
Python
- La aplicación ahora no recibe argumentos en su ejecución
- Tiene configuración por defecto
- Puede cargar la configuración desde un archivo en el path
/home/{user}/.config/notes.conf
- Si la configuración no existe la crea
- Los botones han sido actualizados para una mejor estética e integración
- Se auto-guardan todas las notas después de un tiempo predeterminado
- Se auto-guardan todas las notas si la aplicación pierde foco
- El dialogo que pide el nombre para una nueva nota ahora tiene icono
- La aplicación ahora es
sticky
por lo cual aparece en todos los escritorios virtuales - No se inicia siempre en la esquina inferior derecha, ahora se inicia donde esté el puntero del ratón
- Se pueden personalizar los siguientes aspectos:
- Tamaño (width x height)
- El tipo del TAG para el título de la nota, por defecto es ###
- El path de las notas
- El tiempo de auto-guardado
- Posición del bloque de notas respecto al bloque de botones
- Posición de las pestañas de las notas
- El comportamiento de la aplicación
- Los comportamientos son: Normal, Dock o Desktop
- En modo Normal podemos decidir si la aplicación puede estar encima de otras aplicaciones
- En modo Dock siempre está encima de las demás aplicaciones y es estático
- En modo Desktop siempre está detrás como si estuviera pegada al fondo de pantalla
Bash Script
- Se eliminó la variable que hace relación al path de las notas
- No se envía ningún argumento a la aplicación Python
Código modificado
Puesto que ya expliqué en el post anterior que realiza cada parte del código de la aplicación, no lo voy a hacer nuevamente, en esta ocasión unicamente les voy a mostrar lo que hacen los cambios más significativos.
En verisón anterior de la aplicación prácticamente la totalidad de las opciones configurables estabas quemadas o hardcoded en el código, por lo tanto el cambio en ellas fue solo acceder a la variable respectiva en la configuración, dicho esto la clase de configuración es la más afectada por este cambio y es la que voy a explicar a continuación.
class Config(object):
DEFAULT_CONFIG = """[General]
# Tamaño de la ventana, por defecto 300 x 500
Width = 300
Height = 500
# El "TAG" es utilizado para separar las
# notas en el archivo de texto de las mismas
TitleTag = ###
# Filepath de las notas, por defecto ~/.local/share/notes
NotesPath = ~/.local/share/notes
# Tiempo en segundos para el auto-guardado
AutoSaveTime = 20
[Positions]
# Permite poner las notas y sus pestañas
# encima o debajo del bloque de botones
# valores admitidos -> Top, Bottom
Notes = Top
# Posición de las pestañas en el bloque de notas,
# valores permitidos -> Left, Right, Top, Bottom
Tabs = Bottom
[Behavior]
# HintType se refiere al comportamiento de la aplicación
# - Normal,
# - Dock (no se puede mover y estará siempre
# encima de las otras aplicaciones),
# - Desktop (Estará siempre debajo de todas
# las aplicaciones, como pegada al fondo de pantalla)
HintType = Normal
# Solo si "HintType" es Normal se puede elegir
# que esté o no encima de las demás aplicaciones
KeepAbove = False
"""
# Los valores configurables de la aplicación
APP_WIDTH = 300
APP_HEIGHT = 500
TITLE_TAG = '###'
NOTES_PATH = '~/local/share/notes'
AUTOSAVE_TIME = 20
NOTES_POSITION = (0, 1)
TABS_POSITION = 3
HINT_TYPE = 0
KEEP_ABOVE = True
# Mapas de ayuda para poder trasformar las configuraciones de
# posiciones y comportamientos a sus valores correctos
map_notes_position = {'TOP': (0, 1), 'BOTTOM': (1, 0)}
map_tabs_position = {'LEFT': 0, 'RIGHT': 1, 'TOP': 2, 'BOTTOM': 3}
map_hint_type = {'NORMAL': 0, 'DOCK': 6, 'DESKTOP': 7}
def __init__(self):
import configparser
from os.path import expanduser
# "expanduser" nos permite obtener "/home/{user}" mediante "~"
# por lo tanto el path de configuración es "/home/{user}/.config/notes.conf"
config_file_path = expanduser('~/.config/notes.conf')
# Configparser permite leer el archivo de configuración estilo ".ini"
# notes.conf es un tipo de archivo ".ini"
config = configparser.ConfigParser()
config.read(config_file_path)
# Si el archivo está vacío lee la configuración del String de ejemplo
# y luego procede a guardar dicha configuración en el path de configuración
if len(config.sections()) == 0:
config.read_string(self.DEFAULT_CONFIG)
with open(config_file_path, 'w') as cf:
cf.write(self.DEFAULT_CONFIG)
else:
# Caso contrario revisa que cada sección exista
# para poder cargar la configuración personalizada
# en caso de no existir una sección utiliza los valores por defecto
if config.has_section('General'):
self.add_general_config(config['General'])
if config.has_section('Positions'):
self.add_positions_config(config['Positions'])
if config.has_section('Behavior'):
self.add_behavior_config(config['Behavior'])
# Debido a que algunas configuraciones tienen que ser
# convertidas es necesario validar que esten correctas
self.validate_map_properties(
{
'Notes': self.NOTES_POSITION,
'Tabs': self.TABS_POSITION,
'HintType': self.HINT_TYPE
}
)
# Actualizamos las variables globales de la
# clase config relacionadas a la sección "General"
def add_general_config(self, general):
Config.APP_WIDTH = self.get_int(general.get('Width'), 'Width') \
if general.get('Width') else self.APP_WIDTH
Config.APP_HEIGHT = self.get_int(general.get('Height'), 'Height') \
if general.get('Height') else self.APP_HEIGHT
Config.TITLE_TAG = general.get('TitleTag') \
if general.get('TitleTag') else self.TITLE_TAG
Config.NOTES_PATH = general.get('NotesPath') \
if general.get('NotesPath') else self.NOTES_PATH
Config.AUTOSAVE_TIME = self.get_int(
general.get('AutoSaveTime'), 'AutoSaveTime'
) if general.get('AutoSaveTime') else self.AUTOSAVE_TIME
# Actualizamos las variables globales de la
# clase config relacionadas a la sección "Positions"
def add_positions_config(self, positions):
Config.NOTES_POSITION = \
self.map_notes_position.get(positions.get('Notes').upper()) \
if positions.get('Notes') else self.NOTES_POSITION
Config.TABS_POSITION = \
self.map_tabs_position.get(positions.get('Tabs').upper()) \
if positions.get('Tabs') else self.TABS_POSITION
# Actualizamos las variables globales de la
# clase config relacionadas a la sección "Behavior"
def add_behavior_config(self, behavior):
Config.HINT_TYPE = \
self.map_hint_type.get(behavior.get('HintType').upper()) \
if behavior.get('HintType') else self.APP_WIDTH
try:
from distutils.util import strtobool
Config.KEEP_ABOVE = strtobool(behavior.get('KeepAbove')) \
if behavior.get('KeepAbove') else self.KEEP_ABOVE
except ValueError:
print("'{type} = {value}' is not a valid Boolean".format(value=behavior.get('KeepAbove'), type='KeepAbove'))
import sys
sys.exit(1)
# Método estático que obtiene el valor numérico de una propiedad
# Caso contrario imprime un error y termina la ejecución
@staticmethod
def get_int(value, property_name):
try:
return int(value)
except ValueError:
print("'{type} = {value}' is not valid number".format(value=value, type=property_name))
import sys
sys.exit(1)
# Método estático que valida las propiedades que tienen que ser convertidas
@staticmethod
def validate_map_properties(properties):
for key, value in properties.items():
if value is None:
print("{type} property has an invalid value, please fix it".format(type=key))
import sys
sys.exit(1)
Esta clase se ejecuta al inicio para poder tener cargadas las configuraciones, para poder acceder a las mismas se utiliza de la siguiente forma Config.APP_WIDTH
Mejora estética de los botones
Para poder obtener una mejora estética en los botones, los mismos tienen dos cambios, que son los siguientes:
# Ahora el boton se crea utilizando ".new_from_icon_name" y no utilizando un "Label"
notebook_page_add_button = Gtk.Button.new_from_icon_name(icon_name='add', size=Gtk.IconSize.BUTTON)
# Se quita el estilo de relieve lo que hace que el boton se vea plano y no resalte
notebook_page_add_button.set_relief(Gtk.ReliefStyle.NONE)
Estos cambios están introducidos en la clase
NoteBookWindow::create_buttons_box_and_return()
Icono en el dialogo para el nombre de una nueva nota
Para poder tener un icono en el dialogo que pide el nombre para nuevas notas únicamente se añadió la siguiente linea self.set_default_icon_name('mynotes')
en la clase NewTabWindow
Habilidad de ser sticky
Para poder hacer que la ventana sea sticky sin depender de ninguna regla en algún gestor de ventanas como openbox, xfwm4, fluxbox, kwin, etc se añadió self.stick()
a la clase NoteBookWindow
Bash script
El script en bash solo revisa si la aplicación se está ejecutando para saber si debe ejecutarla o cerrarla, por lo que ahora es más sencillo
SCRIPT_PATH=${0%/*}
NOTES_APP_NAME='notes.py'
NOTES_RUNNING=$(ps aux | grep -i "$NOTES_APP_NAME" | grep -E 'python3')
if [[ -z "$NOTES_RUNNING" ]]; then
exec python3 "$SCRIPT_PATH/$NOTES_APP_NAME" > /dev/null &
else
kill -9 $(echo "$NOTES_RUNNING" | awk '{print $2}')
fi
Imágenes
Aquí les dejo algunas imágenes sobre cómo se ve actualmente la aplicación
El código de esta aplicación está en mis dotfiles:
https://github.com/danesc87/dotfiles/
Dentro de la carpeta: .config/custom_scripts
Los archivos son: notes.py y notes.sh
Eso ha sido todo por hoy.
Happy Hacking!!