jueves, 27 de marzo de 2014

A PC muerto, PC puesto


Mi nueva máquina, un capricho.

La compre en  http://www.framoiz.com/ , decir de verdad que el trato y el favor de cambiarme la torre sin problemas ha sido espectacular.

Al final me decidí por un ;

  • AMD FX-8350 8 núcleos 
  • 16 GIGAS de RAM 1600
  • 120 Disco SSD Samsung
  • 1 Tera Disco Seagate Barracuda
  • La placa 990XDA-UD3, que merece un poco de atención.
  • nVidia GT 750 2Gigas
Como no, Ubuntu 13.10 con GnomeShell. De verdad, cada vez que uso GnomeShell, más me gusta.
Realmente es espectacular y lo limpio que se ve, un gran trabajo de los chicos de Gnome.

Este es escritorio actual, simple y limpio;




La odisea a sido la torre. La que pillé al principio, tenía 4 ventiladores, 2 por la parte de arriba.
Demasiado ruido, y no podían controlarse. Al final, al día siguiente, me hice con otra torre, un pelín más cara, pero con control de los ventiladores, además, los puedo parar manualmente, importante para noches de programación ;-)


Lo de la placa Gigabyte 990XDA-UD3 ha sido de traca y sigue siendo, aunque no tengo USB2, los USB3 funcionan , pero el colmo fue el tema de la red.

Instalo Ubuntu, ni rastro de ella. Probé LiveCD de Debian, LinuxMint, y tampoco. Estuve retocando parámetros de la bios, fuera todo rastro del engendro del EUFI, etc..., mirando por Internet , y nada.

Al final me cansé y me dió por buscar el driver. Lo encontré, y lo único que hice es ejecutar el autorun.sh como root.

Aqui está http://r8168dl.appspot.com/,  o buscar r8168-8.038.00.tar.bz2.
Eso elimina el r8169 que no funciona, y coloca este que va como la seda.

Ahora queda averiguar el tema de los puertos USB2.

Lo que si es impresionante es Steam de Valve. ES ALUCINANTE!! 
Espero que la nena no se percate de ello, sino, me quedo sin ordenador ;-)

video


También el Nexus 4 lo pincho y todo disponible sin hacer nada. Realmente estoy muy muy contento.
Ahora a meterle todo el tema de programación, máquinas virtuales, etc...



sábado, 22 de marzo de 2014

NAS, muerte HP y resurrección del S.O. del pingüino


Hace unos días, para el día del padre, me regalé yo mismo, que mejor que uno mismo para saber que es lo que uno necesita ;-), un NAS Synologic DS114.

La verdad es que la máquina es una pasada. La idea era apagar el PC, que lo tenia 24 horas encendido, haciendo de servidor de MySql, Servidor de DNLA, Git server, etc..

Así que decido dar el paso, y traspasar los BD de MySql al NAS con MariaDB, crear el repo Git y enchufar un disco externo para la biblioteca multimedia.

¡¡OLE OLE OLE!!

Lo tengo todo listo, y decido apagar la máquina HP Pavilion
La verdad que esta máquina a estado a la altura, excepto un cambio de fuente de alimentación, y una tarjeta un poco más potente. Me ha durado más de 7 años, y ha estado prácticamente los 3 últimos años sin apagarse.
A sido la compañera en la cual he desarrollado cantidad de software y en la que los primeros años la dediqué exclusivamente a MediaCenter. Descanse en paz. R.I.P

Pero ¿ que pasa ahora con todos mis proyectos en mi Ubuntu 10.04 ? No quiero cambiar de distro, porque sería demasiado costoso el cambio para adaptar el software que tengo creado.
Así que miro una máquina que tenia para hacer una recreativa, le quito el disco duro y le pongo el del HP, arranco, y:

¡¡El poder del pingüino!! 

Me dice que la tarjeta Nvidia no se encuentra, que si quiero usar una estandard o poner los que se detectan ahora, le digo que si, y ya estoy otra vez funcionando! He perdido solo un minuto.

Y aquí estoy escribiendo este post desde la máquina provisional.

Ahora a buscar máquina, y la verdad que viendo precios, me decanto por un AMD FX-8350,  un micro de 8 núcleos, pero con un coste inferior que por ejemplo un i5 4670K. 
La gráfica estoy dudando, seguramente seguiré con nVidia por su soporte a Linux, ya que las ATI no tenía muy buenas críticas.

