콘텐츠로 건너뛰기
Home » Textual 강좌 2 – 이벤트

Textual 강좌 2 – 이벤트

사용자가 앱에서 키보드의 키를 누르거나, 마우스로 버튼등의 UI 위젯을 클릭하는 등의 조작을 앱은 그에 따른 어떤 동작을 수행하여 사용자 입력에 반응해야 한다. Textual에서 사용자 조작은 이벤트로 취급되어 해당 이벤트를 처리하는 이벤트 핸들러에 의해 미리 지정된 동작이 수행된다. 이벤트의 처리는 메시지 시스템이라는 매커니즘을 통해 처리된다. 이 방식은 웹에서 자바스크립트로 이벤트를 처리하는 것과 유사한 방식으로, 모든 이벤트는 메시지로 발행되고, 메시지를 수신할 수 있는 객체가 해당 메시지를 처리하게 된다. Textual이 내부적으로 미리 정의해놓은 메시지 외에도 커스텀 메시지를 정의하고 발행할 수 있으며, 이는 특정한 조건에서 이벤트를 발생시키는 방법으로 활용할 수 있다.

메시지 처리

모든 Textual 앱이나 위젯은 메시지 큐를 가지고 있다. 객체가 받은 메시지들은 메시지 큐에 저장된 후, 메시지를 처리하는 주기마다 큐에 쌓인 메시지들은 선입선출(FIFO) 방식으로 처리한다.

만약 메시지를 받은 객체가 메시지를 처리하지 않으면, 이 메시지는 무시되는 것이 아니라 주로 레이아웃 계통을 따라 상위 객체로 전달된다. 이를 물속에서 거품이 떠오르는 것처럼 이벤트가 상위로 올라간다고 해서 버블링(bubbling)이라 한다. 예를 들어 앱에 컨테이너가 하나 있고, 다시 이 컨테이너 안에 두 개의 버튼이 있다고 가정하자. 사용자가 한 버튼을 누르게 되면 Button.Pressed 이벤트가 발생하게 되는데, 컨테이너가 이 이벤트를 처리하지 않으면, 그 상위에 있는 앱에게 메시지가 전달된다.

또 어떤 위젯들은 자신이 원래 처리하고 수행할 수 있는 이벤트에 대해서는 그 이벤트를 처리하는 핸들러가 이미 구현되어 있는 경우가 많다. 예를 들어 TextInput 같은 위젯은 키 이벤트를 받아서 사용자가 입력한 글자를 내부에 출력하는 기능을 수행한다.

메시지 처리는 이벤트 핸들러에서 정의할 수 있다. 모든 위젯과 앱은 메시지를 받으면 메시지의 타입과 이름에 기반하여 이를 처리할 메소드 이름을 만들게되고, 그 이름에 해당하는 메소드가 있으면 이를 실행하게 된다. 예를 들어 ColorButton 이라는 클래스가 있고, 이 클래스에 Selected 라는 이벤트 메시지가 정의되어 있다고 하자. 특정한 사용자 액션에 의해 이 이벤트가 발생한다면, 이 이벤트를 처리할 수 있는 이벤트 핸들러의 이름은 다음과 같다.

def on_color_button_selected(self, event):
  pass

위 이벤트 핸들러의 이름은 세 부분으로 나뉘게 된다.

  1. on_ : 이벤트 핸들러라는 것을 알려준다.
  2. color_button_ : 이벤트 메시지의 네임스페이스에 해당한다.
  3. selected : 이벤트 메시지의 이름에 해당한다.

‘on’ 데코레이터

이렇게 이름을 짓는 관례를 사용하는 것 외에도, textual.on 이라는 데코레이터를 사용하면, 임의의 메소드를 특정한 메시지나 이벤트의 핸들러로 지정할 수 있다.

from textual import on

class Sample(App):

  @on(Button.Pressed)
    def handle_button_pressed(self):
      ...

