Skip to main content

4 posts tagged with "gui"

View All Tags

Thiago Lages de Alencar

1 - Performance

Mude o modo de renderização para Compatibility, não é como se você fosse precisar de qualquer renderização além do básico para desenhar uma janela na tela.

Compatibility

Ative Application > Run > Low Processor Mode para que tenha um delay entre as renderizações da janela e apenas renderizar se alguma mudança for detectada. Em jogos a tela muda constantemente, então esse tipo de delay e validação só atrapalham mas como estamos falando de GUI que altera bem menos, isto ajuda muito.

Low Processor Mode

2 - Window Title Bar

Por padrão o nosso sistema operacional nos providência o gráfico básico de uma janela e nos deixa responsável por desenhar o conteúdo dentro dela.

Not borderless

O lado positivo é que isto nos providência o básico de uma janela, como aqueles 3 botões no topo da direita (minimizar, máximizar, fechar, ...).

Porém note que, dependendo do sistema operacional, mais opções podem estar disponíveis! Se eu clicar com o botão direito na title bar do topo, podemos ver mais ações:

Not borderless again

Se ativarmos Display > Window > Size > Borderless, o sistema operacional deixará de adicionar a title bar no topo:

Borderless

Basicamente ele está assumindo que você mesmo irá desenhar a title bar no topo caso queira (normalmente em jogos isto não faz sentido).

info

Borderless ou não, ainda se trata de uma janela no seu sistema operacional então alguns atalhos podem continuar funcionando (Super + Up/Down/Left/Right, Alt + Space).

3 - Multiple Windows

Janelas abertas são tratadas como processos filhos, ou seja, o encerramento de uma janela pai irá encerrar os filhos.

Caso queiramos ter múltiplas janelas idênticas, igual a editores de textos e navegadores, precisamos ter certeza que a janela principal (processo inicial) não possa ser encerrado da maneira padrão (clickando no botão de fechar).

Podemos resolver isto escondendo a janela principal e apenas exibindo as subwindows.

Ative Display > Window > Size > Transparent para que o fundo cinza padrão não seja renderizado durante a execução.

Transparent background

note

Acredito que a cor padrão do canvas é preto, por isto deixar de pintar vai deixar a janela preta

Ative Display > Window > Per Pixel Transparency > Allowed para que o fundo realmente seja transparente (caso contrário vai ficar o canvas preto).

Per Pixel Transparency

warning

Existe uma configuração que eu ainda não entendi a necessidade: Rendering > Viewport > Transparent Background.

Mas a documentação menciona ela como necessária.

Como visto na sessão anterior...

Ative Display > Window > Size > Borderless para o sistema operacional deixará de adicionar a title bar no topo.

Invisible

Embora ela esteja transparente, ela ainda é uma janela como as outras. Podemos conferir que ela ainda aparece quando apertando Alt+Tab (ou apenas apertando Alt no Ubuntu).

Window invisible but exist

Agora nós temos que tratar inputs!

Primeiro podemos notar que se está janela for posta na frente de outra, ela não irá deixar de consumir os seus clicks (mesmo que você queira selecionar algo na janela de trás).

Para resolver isto podemos alterar janela raiz (criada quando seu programa inicia) para repassar adiante clicks do mouse.

func _ready() -> void:
get_window().mouse_passthrough = true

Segundo podemos notar que ela ainda está processando teclas (pode ser selecionada pelo Alt + Tab, fechada por Alt + F4, maximizada com Super + Up, etc).

Ative Display > Window > Size > No Focus para que ela não possa ser focada (até por atalhos).

Process invisible

Lembre que fechar o processo pai fecha todos os filhos, porém fechar todos os filhos não fecha o pai.

Isto quer dizer que o processo pai continua rodando mesmo se o usuário fechar todas as janelas filhas. Agora o usuário apenas conseguiria encerrar o programa pelo "gerenciador de tarefas" ou terminal.

Para tratar isto podemos ligar um signal a um método responsável por notar quando a quantidade de filhos mudar e encerrar o programa se necessário.

Signal child_order_changed

func _on_child_order_changed() -> void:
if get_child_count() == 0:
get_tree().quit()
note

Isto é apenas uma maneira de tratar!

