Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

envvars-dialog: Allow to paste var=value and fix validation #3679

Merged
merged 8 commits into from
Jan 13, 2025
31 changes: 11 additions & 20 deletions bottles/frontend/ui/dialog-env-vars.blp
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
using Gtk 4.0;
using Adw 1;

template $EnvVarsDialog: Adw.Window {
modal: true;
default-width: 500;
default-height: 500;

ShortcutController {
Shortcut {
trigger: "Escape";
action: "action(window.close)";
}
}
template $EnvironmentVariablesDialog: Adw.Dialog {
content-width: 600;
content-height: 800;
title: _("Environment Variables");

Box {
orientation: vertical;

Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("Environment Variables");
};
styles [
"flat",
]
}

Adw.PreferencesPage {
Adw.PreferencesGroup {
description: _("Environment variables are dynamic-named value that can affect the way running processes will behave on your bottle.");
description: _("Environment variables are dynamic-named values that can affect the way running processes will behave in your bottle");

Adw.EntryRow entry_name {
title: _("Variable Name");
Adw.EntryRow entry_new_var {
title: _("New Variable");
show-apply-button: true;
}
}

Adw.PreferencesGroup group_vars {
title: _("Existing Variables");
}
Adw.PreferencesGroup group_vars {}
}
}
}
4 changes: 2 additions & 2 deletions bottles/frontend/ui/env-var-entry.blp
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using Gtk 4.0;
using Adw 1;

