9. Grid

When creating a graphical interface we have features that let us divide the content into different spaces. However, due to the nature of Emacs, the options are particularly exotic and do not resemble the ones we are used to.

  • frames: Each layout will be shown in its own buffer occupying all the available space.
  • panels or group of widgets: Within a buffer, create a horizontal or vertical division to subdivide the content. In Emacs they are called Windows. We even have a special window called a temporary minibuffer.
  • toolbar: Natively we are allowed to create buttons to interact with the application in the toolbar. In this course we will not explore its possibilities.

Now we are going to explore each of these options to make the interfaces more attractive and malleable.

Frames

In the previous lesson we have already learned how to create a layout per buffer and switch between them. Let's review the technique.

The key consists of defining one buffer per layout. Then, when we want to navigate between each layout, we will switch to the corresponding buffer or call the function that declares the target layout.

In the following example I am going to define a form with fields to calculate the area of a rectangle.

If we want to create a layout for the welcome screen, we will create a buffer with the name *welcome* and call the welcome-layout function that will define the 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 "The following application lets you calculate the area of a rectangle.")
  (setq input-height (widget-create 'editable-field
                  :size 5
                  :format "\n\nHeight: %v"))
  (setq input-width (widget-create 'editable-field
                   :size 5
                   :format "\n\nWidth: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
                    (result-layout))
                 "Calculate area")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

The first thing we do is switch to the *welcome* buffer and clear it. Then, we create the widgets we need for the form. In this case, a text field for the height and another for the width. Finally, we create a button to calculate the area and call the result-layout function that will define the layout to show the result. For now we have not defined this function, but we will do so in the next step.

To start the application we will call the function at the end of the script.

(welcome-layout)

Now we create the layout to show the result.

(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" "The area of the triangle is:" (calculate-area)))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
                    (welcome-layout)
                    (kill-buffer result--name-buffer))
                 "Close")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

The same structure of the welcome layout has been defined. The only difference is that we only show a message with the result and a button to close and go back.

In addition, since it is a temporary layout, when switching layouts, we delete the current buffer.

(kill-buffer result--name-buffer)

All the code joined together would form the following code:

;; 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)

;; Functions
(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 "The following application lets you calculate the area of a rectangle.")
  (setq input-height (widget-create 'editable-field
                  :size 5
                  :format "\n\nHeight: %v"))
  (setq input-width (widget-create 'editable-field
                   :size 5
                   :format "\n\nWidth: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
               (result-layout))
                 "Calculate area")
  (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" "The area of the triangle is:" (calculate-area)))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
               (welcome-layout)
               (kill-buffer result--name-buffer))
                 "Close")
  (use-local-map widget-keymap)
  (widget-setup)
  (display-line-numbers-mode 0)
  (widget-forward 1))

;; Init
(welcome-layout)

An important point is that we do not lose the values of the fields as we navigate between layouts, making it easy to retrieve values from different forms even if they are not present. You can create intermediate steps to request extra information without the previous values disappearing.

Panels

Temporary buffer

A temporary buffer is used to show output information or to request additional information.

(let ((buffer-name "*MyTemporaryBuffer*"))
  (with-temp-buffer-window
      buffer-name
      nil
      nil
    (with-current-buffer buffer-name
      (insert "Hello, world! This is my temporary buffer that will only appear at the bottom."))
      ;; Your layout
    ))

It is limited both in position, it can only be at the bottom, and in size, we cannot change it since it adjusts to the content.

If we adapt the previous example so the result is shown in a temporary buffer, the code would look as follows.

The code of the example would be the following:

;; 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)

;; Functions
(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 "The following application lets you calculate the area of a rectangle.")
  (setq input-height (widget-create 'editable-field
                  :size 5
                  :format "\n\nHeight: %v"))
  (setq input-width (widget-create 'editable-field
                   :size 5
                   :format "\n\nWidth: %v"))
  (widget-insert "\n\n")
  (widget-create 'push-button
                 :notify (lambda (&rest ignore)
               (result-layout)

               )
                 "Calculate area")
  (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 ;; New
      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" "The area of the triangle is:" (calculate-area)))
      (widget-insert "\n\n")
      (widget-create 'push-button
             :notify (lambda (&rest ignore)
                   (kill-buffer result--name-buffer)
                   (delete-window))
             "Close")
      (use-local-map widget-keymap)
      (widget-setup)
      (display-line-numbers-mode 0)
      (make-thread
       (lambda ()
     (widget-forward 1))))))

;; Init
(welcome-layout)

When creating the result layout, we have added the with-temp-buffer-window function that lets us create a temporary buffer. Inside it goes all the logic to show the result.

When we want to close the temporary buffer, we will call the delete-window function, and optionally kill-buffer to delete the buffer.

Another element to highlight is the way the widget-forward function has been invoked so the focus is positioned on the close button. It has been wrapped in a make-thread function so it runs in a different thread and does not block the buffer. It is done this way because the temporary buffer needs to know all the content to calculate its height, which causes the focus not to be moved until it has finished rendering. If we launch it in a parallel thread, we can move the focus when it is available.

Unfortunately it is limited in its configuration. We cannot decide what its position or size will be. For that we need to work with the tool for dividing the buffer into parts, or creating new windows.

Windows

For more precise control, we can create a panel with the split-window function that lets us divide the buffer into equal parts.

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

The first argument is the current window, the second is the size of the new window and the third is the position of the new window. In this case, below indicates that the new window will be created below the current window. You can use other options such as left or right. If the size is not indicated, nil, it will be divided into equal parts.

To switch between windows, we will use the other-window function.

(other-window 1)

The argument indicates the number of windows we want to jump. If we want to go back to the previous window, we will use a negative number.

However, you will not always know the order. The most practical thing is to keep storing the references of the windows we create so we can move easily between them using the select-window function.

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

To close a window, we will use the delete-window function.

(delete-window)

If we want to close another window, we need to select it before calling the function.

(other-window 1)
(delete-window)

Or store the reference, as before, and then create it and then close it by giving as the second argument the window we want to close.

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

(let ((mi-ventana (selected-window)))
  ;; Switch to the next window
  (other-window 1)
  ;; Close the top window
  (delete-window mi-ventana))

Closing a window does not delete the buffer, only the window.

In the following example we are going to bring together what we learned to create a text field where everything we type will be shown in another window but with the order of the letters reversed.

Check the following code:

;; -*- 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)

In this example, we have created two windows with their own buffer. In the first we have a text field that, when typing, and the second will be where the reversed text is shown by means of a label (item). We have relied on an init function to do the initial setup, dividing the buffer into two parts and calling the functions that define the layouts.

The most notable thing is the :notify function that changes the order of the letters and prints the result in the second window.

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

It is important that the target buffer is selected so the change is reflected in the correct window with with-current-buffer. Otherwise it will be shown in the current window, breaking the layout.

Activity 1

Modify activity 2 of lesson 8 so the result is shown in a different window.

Activity 2

Create an application to keep track of expenses. The application must have two windows. In the first window, the user will be able to enter the concept and the amount of the expense. In the second window, the list of expenses will be shown with the accumulated total.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Desafíos de programación atemporales y multiparadigmáticos

Desafíos de programación atemporales y multiparadigmáticos

Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.

Buy the book

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.

Comments

There are no comments yet.