Nós poderiamos checar a cada frame se todas as janelas foram fechadas, poderiamos fazer os filhos avisarem o pai quando fossem encerrados, etc.

Agora precisamos entender que cada janela aberta é uma subwindow. Existem dois tipos de subwindows:

  1. Subwindows
    • Quando sua janela pede ao sistema operacional para criar uma janela filha dela
    • Sua janela filha vai possuir a title bar padrão de janelas
  2. Embed subwindows
    • Quando sua janela simula outra janela dentro dela mesmo
    • Isto impossibilita ela de ser mover para fora da janela pai

Se estamos tratando de uma aplicação que possui múltiplas janelas, precisamos que ela se mova para fora da janela pai. Caso contrário isso ocorreria:

Subwindow + Half subwindow

A janela 2 está saindo do limite da janela pai.

Poderiamos inicializar a janela pai maximizada para evitar isto porém outros problemas iriam aparecer, por exemplo: Janela pai ignorar clicks e teclas, tornando impossível interagir com as janelas simuladas nele.

Desative Display > Window > Subwindows > Embed Subwindows para que as subwindows sejam tratadas como janelas reais pelo sistema operacional (em vez de simuladas pela janela pai).

Subwindows

Mas se quisermos ter uma title bar de janela única para as nossas janelas? Podemos fazer o mesmo que fizemos com janela principal, torna-la borderless.

subwindows borderless

Dentro das propriedades da Janela, ative Flags > Borderless.

subwindows borderless activate

Agora nós seriamos responsáveis por criar a title bar no topo da janela. Desta maneira poderiamos fazer uma title bar única igual ao Google Chrome ou Steam!

4 - Custom Title Bar

Ter uma title bar própria é relativamente raro hoje em dia, pois muitas vezes requer reinventar a roda sem trazer benifícios reais.

Mas isto não quer dizer que nenhuma aplicação faz isto:
Custom Title Bars
(Steam, GNOME Files, Google Chrome)

Note que as 3 aplicações aproveitaram o espaço para providênciar mais informações e funcionalidades ao usuário. Porém nós vamos focar em pelo menos reproduzir o básico:

  1. Exibir titulo
  2. Providênciar botões de minimizar, maximizar e fechar
  3. Double click maximizar
  4. Arrastar a title bar deve mover a janela
  5. Redimensionar janela se arrastar as bordas

Depois disso você deve ser capaz de adicionar ou remover mais utilidades conforme a sua vontade.

warning

Estarei partindo do princípio que queremos customizar uma title bar na janela principal, por isto o código utiliza get_window(), mas adaptações podem ser necessárias caso esteja tratando subwindows.

Exibir Titulo

Basta utilizar o node Label.

Minimize, Maximize, Close Buttons

Basta utilizar 3 nodes Button tratando o signal pressed:

func _on_minimize_pressed() -> void:
get_window().mode = Window.MODE_MINIMIZED


func _on_maximize_pressed() -> void:
if get_window().mode == Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
else:
get_window().mode = Window.MODE_MAXIMIZED


func _on_close_pressed() -> void:
get_tree().quit()
subwindows tip

É importante tratar o signal close_requested vindo da janela, pois é por ele que você recebe notificações que o usuário tentou fechar de outras maneiras (taskbar do windows, etc).

Double Click Maximize

Container não possui signal para isto diretamente porém podemos utilizar o signal mais geral gui_input.

func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()


func _on_title_bar_double_click() -> void:
match get_window().mode:
Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
_:
get_window().mode = Window.MODE_MAXIMIZED

Já estamos dividindo em funções menores pois os passos seguintes irão adicionar mais funcionalidades nestas funções gerais.

Drag Window

A princípio, arrastar a janela pode ser resumido em saber duas coisas:

  • Saber se o click do mouse está sendo pressionado
  • Onde que o click estava quando começou
