mirror of
https://github.com/lutris/lutris.git
synced 2026-06-17 10:19:58 -04:00
Port multiple-file widget off Gtk.TreeView to match EditableGrid
_generate_multiple_file was the last Gtk.TreeView in a config dialog. Replace it with the same pattern EditableGrid uses: a Gtk.ListBox of rows where each row is a Gtk.Entry + a small reset-button-styled delete icon, the whole thing wrapped in a Gtk.Frame. Behavioural changes: - Paths are now editable inline (consistent with EditableGrid). Useful for paths the file dialog can't reach easily (network shares, symlinked locations). - The Delete-key handler is gone; per-row delete buttons replace it. - The "Add Files" button is right-aligned in a button_box below the frame so it sits where EditableGrid's "Add" does. Shared .editable-grid CSS now styles both widgets identically. Update GTK4-MIGRATION.md to drop the widget_generator entry from the TreeView holdouts list and describe both ports together. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -362,23 +362,23 @@ isn't worth carrying for the duration of the GTK 4 cycle. When
|
||||
Deprecated in GTK 4.10. The replacement story is the
|
||||
`Gtk.ColumnView` / `Gtk.ListView` + `Gio.ListStore` +
|
||||
`Gtk.SignalListItemFactory` stack — which the main game list and grid
|
||||
already moved to. Remaining holdouts:
|
||||
already moved to. The only remaining `Gtk.ListStore` users live
|
||||
underneath `Gtk.EntryCompletion`:
|
||||
|
||||
- `lutris/gui/config/widget_generator.py` — the file list in the
|
||||
installer-script file-picker widget (`_generate_files()`).
|
||||
- `lutris/gui/widgets/searchable_entrybox.py` and
|
||||
`lutris/gui/widgets/common.py` — `Gtk.ListStore` backing the
|
||||
`Gtk.EntryCompletion` autocomplete; both go away when the
|
||||
`EntryCompletion` swap lands.
|
||||
- `lutris/gui/widgets/searchable_entrybox.py` (service-search dropdown)
|
||||
- `lutris/gui/widgets/common.py` (`FileChooserEntry` path completion)
|
||||
|
||||
Each is small and self-contained. Swap when convenient or when GTK 5
|
||||
removes the API; no urgency until then.
|
||||
Both `ListStore`s go away when the `EntryCompletion` swap lands; until
|
||||
then they're harmless deprecation noise.
|
||||
|
||||
`EditableGrid` (in `widgets/common.py`) used to be a 2-column editable
|
||||
`TreeView`; it now uses a `Gtk.ListBox` of rows, each row holding one
|
||||
`Gtk.Entry` per column and a small per-row delete button. CSS in
|
||||
`share/lutris/ui/lutris.css` strips the entry borders and adds a
|
||||
column separator so the rows still read as a tabular grid.
|
||||
`EditableGrid` (in `widgets/common.py`) and the multiple-file picker
|
||||
in `widget_generator.py` (`_generate_multiple_file()`) both used to be
|
||||
`Gtk.TreeView`s. They now use a shared pattern: a `Gtk.ListBox` of
|
||||
rows where each row is a `Gtk.Entry` plus a small per-row delete
|
||||
button, wrapped in a `Gtk.Frame`. CSS in
|
||||
`share/lutris/ui/lutris.css` (scoped to the `.editable-grid` class)
|
||||
strips entry borders/padding/radius and tightens row padding so the
|
||||
rows still read as a clean tabular list.
|
||||
|
||||
### `Gtk.StyleContext.add_provider_for_display` / `remove_provider_for_display`
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from gettext import gettext as _
|
||||
from inspect import Parameter, signature
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from gi.repository import Gdk, Gio, GLib, Gtk # type: ignore
|
||||
from gi.repository import Gio, GLib, Gtk # type: ignore
|
||||
|
||||
from lutris.config import LutrisConfig
|
||||
from lutris.gui.widgets import NotificationSource
|
||||
@@ -605,17 +605,63 @@ class WidgetGenerator(ABC):
|
||||
def _generate_command_line(self, option, value, default):
|
||||
return self._generate_file(option, value, default, shell_quoting=True)
|
||||
|
||||
# TreeView
|
||||
def _generate_multiple_file(self, option, value, default):
|
||||
"""Generate a multiple file selector."""
|
||||
|
||||
option_key = option["option"]
|
||||
label_text = option["label"]
|
||||
# entries holds the canonical state: one Gtk.Entry per row, in
|
||||
# display order. fire_changed() and the add-files dedupe both
|
||||
# read it; add_row() appends to it; on_remove() removes from it.
|
||||
entries = []
|
||||
|
||||
def current_files():
|
||||
return [e.get_text().strip() for e in entries]
|
||||
|
||||
def fire_changed():
|
||||
self.changed.fire(option_key, current_files())
|
||||
|
||||
def add_row(filename: str) -> None:
|
||||
row = Gtk.ListBoxRow()
|
||||
row.set_activatable(False)
|
||||
row_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
row_box.set_margin_top(2)
|
||||
row_box.set_margin_bottom(2)
|
||||
row_box.set_margin_start(6)
|
||||
row_box.set_margin_end(6)
|
||||
|
||||
entry = Gtk.Entry()
|
||||
entry.set_text(filename)
|
||||
entry.set_hexpand(True)
|
||||
entry.connect("changed", lambda _e: fire_changed())
|
||||
row_box.append(entry)
|
||||
|
||||
delete_button = Gtk.Button.new_from_icon_name("list-remove-symbolic")
|
||||
delete_button.add_css_class("reset-button")
|
||||
delete_button.set_valign(Gtk.Align.CENTER)
|
||||
delete_button.set_halign(Gtk.Align.CENTER)
|
||||
delete_button.set_has_frame(False)
|
||||
delete_button.set_tooltip_text(_("Remove this file"))
|
||||
|
||||
def on_remove(_button: Gtk.Button) -> None:
|
||||
files_listbox.remove(row)
|
||||
entries.remove(entry)
|
||||
fire_changed()
|
||||
|
||||
delete_button.connect("clicked", on_remove)
|
||||
row_box.append(delete_button)
|
||||
|
||||
row.set_child(row_box)
|
||||
files_listbox.append(row)
|
||||
entries.append(entry)
|
||||
|
||||
def on_add_files_clicked(_widget):
|
||||
"""Create and run multi-file chooser dialog."""
|
||||
dialog = Gtk.FileDialog()
|
||||
dialog.set_title(_("Select files"))
|
||||
|
||||
files = [row[0] for row in files_list_store]
|
||||
first_file_dir = os.path.dirname(files[0]) if files else None
|
||||
existing = current_files()
|
||||
first_file_dir = os.path.dirname(existing[0]) if existing else None
|
||||
initial = first_file_dir or self.default_directory
|
||||
if initial:
|
||||
dialog.set_initial_folder(Gio.File.new_for_path(initial))
|
||||
@@ -625,71 +671,66 @@ class WidgetGenerator(ABC):
|
||||
gfiles = _dialog.open_multiple_finish(async_result)
|
||||
except GLib.Error:
|
||||
return
|
||||
added_any = False
|
||||
for i in range(gfiles.get_n_items()):
|
||||
gfile = gfiles.get_item(i)
|
||||
filename = gfile.get_path()
|
||||
if filename and filename not in files:
|
||||
files_list_store.append([filename])
|
||||
files.append(filename)
|
||||
self.changed.fire(option_key, files)
|
||||
if filename and filename not in current_files():
|
||||
add_row(filename)
|
||||
added_any = True
|
||||
if added_any:
|
||||
fire_changed()
|
||||
|
||||
dialog.open_multiple(None, None, on_finish)
|
||||
|
||||
def on_files_treeview_keypress(_controller, keyval, _keycode, _state):
|
||||
"""Action triggered when a row is deleted from the filechooser."""
|
||||
if keyval == Gdk.KEY_Delete:
|
||||
treeview = _controller.get_widget()
|
||||
selection = treeview.get_selection()
|
||||
(model, treepaths) = selection.get_selected_rows()
|
||||
for treepath in treepaths:
|
||||
treeiter = model.get_iter(treepath)
|
||||
model.remove(treeiter)
|
||||
|
||||
files = [row[0] for row in files_list_store]
|
||||
self.changed.fire(option_key, files)
|
||||
|
||||
option_key = option["option"]
|
||||
label = option["label"]
|
||||
|
||||
files_list_store = Gtk.ListStore(str)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
label = Label(label + ":")
|
||||
label.set_halign(Gtk.Align.START)
|
||||
label.set_margin_bottom(5)
|
||||
button = Gtk.Button(label=_("Add Files"))
|
||||
button.connect("clicked", on_add_files_clicked)
|
||||
button.set_margin_start(10)
|
||||
|
||||
if not value:
|
||||
value = default
|
||||
|
||||
if value:
|
||||
if isinstance(value, str):
|
||||
files = [value]
|
||||
initial_files = [value]
|
||||
else:
|
||||
files = value
|
||||
initial_files = list(value)
|
||||
else:
|
||||
files = []
|
||||
for filename in files:
|
||||
files_list_store.append([filename])
|
||||
cell_renderer = Gtk.CellRendererText()
|
||||
files_treeview = Gtk.TreeView(model=files_list_store)
|
||||
files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
|
||||
files_treeview.append_column(files_column)
|
||||
key_controller = Gtk.EventControllerKey()
|
||||
key_controller.connect("key-pressed", on_files_treeview_keypress)
|
||||
files_treeview.add_controller(key_controller)
|
||||
treeview_scroll = Gtk.ScrolledWindow()
|
||||
treeview_scroll.set_min_content_height(130)
|
||||
treeview_scroll.set_margin_start(10)
|
||||
treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
treeview_scroll.set_child(files_treeview)
|
||||
initial_files = []
|
||||
|
||||
vbox.append(label)
|
||||
treeview_scroll.set_hexpand(True)
|
||||
treeview_scroll.set_vexpand(True)
|
||||
vbox.append(treeview_scroll)
|
||||
vbox.append(button)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
vbox.add_css_class("editable-grid")
|
||||
header_label = Label(label_text + ":")
|
||||
header_label.set_halign(Gtk.Align.START)
|
||||
header_label.set_margin_bottom(5)
|
||||
|
||||
files_listbox = Gtk.ListBox()
|
||||
files_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
files_listbox.set_show_separators(True)
|
||||
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_min_content_height(130)
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
scrolled.set_child(files_listbox)
|
||||
|
||||
# Frame gives the listbox a visible border (especially when empty),
|
||||
# matching the EditableGrid styling.
|
||||
frame = Gtk.Frame()
|
||||
frame.set_margin_start(10)
|
||||
frame.set_hexpand(True)
|
||||
frame.set_vexpand(True)
|
||||
frame.set_child(scrolled)
|
||||
|
||||
for filename in initial_files:
|
||||
add_row(filename)
|
||||
|
||||
add_button = Gtk.Button(label=_("Add Files"))
|
||||
add_button.set_size_request(80, -1)
|
||||
add_button.connect("clicked", on_add_files_clicked)
|
||||
|
||||
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
button_box.set_halign(Gtk.Align.END)
|
||||
button_box.set_margin_top(6)
|
||||
button_box.append(add_button)
|
||||
|
||||
vbox.append(header_label)
|
||||
vbox.append(frame)
|
||||
vbox.append(button_box)
|
||||
return self.build_option_widget(option, vbox, no_label=True)
|
||||
|
||||
# FileChooserEntry
|
||||
|
||||
Reference in New Issue
Block a user