Lección 9: Grid | Curso de UI Emacs Lisp

Lección 9: Grid

En la creación de una Interfaz gráfica disponemos de características que nos permiten dividir el contenido en diferentes espacios. Sin embargo, por la naturaleza de Emacs, las opciones son particularmente son exóticas y no se parecen a las que estamos acostumbrados.

  • frames o marcos: Cada layout se mostrará en su propio buffer ocupando todo el espacio disponible.
  • paneles o grupo de widgets: Dentro de un buffer, crear una división horizontal o vertical para subdividir el contenido. En emacs se denominan Ventanas (windows). Incluso disponemos de una ventana especial llamada minibuffer temporales.
  • barra de herramientas: Nativamente se nos permite crear botones para interactuar con la aplicación en la barra de herramientas. En este curso no exploraremos sus posibilidades.

Ahora vamos a explorar cada una de estas opciones para hacer las interfaces más atractivas y maleables.

Frames

En la lección anterior ya hemos aprendido como crear un layout por buffer y cambiar entre ellos. Repasemos la técnica.

La clave consiste en definir un buffer por layout. Mientras que cuando queramos navegar entre cada layout, cambiaremos al buffer correspondiente o llamaremos a la función que declara el layout destino.

En el siguiente ejemplo voy a definir un formulario con campos para calcular el área de un rectángulo.

Si queremos crear un layout para la pantalla de bienvenida, crearemos un buffer con el nombre *welcome* y llamaremos a la función welcome-layout que definirá el layout.

