We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
ENV - Variables - Strip Whitespace + Make "Add" better
- Make "Add" better: When adding ENV variables to a game, you click ADD. Space is allocated for the ENV variable, but nothing actually happens visually (perhaps depending on Color Scheme?).

Expected: When you click add, cursor is set to input field of the ENV variable. This indicated where you should put it, and most likely will give a blinking cursor indicating where it is.

- Strip Whitespace: Accidental whitespace in front / behind ENV variable gets stored. Perhaps it would be better to strip it.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lutris/gui/widgets/common.py`
Content:
```
1 """Misc widgets used in the GUI."""
2 import os
3 
4 from gi.repository import Gtk, GObject, Pango
5 
6 from lutris.util.log import logger
7 from lutris.util import system
8 from lutris.util.linux import LINUX_SYSTEM
9 from lutris.gui.widgets.utils import get_stock_icon
10 
11 
12 class SlugEntry(Gtk.Entry, Gtk.Editable):
13     def __init__(self):
14         super(SlugEntry, self).__init__()
15 
16     def do_insert_text(self, new_text, length, position):
17         """Filter inserted characters to only accept alphanumeric and dashes"""
18         new_text = "".join([c for c in new_text if c.isalnum() or c == "-"]).lower()
19         length = len(new_text)
20         self.get_buffer().insert_text(position, new_text, length)
21         return position + length
22 
23 
24 class NumberEntry(Gtk.Entry, Gtk.Editable):
25     def __init__(self):
26         super(NumberEntry, self).__init__()
27 
28     def do_insert_text(self, new_text, length, position):
29         """Filter inserted characters to only accept numbers"""
30         new_text = "".join([c for c in new_text if c.isnumeric()])
31         if new_text:
32             self.get_buffer().insert_text(position, new_text, length)
33             return position + length
34         return position
35 
36 
37 class FileChooserEntry(Gtk.Box):
38     """Editable entry with a file picker button"""
39 
40     max_completion_items = 15  # Maximum number of items to display in the autocompletion dropdown.
41 
42     def __init__(
43             self,
44             title="Select file",
45             action=Gtk.FileChooserAction.OPEN,
46             path=None,
47             default_path=None,
48             warn_if_non_empty=False,
49             warn_if_ntfs=False
50     ):
51         super().__init__(
52             orientation=Gtk.Orientation.VERTICAL,
53             spacing=12,
54             visible=True
55         )
56         self.title = title
57         self.action = action
58         self.path = os.path.expanduser(path) if path else None
59         self.default_path = os.path.expanduser(default_path) if default_path else path
60         self.warn_if_non_empty = warn_if_non_empty
61         self.warn_if_ntfs = warn_if_ntfs
62 
63         self.path_completion = Gtk.ListStore(str)
64 
65         self.entry = Gtk.Entry(visible=True)
66         self.entry.set_completion(self.get_completion())
67         self.entry.connect("changed", self.on_entry_changed)
68         if path:
69             self.entry.set_text(path)
70 
71         browse_button = Gtk.Button("Browse...", visible=True)
72         browse_button.connect("clicked", self.on_browse_clicked)
73 
74         box = Gtk.Box(spacing=6, visible=True)
75         box.pack_start(self.entry, True, True, 0)
76         box.add(browse_button)
77         self.add(box)
78 
79     def get_text(self):
80         """Return the entry's text"""
81         return self.entry.get_text()
82 
83     def get_filename(self):
84         """Deprecated"""
85         logger.warning("Just use get_text")
86         return self.get_text()
87 
88     def get_completion(self):
89         """Return an EntryCompletion widget"""
90         completion = Gtk.EntryCompletion()
91         completion.set_model(self.path_completion)
92         completion.set_text_column(0)
93         return completion
94 
95     def get_filechooser_dialog(self):
96         """Return an instance of a FileChooserDialog configured for this widget"""
97         dialog = Gtk.FileChooserDialog(
98             title=self.title, transient_for=None, action=self.action
99         )
100         dialog.add_buttons(
101             "_Cancel", Gtk.ResponseType.CLOSE,
102             "_OK", Gtk.ResponseType.OK
103         )
104         dialog.set_create_folders(True)
105         dialog.set_current_folder(self.get_default_folder())
106         dialog.connect("response", self.on_select_file)
107         return dialog
108 
109     def get_default_folder(self):
110         """Return the default folder for the file picker"""
111         default_path = self.path or self.default_path or ""
112         if not default_path or not system.path_exists(default_path):
113             current_entry = self.get_text()
114             if system.path_exists(current_entry):
115                 default_path = current_entry
116         if not os.path.isdir(default_path):
117             default_path = os.path.dirname(default_path)
118         return os.path.expanduser(default_path or "~")
119 
120     def on_browse_clicked(self, _widget):
121         """Browse button click callback"""
122         file_chooser_dialog = self.get_filechooser_dialog()
123         file_chooser_dialog.run()
124 
125     def on_entry_changed(self, widget):
126         """Entry changed callback"""
127         self.clear_warnings()
128         path = widget.get_text()
129         if not path:
130             return
131         path = os.path.expanduser(path)
132         self.update_completion(path)
133         if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
134             ntfs_box = Gtk.Box(spacing=6, visible=True)
135             warning_image = Gtk.Image(visible=True)
136             warning_image.set_from_pixbuf(get_stock_icon("dialog-warning", 32))
137             ntfs_box.add(warning_image)
138             ntfs_label = Gtk.Label(visible=True)
139             ntfs_label.set_markup(
140                 "<b>Warning!</b> The selected path is located on a NTFS drive.\n"
141                 "Installing games on NTFS partitions is known to cause issues."
142             )
143             ntfs_box.add(ntfs_label)
144             self.pack_end(ntfs_box, False, False, 10)
145         if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):
146             non_empty_label = Gtk.Label(visible=True)
147             non_empty_label.set_markup(
148                 "<b>Warning!</b> The selected path "
149                 "contains files. Installation might not work properly."
150             )
151             self.pack_end(non_empty_label, False, False, 10)
152 
153     def on_select_file(self, dialog, response):
154         """FileChooserDialog response callback"""
155         if response == Gtk.ResponseType.OK:
156             target_path = dialog.get_filename()
157             if target_path:
158                 dialog.set_current_folder(target_path)
159                 self.entry.set_text(system.reverse_expanduser(target_path))
160         dialog.hide()
161 
162     def update_completion(self, current_path):
163         """Update the auto-completion widget with the current path"""
164         self.path_completion.clear()
165 
166         if not os.path.exists(current_path):
167             current_path, filefilter = os.path.split(current_path)
168         else:
169             filefilter = None
170 
171         if os.path.isdir(current_path):
172             index = 0
173             for filename in sorted(os.listdir(current_path)):
174                 if filename.startswith("."):
175                     continue
176                 if filefilter is not None and not filename.startswith(filefilter):
177                     continue
178                 self.path_completion.append([os.path.join(current_path, filename)])
179                 index += 1
180                 if index > self.max_completion_items:
181                     break
182 
183     def clear_warnings(self):
184         """Delete all the warning labels from the container"""
185         for index, child in enumerate(self.get_children()):
186             if index > 0:
187                 child.destroy()
188 
189 
190 class Label(Gtk.Label):
191     """Standardised label for config vboxes."""
192 
193     def __init__(self, message=None):
194         """Custom init of label."""
195         super().__init__(label=message)
196         self.set_line_wrap(True)
197         self.set_max_width_chars(22)
198         self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
199         self.set_size_request(230, -1)
200         self.set_alignment(0, 0.5)
201         self.set_justify(Gtk.Justification.LEFT)
202 
203 
204 class VBox(Gtk.Box):
205     def __init__(self, **kwargs):
206         super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)
207 
208 
209 class EditableGrid(Gtk.Grid):
210     __gsignals__ = {"changed": (GObject.SIGNAL_RUN_FIRST, None, ())}
211 
212     def __init__(self, data, columns):
213         self.columns = columns
214         super().__init__()
215         self.set_column_homogeneous(True)
216         self.set_row_homogeneous(True)
217         self.set_row_spacing(10)
218         self.set_column_spacing(10)
219 
220         self.liststore = Gtk.ListStore(str, str)
221         for item in data:
222             self.liststore.append([str(value) for value in item])
223 
224         self.treeview = Gtk.TreeView.new_with_model(self.liststore)
225         self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
226         for i, column_title in enumerate(self.columns):
227             renderer = Gtk.CellRendererText()
228             renderer.set_property("editable", True)
229             renderer.connect("edited", self.on_text_edited, i)
230 
231             column = Gtk.TreeViewColumn(column_title, renderer, text=i)
232             column.set_resizable(True)
233             column.set_min_width(100)
234             column.set_sort_column_id(0)
235             self.treeview.append_column(column)
236 
237         self.buttons = []
238         self.add_button = Gtk.Button("Add")
239         self.buttons.append(self.add_button)
240         self.add_button.connect("clicked", self.on_add)
241 
242         self.delete_button = Gtk.Button("Delete")
243         self.buttons.append(self.delete_button)
244         self.delete_button.connect("clicked", self.on_delete)
245 
246         self.scrollable_treelist = Gtk.ScrolledWindow()
247         self.scrollable_treelist.set_vexpand(True)
248         self.scrollable_treelist.add(self.treeview)
249 
250         self.attach(self.scrollable_treelist, 0, 0, 5, 5)
251         self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)
252         for i, button in enumerate(self.buttons[1:]):
253             self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
254         self.show_all()
255 
256     def on_add(self, widget):
257         self.liststore.append(["", ""])
258         self.emit("changed")
259 
260     def on_delete(self, widget):
261         selection = self.treeview.get_selection()
262         liststore, iter = selection.get_selected()
263         self.liststore.remove(iter)
264         self.emit("changed")
265 
266     def on_text_edited(self, widget, path, text, field):
267         self.liststore[path][field] = text
268         self.emit("changed")
269 
270     def get_data(self):
271         model_data = []
272         for row in self.liststore:
273             model_data.append([col for col in row])
274         return model_data
275
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/lutris/gui/widgets/common.py b/lutris/gui/widgets/common.py
--- a/lutris/gui/widgets/common.py
+++ b/lutris/gui/widgets/common.py
@@ -255,6 +255,9 @@
 
     def on_add(self, widget):
         self.liststore.append(["", ""])