template $EnvVarEntry: Adw.EntryRow {
title: _("Value");
template $EnvironmentVariableEntryRow: Adw.EntryRow {
show-apply-button: true;

Button btn_remove {
valign: center;
icon-name: "user-trash-symbolic";
tooltip-text: _("Remove");

styles [
"flat",
Expand Down
32 changes: 24 additions & 8 deletions bottles/frontend/utils/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,50 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import re
from typing import Optional
from functools import wraps
from inspect import signature

from gi.repository import GLib, Gtk

from bottles.frontend.utils.sh import ShUtils


class GtkUtils:
@staticmethod
def validate_entry(entry, extend=None) -> bool:
text = entry.get_text()
if (
re.search("[@!#$%^&*()<>?/|}{~:.;,'\"]", text)
or len(text) == 0
or text.isspace()
):
var_assignment = entry.get_text()
var_name = ShUtils.split_assignment(var_assignment)[0]
if var_name and not ShUtils.is_name(var_name):
GtkUtils.reset_entry_apply_button(entry)
entry.add_css_class("error")
return False

if not var_name or "=" not in var_assignment:
GtkUtils.reset_entry_apply_button(entry)
entry.remove_css_class("error")
return False

if extend is not None:
if extend(text):
if not extend(var_name):
GtkUtils.reset_entry_apply_button(entry)
entry.add_css_class("error")
return False

entry.set_show_apply_button(True)
entry.remove_css_class("error")
return True

@staticmethod
def reset_entry_apply_button(entry) -> None:
"""
Reset the apply_button within AdwEntryRow to hide it without disabling
the functionality. This is needed because the widget does not provide
an API to control when the button is displayed without disabling it
"""
entry.set_show_apply_button(False)
entry.set_show_apply_button(True)

@staticmethod
def run_in_main_loop(func):
@wraps(func)
Expand Down
1 change: 1 addition & 0 deletions bottles/frontend/utils/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ bottles_sources = [
'gtk.py',
'common.py',
'filters.py',
'sh.py',
]

install_data(bottles_sources, install_dir: utilsdir)
31 changes: 31 additions & 0 deletions bottles/frontend/utils/sh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# sh.py
#
# Copyright 2025 The Bottles Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, in version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import re

_is_name = re.compile(r"""[_a-zA-Z][_a-zA-Z0-9]*""")


class ShUtils:
@staticmethod
def is_name(text: str) -> bool:
return bool(_is_name.fullmatch(text))

@staticmethod
def split_assignment(text: str) -> tuple[str, str]:
name, _, value = text.partition("=")
return (name, value)
4 changes: 2 additions & 2 deletions bottles/frontend/views/bottle_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from bottles.frontend.windows.display import DisplayDialog
from bottles.frontend.windows.dlloverrides import DLLOverridesDialog
from bottles.frontend.windows.drives import DrivesDialog
from bottles.frontend.windows.envvars import EnvVarsDialog
from bottles.frontend.windows.envvars import EnvironmentVariablesDialog
from bottles.frontend.windows.exclusionpatterns import ExclusionPatternsDialog
from bottles.frontend.windows.fsr import FsrDialog
from bottles.frontend.windows.gamescope import GamescopeDialog
Expand Down Expand Up @@ -200,7 +200,7 @@ def __init__(self, details, config, **kwargs):
"activated", self.__show_feature_dialog, DLLOverridesDialog
)
self.row_env_variables.connect(
"activated", self.__show_feature_dialog, EnvVarsDialog
"activated", self.__show_feature_dialog, EnvironmentVariablesDialog
)
self.row_drives.connect("activated", self.__show_feature_dialog, DrivesDialog)
self.btn_manage_gamescope.connect(
Expand Down
134 changes: 90 additions & 44 deletions bottles/frontend/windows/envvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@

from gi.repository import Gtk, GLib, Adw

from bottles.backend.logger import Logger
from bottles.frontend.utils.gtk import GtkUtils
from bottles.frontend.utils.sh import ShUtils

logging = Logger()


@Gtk.Template(resource_path="/com/usebottles/bottles/env-var-entry.ui")
class EnvVarEntry(Adw.EntryRow):
__gtype_name__ = "EnvVarEntry"
class EnvironmentVariableEntryRow(Adw.EntryRow):
__gtype_name__ = "EnvironmentVariableEntryRow"

# region Widgets
btn_remove = Gtk.Template.Child()
Expand All @@ -36,53 +40,97 @@ def __init__(self, parent, env, **kwargs):
self.manager = parent.window.manager
self.config = parent.config
self.env = env

self.set_title(self.env[0])
self.set_text(self.env[1])
self.set_text("=".join(self.env))

# connect signals
self.connect("changed", self.__validate)
self.connect("apply", self.__save)
self.btn_remove.connect("clicked", self.__remove)

self.__customize_layout()

def __customize_layout(self):
"""
Align text input field vertically. Hide unused labels and make layout
changes as needed to display the text correctly. We manually traverse
AdwEntryRow's widget tree to make these changes because it does not
offer options for these customizations on its public API
"""
try:
widget = (
self.get_child().get_first_child().get_next_sibling().get_first_child()
)
while isinstance(widget, Gtk.Label):
widget.set_visible(False)
widget = widget.get_next_sibling()

if isinstance(widget, Gtk.Text):
widget.set_valign(Gtk.Align.CENTER)
else:
raise RuntimeError("Could not find widget Gtk.Text")
except Exception as e:
logging.error(
f"{type(e)}: {e}\nEnvironmentVariableEntryRow could not find text widget. Did AdwEntryRow change it's widget tree?"
)
TheEvilSkeleton marked this conversation as resolved.
Show resolved Hide resolved

def __save(self, *_args):
"""
Change the env var value according to the
user input and update the bottle configuration
Change the environment variable value according to the user input and
update the bottle configuration
"""
if not self.__valid_name:
return

new_name, new_value = ShUtils.split_assignment(self.get_text())
self.manager.update_config(
config=self.config,
key=self.env[0],
value=self.get_text(),
key=new_name,
value=new_value,
scope="Environment_Variables",
)
if new_name != self.env[0]:
self.__remove_config()

self.env = (new_name, new_value)

def __remove(self, *_args):
"""
Remove the env var from the bottle configuration and
Remove the environment variable from the bottle configuration and
destroy the widget
"""
self.__remove_config()
self.parent.remove_entry(self)

def __remove_config(self, *_args):
"""Remove the environment variable from the bottle configuration"""
self.manager.update_config(
config=self.config,
key=self.env[0],
value=False,
remove=True,
scope="Environment_Variables",
)
self.parent.group_vars.remove(self)

def __validate(self, *_args):
self.__valid_name = GtkUtils.validate_entry(
self, lambda var_name: not var_name == "WINEDLLOVERRIDES"
)

if not self.__valid_name:
self.add_css_class("error")


@Gtk.Template(resource_path="/com/usebottles/bottles/dialog-env-vars.ui")
class EnvVarsDialog(Adw.Window):
__gtype_name__ = "EnvVarsDialog"
class EnvironmentVariablesDialog(Adw.Dialog):
__gtype_name__ = "EnvironmentVariablesDialog"

# region Widgets
entry_name = Gtk.Template.Child()
entry_new_var = Gtk.Template.Child()
group_vars = Gtk.Template.Child()
# endregion

def __init__(self, window, config, **kwargs):
super().__init__(**kwargs)
self.set_transient_for(window)

# common variables and references
self.window = window
Expand All @@ -92,52 +140,50 @@ def __init__(self, window, config, **kwargs):
self.__populate_vars_list()

# connect signals
self.entry_name.connect("changed", self.__validate)
self.entry_name.connect("apply", self.__save_var)
self.entry_new_var.connect("changed", self.__validate)
self.entry_new_var.connect("apply", self.__save_var)

def present(self):
return super().present(self.window)

def __validate(self, *_args):
self.__valid_name = GtkUtils.validate_entry(
self.entry_name, lambda envvar: envvar.startswith("WINEDLLOVERRIDES")
self.entry_new_var, lambda var_name: not var_name == "WINEDLLOVERRIDES"
)

def __save_var(self, *_args):
"""
This function save the new env var to the
bottle configuration
"""
"""Save the new environment variable to the bottle configuration"""
if not self.__valid_name:
self.entry_name.set_text("")
self.entry_name.remove_css_class("error")
self.__valid_name = True
return

env_name = self.entry_name.get_text()
env_value = "value"
split_value = env_name.split("=", 1)
if len(split_value) == 2:
env_name = split_value[0]
env_value = split_value[1]
new_name, new_value = ShUtils.split_assignment(self.entry_new_var.get_text())
self.manager.update_config(
config=self.config,
key=env_name,
value=env_value,
key=new_name,
value=new_value,
scope="Environment_Variables",
)
_entry = EnvVarEntry(parent=self, env=[env_name, env_value])
GLib.idle_add(self.group_vars.add, _entry)
self.entry_name.set_text("")
_entry = EnvironmentVariableEntryRow(parent=self, env=(new_name, new_value))
self.group_vars.set_description()
self.group_vars.add(_entry)
self.entry_new_var.set_text("")

def remove_entry(self, _entry):
self.group_vars.remove(_entry)
self.__set_description()

def __set_description(self):
if len(self.config.Environment_Variables.items()) == 0:
self.group_vars.set_description(_("No environment variables defined"))

def __populate_vars_list(self):
"""
This function populate the list of env vars
with the existing ones from the bottle configuration
Populate the list of environment variables with the existing ones from
the bottle configuration
"""
envs = self.config.Environment_Variables.items()
if len(envs) == 0:
self.group_vars.set_description(_("No environment variables defined."))
return
self.__set_description()

self.group_vars.set_description("")
for env in envs:
_entry = EnvVarEntry(parent=self, env=env)
GLib.idle_add(self.group_vars.add, _entry)
_entry = EnvironmentVariableEntryRow(parent=self, env=env)
self.group_vars.add(_entry)
Loading