Hackathon de GTK

No evento de hoje vamos apresentar o GTK, framework de desenvolvimento de aplicações para GNU/Linux. Além disso, usaremos a biblioteca libadwaita, que inclui vários elementos gráficos (widgets) prontos para nos ajudar a fazer um aplicativo do zero. Essa biblioteca foi desenvolvida pelo Projeto GNOME, com o objetivo de padronizar a experiência de usuário nos aplicativos de sua plataforma.

Faremos isso em colaboração com Georges Stavracas, contribuidor notável do projeto GNOME e desenvolvedor de diversos aplicativos que utilizam GTK. Atualmente, é membro Endless OS Foundation, e trabalha no desenvolvimento de um sistema operacional que busca melhorar o acesso a conhecimento e oportunidade por meio da tecnologia.

Para consulta durante o evento, disponibilizamos abaixo uma tradução autoral da Documentação para Desenvolvedores do GNOME (CC-BY-SA). Todas as seções, exceto a primeira, são independentes entre si e podem ser lidas em qualquer ordem.


Começando um projeto

Para começar a desenvolver um aplicativo para o ambiente gráfico GNOME, você deve seguir os seguintes passos:

novo-projeto

  1. instale a última versão do GNOME Builder

  2. na tela de Welcome, selecione Start new project

  3. configure as opções do projeto

    • escreva “text-viewer” como o nome do projeto
    • escreva “com.example.TextViewer” como o ID de aplicação do seu projeto.
    • selecione GPL-3+ como o termo de licenciamento do seu projeto
  4. selecione o template GNOME Application

novo-projeto

Quando o Builder terminar de criar o projeto da sua aplicação, você vai encontrar os seguintes arquivos:

com.example.TextViewer.json

Esse é o manifesto Flatpak para sua aplicação. Você pode usar o manifesto para definir as dependências do projeto. O manifesto padrão depende do runtime estável mais recente da plataforma GNOME. Você também pode incluir dependências não providas pelo runtime do GNOME.

meson.build

Esse é o principal arquivo de build do Meson, que define como e o que compilar para sua aplicação.

src/

Esse é o diretório com o código fonte da sua aplicação, assim como os arquivos de definição de UI para seus elementos gráficos (widgets).

src/text_viewer.gresource.xml

O manifesto GResource para os assets que serão compilados no projeto usando glib-compile-resources.

po/POTFILES

A lista de arquivos strings traduzíveis visíveis para o usuário.

data/com.example.TextViewer.gschema.xml

O arquivo de esquema para as configurações da aplicação.

data/com.example.TextViewer.desktop.in

O arquivo de entrada de desktop para a aplicação.

data/com.example.TextViewer.appdata.xml.in

Os metadados da aplicação usados por lojas de aplicativos e distribuidoras de aplicações.

Se você quiser, agora você pode compilar e rodar aplicação apertando o botão Run ou Ctrl + F5


Adicionando uma visualização de conteúdo

Nessa seção você vai aprender como modificar o arquivo de definição da UI da janela do aplicativo para adicionar um elemento de interface de usuário de área de texto. A área de texto vai ser utilizada para mostrar o conteúdo de um arquivo de texto que vamos aprender a abrir na próxima seção.

content_view.png

Toda aplicação GNOME é composta por uma hierarquia de elementos UI chamada de widgets; GTK permite definir UI usando XML ou inves de escrevê-los em código. O template padrão para aplicações GNOME fornecido pelo Builder usa um arquivo de definição para a janela principal do aplicativo, e vamos editá-lo como qualquer outro arquivo.

  1. Abra o arquivo text_viewer-window.ui dentro do diretóriosrc.

  2. A janela é definida como o template para a classe TextViewerWindow

  3. A janela tem elementos property que descrevem o valor para varias propriedades; por exempo, definir o titulo padrão da janela é feito utilizando a propriedade title

  4. A janela também tem dois elementos child (filho)

    • o primeiro elemento child é do tipo titlebar e é usando para descrever o conteúdo da barra de título; nesse caso a GtkMenuButton com o menu primário da aplicação
    • o segundo elemento child é a área de conteúdo central da janela
  5. Atualmente o principal conteúdo é fornecido por um widget GtkLabel, com uma etiqueta “Hello, World!”

  6. Fora do bloco template você pode achar a definição do menu usando o elemento menu

Definindo o título da janela principal

  1. Encontre a definição TextViewerWindow
  2. Econtre os elementos property que definem o valor padrão para a largura e altura da janela
  3. Adicione a seguinte propriedade:
    <template class="TextViewerWindow" parent="GtkApplicationWindow">
         <property name="default-width">600</property>
         <property name="default-height">300</property>
         <property name="title">Text Viewer</property>
         <child type="titlebar">
           <object class="GtkHeaderBar" id="header_bar">

Definindo o estilo de desenvolvimento para a janela principal

O estilo devel diz para o usuário que a aploicação ainda esta em desenvolvimento.

  1. Adicione o seguinte estilo:
    <template class="TextViewerWindow" parent="GtkApplicationWindow">
         <property name="default-width">600</property>
         <property name="default-height">300</property>
         <property name="title">Text Viewer</property>
         <style>
           <class name="devel"/>
         </style>
         <child type="titlebar">
           <object class="GtkHeaderBar" id="header_bar">

