Add 'shell_quoting' feature to FileChooserEntry.

This is used for the prelaunch and postlaunch commands. When on, the entry has distinct text and path values - text is what the entry shows and the 'real value' but the path will be extracted from the text via shlex.split().

You can then browse for an set the path, leaving any other arguments alone.

This also ensure that if that path needs quoting, it gets quoting.

In most cases the 'text' and 'path' are identical, which should keep existing code happy.

Resolves #4989
This commit is contained in:
Daniel Johnson 2023-08-16 15:28:12 -04:00
parent 636ba5c1aa
commit c5cb3a52b6
5 changed files with 85 additions and 44 deletions

View file

@ -64,6 +64,7 @@ class ConfigBox(VBox):
def update_option_visibility(self):
"""Recursively searches out all the options and shows or hides them according to
the filter and advanced-visibility settings."""
def update_widgets(widgets):
filter_text = self.filter.lower()
@ -305,6 +306,8 @@ class ConfigBox(VBox):
self.generate_directory_chooser(option, value)
elif option_type == "file":
self.generate_file_chooser(option, value)
elif option_type == "command_line":
self.generate_file_chooser(option, value, shell_quoting=True)
elif option_type == "multiple":
self.generate_multiple_file_chooser(option_key, option["label"], value)
elif option_type == "label":
@ -454,7 +457,7 @@ class ConfigBox(VBox):
self.option_changed(spin_button, option, value)
# File chooser
def generate_file_chooser(self, option, path=None):
def generate_file_chooser(self, option, text=None, shell_quoting=False):
"""Generate a file chooser button to select a file."""
option_name = option["option"]
label = Label(option["label"])
@ -462,8 +465,9 @@ class ConfigBox(VBox):
file_chooser = FileChooserEntry(
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=path,
default_path=default_path
text=text,
default_path=default_path,
shell_quoting=shell_quoting
)
# file_chooser.set_size_request(200, 30)
@ -472,19 +476,20 @@ class ConfigBox(VBox):
if default_path and os.path.exists(default_path):
file_chooser.entry.set_text(default_path)
if path:
if text:
# If path is relative, complete with game dir
if not os.path.isabs(path):
path = os.path.expanduser(path)
if not os.path.isabs(path):
if not os.path.isabs(text):
text = os.path.expanduser(text)
if not os.path.isabs(text):
if self.game and self.game.directory:
path = os.path.join(self.game.directory, path)
file_chooser.entry.set_text(path)
text = os.path.join(self.game.directory, text)
file_chooser.entry.set_text(text)
file_chooser.set_valign(Gtk.Align.CENTER)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(file_chooser, True, True, 0)
self.option_widget = file_chooser
file_chooser.connect("changed", self._on_chooser_file_set, option_name)
# Directory chooser
@ -496,7 +501,7 @@ class ConfigBox(VBox):
if not path and self.game and self.game.runner:
default_path = self.game.runner.working_dir
directory_chooser = FileChooserEntry(
title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, text=path, default_path=default_path
)
directory_chooser.connect("changed", self._on_chooser_file_set, option_name)
directory_chooser.set_valign(Gtk.Align.CENTER)

View file

@ -42,7 +42,7 @@ class CacheConfigurationDialog(ModalDialog):
path_chooser = FileChooserEntry(
title=_("Set the folder for the cache path"),
action=Gtk.FileChooserAction.SELECT_FOLDER,
path=self.cache_path,
text=self.cache_path,
activates_default=True
)
path_chooser.connect("changed", self._on_cache_path_set)

View file

@ -181,8 +181,7 @@ class InstallerFileBox(Gtk.VBox):
box.pack_start(label, False, False, 0)
location_entry = FileChooserEntry(
self.installer_file.human_url,
Gtk.FileChooserAction.OPEN,
path=None
Gtk.FileChooserAction.OPEN
)
location_entry.connect("changed", self.on_location_changed)
location_entry.show()

View file