Intentaremos pillar 16GB de RAM, a costa de disco , porque en el NAS tengo 5 Teras, no me preocupa el espacio en la máquina.

A ver si puedo meterle un SSD , para el sistema operativo y hacer un /home aparte en un disco rigido.







martes, 18 de marzo de 2014

Python. Usando TreeStore. PyGtk


En esta 4 entrega, recordad una vez más de hacer un pull del repositorio, veremos como montar en la parte derecha de nuestra pantalla, las bases de datos de nuestro servidor, y dentro de cada base de datos, las tablas correspondientes y dentro , la estructura de cada uno de ellos.

Para usar la vista de arbol desde Glade, simplemente cogemos el objeto;

 self.view_tree = self.glade.get_object('treeview_tables')


Ahora, vamos a montar lo que habíamos comentado a través del método mount_treeview()

    def mount_treeview(self):
        pbd_bd = gtk.gdk.pixbuf_new_from_file("./images/bd.png")
        pbd_table = gtk.gdk.pixbuf_new_from_file("./images/table.png")
        pbd_field = gtk.gdk.pixbuf_new_from_file("./images/field.png")

He visto muchos ejemplos, en que la gente le da por cargar y guardar en el modelo de datos la imagen por cada fila. PITTTTT ERROR!!! INEFICAZ!
No hace falta, hombre. Se abre una sola vez, y se pasa esa imagen que será común a todas la filas. Eso significa , que si las imágenes pesan en memoria 30KB cada una, el consumo será, independientemente de las filas, 90K.
Si cargáramos por cada fila una imagen, pues multiplicar;
total memoria en imagen = filas * ( 3 * 30KB )

En fin, siempre viene bien recordad que la optimizaciones pequeñas, al final son las que marcan la diferencia entre un gran programa y un programa que se arrastra por el escritorio. ;-)

Vamos a ir comentando aunque el código habla por si solo.
Lo primero, es obtener las BD que tenemos en nuestro servidor de MySQL;


        #Preguntamos por la BDs
        sql = "Select schema_name From `INFORMATION_SCHEMA`.`SCHEMATA`"
        cursor = self.db.cursor()
        try:
            cursor.execute(sql)
            result_db = cursor.fetchall()
        except MySQLdb.Error, e:
            self.status_setText( "Error %d: %s" % (e.args[0], e.args[1]) )
            return

En la variable result_db, tenemos nuestras BD.
Ahora de lo que se trata es de empezar a rellenar nuestro TreeStore, que como vemos, estará formado por un tipo Pixbuf y un string;

        self.treestore = gtk.TreeStore(gtk.gdk.Pixbuf, str)

Ahora, lo que vamos hacer es recorrer nuestras bases de datos, y por cada una de ellos vamos a meterle las tablas. 

¿ Y como se hace ? pues a través de un iterator. 

Explicar que es un iterator me puede llevar bastante tiempo, así que vamos a suponer que tú eres un creyente, no importa de que religión o secta, la cuestión es que tienes que tener fe en lo que ves ;-)

Después, por ti mismo, busca documentación sobre ello, o ya reportaré alguna documentación que vi en su momento.

Bueno, como la fe en lo que ves, la introducción del primer grupo, en este caso las bases de datos, verás que el paso de parámetros es None, por la simple razón que es la raiz.

La llamada self.treestore.append nos devuelve un iterator a la vez que introduce el elemento de la base de datos y la imagen, [pbd_bd, bd[0]

Este indica el camino al padre.  ( Tú ves rezando ;-) )

        for bd in result_db:
            iter = self.treestore.append(None,[pbd_bd, bd[0]] )
            cursor1 = self.db.cursor()
            try:
                cursor1.execute( "show tables from " + bd[0])
                result_table = cursor1.fetchall()
            except MySQLdb.Error, e:
                self.status_setText( "Error %d: %s" % (e.args[0], e.args[1]) )
                return

Ahora, recorremos las tablas, que obtuvimos anteriormente con show tables from + la base de dato actual