Adicionando um scrollable container (contêiner rolável)

Siga esses passos para adicionar um scrollable container para a janela|

  1. Primeiro você precisa remover o elemento UI que ja está na janela. Econtre o elementro object que define o GtkLabel e remova todo o bloco mas mantenha o elemento child
  2. Adicione a seguinte deifinição da interface de usuário para o scrollable container (container rolável) dentro do elemento child:
    <child>
         <object class="GtkScrolledWindow">
           <property name="hexpand">true</property>
           <property name="vexpand">true</property>
           <property name="margin-top">6</property>
           <property name="margin-bottom">6</property>
           <property name="margin-start">6</property>
           <property name="margin-end">6</property>
         </object>
       </child>

A definição do scrollable container tem as seguintes propriedades:

  • hexpand e vexpand fazem com que o container se expanda para se adaptar ao conteúdo da janela “pai”
  • margin-top,margin-bottom fazem com que o container adicione uma margem de seis pixels na parte superior e inferior
  • margin-start e margin-end fazem com que o container adicione seis pixels nas bordas iniciais e finais respectivamente; as bordas são determinadas pela direção do texto

Adicionando uma exibição de texto

Siga estes passos para adicionar um text view widget para o contêiner rolável:

  1. Adicione um novo elemento property para a propriedade do child
    <child>
         <object class="GtkScrolledWindow">
           <property name="hexpand">true</property>
           <property name="vexpand">true</property>
           <property name="margin-top">6</property>
           <property name="margin-bottom">6</property>
           <property name="margin-start">6</property>
           <property name="margin-end">6</property>
           <property name="child">
           </property>
         </object>
       </child>
  1. Adicione uma definição object para o GtkTextView widget e atribua o main_text_view como seu identificador.
    <child>
         <object class="GtkScrolledWindow">
           <property name="hexpand">true</property>
           <property name="vexpand">true</property>
           <property name="margin-top">6</property>
           <property name="margin-bottom">6</property>
           <property name="margin-start">6</property>
           <property name="margin-end">6</property>
           <property name="child">
             <object class="GtkTextView" id="main_text_view">
               <property name="monospace">true</property>
             </object>
           </property>
         </object>
       </child>

Vinculando a exibição de texto no código fonte

Templates representam a estrutura de uma interface de usuário associada a uma classe específica; nesse caso, a definição de UI da classe TextViwerWindow. Para acessar um elemento UI de dentro da classe você deve atribuir um indentificador utilizando o atributo id XML para a definição no XML, e dizer para o GTK para vincular o objeto com o mesmo indentificador a um membro na estrutura de instância.

  1. Abra o arquivo window.py
  2. Encontre a classeTextViewerWindow
  3. Substitua o label = Gtk.Template.Child() line with main_text_view = Gtk.Template.Child()
    @Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
    class TextViewerWindow(Gtk.ApplicationWindow):
        __gtype_name__ = 'TextViewerWindow'

        main_text_view = Gtk.Template.Child()

        def __init__(self, **kwargs):
            super().__init__(**kwargs)

Agora você pode apertar o botão “Run” e verificar se a janela possui uma área de texto vazia.

Na próxima seção você vai aprender como selecionar um arquivo e carregar seu conteúdo na área de texto.


Carregando conteúdo de um arquivo

Nessa seção você irá aprender como pedir ao usuário para selecionar um arquivo, carregar o seu conteúdo, e então colocar esse conteúdo na área de texto do nosso text viewer.

abrindo-arquivos

Adicionando um botão de “Open”

Para abrir um arquivo, você precisa deixar o usuário selecionar ele. Você pode seguir essas instruções para adicionar um botão à barra de cabeçalho da janela que irá abrir uma janela de seleção de arquivos:

Atualizando a definição de UI

  1. Abra o arquivo text_viewer-window.ui
  2. Encontre a definição de object para o elemento GtkHeaderBar
  3. Adicione uma definição de object para o elemento GtkButton como uma child da barra de cabeçalho (header bar), empacotando ele na ponta da decoração da janela utilizando o tipo start:
<object class="GtkHeaderBar" id="header_bar">
  <child type="start">
    <object class="GtkButton" id="open_button">
      <property name="label">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>
  <child type="end">
    <object class="GtkMenuButton">
      <property name="icon-name">open-menu-symbolic</property>
      <property name="menu-model">primary_menu</property>
    </object>
  </child>
  1. O botão possui o identificador open_button, para que você possa vinculá-lo ao template da janela.
  2. O botão também possui uma propriedade action-name definida como win.open; essa ação será ativada quando o usuário pressionar o botão.

Vinculando o template ao seu código fonte

  1. Abra o arquivo window.py
  2. Adicione o elemento open_button à instância de estrutura da classe TextViewerWindow:
@Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
class TextViewerWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'TextViewerWindow'

    main_text_view = Gtk.Template.Child()
    open_button = Gtk.Template.Child()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

