{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'>&copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados. \n",
    "   Editado por Equipo Docente IIC2233 2018-1 al 2025-2, y extendido con material creado en 2017-2 por Hugo Navarrete e Ignacio Acevedo.</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Eventos y señales](#Eventos-y-señales)\n",
    "    1. [Obtener al emisor de la señal: `sender`](#Obtener-al-emisor-de-la-señal:-sender)\n",
    "    2. [Eventos de *mouse*](#Eventos-de-mouse)\n",
    "        1. [*Move Event* permanente](#Move-Event-permanente)\n",
    "    3. [Eventos del teclado](#Eventos-del-teclado)\n",
    "    4. [Generar señales personalizadas](#Generar-señales-personalizadas)\n",
    "    5. [Emisión de eventos con información](#Emisión-de-eventos-con-información)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Eventos y señales\n",
    "\n",
    "Recordemos que las interfaces gráficas son aplicaciones construidas con una **arquitectura basada en manejo de eventos**. Esto significa que la aplicación detecta eventos generados por el usuario o por otras partes de la misma aplicación, y los procesa con el elemento apropiado. \n",
    "\n",
    "Una aplicación construida con PyQt empieza a detectar eventos una vez que ha entrado en su *main loop*, y esto ocurre luego de llamar al método `exec()` de `QApplication`. La siguiente figura muestra una comparación, mediante diagramas de flujo, entre un programa con una estructura lineal como los que habíamos construido en las semanas anteriores, y un programa con uso de GUI basada en manejo de eventos.\n",
    "\n",
    "![](img/gui-flowchart.png)\n",
    "\n",
    "En este modelo existen 3 elementos fundamentales:\n",
    "\n",
    "- **La fuente del evento**: Corresponde al objeto que genera el cambio de estado o que genera el evento.\n",
    "- **El objeto evento**: Es el objeto que encapsula el cambio de estado mediante el evento.\n",
    "- **El objeto destino:** Equivale al objeto al que se le desea notificar del cambio de estado.\n",
    "\n",
    "Bajo este modelamiento, la **fuente del evento** delega la tarea de manejar el evento al **objeto destino** pasándole el **objeto evento**. Para implementar este modelo, PyQt utiliza un mecanismo de ***signal*** y ***slot*** que le permite manejar los eventos y hacer posible la comunicación entre *widgets*. De esta forma, cuando un evento ocurre, el objeto que es activado emite una señal (*signal*) a la ranura (*slot*) correspondiente, donde un *slot* puede ser cualquier tipo de elemento llamable (*callable*) en Python.\n",
    "\n",
    "En el siguiente ejemplo, extenderemos el programa anterior que mostraba botones dispuestos en grilla que simulaban un celular, para generar una llamada a la función `boton_clickeado` cada vez que se presione alguno de los botones. Esto se logra conectando el evento que enviará la señal con el *slot* que la recibe. En el caso de los botones, generalmente el método corresponde al evento `clicked`. Mediante el método `connect()` se establece la comunicación entre el evento y el *slot*. Este método recibe una función *llamable* en Python, (es importante no agregar los `()`, ya que con eso le estaríamos entregando el resultado de la función y no el objeto función). El  ejemplo solo muestra la clase `MiVentana`.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/1_manejar-eventos.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-08-26T22:12:48.019505Z",
     "start_time": "2019-08-26T22:12:47.491389Z"
    }
   },
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,\n",
    "                             QGridLayout, QVBoxLayout)\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Agregamos un label para indicar el último botón seleccionado.\n",
    "        self.label1 = QLabel('Último botón seleccionado:', self)\n",
    "        self.label2 = QLabel('', self)\n",
    "        self.grilla = QGridLayout()\n",
    "\n",
    "        valores = ['1', '2', '3',\n",
    "                   '4', '5', '6',\n",
    "                   '7', '8', '9',\n",
    "                   '*', '0', '#']\n",
    "\n",
    "        posiciones = [(i, j) for i in range(4) for j in range(3)]\n",
    "\n",
    "        for i in range(len(valores)):\n",
    "            posicion = posiciones[i]\n",
    "            valor = valores[i]\n",
    "            boton = QPushButton(valor, self)\n",
    "            \"\"\"\n",
    "            Aquí conectamos el evento clicked() de cada botón con el slot\n",
    "            correspondiente. En este ejemplo, todos los botones usan el\n",
    "            mismo slot (self.boton_clickeado).\n",
    "            \"\"\"\n",
    "            boton.clicked.connect(self.boton_clickeado)\n",
    "            self.grilla.addWidget(boton, *posicion)\n",
    "\n",
    "        # Posicionamos tanto el label como la grilla en un layout vertical.\n",
    "        vbox = QVBoxLayout()\n",
    "        vbox.addWidget(self.label1)\n",
    "        vbox.addWidget(self.label2)\n",
    "        vbox.addStretch(1)\n",
    "        vbox.addLayout(self.grilla)\n",
    "        self.setLayout(vbox)\n",
    "\n",
    "        self.setGeometry(300, 150, 200, 200)\n",
    "        self.setWindowTitle('Celular')\n",
    "\n",
    "    def boton_clickeado(self) -> None:\n",
    "        \"\"\"\n",
    "        Esta función se ejecuta cada vez que uno de los botones de la grilla\n",
    "        es presionado. Cada vez que el botón genera el evento, la función\n",
    "        inspecciona cuál botón fue presionado y recupera la posición en que\n",
    "        utiliza en la grilla.\n",
    "        \"\"\"\n",
    "\n",
    "        # Sender retorna el objeto que fue clickeado.\n",
    "        # Ahora, la variable boton referencia una instancia de QPushButton\n",
    "        boton = self.sender()\n",
    "\n",
    "        # Podemos obtener el texto del boton\n",
    "        label_boton = boton.text()\n",
    "\n",
    "        # Obtenemos el identificador del elemento en la grilla.\n",
    "        idx = self.grilla.indexOf(boton)\n",
    "\n",
    "        # Con el identificador obtenemos la posición del ítem en la grilla.\n",
    "        posicion = self.grilla.getItemPosition(idx)\n",
    "\n",
    "        # Actualizamos el texto del label2.\n",
    "        self.label2.setText(f'Botón {idx} con label {label_boton}, en fila/columna: {posicion[:2]}.')\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Nota sobre `label.setText`:** En algunos sistemas operativos, la actualización de *labels* con `setText` no se refleja inmediatamente como se esperaría. Si esto ocurre, hay que ejecutar el método `repaint` de `QLabel` para forzar la actualización, como se muestra a continuación:\n",
    "\n",
    "```python\n",
    "# label es una instancia de QLabel\n",
    "label.setText(\"Nuevo texto\")\n",
    "label.repaint()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Obtener al emisor de la señal: `sender`\n",
    "\n",
    "En ocasiones, como en la función `boton_clickeado()` del código anterior, es necesario conocer cuál de los objetos del formulario envió una señal. El método `sender()` retorna el objeto que generó el evento. El siguiente ejemplo utiliza `sender()` para obtener una referencia al objeto que generó el evento y mostrar cuál objeto generó el evento. \n",
    "\n",
    "En el mismo ejemplo, puedes notar que se usa la clase `QCoreApplication`, de `QtCore`, la que permite obtener una referencia al objeto `QApplication` que está en ejecución. Esto se utiliza para forzar una salida de la ventana al hacer *click* en el *slot* del tercer botón.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/2_obtener-sender.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,\n",
    "                             QHBoxLayout, QVBoxLayout)\n",
    "from PyQt6.QtCore import QCoreApplication\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        self.label_estado = QLabel('Estado: -', self)\n",
    "\n",
    "        \"\"\"\n",
    "        El evento de cada botón es conectado con su slot, en este caso,\n",
    "        el método boton_clickeado(). Importante notar, que al hacer referencia\n",
    "        al método no se agregar los paréntesis finales.\n",
    "        \"\"\"\n",
    "        self.boton1 = QPushButton('Botón 1', self)\n",
    "        self.boton1.resize(self.boton1.sizeHint())\n",
    "        self.boton1.clicked.connect(self.boton_clickeado)\n",
    "\n",
    "        self.boton2 = QPushButton('Botón 2', self)\n",
    "        self.boton2.resize(self.boton2.sizeHint())\n",
    "        self.boton2.clicked.connect(self.boton_clickeado)\n",
    "\n",
    "        self.boton3 = QPushButton('Salir', self)\n",
    "        self.boton3.resize(self.boton3.sizeHint())\n",
    "        self.boton3.clicked.connect(QCoreApplication.instance().quit)\n",
    "\n",
    "        hbox = QHBoxLayout()\n",
    "        hbox.addStretch(1)\n",
    "        hbox.addWidget(self.boton1)\n",
    "        hbox.addWidget(self.boton2)\n",
    "        hbox.addWidget(self.boton3)\n",
    "        hbox.addStretch(1)\n",
    "\n",
    "        vbox = QVBoxLayout()\n",
    "        vbox.addStretch(1)\n",
    "        vbox.addWidget(self.label_estado)\n",
    "        vbox.addLayout(hbox)\n",
    "        vbox.addStretch(1)\n",
    "        self.setLayout(vbox)\n",
    "\n",
    "        self.setGeometry(200, 100, 300, 200)\n",
    "        self.setWindowTitle('Sender')\n",
    "\n",
    "    def boton_clickeado(self) -> None:\n",
    "        # Esta función registra el objeto que envía la señal del evento\n",
    "        # y lo refleja mediante el método sender() en label de estado.\n",
    "        sender = self.sender()\n",
    "        self.label_estado.setText(f'Estado: Presionado botón \"{sender.text()}\"')\n",
    "        self.label_estado.resize(self.label_estado.sizeHint())\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    form = MiVentana()\n",
    "    form.show()\n",
    "    sys.exit(app.exec())\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Eventos de *mouse*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No solo los botones tienen implementados eventos de interacción con el usuario: todo *widget* tiene distintos gatillos de eventos que se pueden conectar a *slots*. Por ejemplo, la clase `QWidget` incluye los métodos `mousePressEvent` y `mouseReleaseEvent`, que se hacen cargo -el *widget* que lo implementa- del comportamiento del programa al presionar y liberar el *mouse*, respectivamente. Por defecto, estos métodos no hacen algo, pero siempre podemos hacer *override* de él en nuestros propios *widgets*. Similar a como `QPushButton` tenía una palabra reservada para el evento `clicked` para el cual podía vincular un método o función, `mousePressEvent` refleja el evento genérico de un *click* para cualquier *widget*. La diferencia es que en vez de hacer conexión evento-método (`.clicked.connect(...)`), para este tipo de eventos (y los siguientes ejemplos) la conexión ya está definida y es solo necesario sobrescribir el método correspondiente.\n",
    "\n",
    "Es decir, al sobrescribir los métodos `mousePressEvent` y `mouseReleaseEvent` de un `QWidget` se está definiendo el código que se ejecutará una vez que se haga *click* sobre ese *widget* y cuando se libere el *click* sobre el mismo.\n",
    "\n",
    "En el siguiente ejemplo, hay un *label* de color dentro del *widget*. Cuando se haga *click* en el *widget* vamos a verificar donde se hizo *click*, si fue dentro del *label*, y vamos a avisar cuando este es liberado. Además, vamos a guardar un estado que indica si el *click* comenzó presionando sobre el `QLabel` o fuera de este.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/3_mouse-press-y-release-event.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel\n",
    "from PyQt6.QtGui import QMouseEvent\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.setGeometry(100, 100, 110, 400)\n",
    "        self.label = QLabel(\"Haz click en mí\", self)\n",
    "        self.label.setGeometry(10, 10, 90, 100)\n",
    "        self.label.setStyleSheet(\"background-color: lightblue;\")\n",
    "        self.click_dentro_del_label = False\n",
    "\n",
    "    def mousePressEvent(self, event: QMouseEvent) -> None:\n",
    "        x = event.position().x()\n",
    "        y = event.position().y()\n",
    "        print(f\"El mouse fue presionado en {x},{y}\")\n",
    "        self.click_dentro_del_label = self.label.underMouse()\n",
    "        if self.click_dentro_del_label:\n",
    "            print(\"\\tFue presionado dentro del QLabel\")\n",
    "        else:\n",
    "            print(\"\\tFue presionado fuera del QLabel\")\n",
    "\n",
    "    def mouseReleaseEvent(self, event: QMouseEvent) -> None:\n",
    "        x = event.position().x()\n",
    "        y = event.position().y()\n",
    "        print(f\"El mouse fue liberado en {x},{y}\")\n",
    "\n",
    "        if self.click_dentro_del_label:\n",
    "            print(\"\\tAntes se había presionado dentro del QLabel\")\n",
    "        else:\n",
    "            print(\"\\tAntes habías presionado fuera del QLabel\")\n",
    "\n",
    "    def mouseMoveEvent(self, event: QMouseEvent) -> None:\n",
    "        x = event.position().x()\n",
    "        y = event.position().y()\n",
    "        print(f\"El mouse se mueve... está en {x},{y}\")\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### *Move Event* permanente\n",
    "\n",
    "Otro aspecto en que nuestro programa puede estar pendiente del usuario, es en revisar la posición en la que se encuentra el *mouse* **cuando este está sobre el *widget* (sin necesariamente presionar)**. \n",
    "\n",
    "En el siguiente ejemplo, vamos a ver la posición global del *mouse* mediante el evento `mouseMoveEvent`. Para esto, vamos a activar el *tracking* del *mouse* en el `widget`, pero no en `Qlabel`.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/4_mouse-move-event.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel\n",
    "from PyQt6.QtGui import QMouseEvent\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.setGeometry(100, 100, 110, 400)\n",
    "        self.label = QLabel(\"Haz click en mí\", self)\n",
    "        self.label.setGeometry(10, 10, 90, 100)\n",
    "        self.label.setStyleSheet(\"background-color: lightblue;\")\n",
    "\n",
    "        \"\"\"\n",
    "        Activamos el tracking del mouse en nuestra ventana.\n",
    "        \"\"\"\n",
    "        self.setMouseTracking(True)\n",
    "\n",
    "        \"\"\"\n",
    "        Si además, queremos trackear la posición del mouse en el label,\n",
    "        se debe descomentar la siguiente línea.\n",
    "        \"\"\"\n",
    "        self.label.setMouseTracking(True)\n",
    "\n",
    "    def mouseMoveEvent(self, event: QMouseEvent) -> None:\n",
    "        x = event.position().x()\n",
    "        y = event.position().y()\n",
    "        print(f\"El mouse se mueve... está en {x},{y}\")\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Existen muchos más eventos relacionados con el *mouse* que puedes revisar explorando la documentación de PyQt en la web. Unos ejemplos son `enterEvent`, `leaveEvent` y `dragEnterEvent`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Eventos del teclado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Otra de las maneras más comunes en que una interfaz gráfica genera eventos por parte del usuario es a través del teclado. Cuando un usuario **presiona o libera una tecla** ocurren eventos para los que muchas veces queremos definir un comportamiento. La clase `QWidget` incluye los métodos `keyPressEvent` y `keyReleaseEvent`, que se hace cargo del comportamiento del programa ante la presión y liberar de una tecla, respectivamente. Por defecto, este método no hace nada, pero siempre podemos hacer *override* de el en nuestros propios *widgets*.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/5_key-press-y-release-event.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QLabel)\n",
    "from PyQt6.QtGui import QKeyEvent\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.inicializa_gui()\n",
    "\n",
    "    def inicializa_gui(self) -> None:\n",
    "        self.teclas_presionadas = set()\n",
    "        self.estado = QLabel('No se está presionando una tecla.', self)\n",
    "        self.estado.move(20, 10)\n",
    "        self.resize(self.estado.sizeHint())\n",
    "\n",
    "        self.setGeometry(300, 300, 600, 150)\n",
    "        self.setWindowTitle('Teclado')\n",
    "        self.show()\n",
    "\n",
    "    def keyPressEvent(self, event: QKeyEvent) -> None:\n",
    "        \"\"\"\n",
    "        Este método maneja el evento que se produce al presionar las teclas.\n",
    "        \"\"\"\n",
    "        nuevo_texto = event.text()\n",
    "        nuevo_codigo = event.key()\n",
    "        self.teclas_presionadas.add((nuevo_texto, nuevo_codigo))\n",
    "\n",
    "        self.estado.setText(f'Presionaron las teclas: {list(self.teclas_presionadas)}')\n",
    "        self.estado.resize(self.estado.sizeHint())\n",
    "\n",
    "    def keyReleaseEvent(self, event: QKeyEvent) -> None:\n",
    "        \"\"\"\n",
    "        Este método maneja el evento que se produce al liberar una tecla.\n",
    "        \"\"\"\n",
    "        texto_eliminado = event.text()\n",
    "        codigo_eliminado = event.key()\n",
    "        self.teclas_presionadas.remove((texto_eliminado, codigo_eliminado))\n",
    "        if len(self.teclas_presionadas) == 0:\n",
    "            self.estado.setText('No se está presionando una tecla.')\n",
    "        else:\n",
    "            self.estado.setText(f'Presionaron las teclas: {list(self.teclas_presionadas)}')\n",
    "        self.estado.resize(self.estado.sizeHint())\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Generar señales personalizadas\n",
    "\n",
    "PyQt permite definir señales personalizadas por el programador, además de los predeterminados sobre el *mouse* y teclado. Para esto se debe crear un objeto que alojará (como atributo) la nueva señal, el que debe ser subclase de `QtCore.QObject`. Dentro de la subclase se crea la nueva señal como una instancia de `QtCore.pyqtSignal`. Finalmente, en los *widgets* involucrados se deben recibir las señales y conectar las funciones que manejan la señal.\n",
    "\n",
    "- Mediante el método `emit` de `pyqtSignal` se emite la señal (indicando que ocurrió un evento).\n",
    "- Mediante el método `connect`, también de `pyqtSignal`, se define la función o método a ejecutar cuando la señal es emitida.\n",
    "\n",
    "El siguiente ejemplo muestra cómo generar una nueva señal entre dos ventanas separadas. La señal se activa al presionar alguna parte de la ventana izquierda, y su efecto es editar el contenido de la ventana derecha.\n",
    "\n",
    "La señal se crea como un atributo de clase dentro de la clase `VentanaPresionable`, que es quien **emite el evento** cada vez que se presiona la ventana, se **conecta** en el *main* a uno de los métodos de `VentanaQueSeEdita`, y finalmente `VentanaQueSeEdita` es quien **recibe el evento** y edita su contenido cada vez que se emite la señal.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/6_crear-pyqtsignals.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtCore import pyqtSignal\n",
    "from PyQt6.QtGui import QMouseEvent\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel\n",
    "\n",
    "\n",
    "class VentanaPresionable(QWidget):\n",
    "    \"\"\"\n",
    "    Creamos una señal como atributo de clase.\n",
    "    \"\"\"\n",
    "    senal_escribir = pyqtSignal()\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.inicializa_gui()\n",
    "\n",
    "    def inicializa_gui(self) -> None:\n",
    "        self.etiqueta = QLabel('Presiona esta ventana', self)\n",
    "        self.etiqueta.move(20, 10)\n",
    "        self.etiqueta.resize(self.etiqueta.sizeHint())\n",
    "\n",
    "        self.setGeometry(300, 300, 290, 150)\n",
    "        self.setWindowTitle('Emite señal')\n",
    "        self.show()\n",
    "\n",
    "    def mousePressEvent(self, event: QMouseEvent) -> None:\n",
    "        \"\"\"\n",
    "        Al ejecutar la siguiente línea, se emite la señal,\n",
    "        y los métodos conectados se llamarán automáticamente.\n",
    "        \"\"\"\n",
    "        self.senal_escribir.emit()\n",
    "\n",
    "\n",
    "class VentanaQueSeEdita(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.inicializa_gui()\n",
    "\n",
    "    def inicializa_gui(self) -> None:\n",
    "        self.etiqueta = QLabel('', self)\n",
    "        self.etiqueta.move(20, 10)\n",
    "        self.etiqueta.resize(self.etiqueta.sizeHint())\n",
    "\n",
    "        self.setGeometry(700, 300, 290, 150)\n",
    "        self.setWindowTitle('Recibe señal')\n",
    "        self.show()\n",
    "\n",
    "    # Este es el método que conectaremos a la señal\n",
    "    def edita_etiqueta(self) -> None:\n",
    "        self.etiqueta.setText('¡Oh! Alguien ha presionado el mouse')\n",
    "        self.etiqueta.resize(self.etiqueta.sizeHint())\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana_click = VentanaPresionable()\n",
    "    ventana_edit = VentanaQueSeEdita()\n",
    "\n",
    "    \"\"\"\n",
    "    Conectamos la señal con el método que debe activar.\n",
    "    \"\"\"\n",
    "    ventana_click.senal_escribir.connect(ventana_edit.edita_etiqueta)\n",
    "\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### (Abre paréntesis...\n",
    "\n",
    "1. ¿Por qué se encapsula la instancia `pyqtSignal` dentro de una clase? ¿No se puede instanciar directamente y trabajar con el objeto `pyqtSignal`?\n",
    "2. ¿Por qué se define como un atributo de clase y no de instancia?\n",
    "3. ¿Esto último significa que todas las instancias de la clase `VentanaPresionable` compartirán la misma señal?\n",
    "\n",
    "¡Muy buenas preguntas! Las tres prácticamente se responden de la misma forma: la razón de fondo es la implementación interna de PyQt que requiere estas condiciones. Ahora, específicamente:\n",
    "\n",
    "1. Las `pyqtSignal` deben estar encapsuladas por objetos que hereden de `QObject`. Se pueden instanciar señales fuera de una clase, pero estas no serán capaces de conectarse a funciones o ser emitidas por alguna entidad. ¿Por qué entonces podemos encapsular la señal en `VentanaPresionable`, que no es un `QObject`? Esto es simplemente porque nuestras ventanas heredan de `QWidget` y todo `QWidget` es a su vez un `QObject`.\n",
    "2. Las instancias de `pyqtSignal` deben ser definidas como atributo de clase y no de instancia por diseño interno de PyQt. Puedes hacer la prueba e instanciarla como un atributo de instancia, y verás que esta no será capaz de conectarse a llamables o de emitirse.\n",
    "3. Hemos visto que el comportamiento de un atributo de clase es distinto para un atributo de instancia para objetos mutables. En general, si tenemos un atributo de clase mutable (como una lista), si cualquier instancia modifica este atributo, todas las instancias tienen acceso a este atributo y ven el efecto. Entonces, ¿todas las instancias de `MiSenal` compartirían la señal `senal`? Soprendentemente, la respuesta es **no**. Esto se debe a implementación interna de PyQt que logra que a pesar de que se define a nivel de atributo de clase, para cada instancia la señal creada como atributo es única y distinta. Entonces, a pesar de que se defina a ese nivel, podemos estar tranquilos en que cada instancia tendrá su propia señal única.\n",
    "\n",
    "**Importante:** Al momento de definir el atributo que contiene la señal, este solo puede utilizar caracteres [ASCII](https://es.wikipedia.org/wiki/ASCII), es decir, no se permite el uso de caracteres especiales como la \"`ñ`\" o el uso de tildes.\n",
    "\n",
    "##### ...cierra paréntesis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Emisión de eventos con información\n",
    "\n",
    "En nuestro primer ejemplo básico de señal personalizada, se emitió y recibió un evento que simplemente ocurría y realizaba un efecto. Este no transmitía más información ni datos que describieran el evento que ocurrió. `pyqtSignal` permite una forma para que, al momento de emitir el evento (`emit`), se envíe también información a la función o método conectado y lo reciba como un argumento. Al instanciar la señal `pyqtSignal`, este recibe como argumentos los tipos de objetos que es capaz de enviar mediante `emit` y que recibirán los objetos llamables como argumentos. En el siguiente ejemplo se expande el anterior con tres señales separadas que emiten distinta información.\n",
    "\n",
    "Este código se encuentra en el archivo `3-eventos-y-señales/7_enviar-informacion-pyqtsignals.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtCore import pyqtSignal\n",
    "from PyQt6.QtGui import QMouseEvent\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit)\n",
    "\n",
    "\n",
    "class VentanaPresionable(QWidget):\n",
    "    \"\"\"\n",
    "    Creamos las señales como atributos de clase.\n",
    "    \"\"\"\n",
    "    senal_simple = pyqtSignal()                 # Señal simple.\n",
    "    senal_texto = pyqtSignal(str)               # Señal que permite enviar texto.\n",
    "    senal_coordenadas = pyqtSignal(int, int)    # Señal que permite enviar dos ints.\n",
    "\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.inicializa_gui()\n",
    "\n",
    "    def inicializa_gui(self) -> None:\n",
    "        self.label = QLabel('Presiona esta ventana', self)\n",
    "        self.label.move(20, 10)\n",
    "        self.label.resize(self.label.sizeHint())\n",
    "\n",
    "        self.etiqueta = QLineEdit(self)\n",
    "        self.etiqueta.move(20, 60)\n",
    "\n",
    "        self.setGeometry(300, 300, 290, 150)\n",
    "        self.setWindowTitle('Emite señal')\n",
    "        self.setMouseTracking(True)\n",
    "        self.show()\n",
    "\n",
    "    def mousePressEvent(self, event: QMouseEvent) -> None:\n",
    "        \"\"\"\n",
    "        Se emite la señal simple, sin argumento.\n",
    "        \"\"\"\n",
    "        self.senal_simple.emit()\n",
    "        \"\"\"\n",
    "        Se emite la señal que permite enviar un str. El contenido de la señal,\n",
    "        será el texto que contenga la etiqueta de la ventana.\n",
    "        \"\"\"\n",
    "        self.senal_texto.emit(self.etiqueta.text())\n",
    "\n",
    "    def mouseMoveEvent(self, event: QMouseEvent) -> None:\n",
    "        \"\"\"\n",
    "        Se emite la señal que permite enviar dos ints,\n",
    "        enviamos la posición del mouse.\n",
    "        \"\"\"\n",
    "        self.senal_coordenadas.emit(event.pos().x(), event.pos().y())\n",
    "\n",
    "\n",
    "class VentanaQueSeEdita(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "        self.inicializa_gui()\n",
    "\n",
    "    def inicializa_gui(self) -> None:\n",
    "        self.etiqueta_1 = QLabel('', self)\n",
    "        self.etiqueta_1.move(20, 10)\n",
    "        self.etiqueta_1.resize(self.etiqueta_1.sizeHint())\n",
    "\n",
    "        self.etiqueta_2 = QLabel('', self)\n",
    "        self.etiqueta_2.move(20, 40)\n",
    "        self.etiqueta_2.resize(self.etiqueta_2.sizeHint())\n",
    "\n",
    "        self.etiqueta_3 = QLabel('', self)\n",
    "        self.etiqueta_3.move(20, 70)\n",
    "        self.etiqueta_3.resize(self.etiqueta_3.sizeHint())\n",
    "\n",
    "        self.setGeometry(700, 300, 290, 150)\n",
    "        self.setWindowTitle('Recibe señal')\n",
    "        self.show()\n",
    "\n",
    "    def edita_etiqueta_click(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método no tiene argumentos,\n",
    "        ya que se conectará a una señal simple.\n",
    "        \"\"\"\n",
    "        self.etiqueta_1.setText('¡Oh! Alguien ha presionado el mouse')\n",
    "        self.etiqueta_1.resize(self.etiqueta_1.sizeHint())\n",
    "\n",
    "    def edita_etiqueta_texto(self, texto) -> None:\n",
    "        \"\"\"\n",
    "        Este método tiene un argumento,\n",
    "        el str que se espera del evento conectado.\n",
    "        \"\"\"\n",
    "        self.etiqueta_2.setText(f'Recibí del evento: {texto}')\n",
    "        self.etiqueta_2.resize(self.etiqueta_2.sizeHint())\n",
    "\n",
    "    def edita_etiqueta_posicion_mouse(self, x, y) -> None:\n",
    "        \"\"\"\n",
    "        Este método tiene dos argumentos,\n",
    "        los ints que se espera del evento conectado.\n",
    "        \"\"\"\n",
    "        self.etiqueta_3.setText(f'Recibí posiciones: {x}, {y}')\n",
    "        self.etiqueta_3.resize(self.etiqueta_3.sizeHint())\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana_click = VentanaPresionable()\n",
    "    ventana_edit = VentanaQueSeEdita()\n",
    "\n",
    "    \"\"\"\n",
    "    Conectamos la señal con el método que cada una debe activar.\n",
    "    \"\"\"\n",
    "    ventana_click.senal_simple.connect(ventana_edit.edita_etiqueta_click)\n",
    "    ventana_click.senal_texto.connect(ventana_edit.edita_etiqueta_texto)\n",
    "    ventana_click.senal_coordenadas.connect(ventana_edit.edita_etiqueta_posicion_mouse)\n",
    "\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "También se pueden enviar estructuras de datos mas complejas como `dict`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.10"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