# 위와 아래의 메소드는 같다. 
  def on_button_pressed(self):
      ...

‘on’ 데코레이터를 사용하는 것은 단지 이름 관례를 적용하지 않아도 된다는 점 외에도 몇 가지 좀 더 세세한 조정을 할 수 있다는 장점이 있다. 예를 들어 on 데코레이터의 두 번째에는 선택적으로 CSS 셀렉터를 지정하는 기능이 있다. 이렇게 하면 같은 종류의 이벤트에 대해서도 어떤 객체가 발행했는지에 따라서 메시지 핸들러를 각각 구분하여 사용할 수 있다는 장점이 있다.

class Sample(App):
  def compose(self) -> ComposeResult:
    yield Button("Bell", id="bell")
    yield Button("Toggle dark", classes="toggle dark")
    yield Button("Quit", id="quit")

  @on(Button.Pressed, "#bell")
  def play_bell(self):
    self.bell()

  @on(Button.Pressed, ".toggle.dark")
  def toggle_dark(self):
    seld.dark = not self.dark

  @on(Button.Pressed, "#quit")
  def quit(self):
    self.exit()

이벤트 핸들러에서는 두 번째 인자는 메시지 객체를 받을 수 있다. 만약 핸들러 내부에서 메시지 객체를 전혀 사용하지 않는다면 생략해도 된다.

비동기 핸들러

메시지 핸들러는 비동기 코루틴으로 작성될 수 있다. 만약 핸들러 내부에서 다른 코루틴을 await 할 필요가 있다면 메시지 핸들러 역시 비동기 코루틴으로 작성되어야 할 것이다. 대표적인 비동기 핸들러를 호출하는 경우는 액션을 실행하는 경우일 것이다. 비동기 핸들러는 async def 키워드를 사용해서 메시지 핸들러를 작성하면 된다. 비동기 핸들러를 호출하기 위한 별도의 다른 코드는 없으며, Textual이 핸들러의 타입에 따라 자동으로 호출방식을 결정하게 된다.

키보드 입력

키보드 입력은 기본적으로 Key 이벤트를 받아서 처리하게 된다. 다음은 RichLog 위젯을 사용하여 사용자가 입력한 키 정보를 로깅하는 예제이다.

from textual.app import App, ComposeResult
from textual.events import Key
from textual.widgets import RichLog

class SampleLog(App):
  def compose(self) -> ComposeResult:
    yield RichLog()

  def on_key(self, message: Key):
    self.query_one(RichLog).write(message)

SampleLog().run()

키 이벤트는 사용자가 키보드의 키를 누를 때 발생하며, 키와 관련한 몇 가지 정보를 확인할 수 있는 속성을 가지고 있다.

key

Key.key 속성을 눌려진 키를 식별하기 위한 문자열이다. 문자나 숫자키를 누른 경우에는 눌려진 키의 문자가 되며, 그 외의 키들은 고유한 이름으로 정의된다. Shift나 Ctrl 키를 함께 누른 경우에는 shift+home,ctrl+c 와 같은 식으로 표현된다.

character

Key.character 속성은 키와 연관된 출력가능한 문자를 말한다. 화면에 출력되지 않는 문자와 관련된 키에 대해서는 이 속성이 None이다.

name

Key.name 속성은 key와 비슷하지만, 파이썬 함수 이름으로 유효한 이름을 사용할 수 있다. 예를 들어서 대문자 P를 누른 경우 key 속성은 “P”로 표시되나,name 속성은 “upper_p”로 표시된다.

is_printable

이 속성은 해당 키의 문자가 화면에 출력가능한 지 여부를 알려준다.

키 메소드

키보드 이벤트를 처리하는 정석적인 방법은 on_ 으로 시작하는 이벤트 핸들러를 작성하는 것이지만, key_ 로 시작하는 메소드를 작성한다면 해당 키에 대한 이벤트를 처리할 수 있다. 이렇게 메소드를 따로 지정하면, 해당 메소드와 on_key 메소드가 같이 실행된다. 아래는 위 예제를 약간 변형하여, 스페이스 바를 누를 때에는 터미널에서 벨 소리를 낸다.(화면도 반짝거림). 스페이스 바를 누를 때에도 로그 창에는 스페이스 바를 눌렀다는 로그도 함께 남게 된다.