Adicionando a ação de Open

Adicione a ação de open à instância de inicialização para TextViewerWindow.

Uma vez que você adicionar a ação de open à janela, você pode se chamar ela através do win.open.

  1. Modifique a instância de inicialização TextViewerWindow para criar uma GSimpleAction e adicionar ela à janela
from gi.repository import Adw, Gio, Gtk

@Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
class TextViewerWindow(Gtk.ApplicationWindow):
  __gtype_name__ = 'TextViewerWindow'
  main_text_view = Gtk.Template.Child()
  open_button = Gtk.Template.Child()

  def __init__(self, **kwargs):
    super().__init__(**kwargs)

    open_action = Gio.SimpleAction(name="open")
    open_action.connect("activate", self.open_file_dialog)
    self.add_action(open_action)

  def open_file_dialog(self, action, _):
    pass
  1. Abra o arquivo fonte main.py source file e encontre a função de inicialização de instância Application
  2. Adicione Ctrl + O como o atalho para a ação de win.open
class Application(Adw.Application):
    def __init__(self):
        super().__init__(application_id='com.example.TextViewer',
                        flags=Gio.ApplicationFlags.FLAGS_NONE)

        self.create_action('about', self.on_about_action)
        self.create_action('preferences', self.on_preferences_action)

        self.set_accels_for_action('win.open', ['<Ctrl>o'])

Selecionando um arquivo

Agora que você adicionou a ação, você deve definir a função que irá ser chamada quando a ação for ativada.

  1. Dentro do método open_file_dialog, crie um objeto GtkFileChooserNative, que irá apresentar uma janela de seleção de arquivos ao usuário:
def open_file_dialog(self, action, parameter):
# Cria uma nova janela de seleção de arquivos, usando o modo "Open"
# e mantendo uma referência a ele
    self._native = Gtk.FileChooserNative(
        title="Open File",
        transient_for=self,
        action=Gtk.FileChooserAction.OPEN,
        accept_label="_Open",
        cancel_label="_Cancel",
    )
    # Conecta o sinal "response" da janela de seleção de arquivos;
    # esse sinal é emitido quando o usuário seleciona um arquivo
    # ou quando ele cancela a operação
    self._native.connect("response", self.on_open_response)
    # Apresenta a janela ao usuário
    self._native.show()
  1. O método on_open_response lida com a resposta do usuário uma vez que ele selecionou um arquivo e fechou a janela, ou simplesmente fechou a janela sem selecionar um arquivo:
def on_open_response(self, dialog, response):
    # Se o usuário selecionu um arquivo...
    if response == Gtk.ResponseType.ACCEPT:
        # ... devolva o local do arquivo a partir da janela e abra ele
        self.open_file(dialog.get_file())
        # Libere a referência na janela de seleção de arquivos agora que
        # não precisamos mais dela
        self._native = None

    def open_file(self, file):
        pass

Lendo o conteúdo de um arquivo

Ler o conteúdo de um arquivo pode levar uma quantidade de tempo arbitrária, e bloqueia o fluxo de controle da aplicação. Por esse motivo, é recomendado que você carregue o arquivo de forma assíncrona. Isso requer iniciar a operação “read” na função open_file:

def open_file(self, file):
    file.load_contents_async(None, self.open_file_complete)

Uma vez que a operação assíncrona está completa, ou se aconteceu um erro, a função open_file_complete será chamada, e você vai precisar completar a operação de carregamento assíncrono:

 def open_file_complete(self, file, result):
    contents = file.load_contents_finish(result)
    if not contents[0]:
        path = file.peek_path()
        print(f"Unable to open {path}: {contents[1]}")

Mostrando o conteúdo dentro de uma área de texto

Agora que você possui o conteúdo do arquivo, você pode mostrar ele na ferramenta GtkTextView.

  1. Verifique que o conteúdo de um arquivo é codificado utilizando UTF-8, já que é o que o GTK requer para todas as suas widgets de texto
def open_file_complete(self, file, result):
    contents = file.load_contents_finish(result)
    if not contents[0]:
        path = file.peek_path()
        print(f"Unable to open {path}: {contents[1]}")
        return

    try:
        text = contents[1].decode('utf-8')
    except UnicodeError as err:
        path = file.peek_path()
        print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
        return
  1. Modifique a função open_file_complete para recuperar a instância GtkTextBuffer que a widget GtkTextView usa para guardar o texto, e defina seu conteúdo
def open_file_complete(self, file, result):
    contents = file.load_contents_finish(result)
    if not contents[0]:
        path = file.peek_path()
        print(f"Unable to open {path}: {contents[1]}")
        return

    try:
        text = contents[1].decode('utf-8')
    except UnicodeError as err:
        path = file.peek_path()
        print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
        return

    buffer = self.main_text_view.get_buffer()
    buffer.set_text(text)
    start = buffer.get_start_iter()
    buffer.place_cursor(start)

Atualizando o título da janela

Since the application now is showing the contents of a specific file, you should ensure that the user interface reflects this new state. One way to do this is to update the title of the window with the name of the file.