Pero, ahora, a la hora de introducir en el TreeStore, vamos a indicar a que Padre, a través del iterator creado anteriormente, y que este, a su vez, nos devuelve otro iterator, iterchild, que lo usaremos posteriormente ;


            for table in result_table:
                iterchild = self.treestore.append(iter,[pbd_table, table[0] ] )
                cursor2 = self.db.cursor()
                try:
                  cursor2.execute( "show columns from " + bd[0] + "." + table[0])
                    result_field = cursor2.fetchall()
                except MySQLdb.Error, e:
                   self.status_setText( "Error %d: %s" % (e.args[0], e.args[1]) )
                   return

Bien, y para acabar, vamos a introducir la estructura en su tabla:

                for field in result_field:
                    self.treestore.append(iterchild,[pbd_field, field[0] ] )

Pues lo único que queda, es crear un par de columnas;

        #Create Columns from names fields
        column = gtk.TreeViewColumn( "", gtk.CellRendererPixbuf(), pixbuf=0)
        self.view_tree.append_column( column )
        column = gtk.TreeViewColumn( "Database", gtk.CellRendererText(), text=1 )
        self.view_tree.append_column( column )


Y asociar nuestro TreeStore a nuestra vista;
        self.view_tree.set_model( self.treestore )

El resultado, en la parte derecha de la imagen;



Ahora va tomando una forma más que elegante, para las pocas líneas que hemos picado ;-)

Os aseguro, que si no fuese por Glade, el picar hubiese sido muy tedioso.

En la próxima, veremos de incorporar una pequeña historia sobre las query que hemos ido realizando.

Ah, se me olvidaba, si pruebas USE BD_QUE_QUIERO, algo hace ;-)



domingo, 16 de marzo de 2014

Python. MVC y MySQL ( PyGtk )




En esta tercera entrega de nuestro aprendizaje de Python con PyGtk, vamos a ver como haciendo nuestras Querys de MySQL se verán reflejadas en nuestra vista.

Antes de continuar, hacer un git pull del proyecto ubicado en github, para ver los nuevos cambios.

Las mejoras que tenemos en esta revisión, son;

  • Usamos una statusbar para dar información y mostrar también errores de MySQL
  • Crearemos a través de un TextView, un modelo de datos ListStore y columnas dinámicamente.
  • Controlamos la salida de la aplicación, preguntado si queremos salir.
  • Pulsando la tecla F5 en la ventana, lanzará la consulta que tengamos en ese momento en el TextView
  • Los eventos saltan ahora a la propia clase, abandonando la clase Handler(), por ser más sencillo la gestión. De todas maneras no la quito del código, para que se vea que podemos elegir el sistema que más nos guste.
Casi todo el trabajo se realiza en el método setQuery(), y es lo que veremos a continuación.

Obtener la sentencia que hemos escrito en el textview;

        textbuffer = self.textview_sql.get_buffer()
        self.cSql = textbuffer.get_text(*textbuffer.get_bounds())


Aquí de lo que se trata es de eliminar las columnas de la antigua sentencia ejecutada y limpiar el modelo de datos asociado a la vista;


        # remove columns the old view
        nOld_Fields = self.getTotalColumns()
        if nOld_Fields != 0:
            for column in self.view_lista.get_columns():
                self.view_lista.remove_column( column )


        #Clear model data of view
        oModel = self.view_lista.get_model()
        if oModel != None:
            oModel.clear()
        self.view_lista.set_model()

Ahora, ejecutamos la sentencia en MySQL, también podríamos adaptarlo fácilmente a otro motor, como Sqlite o PostGres, y en caso de error, lo mostramos en la statusbar;

        #Execute Sql
        cursor = self.db.cursor()
        try:
            cursor.execute(self.cSql)
            result = cursor.fetchall()
        except MySQLdb.Error, e:
            self.status_setText( "Error %d: %s" % (e.args[0], e.args[1]) )
            return

Ahora lo que vamos a realizar es muy simple, obtenemos la cantidad de campos y sus nombres, para crear las columnas correspondientes, que lo logramos a través del método AddListColumn()

        num_fields = len(cursor.description)
        field_names = [i[0] for i in cursor.description] # Name of fields

        #Create Columns from names fields
        i = 0
        for nombre in field_names:
            self.AddListColumn(nombre, i)
            i = i + 1