...
  def on_key(self, event: Key) -> None:
    self.query_one(RichLog).write(event)

  def key_space(self) -> None:
    self.bell()

포커스

키 이벤트는 한 번에 하나의 위젯만 받을 수 있다. 현재 키보드 이벤트를 받는 위젯에 대해서 우리는 포커스를 갖고 있다고 표현한다. 아래 예제는 키 로거를 동시에 네 개를 붙인다. 키를 입력했을 때 모든 로거에 키가 남는 것이 아니라 포커스된 로거에만 키 로그가 찍히는 것을 알 수 있다.

class KeyLogger(RichLog):
  def on_key(self, event: Key) -> None:
    self.write(event)

class TestApp(App):
  def compose(self) -> ComposeResult:
    for _ in range(4):
      yield KeyLogger()

TestApp().run()

대부분의 경우 Textual은 포커스를 자동으로 처리할 수 있으며, 위젯의 focus() 를 호출하여 수동으로 포커스를 줄 수 있다. 또한 위젯이 포커스를 받거나 잃게 되면 그에 따라 각각 Focus, Blur 이벤트가 발생한다.

바인딩

어떤 키들은 눌렸을 때 위젯의 액션이 실행되도록 만들 수 있다. 이러한 연결을 특별히 ‘바인딩’이라고 부른다.

바인딩을 만들기 위해서는 앱이나 위젯 클래스에 BINDING 이라는 클래스 속성을 지정하면 된다. 바인딩 속성은 간단하게는 세 개의 문자열로 된 튜플의 리스트이다. 이 튜플은 (key, action, description) 의 형태로 구성된다. 조금 더 세세한 옵션을 노출하고 싶다면 Binding 타입의 객체를 만들어서 지정할 수 있다.

BINDINGS = [
  Binding("tab", "focus_next", "Focus Next", show=False),
  Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
  Binding("shift+tab", "focus_previous", "Focus Previous", show=False)
]

priority=True 로 설정되는 바인딩은 포커스된 위젯의 바인딩보다 앞서서 작동한다는 의미이다. 이는 앱에서 전역적으로 작동하는 단축키를 정의할 때 사용할 수 있다. Textual 에서는 ctrl + c 에 대한 바인딩이 이렇게 정의되어 있다. (언제든 눌러서 앱을 종료할 수 있다.) 위의 예제는 Textual에서 기본적으로 정의하고 있는 바인딩의 예시이다. show= 속성은 Footer 위젯을 추가했을 때, 바인딩 정보를 표시할 것인지 여부를 결정한다.

마우스 이벤트

Textual은 마우스의 움직임이나 버튼 클릭에 응답하기 위해 이벤트를 생성한다. 이 이벤트에는 이벤트가 발생한 터미널 혹은 위젯에서의 위치가 상대좌표로 제공된다. 좌표계는 좌측 상단을 (0, 0)으로 하는 좌표계가 된다. 마우스 이벤트에는 MouseMove, MouseClick, MouseEnter, MouseLeave, MouseScrollUp, MouseScrollDown 이벤트 등이 있다.

마우스 캡처

특정한 위젯이 마우스를 캡쳐하면, 마우스포인터가 해당 위젯의 외부에 있더라도 모든 마우스 이벤트를 그 위젯이 받게된다. 마우스를 캡쳐하려면 mouse_capture() 메소드를 사용한다. 반대로 마우스 캡쳐를 해제하려면 mouse_release() 메소드를 호출하면 된다. 캡쳐와 릴리즈 시점에도 각각 MouseCapture, MouseRelease 이벤트가 발생한다.

댓글 남기기