Since the name of the file uses the raw encoding for files provided by the operating system, we need to query the file for its display name.

  1. Modify the open_file_complete function to query the “display name” file attribute
  2. Set the title of the window using the display name

Como a aplicação agora está mostrando o conteúdo de um arquivo específico, você deve garantir que a interface de usuário reflete esse novo estado. Uma maneira de fazer isso é atualizando o título da janela com o nome do arquivo.

Uma vez que o nome dos arquivos utiliza a codificação crua para arquivos providos pelo sistema operacional, nós precisamos consultar o arquivo pelo nome de exibição.

  1. Modifique a função open_file_complete para consultar o atributo “display name” do arquivo
  2. Defina o título da janela utilizando o nome de exibição
def open_file_complete(self, file, result):
    contents = file.load_contents_finish(result)

    info = file.query_info("standard::display-name", Gio.FileQueryInfoFlags.NONE)
    if info:
        display_name = info.get_attribute_string("standard::display-name")
    else:
        display_name = file.get_basename()

    if not contents[0]:
        path = file.peek_path()
        print(f"Unable to open {path}: {contents[1]}")
        return

    try:
        text = contents[1].decode('utf-8')
    except UnicodeError as err:
        path = file.peek_path()
        print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8")
        return

    buffer = self.main_text_view.get_buffer()
    buffer.set_text(text)
    start = buffer.get_start_iter()
    buffer.place_cursor(start)

    self.set_title(display_name)

Adicionando o atalho “Open” à ajuda dos atalhos de teclado

A janela de ajuda Keyboard Shortcuts é uma parte do template de aplicações GNOME no GNOME Builder. O GTK automaticamente maneja a sua crição e a ação que a apresenta ao usuário.

  1. Encontre o arquivo help-overlay.ui no diretório de fontes
  2. Encontre a definição GtkShortcutsGroup
  3. Adicione uma nova definição GtkShortcutsShortcut para a ação win.open no grupo de atalhos
<object class="GtkShortcutsGroup">
  <property name="title" translatable="yes" context="shortcut window">General</property>
  <child>
    <object class="GtkShortcutsShortcut">
      <property name="title" translatable="yes" context="shortcut window">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>

Você agora deve ser capaz de rodar a aplicação. Pressione o botão Open ou Ctrl + O, e selecione um arquivo de texto no seu sistema. Como exemplo, você pode navegat até o diretório do projeto text viewer, e selecionar o arquivo COPYING na fonte:

arquivo-copying


Mostrando a posição do cursor

Nesta seção, você aprenderá como usar o GtkTextBuffer para ser notificado sobre posição do cursor dentro de um elemento gráfico (widget) e como atualizar a barra superior da aplicação de visualização de texto.

Adicionando o indicador de posição do cursor

Atualizando a definição da interface

  1. Adicione uma GtkLabel como filha de GtkHeaderBar no arquivo de definição da interface para a classe TextViewerWindow; o rótulo (label) deve ser colocada como uma filha de tipo end (fim) e logo após GtkMenuButton
  2. O rótulo tem o identificador cursor_pos, que vai ser usado para ligá-lo ao template TextViewerWindow
  3. O rótulo tem um conteúdo inicial Ln 0, Col 0 configurado por meio da propriedade label
  4. Além disso, o rótulo tem duas classes de estilo:
    • dim-label, para reduzir o contraste do tema padrão
    • numeric, que usará números tabulares na fonte utilizada pelo rótulo
<object class="GtkHeaderBar" id="header_bar">
  <child type="start">
    <object class="GtkButton" id="open_button">
      <property name="label">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>
  <child type="end">
    <object class="GtkMenuButton">
      <property name="icon-name">open-menu-symbolic</property>
      <property name="menu-model">primary_menu</property>
    </object>
  </child>
  <child type="end">
    <object class="GtkLabel" id="cursor_pos">
      <property name="label">Ln 0, Col 0</property>
      <style>
        <class name="dim-label"/>
        <class name="numeric"/>
      </style>
    </object>
  </child>
</object>

Vinculando o template com o código fonte

  1. Adicione o elemento gráfico cursor_pos na classe TextViewerWindow

    @Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
    class TextViewerWindow(Gtk.ApplicationWindow):
        __gtype_name__ = 'TextViewerWindow'
    
        main_text_view = Gtk.Template.Child()
        open_button = Gtk.Template.Child()
        cursor_pos = Gtk.Template.Child()
    

Atualizando o rótulo de posição do cursor

  1. Pegue o GtkTextBuffer a partir do elemento gráfico main_text_view e conecte uma função callback para o sinal notify::cursor-position de forma a receber notificações toda vez que a propriedade cursor-position for alterada:

    @Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
    class TextViewerWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'TextViewerWindow'
    
    main_text_view = Gtk.Template.Child()
    open_button = Gtk.Template.Child()
    cursor_pos = Gtk.Template.Child()
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
    
        open_action = Gio.SimpleAction(name="open")
        open_action.connect("activate", self.open_file_dialog)
        self.add_action(open_action)
    
        buffer = self.main_text_view.get_buffer()
        buffer.connect("notify::cursor-position", self.update_cursor_position)
    
  2. Defina a função notify::cursor-position callback para devolver a posição do cursor a partir do objeto da classe GetTextBuffer e atualizar o conteúdo do rótulo cursor_pos.

    def update_cursor_position(self, buffer, _):
        # Retrieve the value of the "cursor-position" property
        cursor_pos = buffer.props.cursor_position
        # Construct the text iterator for the position of the cursor
        iter = buffer.get_iter_at_offset(cursor_pos)
        line = iter.get_line() + 1
        column = iter.get_line_offset() + 1
        # Set the new contents of the label
        self.cursor_pos.set_text(f"Ln {line}, Col {column}")
    

Salvando o conteúdo em um arquivo

Nessa seção, você vai aprender como adicionar uma entrada de menu a partir de uma tecla de atalho, pedir ao usuário para selecionar um diretório que irá salvar o conteúdo do GtkTextBuffer e salvar o arquivo de forma assíncrona.

Adicionando o “Save as” como item do menu

  1. Abra o arquivo de definição da interface da sua janela e procure pela definição do primary-menu no fim do arquivo.
  2. Remova o item “Preferences” do menu, pois ele não será necessário
  3. No lugar do item removido, adicione a definição do item de menu Save as.
<menu id="primary_menu">
  <section>
    <item>
      <attribute name="label" translatable="yes">_Save as...</attribute>
      <attribute name="action">win.save-as</attribute>
    </item>
    <item>
      <attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
      <attribute name="action">win.show-help-overlay</attribute>
    </item>
    <item>
      <attribute name="label" translatable="yes">_About {{name}}</attribute>
      <attribute name="action">app.about</attribute>
    </item>
  </section>
</menu>

O item de menu Save as está vinculado a win.save-as; isso significa que a ativação do item ativará a ação save-as registrada na janela TextViewerWindow.

Adicionando a ação “Save As”

  1. Abra o arquivo window.py e procure o método de inicialização da instância do elemento gráfico (widget) TextViewerWindow.
  2. Crie uma ação save-as, conecte uma função callback ao seu sinal de activate (ativação) e adicione a ação à janela.
def __init__(self, **kwargs):
    super().__init__(**kwargs)

    open_action = Gio.SimpleAction(name="open")
    open_action.connect("activate", self.open_file_dialog)
    self.add_action(open_action)

    save_action = Gio.SimpleAction(name="save-as")
    save_action.connect("activate", self.save_file_dialog)
    self.add_action(save_action)

    buffer = self.main_text_view.get_buffer()
    buffer.connect("notify::cursor-position", self.update_cursor_position)

Selecionando um arquivo

  1. Na função callback activate para a ação save-as, crie uma caixa de diálogo de seleção do arquivo usando a ação GTK_FILE_CHOOSER_ACTION_SAVE e conecte com seu sinal response.

    def save_file_dialog(self, action, _):
        self._native = Gtk.FileChooserNative(
            title="Save File As",
            transient_for=self,
            action=Gtk.FileChooserAction.SAVE,
            accept_label="_Save",
            cancel_label="_Cancel",
        )
        self._native.connect("response", self.on_save_response)
        self._native.show()
    
  2. Na função callback on_save_response, cheque o identificador response, recupere o GFile para o local selecionado pelo usuário e chame a função save_file().

    def on_save_response(self, native, response):
        if response == Gtk.ResponseType.ACCEPT:
            self.save_file(native.get_file())
        self._native = None
    

Salvando o conteúdo do buffer do texto

  1. Importe o modulo GLib assim como Adw, Gio e Gtk.

    from gi.repository import Adw, Gio, GLib, Gtk
    
  2. Na função save_file, recupere o conteúdo de GtkTextBuffer usando o GtkTextIter inicial e final como os limites do buffer e, em seguida, inicie uma operação assíncrona para salvar os dados no local apontado pelo GFile

    def save_file(self, file):
        buffer = self.main_text_view.get_buffer()
    
        # Retrieve the iterator at the start of the buffer
        start = buffer.get_start_iter()
        # Retrieve the iterator at the end of the buffer
        end = buffer.get_end_iter()
        # Retrieve all the visible text between the two bounds
        text = buffer.get_text(start, end, False)
    
        # If there is nothing to save, return early
        if not text:
            return
    
        bytes = GLib.Bytes.new(text.encode('utf-8'))
    
        # Start the asynchronous operation to save the data into the file
        file.replace_contents_bytes_async(bytes,
                                          None,
                                          False,
                                          Gio.FileCreateFlags.NONE,
                                          None,
                                          self.save_file_complete)
    
  3. Na função save_file_complete, finalize as operações assíncronas e reporte qualquer erro

def save_file_complete(self, file, result):
    res = file.replace_contents_finish(result)
    info = file.query_info("standard::display-name",
                           Gio.FileQueryInfoFlags.NONE)
    if info:
        display_name = info.get_attribute_string("standard::display-name")
    else:
        display_name = file.get_basename()
    if not res:
        print(f"Unable to save {display_name}")