(defun welcome-layout ()
  "Create the main layout for the welcome screen."
  (switch-to-buffer welcome--name-buffer)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (erase-buffer)
  (widget-insert "\n\n")
  (widget-create 'item :value "La siguiente aplicación te permite calcular el área de un rectángulo.")
  (setq input-height (widget-create 'editable-field
				  :size 5
				  :format "\n\nAltura: %v"))
  (setq input-width (widget-create 'editable-field
				   :size 5
				   :format "\n\nAncho: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
			        (result-layout))
                 "Calcular área")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

Lo primero que hacemos es cambiar al buffer *welcome* y limpiarlo. Después, creamos los widgets que necesitamos para el formulario. En este caso, un campo de texto para la altura y otro para el ancho. Por último, creamos un botón para calcular el área y llamamos a la función result-layout que definirá el layout para mostrar el resultado. De momento no hemos definido esta función, pero lo haremos en el siguiente paso.

Para iniciar la aplicación llamaremos a la función al final del script.

(welcome-layout)

Ahora creamos el layout para mostrar el resultado.

(defun result-layout ()
  "Create the main layout for the result screen."
  (switch-to-buffer result--name-buffer)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (erase-buffer)
  (widget-insert "\n\n")
  (widget-create 'item :value (format "%s %s" "El área del triángulo es:" (calculate-area)))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
                    (welcome-layout)
                    (kill-buffer result--name-buffer))
                 "Cerrar")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

Se ha definido la misma estructura del layout de bienvenida. La única salvedad es que solo mostramos un mensaje con el resultado y un botón para cerrar para volver.

Además, como es un layout temporal, al cambiar de layout, eliminamos el buffer actual.

(kill-buffer result--name-buffer)

Todo el código unido formaría el siguiente código:

;; Imports
(require 'widget)

(eval-when-compile
  (require 'wid-edit))

;; Variables
(defvar welcome--name-buffer "*welcome*")
(defvar result--name-buffer "*result*")
(defvar input-height)
(defvar input-width)
(defvar label-result)

;; Funciones
(defun calculate-area ()
  (let ((long (string-to-number (widget-value input-height)))
	(width (string-to-number (widget-value input-width))))
    (* long width)))

;; Layouts
(defun welcome-layout ()
  "Create the main layout for the welcome screen."
  (switch-to-buffer welcome--name-buffer)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (erase-buffer)
  (widget-insert "\n\n")
  (widget-create 'item :value "La siguiente aplicación te permite calcular el área de un rectángulo.")
  (setq input-height (widget-create 'editable-field
				  :size 5
				  :format "\n\nAltura: %v"))
  (setq input-width (widget-create 'editable-field
				   :size 5
				   :format "\n\nAncho: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
			   (result-layout))
                 "Calcular área")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

(defun result-layout ()
  "Create the main layout for the result screen."
  (switch-to-buffer result--name-buffer)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (erase-buffer)
  (widget-insert "\n\n")
  (widget-create 'item :value (format "%s %s" "El área del triángulo es:" (calculate-area)))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
			   (welcome-layout)
			   (kill-buffer result--name-buffer))
                 "Cerrar")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

;; Init
(welcome-layout)

Un punto importante es que no perdemos los valores de los campos según navegamos entre layouts, haciendo que sea sencillo rescatar valores de diferentes formularios aunque no estén presentes. Puedes crear pasos intermedios para solicitar información extra sin que desaparezcan los valores anteriores.

Paneles

Buffer temporal

Un buffer temporal se utiliza para mostrar la salida de información o para solicitar información adicional.

(let ((buffer-name "*MiBufferTemporal*"))
  (with-temp-buffer-window
      buffer-name
      nil
      nil
    (with-current-buffer buffer-name
      (insert "¡Hola, mundo! Este es mi buffer temporal que solo aparecerá en la parte inferior."))
      ;; Tu layout
    ))

Esta limitado tanto en posición, solo puede estar en la parte inferior, como en tamaño, no podemos cambiarlo ya que se ajusta al contenido.

Si adaptamos el ejemplo anterior para que el resultado se muestre en un buffer temporal, el código quedaría de la siguiente manera.

El código del ejemplo sería el siguiente:

;; Imports
(require 'widget)

(eval-when-compile
  (require 'wid-edit))

;; Variables
(defvar welcome--name-buffer "*welcome*")
(defvar result--name-buffer "*result*")
(defvar input-height)
(defvar input-width)
(defvar label-result)

;; Funciones
(defun calculate-area ()
  (let ((long (string-to-number (widget-value input-height)))
	(width (string-to-number (widget-value input-width))))
    (* long width)))

;; Layouts
(defun welcome-layout ()
  "Create the main layout for the welcome screen."
  (switch-to-buffer welcome--name-buffer)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (erase-buffer)
  (widget-insert "\n\n")
  (widget-create 'item :value "La siguiente aplicación te permite calcular el área de un rectángulo.")
  (setq input-height (widget-create 'editable-field
				  :size 5
				  :format "\n\nAltura: %v"))
  (setq input-width (widget-create 'editable-field
				   :size 5
				   :format "\n\nAncho: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
			   (result-layout)

			   )
                 "Calcular área")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

(defun result-layout ()
  "Create the main layout for the result screen."
  (with-temp-buffer-window ;; Nuevo
      result--name-buffer
      nil
      nil
    (with-current-buffer result--name-buffer
      (switch-to-buffer-other-window result--name-buffer)
      (kill-all-local-variables)
      (let ((inhibit-read-only t))
	(erase-buffer))
      (remove-overlays)
      (erase-buffer)
      (widget-insert "\n\n")
      (widget-create 'item :value (format "%s %s" "El área del triángulo es:" (calculate-area)))
      (widget-insert "\n\n")
      (widget-create 'push-button
		     :notify (lambda (&rest ignore)
			       (kill-buffer result--name-buffer)
			       (delete-window))
		     "Cerrar")
      (use-local-map widget-keymap)
      (widget-setup)
      (display-line-numbers-mode 0)
      (make-thread
       (lambda ()
	 (widget-forward 1))))))

;; Init
(welcome-layout)

A la hora de crear el layout de resultado, hemos añadido la función with-temp-buffer-window que nos permite crear un buffer temporal. Dentro de esta toda la lógica para mostrar el resultado.

Cuando queramos cerrar el buffer temporal, llamaremos a la función delete-window, y opcionalmente kill-buffer para eliminar el buffer.

Otro elemento a destacar en la manera que se ha invocado la función widget-forward para que el foco se posicione en el botón de cerrar. Se ha envuelto en una función make-thread para que se ejecute en un hilo diferente y no bloquee el buffer. Se realiza de esta manera porque el buffer temporal necesita conocer todo el contenido para calcular su altura, lo que provoca que no se pueda mover el foco hasta que no se haya terminado de renderizar. Si lo lanzamos en un hilo paralelo, podremos mover el foco cuando esté disponible.

Lamentablemente esta limitado en su configuración. No podemos decidir cual será su posición o tamaño. Para ello necesitamos trabajar con la herramienta de dividir el buffer en partes, o crear nuevas ventanas.

Ventanas

Para tener un control más preciso, podemos crear un panel con la función split-window que nos permite dividir el buffer en partes iguales.

(split-window (selected-window) 10 'below)

El primer argumento es la ventana actual, el segundo es el tamaño de la nueva ventana y el tercero es la posición de la nueva ventana. En este caso, below indica que la nueva ventana se creará debajo de la ventana actual. Puedes usar otras opciones como left o right. Si no se indica el tamaño, nil, se dividirá en partes iguales.

Para cambiar entre ventanas, usaremos la función other-window.

(other-window 1)

El argumento indica el número de ventanas que queremos saltar. Si queremos volver a la ventana anterior, usaremos un número negativo.

No obstante, no siempre sabrás el orden. Lo más práctico es ir guardando las referencias de las ventanas que vayamos creando para movernos fácilmente entre ellas usando la función select-window.

(let* ((primera-ventana (selected-window))
      (segunda-ventana (split-window (selected-window) 10 'below)))
  (select-window segunda-ventana))

Para cerrar una ventana, usaremos la función delete-window.

(delete-window)

Si queremos cerrar otra ventana, necesitamos seleccionarla antes de llamar a la función.

(other-window 1)
(delete-window)

O guardar la referencia, como antes, y después crearla y después cerrarla dando como segundo argumento la ventana que queremos cerrar.

(split-window (selected-window) 10 'below)

(let ((mi-ventana (selected-window)))
  ;; Cambia a la siguiente ventana
  (other-window 1)
  ;; Cerrar la ventana superior
  (delete-window mi-ventana))

Cerrar una ventana no elimina el buffer, solo la ventana.

En el siguiente ejemplo vamos a recoger lo aprendido para crear un campo de texto donde todo lo que escribamos se mostrará en otra ventana pero con el orden de las letras invertido.

Revisa el siguiente código:

;; -*- coding: utf-8 -*-
;; Imports
(require 'widget)

(eval-when-compile
  (require 'wid-edit))

;; Variables
(defvar my-buffer-name-form "*Reverse text Form*") ;; Buffer for form
(defvar my-window-form) ;; Window for form
(defvar my-buffer-name-result "*Reverse text Result*") ;; Buffer for result
(defvar my-window-result) ;; Window for result
(defvar my-input-text) ;; Widget text for input
(defvar my-label-output) ;; Widget label for output

;; Functions
(defun init ()
  "Make the initial setup"
  ;; Kill buffers
  (when (buffer-live-p my-buffer-name-form) (kill-buffer my-buffer-name-form))
  (when (buffer-live-p my-buffer-name-result) (kill-buffer my-buffer-name-result))
  ;; Make the form window
  (setq my-window-form (selected-window))
  (my-layout-form)
  ;; Slit the window
  (setq my-window-result (split-window my-window-form nil 'right))
  ;; Make the result window
  (select-window my-window-result)
  (my-layout-result)
  ;; Go back to the form window
  (select-window my-window-form)
  (widget-forward 1))

;; Layouts
(defun my-layout-form ()
  "Create the form layout"
  (interactive)
  (switch-to-buffer my-buffer-name-form)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  ;; Widgets
  (widget-insert "\nReverse text \n\n")
  (setq my-input-text (widget-create 'text
				     :help-echo "Type the text to reverse"
				     :notify (lambda (widget &rest ignore)
					       (with-current-buffer my-buffer-name-result
						 (widget-value-set my-label-output (nreverse (widget-value widget)))))
				     :format "%v"))
  (use-local-map widget-keymap)
  (widget-setup))

(defun my-layout-result ()
  "Create the form layout"
  (switch-to-buffer my-buffer-name-result)
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  ;; Widgets
  (setq my-label-output (widget-create 'item ""))
  ;; End widgets
  (use-local-map widget-keymap)
  (widget-setup))

;; Init
(init)

En este ejemplo, hemos creado dos ventanas con su propio buffer. En la primera disponemos de un campo de texto que al escribir, y la segunda será donde se muestre el texto invertido por medio de un label (item). Nos hemos apoyado en una función init para hacer la configuración inicial dividiendo el buffer en dos partes y llamando a las funciones que definen los layouts.

Lo más destacable es la función :notify que cambia el orden de las letras he imprime el resultado en la segunda ventana.

(with-current-buffer my-buffer-name-result
    (widget-value-set my-label-output (nreverse (widget-value widget)))))

Es importante que el buffer destino esté seleccionado para que el cambio se refleje en la ventana correcta con with-current-buffer. En caso contrario se mostrará en la ventana actual rompiendo el layout.

Actividad 1

Modifica la actividad 2 de la lección 8 para que el resultado se muestre en una ventana diferente.

Actividad 2

Crea una aplicación para llevar un registro de gastos. La aplicación debe tener dos ventanas. En la primera ventana, el usuario podrá introducir el concepto y el importe del gasto. En la segunda ventana, se mostrará el listado de gastos con el total acumulado.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me ayudas?

Comprame un café
Pulsa sobre la imagen

No te sientas obligado a realizar una donación, pero cada aportación mantiene el sitio en activo logrando que continúe existiendo y sea accesible para otras personas. Además me motiva a crear nuevo contenido.

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario