ui: first version using webview as a render for the response
This commit is contained in:
parent
8259cfbb70
commit
d6f26e7fc9
|
@ -1,5 +1,6 @@
|
|||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
using WebKit 6.0;
|
||||
|
||||
template BavarderWindow : Adw.ApplicationWindow {
|
||||
|
||||
|
@ -139,7 +140,7 @@ template BavarderWindow : Adw.ApplicationWindow {
|
|||
|
||||
styles ["card", "text-box"]
|
||||
|
||||
ScrolledWindow {
|
||||
ScrolledWindow scrolled_response_window {
|
||||
margin-top:12;
|
||||
margin-bottom:0;
|
||||
margin-start:12;
|
||||
|
@ -147,6 +148,8 @@ template BavarderWindow : Adw.ApplicationWindow {
|
|||
styles ["scrolled-window"]
|
||||
|
||||
Gtk.Stack response_stack {
|
||||
vexpand: true;
|
||||
hexpand: true;
|
||||
Gtk.StackPage {
|
||||
name: "page_response";
|
||||
child: TextView bot_text_view {
|
||||
|
|
683
src/main.py
683
src/main.py
|
@ -27,19 +27,29 @@ gi.require_version("Gtk", "4.0")
|
|||
gi.require_version("Adw", "1")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version('WebKit', '6.0')
|
||||
|
||||
from gi.repository import Gtk, Gio, Adw, Gdk, GLib, Gst
|
||||
from gi.repository import Gtk, Gio, Adw, Gdk, GLib, Gst, WebKit
|
||||
from .window import BavarderWindow
|
||||
from .preferences import Preferences
|
||||
from enum import auto, IntEnum
|
||||
|
||||
from .constants import app_id, version
|
||||
|
||||
from gtts import gTTS
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from .provider import PROVIDERS
|
||||
import platform
|
||||
import os
|
||||
import markdown
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
class Step(IntEnum):
|
||||
CONVERT_HTML = auto()
|
||||
LOAD_WEBVIEW = auto()
|
||||
RENDER = auto()
|
||||
|
||||
|
||||
|
||||
class BavarderApplication(Adw.Application):
|
||||
|
@ -71,6 +81,15 @@ class BavarderApplication(Adw.Application):
|
|||
)
|
||||
self.latest_provider = self.settings.get_string("latest-provider")
|
||||
|
||||
self.web_view = None
|
||||
self.web_view_pending_html = None
|
||||
|
||||
self.loading = False
|
||||
self.shown = False
|
||||
self.preview_visible = False
|
||||
|
||||
|
||||
|
||||
def quitting(self, *args, **kwargs):
|
||||
"""Called before closing main window."""
|
||||
self.settings.set_strv("enabled-providers", list(self.enabled_providers))
|
||||
|
@ -113,8 +132,6 @@ class BavarderApplication(Adw.Application):
|
|||
self.win = BavarderWindow(application=self)
|
||||
self.win.present()
|
||||
|
||||
self.win.response_stack.set_visible_child_name("page_response")
|
||||
|
||||
self.win.connect("close-request", self.quitting)
|
||||
|
||||
self.load_dropdown()
|
||||
|
@ -261,8 +278,664 @@ Providers: {self.enabled_providers}
|
|||
def ask(self, prompt):
|
||||
return self.providers[self.provider].ask(prompt)
|
||||
|
||||
@staticmethod
|
||||
def on_click_link(web_view, decision, _decision_type):
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
Glib.spawn_command_line_async(f"xdg-open {web_view.get_uri()}")
|
||||
decision.ignore()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def on_right_click(web_view, context_menu, _event, _hit_test):
|
||||
# disable some context menu option
|
||||
for item in context_menu.get_items():
|
||||
if item.get_stock_action() in [WebKit.ContextMenuAction.RELOAD,
|
||||
WebKit.ContextMenuAction.GO_BACK,
|
||||
WebKit.ContextMenuAction.GO_FORWARD,
|
||||
WebKit.ContextMenuAction.STOP]:
|
||||
context_menu.remove(item)
|
||||
|
||||
|
||||
def show(self, html=None, step=Step.LOAD_WEBVIEW):
|
||||
if step == Step.LOAD_WEBVIEW:
|
||||
self.loading = True
|
||||
if not self.web_view:
|
||||
self.web_view = WebKit.WebView()
|
||||
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
#TODO: enable devtools on Devel profile
|
||||
self.web_view.get_settings().set_enable_developer_extras(True)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.web_view.connect("load-changed", self.on_load_changed)
|
||||
|
||||
# All links will be opened in default browser, but local files are opened in apps.
|
||||
self.web_view.connect("decide-policy", self.on_click_link)
|
||||
|
||||
self.web_view.connect("context-menu", self.on_right_click)
|
||||
|
||||
self.web_view.set_hexpand(True)
|
||||
self.web_view.set_vexpand(True)
|
||||
|
||||
self.win.response_stack.add_child(self.web_view)
|
||||
self.win.response_stack.set_visible_child(self.web_view)
|
||||
|
||||
|
||||
print(html)
|
||||
if self.web_view.is_loading():
|
||||
self.web_view_pending_html = html
|
||||
else:
|
||||
self.web_view.load_html(html, "file://localhost/")
|
||||
|
||||
|
||||
elif step == Step.RENDER:
|
||||
if not self.preview_visible:
|
||||
self.preview_visible = True
|
||||
self.show()
|
||||
|
||||
def reload(self, *_widget, reshow=False):
|
||||
if self.preview_visible:
|
||||
if reshow:
|
||||
self.hide()
|
||||
self.show()
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
if event == WebKit.LoadEvent.FINISHED:
|
||||
self.loading = False
|
||||
if self.web_view_pending_html:
|
||||
self.show(html=self.web_view_pending_html, step=Step.LOAD_WEBVIEW)
|
||||
self.web_view_pending_html = None
|
||||
else:
|
||||
# we only lazyload the webview once
|
||||
self.show(step=Step.RENDER)
|
||||
|
||||
def parse_css(path):
|
||||
|
||||
adw_palette_prefixes = [
|
||||
"blue_",
|
||||
"green_",
|
||||
"yellow_",
|
||||
"orange_",
|
||||
"red_",
|
||||
"purple_",
|
||||
"brown_",
|
||||
"light_",
|
||||
"dark_"
|
||||
]
|
||||
|
||||
# Regular expressions
|
||||
not_define_color = re.compile(r"(^(?:(?!@define-color).)*$)")
|
||||
define_color = re.compile(r"(@define-color .*[^\s])")
|
||||
css = ""
|
||||
variables = {}
|
||||
palette = {}
|
||||
|
||||
for color in adw_palette_prefixes:
|
||||
palette[color] = {}
|
||||
|
||||
with open(path, "r", encoding="utf-8") as sheet:
|
||||
for line in sheet:
|
||||
cdefine_match = re.search(define_color, line)
|
||||
not_cdefine_match = re.search(not_define_color, line)
|
||||
if cdefine_match != None: # If @define-color variable declarations were found
|
||||
palette_part = cdefine_match.__getitem__(1) # Get the second item of the re.Match object
|
||||
name, color = palette_part.split(" ", 1)[1].split(" ", 1)
|
||||
if name.startswith(tuple(adw_palette_prefixes)): # Palette colors
|
||||
palette[name[:-1]][name[-1:]] = color[:-1]
|
||||
else: # Other color variables
|
||||
variables[name] = color[:-1]
|
||||
elif not_cdefine_match != None: # If CSS rules were found
|
||||
css_part = not_cdefine_match.__getitem__(1)
|
||||
css += f"{css_part}\n"
|
||||
|
||||
sheet.close()
|
||||
return variables, palette, css
|
||||
|
||||
def update_response(self, response):
|
||||
self.win.bot_text_view.get_buffer().set_text(response)
|
||||
"""Update the response text view with the response."""
|
||||
response = markdown.markdown(response, extensions=["markdown.extensions.extra"])
|
||||
|
||||
TEMPLATE = """
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: fira-sans;
|
||||
src: url("/app/share/fonts/FiraSans-Regular.ttf") format("ttf"),
|
||||
local("FiraSans-Regular"),
|
||||
url("https://fonts.gstatic.com/s/firasans/v10/va9E4kDNxMZdWfMOD5Vvl4jL.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: fira-mono;
|
||||
src: url("/app/share/fonts/FiraMono-Regular.ttf") format("ttf"),
|
||||
local("FiraMono-Regular"),
|
||||
url("https://fonts.gstatic.com/s/firamono/v9/N0bX2SlFPv1weGeLZDtgJv7S.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: color-emoji;
|
||||
src: local("Noto Color Emoji"), local("Apple Color Emoji"), local("Segoe UI Emoji"), local("Segoe UI Symbol");
|
||||
}
|
||||
|
||||
:root {
|
||||
--text-color: #2e3436;
|
||||
--background-color: #f6f5f4;
|
||||
--alt-background-color: #edeeef;
|
||||
--link-color: #0d71de;
|
||||
--blockquote-text-color: #747e85;
|
||||
--blockquote-border-color: #d6d8da;
|
||||
--header-border-color: #e1e2e4;
|
||||
--hr-background-color: #d8dadd;
|
||||
--table-tr-border-color: #bdc1c6;
|
||||
--table-td-border-color: #d6d8da;
|
||||
--kbd-text-color: #4e585e;
|
||||
--kbd-background-color: #f1f1f1;
|
||||
--kbd-border-color: #bdc1c6;
|
||||
--kbd-shadow-color: #8c939a;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: #eeeeec;
|
||||
--background-color: #353535;
|
||||
--alt-background-color: #3a3a3a;
|
||||
--link-color: #b5daff;
|
||||
--blockquote-text-color: #a8a8a6;
|
||||
--blockquote-border-color: #525252;
|
||||
--header-border-color: #474747;
|
||||
--hr-background-color: #505050;
|
||||
--table-tr-border-color: #696969;
|
||||
--table-td-border-color: #525252;
|
||||
--kbd-text-color: #cececc;
|
||||
--kbd-background-color: #3c3c3c;
|
||||
--kbd-border-color: #696969;
|
||||
--kbd-shadow-color: #979797;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
font-family: "Fira Sans", fira-sans, sans-serif, color-emoji;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
max-width: 980px;
|
||||
margin: auto;
|
||||
padding: 4em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 799px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline-width: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 1.5em 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--hr-background-color);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
hr::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
hr::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.625em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
ul ul ol,
|
||||
ul ol ol,
|
||||
ol ul ol,
|
||||
ol ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre {
|
||||
font-family: "Fira Mono", fira-mono, monospace, color-emoji;
|
||||
font-size: 1em;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 0.1875em;
|
||||
font-size: 0.85em;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.5;
|
||||
background-color: var(--alt-background-color);
|
||||
border-radius: 0.1875em;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.pl-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25em !important;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5em !important;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 1em !important;
|
||||
}
|
||||
|
||||
.pl-4 {
|
||||
padding-left: 1.5em !important;
|
||||
}
|
||||
|
||||
.pl-5 {
|
||||
padding-left: 2em !important;
|
||||
}
|
||||
|
||||
.pl-6 {
|
||||
padding-left: 2.5em !important;
|
||||
}
|
||||
|
||||
.markdown-body::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.markdown-body>*:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body>*:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.anchor {
|
||||
float: left;
|
||||
padding-right: 0.25em;
|
||||
margin-left: -1.25em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.anchor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p,
|
||||
blockquote,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
table,
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 1em;
|
||||
color: var(--blockquote-text-color);
|
||||
border-left: 0.25em solid var(--blockquote-border-color);
|
||||
}
|
||||
|
||||
blockquote>:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
blockquote>:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: 0.1875em 0.3125em;
|
||||
font-size: 0.6875em;
|
||||
line-height: 1;
|
||||
color: var(--kbd-text-color);
|
||||
vertical-align: middle;
|
||||
background-color: var(--kbd-background-color);
|
||||
border: solid 1px var(--kbd-border-color);
|
||||
border-bottom-color: var(--kbd-shadow-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 var(--kbd-shadow-color);;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h1:hover .anchor,
|
||||
h2:hover .anchor,
|
||||
h3:hover .anchor,
|
||||
h4:hover .anchor,
|
||||
h5:hover .anchor,
|
||||
h6:hover .anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid var(--header-border-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--header-border-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.67;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
li>p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
li+li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
dl {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
padding: 0;
|
||||
margin-top: 1em;
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
padding: 0 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 0.375em 0.8125em;
|
||||
border: 1px solid var(--table-td-border-color);
|
||||
}
|
||||
|
||||
table tr {
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--table-tr-border-color);
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(--alt-background-color);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
img[align=right] {
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
|
||||
img[align=left] {
|
||||
padding-right: 1.25em;
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.task-list-item+.task-list-item {
|
||||
margin-top: 0.1875em;
|
||||
}
|
||||
|
||||
.task-list-item input {
|
||||
margin: 0 0.2em 0.25em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{response}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.show(TEMPLATE.replace("{response}", response), Step.LOAD_WEBVIEW)
|
||||
|
||||
def on_ask_action(self, widget, _):
|
||||
"""Callback for the app.ask action."""
|
||||
|
|
|
@ -21,7 +21,6 @@ class BavarderProvider:
|
|||
def __init__(self, win, app, *args, **kwargs):
|
||||
self.win = win
|
||||
self.banner = win.banner
|
||||
self.bot_text_view = win.bot_text_view
|
||||
self.app = app
|
||||
self.chat = None
|
||||
self.update_response = app.update_response
|
||||
|
|
|
@ -27,10 +27,11 @@ class BavarderWindow(Adw.ApplicationWindow):
|
|||
|
||||
toast_overlay = Gtk.Template.Child()
|
||||
prompt_text_view = Gtk.Template.Child()
|
||||
bot_text_view = Gtk.Template.Child()
|
||||
spinner = Gtk.Template.Child()
|
||||
ask_button = Gtk.Template.Child()
|
||||
wait_button = Gtk.Template.Child()
|
||||
scrolled_response_window = Gtk.Template.Child()
|
||||
bot_text_view = Gtk.Template.Child()
|
||||
response_stack = Gtk.Template.Child()
|
||||
banner = Gtk.Template.Child()
|
||||
# listen = Gtk.Template.Child()
|
||||
|
|
Loading…
Reference in a new issue