Adicionando uma tecla de atalho para a ação “Save As”

  1. Abra o arquivo de origem main.py e procure a função de inicialização da instancia da classe Application
  2. Adicione Ctrl+Shift+S como atalho para acelerar a ação win.save-as.
class Application(Adw.Application):
    def __init__(self):
        super().__init__(application_id='com.example.PyTextViewer',
                         flags=Gio.ApplicationFlags.FLAGS_NONE)

        self.create_action('about', self.on_about_action)
        self.create_action('preferences', self.on_preferences_action)

        self.set_accels_for_action('win.open', ['<Ctrl>o'])
        self.set_accels_for_action('win.save-as', ['<Ctrl><Shift>s'])

Adicionando “Save As” como ajuda nos atalhos do teclado

  1. Encontre o arquivo help-overlay.ui no diretório de fontes
  2. Procure a definição GtkShortcutsGroup
  3. Adicione uma nova definição GtkShortcutsShortcut para a ação win.save no grupo de atalhos
<object class="GtkShortcutsGroup">
  <property name="title" translatable="yes" context="shortcut window">General</property>
  <child>
    <object class="GtkShortcutsShortcut">
      <property name="title" translatable="yes" context="shortcut window">Open</property>
      <property name="action-name">win.open</property>
    </object>
  </child>
  <child>
    <object class="GtkShortcutsShortcut">
      <property name="title" translatable="yes" context="shortcut window">Save As</property>
      <property name="action-name">win.save-as</property>
    </object>
  </child>

Salvando o estado do aplicativo

O objetivo dessa seção é ensinar como definir novas configurações a partir de seus esquemas, e como vinculá-las às propriedades da janela da aplicação para salvar e restaurar o estado da janela em diferentes sessões.

Adicionando novas chaves ao esquema de configuração

Configurações são guardadas em um banco de dados, e cada chave é descrita dentro de um esquema; o esquema contém o tipo de valor associado à chave, assim como o valor padrão da chave.

  1. Abra o arquivo com.example.TextViewer.gschema.xml que está na pasta data
  2. Adicione as chaves window-width, window-height, e window-maximized ao esquema, incluindo seus valores padrão de 600, 400, e false, respectivamente
   <schemalist gettext-domain="text-viewer">
     <schema id="com.example.TextViewer" path="/com/example/TextViewer/">
       <key name="window-width" type="i">
         <default>600</default>
       </key>
       <key name="window-height" type="i">
         <default>400</default>
       </key>
       <key name="window-maximized" type="b">
         <default>false</default>
       </key>
     </schema>
   </schemalist>
Nota: o esquema será instalado automaticamente no diretório esperado ao compilar a aplicação. Isso significa que a aplicação só pode ser usada quando ela estiver instalada

Usando GSettings

GSettings é o objeto que observa as chaves de um id de esquema específico. Você usa a API do GSettings para acessar os valores das chaves e para ser notificado de mudanças nas configurações.

  1. Abra o arquivo window.py
  2. Modifique a função de inicialização da instância de TextViewerWindow para criar uma instância de GSettings para o id de esquema com.example.TextViewer.
def __init__(self, **kwargs):
    super().__init__(**kwargs)

    open_action = Gio.SimpleAction(name="open")
    open_action.connect("activate", self.open_file_dialog)
    self.add_action(open_action)

    save_action = Gio.SimpleAction(name="save-as")
    save_action.connect("activate", self.save_file_dialog)
    self.add_action(save_action)

    buffer = self.main_text_view.get_buffer()
    buffer.connect("notify::cursor-position", self.update_cursor_position)

    self.settings = Gio.Settings(schema_id="com.example.TextViewer")

Vinculando as configurações às propriedades da janela

Chaves dentro de um esquema GSettings podem ser vinculadas às propriedades de um GObject; propriedades vinculadas serão automaticamente salvas dentro do banco de dados de configurações sempre que elas mudarem, e serão restauradas no momento de criação.

  1. Modifique a função de inicialização de instância de TextViewerWindow para vincular as chaves window-width, window-height, e window-maximize às propriedades default-width, default-height, e maximized, respectivamente
def __init__(self, **kwargs):
    super().__init__(**kwargs)

    open_action = Gio.SimpleAction(name="open")
    open_action.connect("activate", self.open_file_dialog)
    self.add_action(open_action)

    save_action = Gio.SimpleAction(name="save-as")
    save_action.connect("activate", self.save_file_dialog)
    self.add_action(save_action)

    buffer = self.main_text_view.get_buffer()
    buffer.connect("notify::cursor-position", self.update_cursor_position)

    self.settings = Gio.Settings(schema_id="com.example.TextViewer")
    self.settings.bind("window-width", self, "default-width",
                       Gio.SettingsBindFlags.DEFAULT)
    self.settings.bind("window-height", self, "default-height",
                       Gio.SettingsBindFlags.DEFAULT)
    self.settings.bind("window-maximized", self, "maximized",
                       Gio.SettingsBindFlags.DEFAULT)

Notificando o usuário com avisos