@ -1,6 +1,7 @@
"""Misc widgets used in the GUI."""
# Standard Library
import os
import shlex
import urllib.parse
from gettext import gettext as _
@ -10,7 +11,6 @@ from gi.repository import GLib, GObject, Gtk, Pango
# Lutris Modules
from lutris.util import system
from lutris.util.linux import LINUX_SYSTEM
from lutris.util.log import logger
class SlugEntry(Gtk.Entry, Gtk.Editable):
@ -35,7 +35,6 @@ class NumberEntry(Gtk.Entry, Gtk.Editable):
class FileChooserEntry(Gtk.Box):
"""Editable entry with a file picker button"""
max_completion_items = 15 # Maximum number of items to display in the autocompletion dropdown.
@ -48,11 +47,12 @@ class FileChooserEntry(Gtk.Box):
self,
title=_("Select file"),
action=Gtk.FileChooserAction.OPEN,
path=None,
text=None,
default_path=None,
warn_if_non_empty=False,
warn_if_ntfs=False,
activates_default=False,
shell_quoting=False
):
super().__init__(
orientation=Gtk.Orientation.VERTICAL,
@ -61,14 +61,17 @@ class FileChooserEntry(Gtk.Box):
)
self.title = title
self.action = action
self.path = os.path.expanduser(path) if path else None
self.default_path = os.path.expanduser(default_path) if default_path else path
self.warn_if_non_empty = warn_if_non_empty
self.warn_if_ntfs = warn_if_ntfs
self.shell_quoting = shell_quoting
self.path_completion = Gtk.ListStore(str)
self.entry = Gtk.Entry(visible=True)
self.set_text(text) # do before set up signal handlers
self.original_text = self.get_text()
self.default_path = os.path.expanduser(default_path) if default_path else self.get_path()
self.entry.set_activates_default(activates_default)
self.entry.set_completion(self.get_completion())
self.entry.connect("changed", self.on_entry_changed)
@ -76,9 +79,6 @@ class FileChooserEntry(Gtk.Box):
self.entry.connect("focus-out-event", self.on_focus_out)
self.entry.connect("backspace", self.on_backspace)
if path:
self.entry.set_text(path)
browse_button = Gtk.Button(_("Browse..."), visible=True)
browse_button.connect("clicked", self.on_browse_clicked)
@ -87,18 +87,49 @@ class FileChooserEntry(Gtk.Box):
box.add(browse_button)
self.pack_start(box, False, False, 0)
def set_text(self, path):
self.path = os.path.expanduser(path)
self.entry.set_text(self.path)
def set_text(self, text):
if self.shell_quoting and text:
command_array = shlex.split(text)
if command_array:
expanded = os.path.expanduser(command_array[0])
command_array[0] = expanded
rejoined = shlex.join(command_array)
self.original_text = rejoined
self.entry.set_text(rejoined)
return
expanded = os.path.expanduser(text) if text else ""
self.original_text = expanded
self.entry.set_text(expanded)
def set_path(self, path):
if self.shell_quoting:
command_array = shlex.split(self.get_text())
if command_array:
command_array[0] = os.path.expanduser(path) if path else ""
rejoined = shlex.join(command_array)
self.original_text = rejoined
self.entry.set_text(rejoined)
return
expanded = os.path.expanduser(path) if path else ""
self.original_text = expanded
self.entry.set_text(expanded)
def get_text(self):
"""Return the entry's text"""
"""Return the entry's text. If shell_quoting is one, this is actually a command
line (with argument quoting) and not a simple path."""
return self.entry.get_text()
def get_filename(self):
"""Deprecated"""
logger.warning("Just use get_text")
return self.get_text()
def get_path(self):
"""Returns the path in the entry; if shell_quoting is set, this extracts
the command from the text and returns only that."""
text = self.get_text()
if self.shell_quoting:
command_array = shlex.split(text)
return command_array[0] if command_array else ""
return text
def get_completion(self):
"""Return an EntryCompletion widget"""
@ -116,7 +147,7 @@ class FileChooserEntry(Gtk.Box):
def get_default_folder(self):
"""Return the default folder for the file picker"""
default_path = self.path or self.default_path or ""
default_path = self.get_path() or self.default_path or ""
if not default_path or not system.path_exists(default_path):
current_entry = self.get_text()
if system.path_exists(current_entry):
@ -132,17 +163,21 @@ class FileChooserEntry(Gtk.Box):
if response == Gtk.ResponseType.ACCEPT:
target_path = file_chooser_dialog.get_filename()
if target_path:
self.entry.set_text(system.reverse_expanduser(target_path))
if target_path and self.shell_quoting:
command_array = shlex.split(self.entry.get_text())
text = shlex.join([target_path] + command_array[1:])
else:
text = target_path
self.original_text = text
self.entry.set_text(text)
file_chooser_dialog.destroy()
def on_entry_changed(self, widget):
"""Entry changed callback"""
self.clear_warnings()
path = widget.get_text()
if not path:
return
# If the user isn't editing this entry, we'll apply updates
# immediately upon any change
@ -152,7 +187,9 @@ class FileChooserEntry(Gtk.Box):
# We changed the text on commit, so we return here to avoid a double changed signal
return
self.path = path
text = self.get_text()
path = self.get_path()
self.original_text = text
if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
ntfs_box = Gtk.Box(spacing=6, visible=True)
@ -196,17 +233,17 @@ class FileChooserEntry(Gtk.Box):
GLib.idle_add(self.detect_changes)
def detect_changes(self):
"""Detects if the text has changed and updates self.path and fires
"""Detects if the text has changed and updates self.original_text and fires
the changed signal. Lame, but Gtk.Entry does not always fire its
changed event when edited!"""
new_path = self.get_text()
if self.path != new_path:
self.path = new_path
new_text = self.get_text()
if self.original_text != new_text:
self.original_text = new_text
self.emit("changed")
return False # used as idle function
def normalize_path(self):
original_path = self.get_text()
original_path = self.get_path()
path = original_path.strip("\r\n")
if path.startswith('file:///'):
@ -217,7 +254,7 @@ class FileChooserEntry(Gtk.Box):
self.update_completion(path)
if path != original_path:
self.entry.set_text(path)
self.set_path(path)
return True
return False

View file

@ -524,7 +524,7 @@ system_options = [ # pylint: disable=invalid-name
{
"section": "Game execution",
"option": "prelaunch_command",
"type": "file",
"type": "command_line",
"label": _("Pre-launch script"),
"advanced": True,
"help": _("Script to execute before the game starts"),
@ -541,7 +541,7 @@ system_options = [ # pylint: disable=invalid-name
{
"section": "Game execution",
"option": "postexit_command",
"type": "file",
"type": "command_line",
"label": _("Post-exit script"),
"advanced": True,
"help": _("Script to execute when the game exits"),