Una vez que tenemos nuestras columnas creadas, lo que nos resta es crear el modelo de datos y rellenarlo con nuestra Query y asignarlo a la vista;

        #Create model dinamic of types str for view
        ListStore = gtk.ListStore(*([str] * num_fields))
        for value in result:
            ListStore.append(value)
        self.view_lista.set_model(ListStore)



Pues esto es todo en esta tercera entrega, ahora podemos empezar a ver resultados visibles ;-)

Para la próxima, usaré un TreeStore, para montar las estructuras de nuestras tablas que tengamos en la base de datos, además, incorporaremos alguna que otra imagen en la vista, para que quede más chulo.

Dejo unas imágenes con los resultados del código a ver que os parecen.









sábado, 15 de marzo de 2014

Python. Conexión de señales( PyGtk )




Seguimos con nuestro particular estudio de Python y nuestro pequeño programa.

Hasta ahora, hemos visto como hacer uso de Glade, y vemos como conectamos las respectivas señales.

   #Create object gtk.Window , from load window the glade
   self.window = self.glade.get_object('consultas')
   # Example connect manual signal.
   self.window.connect("destroy", self.destroy)

Si le echamos un vistazo al commit 78f6437,podemos observamos los cambios que teníamos anteriormente, git diff 753f400 78f6437 , observaremos como podemos determinar en Glade donde tiene que saltar.

En la imagen de tinnydb.ui puedes ver donde tienes que declarar que señal 
vamos a controlar.


En este caso, vamos a controlar el evento delete-event
En Gtk, este evento se dispara ANTES del destroy, y por ejemplo, podemos usarlo para preguntar si queremos salir de la aplicación o no.
Eso se consigue simplemente deteniendo la propagación de la señal al sistema de eventos de GTK, o por el contrario dejamos a Gtk que siga su curso, en este caso, sería el salto a la señal destroy.

¿ Pero no habíamos conectado manualmente el destroy ? Cierto. Podemos conectarlo combinándolo glade, además, las señales que conectamos se suman a las existen no se eliminan, aunque yo jamás lo he realizado, la teoría es simple: Para un mismo evento de Gtk, se puede disparar una o varias señales.
( Un poco de teoría de Gtk tampoco viene mal ;-) )

Bueno, seguimos. ¿ Como le decimos que use las señales definidas en Glade ?

# From Glade, signal delete_event the window consultas,  at class Handler.
  self.glade.connect_signals( Handler() )
Ya está, muy simple. Esto lo que hace es que decirme que clase va a tratar los eventos.

Creamos la clase que quiero que procese las señales;

class Handler():
    def delete_event(self, widget,data=None):
        print( "Call from Glade." )
        # TIP:if you return 0 , destroy, but if you return 1, stop , not kill program
        # TODO: Here create dialog ask if I want exit program
        return 0

Esto lo he realizado a propósito para que veáis lo que podemos hacer.
Podía hacer realizado;
  self.glade.connect_signals( self )
Ahora es nuestra propia clase principal, lo que tendrá un método llamado delete_event que gestiona esa señal.

También se puede conectar señales de esta manera;

 #Create our dictionay and connect it
dic = { "delete_event" : self.delete_event,
 "destroy" : gtk.main_quit }
self.window.signal_autoconnect(dic)

Hasta aquí un poquito de PyGtk , Glade y sus señales.




jueves, 13 de marzo de 2014

Un nuevo proyecto, TinnyDb.



Como me gusta aprender Python, siempre me ha gustado compartir con la gente lo que voy descubriendo y de paso mostrar mis avances desde el inicio, de esta manera, otro pueda ir cogiendo el hilo poco a poco.

En este paso voy a realizar un pequeño programa para hacer consultas a MySQL a través de PyGtk.
Para ello, las pantallas estarán realizadas con Glade.

De esta manera, podemos ver un poco de todo, Python , POO, Gtk , Glade, conexión con MySQL, como montar las vistas, etc..

En esta entrada, veremos como descargar el código desde GitHub.
Si no usas Git como control de versiones, es una buena oportunidad de empezar.

No voy a explicar como instalar Python , PyGtk o las librerías de MySQL, porque hay excelentes post que explican como hacerlo, es cuestión de buscar por la web.

No sé cuanto tiempo ni el grado de complejidad que va a requerirme llevar a cabo el proyecto, lo que sí sé seguro, es que nos vamos a divertir ;-)

