mirror of
https://github.com/lutris/lutris
synced 2024-11-02 07:10:17 +00:00
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:
parent
636ba5c1aa
commit
c5cb3a52b6
5 changed files with 85 additions and 44 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in a new issue