var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)
elif event is InputEventMouseMotion:
_on_title_bar_mouse_motion(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()
elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_title_bar_dragging = true
_title_bar_dragging_start = get_global_mouse_position()
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_title_bar_dragging = false


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
if _title_bar_dragging:
_on_title_bar_dragged()


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
subwindows tip

Primeiro: Talvez seja bom mover para o centro da tela a janela pois a posição poder não estar correta durante a inicialização (bug?):

func _ready() -> void:
get_window().move_to_center()

Segundo: Talvez seja necessário utilizar get_local_mouse_position() em vez de get_global_mouse_position() pois deve ser necessário o canvas da própria subwindow.

Esse foi apenas o essencial sobre arrastar, agora podemos pensar em implementar detalhes sobre a ação de arrastar janelas.

Por exemplo: Quando o usuário tentar arrastar uma janela máximizada, ela automaticamente sai do máximizado e se posiciona para que o mouse esteja proporcionalmente na posição correta.

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
Window.MODE_MAXIMIZED:
_title_bar_dragging_adjustment = get_global_mouse_position().x / get_window().size.x
get_window().mode = Window.MODE_WINDOWED


func _on_resized() -> void:
if _title_bar_dragging_adjustment != 0:
get_window().position += (get_global_mouse_position() as Vector2i)
get_window().position.x -= get_window().size.x * _title_bar_dragging_adjustment
_title_bar_dragging_start = get_global_mouse_position()
_title_bar_dragging_adjustment = 0

Resize Window

Redimensionar pode ser facilmente implementado se utilizarmos o node MarginContainer que nos permite adicionar bordas às laterais, estas serão nossas bordas que devem reagir ao mouse.

Nodes do tipo Control possuem lógica para lidar com inputs do mouse, eles podem consumir ou passar ao node de cima as input do mouse.

Isso quer dizer que qualquer input do mouse na nossa janela (que não tiver sido consumida) chegará ao nossoMarginContainer. Isto não é o que queremos, para nós só é interessante que chegue inputs interagindo com a borda do nosso container.

Podemos resolver isto parando o consumo de inputs no container logo abaixo do MarginContainer:

Margin

Agora temos certeza que interações vindo do signal gui_input são interações diretas com o MarginContainer.

enum Margin {
NONE,
TOP,
RIGHT,
BOTTOM,
LEFT,
TOP_RIGHT,
TOP_LEFT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
}

var _margin_dragging: bool = false

var _margin_dragging_edge_start: Vector2i

var _margin_selected: Margin

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0

func _get_current_margin() -> Margin:
var margin: Margin = Margin.NONE

if get_global_mouse_position().x < get_theme_constant("margin_left"):
margin = Margin.LEFT
elif get_global_mouse_position().x > size.x - get_theme_constant("margin_right"):
margin = Margin.RIGHT

if get_global_mouse_position().y < get_theme_constant("margin_top"):
match margin:
Margin.LEFT:
return Margin.TOP_LEFT
Margin.NONE:
return Margin.TOP
Margin.RIGHT:
return Margin.TOP_RIGHT
elif get_global_mouse_position().y > size.y - get_theme_constant("margin_bottom"):
match margin:
Margin.LEFT:
return Margin.BOTTOM_LEFT
Margin.NONE:
return Margin.BOTTOM
Margin.RIGHT:
return Margin.BOTTOM_RIGHT

return margin


func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_mouse_button(event)
elif event is InputEventMouseMotion:
_on_mouse_motion(event)


func _on_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_margin_dragging = true
_margin_selected = _get_current_margin()
_margin_dragging_edge_start = get_window().position + get_window().size
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_margin_dragging = false


func _on_mouse_motion(_event: InputEventMouseMotion) -> void:
if _margin_dragging:
_on_dragged()
else:
_on_hover()


func _on_dragged() -> void:
if get_window().mode != Window.MODE_WINDOWED:
return

var mouse_position: Vector2i = get_global_mouse_position()

match _margin_selected:
Margin.TOP:
get_window().position.y += mouse_position.y # TODO: Fixing moving window
get_window().size.y = _margin_dragging_edge_start.y - get_window().position.y
Margin.RIGHT:
get_window().size.x = mouse_position.x
Margin.BOTTOM:
get_window().size.y = mouse_position.y
Margin.LEFT:
get_window().position.x += mouse_position.x # TODO: Fixing moving window
get_window().size.x = _margin_dragging_edge_start.x - get_window().position.x
Margin.TOP_RIGHT:
get_window().position.y += mouse_position.y # Top
get_window().size = Vector2i(
mouse_position.x, # Right
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.TOP_LEFT:
get_window().position = Vector2i(
get_window().position.x + mouse_position.x, # Left,
get_window().position.y + mouse_position.y, # Top
)

get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.BOTTOM_RIGHT:
get_window().size = Vector2i(
mouse_position.x, # Right
mouse_position.y, # Bottom
)
Margin.BOTTOM_LEFT:
get_window().position.x += mouse_position.x # Left
get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
mouse_position.y, # Bottom
)


func _on_hover() -> void:
match _get_current_margin():
Margin.TOP:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.RIGHT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.BOTTOM:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.LEFT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.TOP_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE
Margin.TOP_LEFT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_LEFT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
...


func _on_resized() -> void:
...

Dentro das funções novas, muitas possuem a mesma lógica utilizada para arrastar janela. Porém duas possuem lógica nova: _get_current_margin() e _on_dragged()

A primeira é responsável por identificar a borda a qual o mouse se encontra (varias validações para identificar a posição do mouse em relação as bordas).

A segunda é a lógica de redimensionar, para resolver ela é recomendado primeiro resolver a lógica para cima, direita, baixo e esquerda (as diagonais são combinações das lógicas das outras).

note

Redimensionar uma janela inclui redimensionar os items dentro dela, isso pode ser um tanto quanto custoso de se fazer todas as frames.

Eu penso em testar redimensionar de tempos em tempos e apenas se tiver algum redimensionamento pendente 🤔.

references

Thiago Lages de Alencar

Esse post é meio que uma compilação do que eu entendi de cada assunto após de horas lendo na internet e perguntando para meu pai.
Em outras palavras: Pode ter informação incorreta!

TTY

Teletype
(https://en.wikipedia.org/wiki/Teleprinter)

TTY é um comando em linux para saber o nome do terminal o qual a input está diretamente conectada.

$ tty
/dev/pts/1

$ echo "example" | tty
not a tty

No primeiro exemplo, o comando tty veio como input diretamente do terminal.

No segundo exemplo, o comando tty recebeu a input example do comando anterior (não de um terminal).

Fim! Pode ir para a próxima sessão, ao menos que você queira saber o que diabos é uma teletype.

Rosto curioso

Já notou que muitas coisas no computador possuem o nome de objetos que existem fora do computador?

Acontece que o nome é dado baseado nestes objetos para ajudar usuários a entenderem melhor o uso deles no computador! Por exemplo:

  • file
  • folder
  • trash can
  • window

O mesmo vale para TTY, onde o nome veio de teletypes. Infelizmente não é um nome que ajude muito pois computadores já subsittuiram o uso delas então esse nome não ajuda ninguém a saber do que o comando se trata 🤣.

O que são teletypes?
Entenda que elas são uma junção de typewriters e telegraph key.
O primeiro utilizado para escrever em papel e o segundo utilizado para enviar morse code a distância.

Morse code era muito utilizado como uma forma de comunicação binária (som curto/longo) via cabo. Porém para isso acontecer era necessário duas pessoas treinadas em morse code (uma para enviar e outra para receber).

mensagem <=> tradutor <=> morse code <=> transmissão

Com a chegada das teletypes, os tradutores foram substituidos por estas máquinas que eram capaz de traduzir e ler morse code.

mensagem <=> teletype <=> morse code <=> transmissão

Computadores da época utilizavam paper tape como forma de armazenamento de dados e monitores não eram algo acessível. Teletypes viram está oportunidade para preencher outro meio de comunicação (dado que computadores também utilizam binário).

mensagem <=> teletype <=> binário <=> computador

Este video mostra uma teletype recebendo e enviando dados de/a um terminal:
https://www.youtube.com/watch?v=S81GyMKH7zw

Por isto que o termo TTY era utilizado para referência aparelhos enviando/recebendo (input/output) mensagem do computador.

Algumas linguagens até incluem código para fazer essa verificação:

  • C
    • #include <unistd.h>

      isatty(fildes);
  • Python
    • import os

      os.isatty(fd)
  • NodeJS
    • tty.isatty(fd)

Onde a funcionalidade das funções é identificar se a input/ouput está vinculada a um aparelho (device).

warning

Preste bem atenção que sua input pode estar ligada ou não a um aparelho E sua output pode estar ligada ou não a um aparelho.
Um deles estar ligado não quer dizer que ambos estão.

As funções recebem um file descriptor e dizem se ele está ou não linkado a um aparelho.
Você poderia passar STDIN, STDOUT ou até STDERR para a função analisar.

Esse video cobre bem o assunto: https://www.youtube.com/watch?v=SYwbEcNrcjI

Se realmente quiser saber detalhes sobre TTY, existe este blog cheio de informações (que eu não li):
https://www.linusakesson.net/programming/tty/

TTY

Terminal

(https://en.wikipedia.org/wiki/Computer_terminal)

Teletypes originalmente eram conhecidas como "hard-copy terminals" por usarem papel, mas com a vinda de telas nós formamos uma nova ideia de terminal nas nossas cabeças (a tela preta).

Terminal não possui armazenamento de dados, da mesma maneira que teletypes apenas eram responsáveis por ler e escrever do computador, ou seja, a lógica ainda estava no computador. Alguns terminais possuiam um pouco de lógica neles porém nada comparado ao computador.

Se você viu o video da sessão anterior então já deve ter ganhado uma ideia do que é um terminal, pois nele é mostrado uma teletype lendo e escrevendo para um terminal.
Mas caso queira outro video mostrando melhor um terminal:
https://www.youtube.com/watch?v=UNdu0YQfvF0

Terminal

Terminal Emulator

(https://en.wikipedia.org/wiki/Terminal_emulator)

Hoje em dia usamos o termo terminal para representarmos emuladores de terminais.

Diferentemente de terminais, estes estão fortemente ligados a computador e não são máquinas separadas da lógica. Basicamente estamos falando da janela que finge ser um terminal (GNOME terminal).

Terminal Emulator

Shell

(https://en.wikipedia.org/wiki/Shell_script)

Um programa responsável por ficar em loop esperando comandos do usuário para serem executados.

Comandos podem ser:

  • Programas
    • echo
    • ls
    • touch
    • mkdir
    • Buscados em lugares pré definidos (/bin, /usr/bin, ...)
      • Use echo $PATH para ver a lista de lugares a se olhar
  • Comandos do próprio shell
    • type
    • which
    • help
    • man
    • Estes existem dentro do shell e não precisam ser buscados.
  • Shell functions
  • Aliases
    • Comandos definidos por nós, construido de outros comandos

Existem variações e alternativas de shell:

Shell

CLI

Command-line interface
(https://en.wikipedia.org/wiki/Command-line_interface)

É uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa.

Está interface se basea no usuário passar flags e mais informações em conjunto ao comando, dessa maneira mudando o comportamento do commando. Por exemplo, o programa ls disponibiliza diversas flags para alterar o comportamento:

  • ls
    • Lista tudo no diretório atual mas ignora os começando com .
  • ls -a
    • Lista tudo no diretório atual e não ignora os começando com .
  • ls -l
    • Lista tudo no diretório atual mas com mais detalhes

Fique bem claro que é o programa te dando opções de como interagir com ele, não o shell ou terminal, então resta ao programa implementar comportamentos para certas flags.

note

É muito comum programas oferecerem detalhes sobre as flags quando utilizando a flag --help (ls --help).

CLI

TUI

Terminal user interface
(https://en.wikipedia.org/wiki/Text-based_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém está foca em dar uma interação mais visual e continua.

Diferente de CLI's onde toda a interação começa e termina em um comando só, TUI's continuam esperando mais interações do usuário até um dos dois decidirem terminar.

Um exemplo bem comum é top que providência uma visão dos programas/processos/threads em execução do sistema, uma vez inicializado ele esperar por mais interações do usuário. Se você apertar q ele termina, se você apertar h ele fornece a lista de comandos, etc.

Note que a TUI's ainda podem providênciar flags para alterar o comportamento (top --help).

TUI

GUI

Graphical user interface
(https://en.wikipedia.org/wiki/Graphical_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém não está limitada a usar texto para a visualização, pois tem a capacidade de desenhar na tela.

Hoje em dia é o meio mais popular de se usar uma aplicação, quando se abre VSCode, Google Chrome, Discord... Todos são GUI's pois utilizaram a capacidade de desenhar para dar uma interface ao usuário.

Mesmo programas focados em GUI's podem aceitar flags (VSCode: code --help).

GUI

References

Thiago Lages de Alencar

No post anterior declarei que busquei por um tempo uma interface gráfica (GUI) para MongoDB porém não fiquei satisfeita com nenhuma.

Neste post vou falar do desenvolvimento do Mondot, interface gráfica para o banco MongoDB.

Por favor, levar em conta que eu posso tomar decisões ruins 🤣

Reason

Em 2021, eu tinha duas coisas em mente:

  • Nenhuma GUI de Mongo me agrada
  • Quero aprender Godot

Bastou essas duas coisas para eu querer começar este projeto.
Eu não queria fazer a melhor GUI do mundo, eu queria me divertir com Godot ao mesmo tempo que resolvia um incomodo que eu tinha com GUIs.

Start

Abri o Godot, comecei a criar containers, janelas, botões, etc. Dias depois me veio a pergunta:

Como diabos eu vou me comunicar com o Mongo?

Rosto de idiota com o olhar torto para fora

Eu vou pegar o texto que o usuário escrever e fazer o que com ele?
Passar para um Node.js?
Eu vou ter que instalar Node.js na máquina da pessoa?
Como eu pego a resposta?
Eu conseguiria usar GDNative?
Como que os outros projetos fazem isto?

Agora eu precisava descobrir como faria isto acontecer.

Cheating

Colando do colega do lado

Robomongo! Também conhecido como Robo 3T.
Um projeto open-source que em 2017 foi adquirido pelos criadores do Studio 3T.

O que importa é que eu tenho um projeto para copiar estudar!

Robo 3T possui dois repositórios:

  • robomongo
    • Responsável pela interface gráfica
  • robomongo-shell
    • Responsável pela interação com o Mongo.

Importante notar que o segundo é um fork do Mongo oficial, justamente pois nele é incluido um shell interativo para se comunicar com o banco.

Isso faz sentido, já que a interface de linha de comando geralmente é a primeira coisa a ser feita para interagir com bancos. Como mais as pessoas interagiriam com o banco antes de GUI existirem? Poderia ser por código mas seria um baita trabalho cada operação.

Então era isso, tudo que eu tinha que fazer era:

  1. Incluir o shell do Mongo na minha interface gráfica
  2. Utilizar o modo iterativo do shell

Problems

Eu não tinha ideia de como fazer para incluir o shell na interface gráfica.
Decidi aceitar que passaria como o shell separado para o usuário e dei essa parte como dada.

Eu que não vou reclamar das minhas decisões idiotas no meu projeto pessoal.

Shrug face

Agora tudo que eu tinha que fazer é chamar o shell pelo Godot.
Para isto eu iria precisar usar o método OS.execute().

int execute (
String path,
PoolStringArray arguments,
bool blocking=true,
Array output=[],
bool read_stderr=false,bool open_console=false
)

O problema é que essa função possui dois comportamentos dependendo se blocking for true ou false, porém nenhum dos dois era o que eu buscava.

Outro rosto de idiota com o olhar torto para fora

blocking -> true: Godot vai pausar enquanto a saída não é escrita em output.

Não queremos isto pois uma busca pode resultar em milhares de documentos. Queremos retornar mais documentos conforme o usuário pedir por mais documentos.

blocking -> false: Godot vai continuar executando e o comando irá rodar em um processo separado. Porém não será possível recuperar o output do comando.

Nós queremos receber a saída aos poucos se estivermos falando de pesquisas que resultam em muitos documentos.

Função do Godot não é interativa, como vou usar o shell interativo?
Posso ler input e output usando algum conhecimento do sistema operacional?
Eu vou ter que conhecer bem todos sistemas operacionais?
Vou ter que descobrir como Robo 3T linka o shell?

Solution

Criar meu próprio shell.

Shell

Vantagens:

  • Utilizar a linguagem que mais estou em dia Python
    • Em outras palavras: Nenhum tempo perdido por errar algo de uma linguagem que não estou acostumado (JavaScript)
  • Utilizar a mesma linguagem com qual trabalho para fazer queryies
  • Utilizar o padrão de comunicação que eu quiser
    • No próximo tópico você vai entender o que eu quero dizer com isto

Se estivessemos falando de um produto para diversos usuários, definitivamente JavaScript seria a melhor escolha.

Para projeto pessoal com interesse em Godot? Utilizar a linguagem que mais tenho conhecimento no momento.

Communication

Shell

De todas as maneiras para duas aplicações se comunicarem, eu escolhi a mais simples.

Arquivos... :D

Em vez de quebrar a cabeça para entender como eu poderia reproduzir a interatividade do shell do Mongo, eu poderia apenas fazer com que ambos escrevessem em arquivos quando quisessem se comunicar um com o outro.

O processo em si é bem simples:

  1. Godot executa o shell, passando a query
  2. Shell escreve X documentos em um arquivo
  3. Godot solicita mais documentos

Note que você deve repetir etapa 2 e 3 até acabar os documentos ou Godot mandar parar.

Start

Primeira parte da comunicação entre shell e Mondot

  1. Godot escreve o código Python em um arquivo
    • xxxxx representa o nome do arquivo
    • O nome do arquivo é aleatório
  2. Godot executa o shell passando o caminho para o arquivo como parâmetro

Output

Segunda parte da comunicação entre shell e Mondot

  • Godot fica periodicamente conferindo se o arquivo de saída existe
    • xxxxx_out_0 representa o primeiro arquivo de saída
      • xxxxx_out_1 segundo arquivo de saída
      • xxxxx_out_2 terceiro arquivo de saída
      • etc
  • Shell escreve no arquivo de saída um JSON com o resultado da query
    • O resultado de uma query pode ser partido em diversos arquivos
      • xxxxx_out_0, xxxxx_out_1, xxxxx_out_2, ...
      • Esses arquivos são criados conforme o usuário solicita mais documentos do resultado

Input

Terceira parte da comunicação entre shell e Mondot

  • Godot escreve no arquivo de entrada solicitando mais documentos do resultado da query
  • Shell fica periodicamente conferindo se tem algo escrito no arquivo de entrada
    • xxxxx_in representa o arquivo de entrada
    • Ele lê o arquivo e remove o conteúdo do arquivo
      • Justamente para receber futuras solicitações pelo mesmo arquivo

Shell

O que o meu shell precisa fazer?

  • Receber o código do usuário
  • Executar o código do usuário
  • Iterar sobre o resultado do código
    • Iterar em partes (pegando X documentos por vez)

Em outras palavras, eu preciso acoplar o código do usuário ao código do shell.

Código do usuário sendo acoplado no meio do código do shell

Note que existem operações que meu shell irá fazer antes e após a execução do código do usuário. Por causa disso, o desenho mostra o código no meio do código shell.

O plano era receber um código simples do usuário, parecido com o do shell do Mongo:

db.test.find({})

Attempt 1

eval(expression, globals=None, locals=None)

Bom: A função faz o parser e avalia a expressão, retornando o resultado dela.

result = eval("""
db.test.find({})
""")

Ruim: Precisa ser uma expressão. Uma ou mais declarações não funcionam.

# Error
result = eval("""
db.test.find({})
db.test.find({})
""")

Attempt 2

exec(object, globals=None, locals=None, /, *, closure=None)

Bom: Executa múltiplas declarações.

exec("""
db.test.find({})
db.test.find({})
""")

Ruim: Cada declaração pode ter ou não um resultado, então essa função não retornar nada.

# result is None
result = exec("""
db.test.find({})
db.test.find({})
""")

Problem

Nas tentativas acima, para obter o resultado desejado eu teria que alterar a string. Porém, isto apresenta grande risco da alteração dar erro pois cada caso pode requer uma alteração diferente.

No final eu seria forçado a ler o código, entende-lo para depois alterar sem grande chance de erros. Mas sabe quem já faz isso de ler e entender código? O compilador.

AST

Desenho de uma árvore para representar AST

A compilação de uma linguagem envolve diversas etapas. Uma delas envolve gerar uma árvore sintática abstrata, uma árvore que garante que os objetos estão ligados corretamente.

Podemos usar este conhecimento para compilar parcialmente o código do usuário. Por exemplo, o seguinte código:

for x in db.test.find({}):
db.test.update({"_id": x["_id"]}, {"code": x["code"] + 1})
db.test.find({})

Gera uma árvore com a seguinte raiz:

Simples AST

Note que eu só mostrei o início da árvore pois todo o resto é irrelevante para nós.
Não vamos alterar nada que esteja a fundo do código do usuário, apenas na raiz.

Com esse conhecimento em mão, podemos finalmente alterar o código do usuário!

info

Utilizei um módulo do próprio Python chamado ast para analisar e reestruturar a AST.

Rewriting code

Vamos criar um código inútil apenas para usar como exemplo:

# User code
doc = db.test.find_one({})
db.test.update_one({}, {"valid": True})
db.test.find({})

A idéia é transformar este código em uma função que o shell poderá chamar e receber de volta o valor da última expressão. Em outras palavras, queremos isto:

def code():
# User code
doc = db.test.find_one({})
db.test.update_one({}, {"valid": True})
return db.test.find({})

Vamos ao passo a passo de como obter isto:

  1. Análisar o código do usuário com AST
  2. Inserir o conteúdo do módulo dentro de uma função pré montada
  3. Encapsular a última expressão em um return
    • Apenas fazer isto se for uma expressão que retorna valor

O video seguinte demonstra a transformação que está sendo feita de certa forma.

Conclusion

Com isto eu consegui preparar justamente uma interface gráfica para o Mongo em Godot. Esse foi meu projeto de 2021!


Em 2023 eu decidi dar uma refatorada e criei uma segunda versão. Mas toda essa base vista do Mondot ainda é a mesma!


Agora já estou cansado de falar deste projeto hahahaha, foi bom enquanto durou.

References

Thiago Lages de Alencar

Em 2019 eu comecei a trabalhar em uma startup que usava banco de dados MongoDB.

Na época mantermos diversas licenças da interface gráfica Studio 3T não era uma boa ideia, não eramos uma empresa lucrativa e o uso dela por pessoa não iria valer a pena.

E por isso comecei a buscar por um interface gráfica que fosse me satisfazer.

Pessoa caminhando com uma madeira carregando seus pertences na ponta

Robot 3T

Uma alternativa considerada foi Robo 3T/Robomongo mas pelo fato dele ter sido adquirido pelo Studio 3T já não era um bom sinal para mim.

É muito normal ver empresas grandes comprando concorrentes e depois deixando eles aos poucos sumirem, então meu palpite era que Robot 3T não iria receber tantas atualizações e não cresceria como ferramenta.

Hoje em em 2023: Fazendo 6 anos desde que foi adquirido e uma versão de graça do Studio 3T foi lançada para substituir o Robo 3T que foi descontinuado.

Mongo-Express

Utilizei mongo-express por um tempo e satisfazia o mínimo que eu precisava mas a interface gráfica não me agradava.

Hoje em em 2023: Parece ter mantido a mesma interface, o que não me atrai.

QueryAssist

Em 2020, QueryAssist foi a ferramenta que eu decidi utilizar sempre para interagir com o Mongo.

Ela possuia uma versão gratuita mas você podia comprar a versão vitalícia e ganhar mais funcionalidades. A versão gratuita era mais que o suficiente para mim e funcionava muito bem.

Infelizmente durou pouco, fui notando a falta de atualização no blog e nos posts do Twitter. Claramente um sinal que a ferramenta iria desaparecer, por causa disso decidi retornar a minha busca por outra interface gráfica antes que está parece de funcionar sem aviso prévio.

Hoje em em 2023: Site deles foi desligado e eles não tem postagens novas desde 2020.

MongoDB Compass

MongoDB Compass parece estar em construção desde 2015, mas na época que eu estava buscando interfaces gráficas era como se ele não existisse para mim. Meu palpite era que não estava divulgando já que não estava pronto para produção 🤔.

Tudo que posso dizer é que quando finalmente descobri a existência dele, a ausência de alguma funcionalidade não me agradou...

Hoje em em 2023: Aposto que hoje em dia já devem ter implementado a funcionalidade que eu queria mas não estou em dia com as ferramentas do Mongo para saber.

MongoDB for VS Code

Quando MongoDB for VS Code lançou oficialmente para mim era um sinal que minha busca tinha terminado, pois era uma ferramenta desenvolvida pelos mesmos criadores do MongoDB e que era só uma questão de tempo até implementarem diversas funcionalidades legais.

Nesta época eu até parei o desenvolvimento de uma ferramenta que eu estava construindo.

Hoje em em 2023: Parece que a extensão apenas foi criada para fazer o mínimo e não deve evoluir muito.