Para empezar a trastear  e ir abriendo boca, tenemos un poco de código, es muy simple, un par de clases con muy poquitas lineas, puedes descargarlo desde github:

git clone https://github.com/rafathefull/tinnydb.git

Podemos observar en el fichero login.py, la pantalla que ilustra este post, y que está contenido en el fichero tinnydb.ui que lo puedes abrir con Glade.

Para ejecutarlo, simplemente : python main.py

De momento, os dejo el código que he realizado en un par de horas, funcionando con conexión a datos.

En el siguiente entrega, explicaremos un poco como la clase login se conecta a MySQL y el uso de Glade desde Python a través de PyGtk.




domingo, 9 de marzo de 2014

Maneras de hacer en PyGTK( Python ) vs T-GTK ( Harbour )


El haber usado Glade para el diseño del GUI para mi programa de Maquinas-Herramientas, me ha va permitir aprender Python con PyGtk, muchos más rápido, porque las pantallas ya las tengo creadas.

De esta manera, todo el interface es reusable 100%.

Siempre he pensado que aprender un nuevo lenguaje haciendo simples funciones que muestran como hacer el factorial de un número es súper aburrido.

¿ A quién le resulta divertido "ver", de hecho no vas a ver un carajo, como funciona una función recursiva? ¿ De verdad te vas a sentir realizado viendo un puñado de números sin sentido ? ;-)

Por eso, voy a intentar portar mi programa a Python para aprender de él, y de paso, ver como evoluciona, y sentirme satisfecho de ver ventanas, botones, carga de datos en una vista, etc..

¿ A que suena mejor ? Como no, comparar un triste número en un interprete a ver una ventana que responde a tus acciones no tiene color ;-)

Esta es el código de la ventana de Login usando T-GTK


    SET RESOURCES pGlade FROM FILE "./gui/pol.ui"

    DEFINE WINDOW oWnd ID "inicio" RESOURCE pGlade

            DEFINE ENTRY VAR s_cUser      ID "entry_user"     RESOURCE pGlade
            DEFINE ENTRY VAR s_cPass      ID "entry_pass"     RESOURCE pGlade
            DEFINE ENTRY VAR s_cServer    ID "entry_server"   RESOURCE pGlade
            DEFINE SPIN  VAR s_nPort      ID "spin_port"      RESOURCE pGlade
            DEFINE ENTRY VAR s_cDBName    ID "entry_bd"       RESOURCE pGlade
            DEFINE LABEL oLabelInf        ID "information"    RESOURCE pGlade

            DEFINE BUTTON oBtn ID "btn_access" RESOURCE pGlade;
                          ACTION ( oBtn:Disable(),;
                                   IF( !empty( oServer := Connect_Db( oLabelInf )),( oWnd:End() ), ),;
                                   oBtn:Enable() )

            DEFINE BUTTON ID "btn_cancel" RESOURCE pGlade ACTION ( oWnd:End() )

    ACTIVATE WINDOW oWnd INITIATE


En PyGTK;

__author__ = 'rafa'

import pygtk
pygtk.require('2.0')
import gtk
import MySQLdb
import os

class Login:

    def __init__(self):

        """
        Ventana de acceso a la BD
        """

        self.conectado = False

        self.gladefile = "./gui/pol.ui"
        self.glade = gtk.Builder()
        self.glade.add_from_file(self.gladefile)

        #accediendo a los controles
        self.window = self.glade.get_object('inicio')
        self.window.connect("destroy", self.destroy)

        self.btn_access = self.glade.get_object('btn_access')

        # este salta a un method
        self.btn_access.connect("clicked", self.Conect_Db, None)

        self.btn_cancel = self.glade.get_object('btn_cancel')
        self.btn_cancel.connect("clicked", self.destroy )

        self.label = self.glade.get_object('information')

        self.entry_user   = self.glade.get_object('entry_user')
        self.entry_user.connect("key-press-event", self.on_key_press_event )

        self.entry_pass   = self.glade.get_object('entry_pass')
        self.entry_pass.connect("key-press-event", self.on_key_press_event )

        self.entry_server = self.glade.get_object('entry_server')
        self.entry_bd     = self.glade.get_object('entry_bd')
        self.spin_port    = self.glade.get_object('spin_port')

    def on_key_press_event(self, widget, event):
        keyname = gtk.gdk.keyval_name(event.keyval)

        if keyname == "Return" or keyname == "KP_Enter":
            widget.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD)

    # Cuando destruimos la pantalla, si no estamos conectado, salimos de la aplicacion.
    def destroy(self, widget, data=None):
        if not self.conectado:
            gtk.main_quit()
            os.sys.exit (1)
        else:
            gtk.main_quit()

    def main(self):
        gtk.main()

        return 0

