9 de septiembre de 2003

Crear una funcionalidad Deshacer en cuadros de texto de Visual FoxPro

Artículo original: Creating Undo functionality in a Visual FoxPro TextBox
http://west-wind.com/weblog/posts/3296.aspx
Autor: Rick Strahl
Traducido por: Ana María Bisbé York


El cuadro de texto de Visual FoxPro no es precisamente un gran control como el que yo tengo en el Help Builder (http://www.west-wind.com/wwHelp/). Tuve que trabajarlo para hacer que funcione como si estuviera basado en un editor de textos que incluye formato. Pero al mismo tiempo no era capaz de encontrar una forma decente de sustitución. La mayoría de los controles ActiveX basados en textos son poco menos que un infierno (al menos en VFP) o son muy muy lentos si trata de enganchar algún evento COM a la clave al procesar, como necesito que haga el Help Builder.

En general el TextBox de VFP funciona bien, salvo en dos cosas:
  1. Existe un bug en el control que provoca que el control envuelva al original si existen avance de línea en un área específica del margen derecho. Puede causar avances de línea que sean "comidos" por el cuadro de texto con el texto que puede ser un salto súbito cuando el cuadro de texto es redimensionado u otros cuadros de texto entren  fuera del margen. Esto se puede ver como un bug muy oscuro; pero si trabaja modificando grandes cantidades de textos se lo encontrará muy pronto. Según Calvin Hsia se corrige en el VFP 9.0 SP1...
  2. Comportamiento para Deshacer. El cuadro de texto de Visual FoxPro no tiene un comportamiento para Deshacer - el buffer para Deshacer se pierde cada vez que hay cualquier tipo de actualización del dato. Esto incluye el enlace al origen de datos (ControlSource), establecer el valor explícitamente, cambiar SelText o incluso pasar texto al control. Se limpia también si se oprime la tecla Tab y sale del control e inmediatamente regresa. Todo esto es realmente limitado y no es un comportamiento estándar.
Hasta la salida del SP1 no puedo hacer nada con el punto 1; pero he pensado que puedo controlar mi propio Deshacer con buffer en mi control TextBox personalizado. Mientras hablaba con Calvin en SoutWest Fox comenzamos a colocar un mejor comportamiento en el TextBox; pero sobre el comportamiento Deshacer está profundamente dentro del runtime de VFP y cambiar eso es algo como romper mucho del código que ya está. Por tanto no hay ayuda en este sentido. Entonces Calvin me sugirió... escribe el tuyo propio....

Lo primero que pensé - sí, bien. Controlar Deshacer buffer con código Fox es muy lento y ocuparía mucha memoria, porque hay que guardar el buffer entero del valor del control ya que los eventos InteractiveChange y ProgrammaticChange no brindan información de qué es lo que ha cambiado, entonces, no hay una forma fácil de capturar cuál de los cambios es el que hay que deshacer.


Después de pensarlo un poco, intenté de todas formas para observar cómo van a trabajar las cosas y entonces trabajé con estas variantes:
  • Comportamiento opcional para Deshacer personalizado
  • Deshacer que sobreviva a cambios por programa
  • Deshacer que sobreviva al foco de otro control
  • Deshacer que se limpie sólo con Refresh o un Clear explícito del buffer de Deshacer
  • Rehacer que permita Deshacer lo deshecho
He aquí un código que se encarga de un control que controla un comportamiento Deshacer en mi clase TextBox.
DEFINE CLASS wwhtmleditbox AS editbox
  OLEDropMode = 1
  FontName = "Tahoma"
  FontSize = 8
  Alignment = 0
  AllowTabs = .T.
  Height = 188
  ScrollBars = 2
  Width = 443
  oundobuffer = .NULL.
  *-- La última vez, UndoBuffer fue actualizado  en segundos. 
  *-- Internamente el valor utilizado mantiene cada carácter 
  *-- por tenerlo añadido al buffer de deshacer. 
  nlastundoupdate = 0
  *-- El indicador utilizado inhabilita los cambios de programación
  *-- en el bufer de deshacer.
  lundonoprogrammaticchange = .F.
  lundotracking = .F.
  oredobuffer = .NULL.
  Name = "wwhtmleditbox"

  PROCEDURE Init
    this.oUndoBuffer = CREATEOBJECT("wwNameValueCollection")
    this.oRedoBuffer = CREATEOBJECT("wwNameValueCollection")
  ENDPROC

  PROCEDURE undo
    IF THIS.lundotracking AND THIS.oUndoBuffer.Count > 0
      THIS.lUndoNoProgrammaticChange = .T.
      this.oRedoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
      lcValue = this.oUndoBuffer.aItems[this.oUndoBuffer.Count,2] 
      IF lcValue = this.Value AND this.oUndoBuffer.Count > 1
        THIS.Value = this.oUndoBuffer.aItems[this.oUndoBuffer.Count-1,2] 
        this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
      ELSE
        this.Value = lcValue
      ENDIF
      this.SelStart = VAL(this.oUndoBuffer.aItems[this.oUndoBuffer.Count,1])
      THIS.lUndoNoProgrammaticChange = .F.
      this.oUndoBuffer.Remove(this.oUndoBuffer.Count)
    ENDIF
  ENDPROC

  PROCEDURE redo
    IF THIS.lundotracking AND THIS.oRedoBuffer.Count > 0
      THIS.lUndoNoProgrammaticChange = .T.
      this.Value = this.oRedoBuffer.aItems[this.oReDoBuffer.Count,2]
      this.SelStart = VAL(this.oRedoBuffer.aItems[this.oRedoBuffer.Count,1])
      THIS.lUndoNoProgrammaticChange = .F.
      this.oRedoBuffer.Remove(this.oRedoBuffer.Count)
    ENDIF
  ENDPROC

  PROCEDURE KeyPress
    LPARAMETERS nKeyCode, nShiftAltCtrl
    *** No desea oprimir ESC para eliminar el contenido del campo.
    IF nKeyCode = 27
      *** Se come la tecla, la ignora
      NODEFAULT
    ENDIF
    *** Ctrl-Z
    IF THIS.lUndoTracking 
      IF nKeyCode = 26
        *** Debe verificar la tecla Ctrl-<- la que tiene número 26
        DECLARE INTEGER GetKeyState IN WIN32API INTEGER
        IF GetKeyState(0x25) > -1
          THIS.Undo()
          NODEFAULT
        ENDIF
      ENDIF
      *** Rehacer Ctrl-R
      IF nKeyCode = 18
        THIS.Redo() 
        NODEFAULT
      ENDIF
    ENDIF 
  ENDPROC

  PROCEDURE ProgrammaticChange
    IF THIS.lUndoTracking AND !THIS.lUndoNoProgrammaticChange
      this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
      this.oRedoBuffer.Clear()
    ENDIF 
  ENDPROC

  PROCEDURE InteractiveChange
    IF THIS.lUndoTracking
      *** Actualizar sólo en la segunda mitad del intervalo, 
      *** entonces, si escribe varias letras va en lote 
      IF SECONDS() - THIS.nLastUndoUpdate < 1
        this.nLastUndoUpdate = SECONDS()
        RETURN
      ENDIF
      *** Solo deshace la escritura de la última palabra
      IF LASTKEY() = 32 OR LASTKEY() = 13 OR LASTKEY() = 44 OR ;
        LASTKEY() = 46 OR LASTKEY() = 9
        this.oUndoBuffer.FastAdd(TRANSFORM(this.SelStart),this.Value)
        this.oRedoBuffer.Clear()
        this.nLastUndoUpdate = SECONDS()
      ENDIF
    ENDIF
  ENDPROC

  PROCEDURE Refresh
    IF THIS.lUndoTracking
      THIS.oUndobuffer.Clear()
    ENDIF
  ENDPROC
ENDDEFINE

Vea que este código tiene una dependencia que no incluyo aquí. Estoy utilizando una clase de usuario llamada NameValueCollection la que guarda el nombre y el valor en una matriz. Puede cambiar este código para utilizar una Collection y un objeto que guarde el valor del buffer en la posición SelStart.
La idea es esencialmente que cada InteractiveChange y ProgrammaticChange son monitoreados y potencialmente escritos fuera del valor de la colección UndoBuffer. El método InteractiveChange se  alterna de tal forma que solo escribe fuera el dato si el usuario en realidad no lo está escribiendo activamente y si el cursor está en el límite de una palabra. Esto reduce tremendamente la cantidad de valores que se guardan. Parece  que otras aplicaciones como Word utilizan un proceder similar aunque el comportamiento de Word es algo diferente.

Vea además este código
IF nKeyCode = 26
  *** Debe verificar Ctrl-<- el cual es  26
  DECLARE INTEGER GetKeyState IN WIN32API INTEGER
  IF GetKeyState(0x25) > -1
    THIS.Undo()
    NODEFAULT
  ENDIF
ENDIF
En su infinita sabiduría alguien decidió que el mapa de código de teclas (KeyCode) fuera 26 para Ctrl-Z y para Ctrl-Flecha izquierda, por lo que no hay una forma sencilla de decir el carácter. En su lugar, tiene que hacer otra verificación sobre el KeyCode para ver si tiene una Flecha izquierda (0X25). Si devuelve -127 ó -128. Diga que esto es HACK; pero funciona. Fue simpático por unos minutos tener Ctrl+Flecha izquierda atado a la tecla Deshacer (Retroceso). Menos mal que decidí generar un comportamiento de Rehacer desde el inicio...

Ahora he colocado esto en el Help Builder y como yo trabajo con los documentos del Web Connection 5.0 (http://www.west-wind.com/wconnect/) utilizo mucho características con tópicos muy largos. Entonces, este comportamiento se ve muy bien. No he visto problemas de rendimiento ni por la memoria ni nada apreciable mientras escribo.

Puedo imaginar que puedo marcar este en mi lista de deseos para VFP que nunca iba a tener.

No hay comentarios. :

Publicar un comentario