+        row_position = len(self.liststore)-1
+        self.treeview.set_cursor(row_position, None, False)
+        self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)
         self.emit("changed")
 
     def on_delete(self, widget):
@@ -264,7 +267,7 @@
         self.emit("changed")
 
     def on_text_edited(self, widget, path, text, field):
-        self.liststore[path][field] = text
+        self.liststore[path][field] = text.strip()
         self.emit("changed")
 
     def get_data(self):

{"golden_diff": "diff --git a/lutris/gui/widgets/common.py b/lutris/gui/widgets/common.py\n--- a/lutris/gui/widgets/common.py\n+++ b/lutris/gui/widgets/common.py\n@@ -255,6 +255,9 @@\n \n     def on_add(self, widget):\n         self.liststore.append([\"\", \"\"])\n+        row_position = len(self.liststore)-1\n+        self.treeview.set_cursor(row_position, None, False)\n+        self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)\n         self.emit(\"changed\")\n \n     def on_delete(self, widget):\n@@ -264,7 +267,7 @@\n         self.emit(\"changed\")\n \n     def on_text_edited(self, widget, path, text, field):\n-        self.liststore[path][field] = text\n+        self.liststore[path][field] = text.strip()\n         self.emit(\"changed\")\n \n     def get_data(self):\n", "issue": "ENV - Variables - Strip Whitespace + Make \"Add\" better\n- Make \"Add\" better: When adding ENV variables to a game, you click ADD. Space is allocated for the ENV variable, but nothing actually happens visually (perhaps depending on Color Scheme?).\r\n\r\nExpected: When you click add, cursor is set to input field of the ENV variable. This indicated where you should put it, and most likely will give a blinking cursor indicating where it is.\r\n\r\n- Strip Whitespace: Accidental whitespace in front / behind ENV variable gets stored. Perhaps it would be better to strip it.\n", "before_files": [{"content": "\"\"\"Misc widgets used in the GUI.\"\"\"\nimport os\n\nfrom gi.repository import Gtk, GObject, Pango\n\nfrom lutris.util.log import logger\nfrom lutris.util import system\nfrom lutris.util.linux import LINUX_SYSTEM\nfrom lutris.gui.widgets.utils import get_stock_icon\n\n\nclass SlugEntry(Gtk.Entry, Gtk.Editable):\n    def __init__(self):\n        super(SlugEntry, self).__init__()\n\n    def do_insert_text(self, new_text, length, position):\n        \"\"\"Filter inserted characters to only accept alphanumeric and dashes\"\"\"\n        new_text = \"\".join([c for c in new_text if c.isalnum() or c == \"-\"]).lower()\n        length = len(new_text)\n        self.get_buffer().insert_text(position, new_text, length)\n        return position + length\n\n\nclass NumberEntry(Gtk.Entry, Gtk.Editable):\n    def __init__(self):\n        super(NumberEntry, self).__init__()\n\n    def do_insert_text(self, new_text, length, position):\n        \"\"\"Filter inserted characters to only accept numbers\"\"\"\n        new_text = \"\".join([c for c in new_text if c.isnumeric()])\n        if new_text:\n            self.get_buffer().insert_text(position, new_text, length)\n            return position + length\n        return position\n\n\nclass FileChooserEntry(Gtk.Box):\n    \"\"\"Editable entry with a file picker button\"\"\"\n\n    max_completion_items = 15  # Maximum number of items to display in the autocompletion dropdown.\n\n    def __init__(\n            self,\n            title=\"Select file\",\n            action=Gtk.FileChooserAction.OPEN,\n            path=None,\n            default_path=None,\n            warn_if_non_empty=False,\n            warn_if_ntfs=False\n    ):\n        super().__init__(\n            orientation=Gtk.Orientation.VERTICAL,\n            spacing=12,\n            visible=True\n        )\n        self.title = title\n        self.action = action\n        self.path = os.path.expanduser(path) if path else None\n        self.default_path = os.path.expanduser(default_path) if default_path else path\n        self.warn_if_non_empty = warn_if_non_empty\n        self.warn_if_ntfs = warn_if_ntfs\n\n        self.path_completion = Gtk.ListStore(str)\n\n        self.entry = Gtk.Entry(visible=True)\n        self.entry.set_completion(self.get_completion())\n        self.entry.connect(\"changed\", self.on_entry_changed)\n        if path:\n            self.entry.set_text(path)\n\n        browse_button = Gtk.Button(\"Browse...\", visible=True)\n        browse_button.connect(\"clicked\", self.on_browse_clicked)\n\n        box = Gtk.Box(spacing=6, visible=True)\n        box.pack_start(self.entry, True, True, 0)\n        box.add(browse_button)\n        self.add(box)\n\n    def get_text(self):\n        \"\"\"Return the entry's text\"\"\"\n        return self.entry.get_text()\n\n    def get_filename(self):\n        \"\"\"Deprecated\"\"\"\n        logger.warning(\"Just use get_text\")\n        return self.get_text()\n\n    def get_completion(self):\n        \"\"\"Return an EntryCompletion widget\"\"\"\n        completion = Gtk.EntryCompletion()\n        completion.set_model(self.path_completion)\n        completion.set_text_column(0)\n        return completion\n\n    def get_filechooser_dialog(self):\n        \"\"\"Return an instance of a FileChooserDialog configured for this widget\"\"\"\n        dialog = Gtk.FileChooserDialog(\n            title=self.title, transient_for=None, action=self.action\n        )\n        dialog.add_buttons(\n            \"_Cancel\", Gtk.ResponseType.CLOSE,\n            \"_OK\", Gtk.ResponseType.OK\n        )\n        dialog.set_create_folders(True)\n        dialog.set_current_folder(self.get_default_folder())\n        dialog.connect(\"response\", self.on_select_file)\n        return dialog\n\n    def get_default_folder(self):\n        \"\"\"Return the default folder for the file picker\"\"\"\n        default_path = self.path or self.default_path or \"\"\n        if not default_path or not system.path_exists(default_path):\n            current_entry = self.get_text()\n            if system.path_exists(current_entry):\n                default_path = current_entry\n        if not os.path.isdir(default_path):\n            default_path = os.path.dirname(default_path)\n        return os.path.expanduser(default_path or \"~\")\n\n    def on_browse_clicked(self, _widget):\n        \"\"\"Browse button click callback\"\"\"\n        file_chooser_dialog = self.get_filechooser_dialog()\n        file_chooser_dialog.run()\n\n    def on_entry_changed(self, widget):\n        \"\"\"Entry changed callback\"\"\"\n        self.clear_warnings()\n        path = widget.get_text()\n        if not path:\n            return\n        path = os.path.expanduser(path)\n        self.update_completion(path)\n        if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == \"ntfs\":\n            ntfs_box = Gtk.Box(spacing=6, visible=True)\n            warning_image = Gtk.Image(visible=True)\n            warning_image.set_from_pixbuf(get_stock_icon(\"dialog-warning\", 32))\n            ntfs_box.add(warning_image)\n            ntfs_label = Gtk.Label(visible=True)\n            ntfs_label.set_markup(\n                \"<b>Warning!</b> The selected path is located on a NTFS drive.\\n\"\n                \"Installing games on NTFS partitions is known to cause issues.\"\n            )\n            ntfs_box.add(ntfs_label)\n            self.pack_end(ntfs_box, False, False, 10)\n        if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):\n            non_empty_label = Gtk.Label(visible=True)\n            non_empty_label.set_markup(\n                \"<b>Warning!</b> The selected path \"\n                \"contains files. Installation might not work properly.\"\n            )\n            self.pack_end(non_empty_label, False, False, 10)\n\n    def on_select_file(self, dialog, response):\n        \"\"\"FileChooserDialog response callback\"\"\"\n        if response == Gtk.ResponseType.OK:\n            target_path = dialog.get_filename()\n            if target_path:\n                dialog.set_current_folder(target_path)\n                self.entry.set_text(system.reverse_expanduser(target_path))\n        dialog.hide()\n\n    def update_completion(self, current_path):\n        \"\"\"Update the auto-completion widget with the current path\"\"\"\n        self.path_completion.clear()\n\n        if not os.path.exists(current_path):\n            current_path, filefilter = os.path.split(current_path)\n        else:\n            filefilter = None\n\n        if os.path.isdir(current_path):\n            index = 0\n            for filename in sorted(os.listdir(current_path)):\n                if filename.startswith(\".\"):\n                    continue\n                if filefilter is not None and not filename.startswith(filefilter):\n                    continue\n                self.path_completion.append([os.path.join(current_path, filename)])\n                index += 1\n                if index > self.max_completion_items:\n                    break\n\n    def clear_warnings(self):\n        \"\"\"Delete all the warning labels from the container\"\"\"\n        for index, child in enumerate(self.get_children()):\n            if index > 0:\n                child.destroy()\n\n\nclass Label(Gtk.Label):\n    \"\"\"Standardised label for config vboxes.\"\"\"\n\n    def __init__(self, message=None):\n        \"\"\"Custom init of label.\"\"\"\n        super().__init__(label=message)\n        self.set_line_wrap(True)\n        self.set_max_width_chars(22)\n        self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)\n        self.set_size_request(230, -1)\n        self.set_alignment(0, 0.5)\n        self.set_justify(Gtk.Justification.LEFT)\n\n\nclass VBox(Gtk.Box):\n    def __init__(self, **kwargs):\n        super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)\n\n\nclass EditableGrid(Gtk.Grid):\n    __gsignals__ = {\"changed\": (GObject.SIGNAL_RUN_FIRST, None, ())}\n\n    def __init__(self, data, columns):\n        self.columns = columns\n        super().__init__()\n        self.set_column_homogeneous(True)\n        self.set_row_homogeneous(True)\n        self.set_row_spacing(10)\n        self.set_column_spacing(10)\n\n        self.liststore = Gtk.ListStore(str, str)\n        for item in data:\n            self.liststore.append([str(value) for value in item])\n\n        self.treeview = Gtk.TreeView.new_with_model(self.liststore)\n        self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)\n        for i, column_title in enumerate(self.columns):\n            renderer = Gtk.CellRendererText()\n            renderer.set_property(\"editable\", True)\n            renderer.connect(\"edited\", self.on_text_edited, i)\n\n            column = Gtk.TreeViewColumn(column_title, renderer, text=i)\n            column.set_resizable(True)\n            column.set_min_width(100)\n            column.set_sort_column_id(0)\n            self.treeview.append_column(column)\n\n        self.buttons = []\n        self.add_button = Gtk.Button(\"Add\")\n        self.buttons.append(self.add_button)\n        self.add_button.connect(\"clicked\", self.on_add)\n\n        self.delete_button = Gtk.Button(\"Delete\")\n        self.buttons.append(self.delete_button)\n        self.delete_button.connect(\"clicked\", self.on_delete)\n\n        self.scrollable_treelist = Gtk.ScrolledWindow()\n        self.scrollable_treelist.set_vexpand(True)\n        self.scrollable_treelist.add(self.treeview)\n\n        self.attach(self.scrollable_treelist, 0, 0, 5, 5)\n        self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)\n        for i, button in enumerate(self.buttons[1:]):\n            self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)\n        self.show_all()\n\n    def on_add(self, widget):\n        self.liststore.append([\"\", \"\"])\n        self.emit(\"changed\")\n\n    def on_delete(self, widget):\n        selection = self.treeview.get_selection()\n        liststore, iter = selection.get_selected()\n        self.liststore.remove(iter)\n        self.emit(\"changed\")\n\n    def on_text_edited(self, widget, path, text, field):\n        self.liststore[path][field] = text\n        self.emit(\"changed\")\n\n    def get_data(self):\n        model_data = []\n        for row in self.liststore:\n            model_data.append([col for col in row])\n        return model_data\n", "path": "lutris/gui/widgets/common.py"}], "after_files": [{"content": "\"\"\"Misc widgets used in the GUI.\"\"\"\nimport os\n\nfrom gi.repository import Gtk, GObject, Pango\n\nfrom lutris.util.log import logger\nfrom lutris.util import system\nfrom lutris.util.linux import LINUX_SYSTEM\nfrom lutris.gui.widgets.utils import get_stock_icon\n\n\nclass SlugEntry(Gtk.Entry, Gtk.Editable):\n    def __init__(self):\n        super(SlugEntry, self).__init__()\n\n    def do_insert_text(self, new_text, length, position):\n        \"\"\"Filter inserted characters to only accept alphanumeric and dashes\"\"\"\n        new_text = \"\".join([c for c in new_text if c.isalnum() or c == \"-\"]).lower()\n        length = len(new_text)\n        self.get_buffer().insert_text(position, new_text, length)\n        return position + length\n\n\nclass NumberEntry(Gtk.Entry, Gtk.Editable):\n    def __init__(self):\n        super(NumberEntry, self).__init__()\n\n    def do_insert_text(self, new_text, length, position):\n        \"\"\"Filter inserted characters to only accept numbers\"\"\"\n        new_text = \"\".join([c for c in new_text if c.isnumeric()])\n        if new_text:\n            self.get_buffer().insert_text(position, new_text, length)\n            return position + length\n        return position\n\n\nclass FileChooserEntry(Gtk.Box):\n    \"\"\"Editable entry with a file picker button\"\"\"\n\n    max_completion_items = 15  # Maximum number of items to display in the autocompletion dropdown.\n\n    def __init__(\n            self,\n            title=\"Select file\",\n            action=Gtk.FileChooserAction.OPEN,\n            path=None,\n            default_path=None,\n            warn_if_non_empty=False,\n            warn_if_ntfs=False\n    ):\n        super().__init__(\n            orientation=Gtk.Orientation.VERTICAL,\n            spacing=12,\n            visible=True\n        )\n        self.title = title\n        self.action = action\n        self.path = os.path.expanduser(path) if path else None\n        self.default_path = os.path.expanduser(default_path) if default_path else path\n        self.warn_if_non_empty = warn_if_non_empty\n        self.warn_if_ntfs = warn_if_ntfs\n\n        self.path_completion = Gtk.ListStore(str)\n\n        self.entry = Gtk.Entry(visible=True)\n        self.entry.set_completion(self.get_completion())\n        self.entry.connect(\"changed\", self.on_entry_changed)\n        if path:\n            self.entry.set_text(path)\n\n        browse_button = Gtk.Button(\"Browse...\", visible=True)\n        browse_button.connect(\"clicked\", self.on_browse_clicked)\n\n        box = Gtk.Box(spacing=6, visible=True)\n        box.pack_start(self.entry, True, True, 0)\n        box.add(browse_button)\n        self.add(box)\n\n    def get_text(self):\n        \"\"\"Return the entry's text\"\"\"\n        return self.entry.get_text()\n\n    def get_filename(self):\n        \"\"\"Deprecated\"\"\"\n        logger.warning(\"Just use get_text\")\n        return self.get_text()\n\n    def get_completion(self):\n        \"\"\"Return an EntryCompletion widget\"\"\"\n        completion = Gtk.EntryCompletion()\n        completion.set_model(self.path_completion)\n        completion.set_text_column(0)\n        return completion\n\n    def get_filechooser_dialog(self):\n        \"\"\"Return an instance of a FileChooserDialog configured for this widget\"\"\"\n        dialog = Gtk.FileChooserDialog(\n            title=self.title, transient_for=None, action=self.action\n        )\n        dialog.add_buttons(\n            \"_Cancel\", Gtk.ResponseType.CLOSE,\n            \"_OK\", Gtk.ResponseType.OK\n        )\n        dialog.set_create_folders(True)\n        dialog.set_current_folder(self.get_default_folder())\n        dialog.connect(\"response\", self.on_select_file)\n        return dialog\n\n    def get_default_folder(self):\n        \"\"\"Return the default folder for the file picker\"\"\"\n        default_path = self.path or self.default_path or \"\"\n        if not default_path or not system.path_exists(default_path):\n            current_entry = self.get_text()\n            if system.path_exists(current_entry):\n                default_path = current_entry\n        if not os.path.isdir(default_path):\n            default_path = os.path.dirname(default_path)\n        return os.path.expanduser(default_path or \"~\")\n\n    def on_browse_clicked(self, _widget):\n        \"\"\"Browse button click callback\"\"\"\n        file_chooser_dialog = self.get_filechooser_dialog()\n        file_chooser_dialog.run()\n\n    def on_entry_changed(self, widget):\n        \"\"\"Entry changed callback\"\"\"\n        self.clear_warnings()\n        path = widget.get_text()\n        if not path:\n            return\n        path = os.path.expanduser(path)\n        self.update_completion(path)\n        if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == \"ntfs\":\n            ntfs_box = Gtk.Box(spacing=6, visible=True)\n            warning_image = Gtk.Image(visible=True)\n            warning_image.set_from_pixbuf(get_stock_icon(\"dialog-warning\", 32))\n            ntfs_box.add(warning_image)\n            ntfs_label = Gtk.Label(visible=True)\n            ntfs_label.set_markup(\n                \"<b>Warning!</b> The selected path is located on a NTFS drive.\\n\"\n                \"Installing games on NTFS partitions is known to cause issues.\"\n            )\n            ntfs_box.add(ntfs_label)\n            self.pack_end(ntfs_box, False, False, 10)\n        if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):\n            non_empty_label = Gtk.Label(visible=True)\n            non_empty_label.set_markup(\n                \"<b>Warning!</b> The selected path \"\n                \"contains files. Installation might not work properly.\"\n            )\n            self.pack_end(non_empty_label, False, False, 10)\n\n    def on_select_file(self, dialog, response):\n        \"\"\"FileChooserDialog response callback\"\"\"\n        if response == Gtk.ResponseType.OK:\n            target_path = dialog.get_filename()\n            if target_path:\n                dialog.set_current_folder(target_path)\n                self.entry.set_text(system.reverse_expanduser(target_path))\n        dialog.hide()\n\n    def update_completion(self, current_path):\n        \"\"\"Update the auto-completion widget with the current path\"\"\"\n        self.path_completion.clear()\n\n        if not os.path.exists(current_path):\n            current_path, filefilter = os.path.split(current_path)\n        else:\n            filefilter = None\n\n        if os.path.isdir(current_path):\n            index = 0\n            for filename in sorted(os.listdir(current_path)):\n                if filename.startswith(\".\"):\n                    continue\n                if filefilter is not None and not filename.startswith(filefilter):\n                    continue\n                self.path_completion.append([os.path.join(current_path, filename)])\n                index += 1\n                if index > self.max_completion_items:\n                    break\n\n    def clear_warnings(self):\n        \"\"\"Delete all the warning labels from the container\"\"\"\n        for index, child in enumerate(self.get_children()):\n            if index > 0:\n                child.destroy()\n\n\nclass Label(Gtk.Label):\n    \"\"\"Standardised label for config vboxes.\"\"\"\n\n    def __init__(self, message=None):\n        \"\"\"Custom init of label.\"\"\"\n        super().__init__(label=message)\n        self.set_line_wrap(True)\n        self.set_max_width_chars(22)\n        self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)\n        self.set_size_request(230, -1)\n        self.set_alignment(0, 0.5)\n        self.set_justify(Gtk.Justification.LEFT)\n\n\nclass VBox(Gtk.Box):\n    def __init__(self, **kwargs):\n        super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)\n\n\nclass EditableGrid(Gtk.Grid):\n    __gsignals__ = {\"changed\": (GObject.SIGNAL_RUN_FIRST, None, ())}\n\n    def __init__(self, data, columns):\n        self.columns = columns\n        super().__init__()\n        self.set_column_homogeneous(True)\n        self.set_row_homogeneous(True)\n        self.set_row_spacing(10)\n        self.set_column_spacing(10)\n\n        self.liststore = Gtk.ListStore(str, str)\n        for item in data:\n            self.liststore.append([str(value) for value in item])\n\n        self.treeview = Gtk.TreeView.new_with_model(self.liststore)\n        self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)\n        for i, column_title in enumerate(self.columns):\n            renderer = Gtk.CellRendererText()\n            renderer.set_property(\"editable\", True)\n            renderer.connect(\"edited\", self.on_text_edited, i)\n\n            column = Gtk.TreeViewColumn(column_title, renderer, text=i)\n            column.set_resizable(True)\n            column.set_min_width(100)\n            column.set_sort_column_id(0)\n            self.treeview.append_column(column)\n\n        self.buttons = []\n        self.add_button = Gtk.Button(\"Add\")\n        self.buttons.append(self.add_button)\n        self.add_button.connect(\"clicked\", self.on_add)\n\n        self.delete_button = Gtk.Button(\"Delete\")\n        self.buttons.append(self.delete_button)\n        self.delete_button.connect(\"clicked\", self.on_delete)\n\n        self.scrollable_treelist = Gtk.ScrolledWindow()\n        self.scrollable_treelist.set_vexpand(True)\n        self.scrollable_treelist.add(self.treeview)\n\n        self.attach(self.scrollable_treelist, 0, 0, 5, 5)\n        self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)\n        for i, button in enumerate(self.buttons[1:]):\n            self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)\n        self.show_all()\n\n    def on_add(self, widget):\n        self.liststore.append([\"\", \"\"])\n        row_position = len(self.liststore)-1\n        self.treeview.set_cursor(row_position, None, False)\n        self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)\n        self.emit(\"changed\")\n\n    def on_delete(self, widget):\n        selection = self.treeview.get_selection()\n        liststore, iter = selection.get_selected()\n        self.liststore.remove(iter)\n        self.emit(\"changed\")\n\n    def on_text_edited(self, widget, path, text, field):\n        self.liststore[path][field] = text.strip()\n        self.emit(\"changed\")\n\n    def get_data(self):\n        model_data = []\n        for row in self.liststore:\n            model_data.append([col for col in row])\n        return model_data\n", "path": "lutris/gui/widgets/common.py"}]}