En parte , la ventaja de T-GTK, en este caso es de Harbour y su maravilloso Preprocesado , que nos permite crear comandos, "ocultando " el uso de clases, aunque por debajo, el trabajo realizado es similar a la librería de PyGtk.

Pero, he aquí donde la potencia de Glade:

        SET RESOURCES pGlade FROM FILE "./gui/pol.ui"
        DEFINE WINDOW oWnd ID "inicio" RESOURCE pGlade

en PyGtk;

        self.gladefile = "./gui/pol.ui"
        self.glade = gtk.Builder()
        self.glade.add_from_file(self.gladefile)

        #accediendo a los controles
        self.window = self.glade.get_object('inicio')
        self.window.connect("destroy", self.destroy)

En Harbour escribimos muchísimo menos para obtener lo mismo, gracias a su potente preprocesado.
Pero lo importante, es que convertir un programa en otro a través de Glade, es de una gran ayuda.

O por decirlo de otra manera, un diseñador puede hacer las pantallas, mientras el programador lo dota de funcionalidad.

Esta manera , la de separar GUI y funcionalidad, me encanta. ;-)


sábado, 8 de marzo de 2014

Ubuntu 10.04 y Git


Si usas Ubuntu 10.04 todavía, como yo, seguramente la última versión que vas a tener en el sistema de git será la  1.7.0.4.

Y eso es un problema si quieres usar PyCharm con git, pues simplemente no te dejará usar repositorios de git desde el entorno de programación

. Pues tan simple como esto:

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git

Ahora dispones en el sistema de la versión 1.9.0 ;-)
Seguimos jugando con Python....


viernes, 7 de marzo de 2014

Software Maquinas-Herramientas

Después de más de 2 años, sacrificando fin de semanas y tiempo libre, he podido pasar a una versión totalmente funcional.

El Sofware es capaz de cálcular :

  • Tornillos Sin Fin 
  • Corona
  • Metrico ISO
  • Fresas-Madres
  • Husillos
  • Engranajes Rectos y Helicoidales
  • Noyos

Las pantallas de cálculo son similares, por ejemplo, las del tornillo SINFIN y su CORONA;


A parte de calcular , también dispone de la realización de la orden de fabricación:

Y toda la gestión de albaranes con su correspondiente facturación;

La verdad, es que estoy muy satisfecho con el resultado final, a costado, pero al menos, objetivo cumplido. Video del estado de como estaba de avanzado del software y ver más o menos como funciona el programa en si, tanto en GNU/Linux como en Windows.





martes, 4 de marzo de 2014

Python, enamorado de la simplicidad.


Estaba yo trasteando con Python, y estaba traspasando unas clases de cálculos de mecánica y me he encontrado que he implementado esta linea de Python, la cual es asombrosamente simple y potente;

  def aproxima( self, aMatriz, nNumber):
      """
        Devuelve el valor que sea el más aproximado dentro del array a nNumber
      """
      return min( aMatriz, key=lambda x:abs( x - nNumber ) )

Tenía una rutina en Harbour , hace lo típico, recorre el array, me guardo el valor de la posición que cumple y después devuelvo el elemento de ese array según la posición, lo cual, después de 25 lineas más o menos de código fuente, obtenía el premio ;-)

Pero la linea en Python hace lo mismo, en UNA SOLA LINEA!
Me he quedado prendado. 

Aunque a veces su simplicidad y el 'paso de todo', como esto;

   class coche():
         pass
 
   coche = coche()

   coche.color = 'rojo'
   coche.puertas = 5


Pero esa simplicidad, a veces, te puede volver loco ;-)

Ah! Se me olvidaba, muy importante , TODO el código ordenado!!!

ODIO el tener que estar repasando códigos de otros y cada cual usa la indentación como le da la gana, y era reacio a su uso al principio, pero si usas código de otros, lo agradeces de todo corazón!!! ;-)