Avisos, ou notificações, são úteis para comunicar uma mudança de estado interna da aplicação e também para coletar feedbacks do usuário.

Nesta seção, aprenderemos como adicionar avisos sobrepostos em nossa aplicação e como exibir esses avisos ao abrir um arquivo.

avisos

Adicionando uma camada de avisos

Os avisos são exibidos por uma camada de visualização, que deve conter o resto da área de conteúdo da aplicação.

Atualizando o arquivo de definição da UI

  1. Localize o arquivo de definição da UI para TextViewerWindow.
  2. Localize a definição para o GtkScrolledWindow, que contém a seção de texto principal.
  3. Insira o widget AdwToastOverlay como filho do TextViewerWindow e pai do GtkScrolledWindow, use como id “toast_overlay”!
<child>
  <object class="AdwToastOverlay" id="toast_overlay">
    <property name="child">
      <object class="GtkScrolledWindow">
        <property name="hexpand">true</property>
        <property name="vexpand">true</property>
        <property name="margin-top">6</property>
        <property name="margin-bottom">6</property>
        <property name="margin-start">6</property>
        <property name="margin-end">6</property>
        <property name="child">
          <object class="GtkTextView" id="main_text_view">
            <property name="monospace">true</property>
          </object>
        </property>
      </object>
    </property>
  </object>
</child>

Vinculando a camada de avisos ao código fonte

Adicione o widget agora nomeado como “toast_overlay” na classe TextViewerWindow.

@Gtk.Template(resource_path='/com/example/TextViewer/window.ui')
class TextViewerWindow(Gtk.ApplicationWindow):
    __gtype_name__ = 'TextViewerWindow'

    main_text_view = Gtk.Template.Child()
    open_button = Gtk.Template.Child()
    cursor_pos = Gtk.Template.Child()
    toast_overlay = Gtk.Template.Child()

Exibindo os avisos

Os avisos são, em geral, úteis para notificar o usuário de que uma operação assíncrona foi executada. Abrir ou salvar um arquivo são dois casos onde é comum utilizar notificações.

Notificando ao abrir um arquivo

  1. Localize a função open_file_complete do TextViewerWindow.
  2. Ache os blocos de tratamento de exceções e substitua-os com um aviso.
  3. Adicione um aviso ao final da função.
