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

notes app with flat buttons

notes with tab name


notes app with buttons above

notes with tabs on the left side

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!!

You may also like...