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
- Adicionando uma visualização de conteúdo
- Carregando conteúdo de um arquivo
- Mostrando a posição do cursor
- Salvando o conteúdo em um arquivo
- Salvando o estado do aplicativo
- Notificando o usuário com avisos
- Forçando o esquema de cores escuras
Começando um projeto
Para começar a desenvolver um aplicativo para o ambiente gráfico GNOME, você deve seguir os seguintes passos:
-
instale a última versão do GNOME Builder
-
na tela de Welcome, selecione Start new project
-
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
-
selecione o template GNOME Application
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.
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.
-
Abra o arquivo
text_viewer-window.ui
dentro do diretóriosrc
. -
A janela é definida como o template para a classe TextViewerWindow
-
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
-
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
-
Atualmente o principal conteúdo é fornecido por um widget GtkLabel, com uma etiqueta “Hello, World!”
-
Fora do bloco template você pode achar a definição do menu usando o elemento menu
Definindo o título da janela principal
- Encontre a definição TextViewerWindow
- Econtre os elementos property que definem o valor padrão para a largura e altura da janela
- 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.
- 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|
- 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
- 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:
- 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>
- 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.
- Abra o arquivo
window.py
- Encontre a classe
TextViewerWindow
- Substitua o
label = Gtk.Template.Child()
line withmain_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.
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
- Abra o arquivo
text_viewer-window.ui
- Encontre a definição de object para o elemento GtkHeaderBar
- 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>
- O botão possui o identificador open_button, para que você possa vinculá-lo ao template da janela.
- 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
- Abra o arquivo
window.py
- 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.
- 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
- Abra o arquivo fonte
main.py
source file e encontre a função de inicialização de instância Application - 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.
- 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()
- 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.
- 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
- 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.
- Modify the
open_file_complete
function to query the “display name” file attribute - 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.
- Modifique a função
open_file_complete
para consultar o atributo “display name” do arquivo - 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.
- Encontre o arquivo
help-overlay.ui
no diretório de fontes - Encontre a definição GtkShortcutsGroup
- 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:
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
- Adicione uma
GtkLabel
como filha deGtkHeaderBar
no arquivo de definição da interface para a classeTextViewerWindow
; o rótulo (label) deve ser colocada como uma filha de tipoend
(fim) e logo apósGtkMenuButton
- O rótulo tem o identificador
cursor_pos
, que vai ser usado para ligá-lo ao templateTextViewerWindow
- O rótulo tem um conteúdo inicial Ln 0, Col 0 configurado por meio da
propriedade
label
- Além disso, o rótulo tem duas classes de estilo:
dim-label
, para reduzir o contraste do tema padrãonumeric
, 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
-
Adicione o elemento gráfico
cursor_pos
na classeTextViewerWindow
@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
-
Pegue o
GtkTextBuffer
a partir do elemento gráficomain_text_view
e conecte uma função callback para o sinalnotify::cursor-position
de forma a receber notificações toda vez que a propriedadecursor-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)
-
Defina a função
notify::cursor-position
callback para devolver a posição do cursor a partir do objeto da classeGetTextBuffer
e atualizar o conteúdo do rótulocursor_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
- Abra o arquivo de definição da interface da sua janela e procure
pela definição do
primary-menu
no fim do arquivo. - Remova o item “Preferences” do menu, pois ele não será necessário
- 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”
- Abra o arquivo
window.py
e procure o método de inicialização da instância do elemento gráfico (widget)TextViewerWindow
. - Crie uma ação
save-as
, conecte uma função callback ao seu sinal deactivate
(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
-
Na função callback
activate
para a açãosave-as
, crie uma caixa de diálogo de seleção do arquivo usando a açãoGTK_FILE_CHOOSER_ACTION_SAVE
e conecte com seu sinalresponse
.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()
-
Na função callback
on_save_response
, cheque o identificadorresponse
, recupere oGFile
para o local selecionado pelo usuário e chame a funçãosave_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
-
Importe o modulo GLib assim como Adw, Gio e Gtk.
from gi.repository import Adw, Gio, GLib, Gtk
-
Na função
save_file
, recupere o conteúdo deGtkTextBuffer
usando oGtkTextIter
inicial e final como os limites do buffer e, em seguida, inicie uma operação assíncrona para salvar os dados no local apontado peloGFile
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)
-
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”
- Abra o arquivo de origem
main.py
e procure a função de inicialização da instancia da classeApplication
- Adicione
Ctrl
+Shift
+S
como atalho para acelerar a açãowin.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
- Encontre o arquivo
help-overlay.ui
no diretório de fontes - Procure a definição
GtkShortcutsGroup
- Adicione uma nova definição
GtkShortcutsShortcut
para a açãowin.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.
- Abra o arquivo
com.example.TextViewer.gschema.xml
que está na pastadata
- 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>
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.
- Abra o arquivo
window.py
- 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.
- 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.
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
- Localize o arquivo de definição da UI para TextViewerWindow.
- Localize a definição para o GtkScrolledWindow, que contém a seção de texto principal.
- 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
- Localize a função
open_file_complete
do TextViewerWindow. - Ache os blocos de tratamento de exceções e substitua-os com um aviso.
- 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.
Adicionando o item “Modo Escuro” ao menu da aplicação
- Abra o arquivo de definição da interface de usuário para o widget TextViewerWindow
- 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
- Abra a fonte TextViewApplication
- Ache a função de inicialização de instância TextViewApplication
- Crie a ação stateful dark-mode e conecte ao seus sinais
activate
echange-state
- 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)
- 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))
- 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
- Abra o arquivo
com.example.TextViewer.gschema.xml
- 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
- 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
- Obtenha o valor da chave de GSettings dark-mode
- Estabeleça o esquema de cores usando o valor da chave
- 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
- 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.