def open_file_complete(self, file, result):
    # Completa a operação assíncrona ; essa função também
    # irá retornar o conteúdo de um arquivo como um vetor de bytes
    # ou irá ativar o bloco de tratamento de exceções.
    contents = file.load_contents_finish(result)
    # Consulta o nome de exibição do arquivo
    info = file.query_info("standard::display-name", Gio.FileQueryInfoFlags.NONE)
    if info:
        display_name = info.get_attribute_string("standard::display-name")
    else:
        display_name = file.get_basename()
    # No caso de algum erro, mostra o aviso
    if not contents[0]:
        self.toast_overlay.add_toast(Adw.Toast(title=f"Unable to open “{display_name}”"))
        return
    # Certifica de que o arquivo está codificado de acordo com UTF-8
    try:
        text = contents[1].decode('utf-8')
    except UnicodeError as err:
        self.toast_overlay.add_toast(Adw.Toast(title=f"Invalid text encoding for “{display_name}”")
        return
    # Busca a instância do GtkTextBuffer que armazena o
    # texto exibido pelo widget GtkTextView
    buffer = self.main_text_view.get_buffer()
    # Define o texto usando o conteúdo do arquivo
    buffer.set_text(text)
    # Reposiciona o cursor para que fique no início do texto
    start = buffer.get_start_iter()
    buffer.place_cursor(start)
    # Define o título utilizando o nome de exibição
    self.set_title(display_name)
    # Exibe um aviso notificando que o arquivo foi carregado com sucesso
    self.toast_overlay.add_toast(Adw.Toast(title=f"Opened
    {display_name}"))

Notificando após salvar

Na função save_file_complete você pode usar um aviso para notificar ao usuário se a operação foi um sucesso ou não.

def save_file_complete(self, file, result):
    res = file.replace_contents_finish(result)
    # Consulta o nome de exibição do arquivo
    info = file.query_info("standard::display-name",
                           Gio.FileQueryInfoFlags.NONE)
    if info:
        display_name = info.get_attribute_string("standard::display-name")
    else:
        display_name = file.get_basename()
    if not res:
        msg = f"Unable to save as “{display_name}”")
    else:
        msg = f"Saved as “{display_name}”"
    self.toast_overlay.add_toast(Adw.Toast(title=msg))

Forçando o esquema de cores escuras

Aplicações do GNOME vão respeitar a configuração do sistema para o tema claro ou escuro. É possível, porém, apresentar a escolha de forçar o tema escuro ao usuário na interface de usuário de sua aplicação.

dark_mode.png

Adicionando o item “Modo Escuro” ao menu da aplicação

  1. Abra o arquivo de definição da interface de usuário para o widget TextViewerWindow
  2. Adicione um item de menu para a ação app.dark, chamado Dark Mode
   <menu id="primary_menu">
     <section>
       <item>
         <attribute name="label" translatable="yes">Save _as...</attribute>
         <attribute name="action">win.save-as</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_Dark mode</attribute>
         <attribute name="action">app.dark-mode</attribute>
       </item>

Adicionando a ação de tema escuro à aplicação

  1. Abra a fonte TextViewApplication
  2. Ache a função de inicialização de instância TextViewApplication
  3. Crie a ação stateful dark-mode e conecte ao seus sinais activate e change-state
  4. Adicione a ação à aplicação
from gi.repository import Adw, Gio, GLib, Gtk

from .window import PytextViewerWindow, AboutDialog


class TextViewerApplication(Adw.Application):
	def __init__(self):
		super().__init__(application_id='com.example.TextViewer',
					   flags=Gio.ApplicationFlags.FLAGS_NONE)

		self.create_action('quit', self.on_quit_action, ['<Ctrl>q'])
		self.create_action('about', self.on_about_action)

		dark_mode_action = Gio.SimpleAction(name="dark-mode",
						                  state=GLib.Variant.new_boolean(False))
		dark_mode_action.connect("activate", self.toggle_dark_mode)
		dark_mode_action.connect("change-state", self.change_color_scheme)
		self.add_action(dark_mode_action)
  1. Adicione a callback toggle_dark_mode; essa callback alterna o estado da ação dark-mode entre “verdadeiro” e “falso”
def toggle_dark_mode(self, action, _):
	state = action.get_state()
	old_state = state.get_boolean()
	new_state = not old_state
	action.change_state(GLib.Variant.new_boolean(new_state))
  1. Adicione a callback change_color_scheme; essa callback é responsável por trocar o esquema de cores da aplicação usando a API AdwStyleManager
def change_color_scheme(self, action, new_state):
	dark_mode = new_state.get_boolean()
	style_manager = Adw.StyleManager.get_default()
	if dark_mode:
		style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
	else:
		style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)
	action.set_state(new_state)

Armazenando o estado de modo escuro como uma configuração

Se você quiser preservar o esquema de cores escolhido ao longo de sessões você o armazena dentro de GSettings.

Adicionando a nova chave ao esquema de configurações

  1. Abra o arquivo com.example.TextViewer.gschema.xml
  2. Adicione a chave booleana dark-mode
   <?xml version="1.0" encoding="UTF-8"?>
   <schemalist gettext-domain="text-viewer">
     <schema id="com.example.TextViewer" path="/com/example/TextViewer/">
       <key name="window-width" type="i">
         <default>600</default>
       </key>
       <key name="window-height" type="i">
         <default>400</default>
       </key>
       <key name="window-maximized" type="b">
         <default>false</default>
       </key>
       <key name="dark-mode" type="b">
         <default>false</default>
       </key>
     </schema>
   </schemalist>

Adicionando GSettings à aplicação

  1. Adicione uma instância de GSettings ao seu TextViewerApplication
class TextViewerApplication(Adw.Application):
	def __init__(self):
		super().__init__(application_id='com.example.TextViewer', flags=Gio.ApplicationFlags.FLAGS_NONE)

		self.settings = Gio.Settings(schema_id="com.example.TextViewer")

Estabelecendo o estado inicial para o esquema de cores

  1. Obtenha o valor da chave de GSettings dark-mode
  2. Estabeleça o esquema de cores usando o valor da chave
  3. Inicialize o estado da ação dark-mode com o valor da chave
class TextViewerApplication(Adw.Application):
	def __init__(self):
	    super().__init__(application_id='com.example.TextViewer', flags=Gio.ApplicationFlags.FLAGS_NONE)

	    self.settings = Gio.Settings(schema_id="com.example.TextViewer")

	    self.create_action('quit', self.on_quit_action, ['<Ctrl>q'])
	    self.create_action('about', self.on_about_action)

	    dark_mode = self.settings.get_boolean("dark-mode")
	    style_manager = Adw.StyleManager.get_default()
	    if dark_mode:
		    style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
	    else:
		    style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)

	    dark_mode_action = Gio.SimpleAction(name="dark-mode", state=GLib.Variant.new_boolean(dark_mode))
	    dark_mode_action.connect("activate", self.toggle_dark_mode)
	    dark_mode_action.connect("change-state", self.change_color_scheme
	    self.add_action(dark_mode_action)

Salvando o esquema de cores quando ele muda

  1. Atualize a chave de GSettings dark-mode usando o estado da ação dark-mode sempre que ela mudar.
def change_color_scheme(self, action, new_state):
	dark_mode = new_state.get_boolean()
	style_manager = Adw.StyleManager.get_default()
	if dark_mode:
	    style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
	else:
	    style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)
	action.set_state(new_state)
	self.settings.set_boolean("dark-mode", dark_mode)

Com isso, finalizamos nossa postagem sobre desenvolvimento de aplicativos utilizando GTK! Para mais informações, recomendamos consultar a Documentação para Desenvolvedores do GNOME. Ela inclui versões desse tutorial para outras linguagens de programação (C, Vala, etc), bem como um guia bastante completo dos widgets disponibilizados pelo GTK4 em conjunção com a libadwaita.