pygpt-net 2.6.67__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +182 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/painter.py +2 -2
  19. pygpt_net/controller/presets/experts.py +68 -15
  20. pygpt_net/controller/presets/presets.py +72 -36
  21. pygpt_net/controller/settings/profile.py +76 -35
  22. pygpt_net/controller/settings/workdir.py +70 -39
  23. pygpt_net/core/assistants/files.py +20 -18
  24. pygpt_net/core/filesystem/actions.py +111 -10
  25. pygpt_net/core/filesystem/filesystem.py +2 -1
  26. pygpt_net/core/idx/idx.py +12 -11
  27. pygpt_net/core/idx/worker.py +13 -1
  28. pygpt_net/core/models/models.py +4 -4
  29. pygpt_net/core/profile/profile.py +13 -3
  30. pygpt_net/data/config/config.json +3 -3
  31. pygpt_net/data/config/models.json +3 -3
  32. pygpt_net/data/css/style.dark.css +39 -1
  33. pygpt_net/data/css/style.light.css +39 -1
  34. pygpt_net/data/locale/locale.de.ini +3 -1
  35. pygpt_net/data/locale/locale.en.ini +3 -1
  36. pygpt_net/data/locale/locale.es.ini +3 -1
  37. pygpt_net/data/locale/locale.fr.ini +3 -1
  38. pygpt_net/data/locale/locale.it.ini +3 -1
  39. pygpt_net/data/locale/locale.pl.ini +4 -2
  40. pygpt_net/data/locale/locale.uk.ini +3 -1
  41. pygpt_net/data/locale/locale.zh.ini +3 -1
  42. pygpt_net/provider/api/openai/__init__.py +4 -2
  43. pygpt_net/provider/core/config/patch.py +9 -1
  44. pygpt_net/tools/image_viewer/tool.py +17 -0
  45. pygpt_net/tools/text_editor/tool.py +9 -0
  46. pygpt_net/ui/__init__.py +2 -2
  47. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  48. pygpt_net/ui/main.py +3 -1
  49. pygpt_net/ui/widget/calendar/select.py +3 -3
  50. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  51. pygpt_net/ui/widget/lists/assistant.py +185 -24
  52. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  53. pygpt_net/ui/widget/lists/attachment.py +230 -47
  54. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  55. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  56. pygpt_net/ui/widget/lists/context.py +1253 -70
  57. pygpt_net/ui/widget/lists/experts.py +110 -8
  58. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  59. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  60. pygpt_net/ui/widget/lists/preset.py +460 -71
  61. pygpt_net/ui/widget/lists/profile.py +149 -27
  62. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  63. pygpt_net/ui/widget/option/combo.py +1046 -32
  64. pygpt_net/ui/widget/option/dictionary.py +35 -7
  65. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +14 -57
  66. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +69 -69
  67. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  68. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  69. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,14 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.05 18:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
13
  import os
14
14
  from typing import List, Dict, Optional, Any
15
15
 
16
+ from PySide6 import QtCore
16
17
  from PySide6.QtWidgets import QApplication
17
18
 
18
19
  from pygpt_net.core.events import Event
@@ -45,8 +46,10 @@ class Importer:
45
46
  self.items_current = {} # current models in use
46
47
  self.pending = {} # waiting to be imported models
47
48
  self.removed = {} # waiting to be removed models
48
- self.selected_available = None # selected available model
49
- self.selected_current = None # selected current model
49
+ self.selected_available = None # selected available model (single)
50
+ self.selected_current = None # selected current model (single)
51
+ self.selected_available_ids: List[str] = [] # multi-selected available models
52
+ self.selected_current_ids: List[str] = [] # multi-selected current models
50
53
  self.all = False # show all models, not only available for import
51
54
  self.provider = "_" # default provider
52
55
 
@@ -69,43 +72,71 @@ class Importer:
69
72
 
70
73
  def change_available(self):
71
74
  """On change available model selection"""
72
- val = self.window.ui.nodes["models.importer.available"].selectionModel().currentIndex()
73
- idx = val.row()
74
- if idx < 0:
75
+ view = self.window.ui.nodes["models.importer.available"]
76
+ sel_model = view.selectionModel()
77
+ rows = sel_model.selectedRows() if sel_model else []
78
+ # collect selected model keys by reading stored tooltip role (stable ID)
79
+ selected_ids = []
80
+ for ix in rows:
81
+ key = ix.data(QtCore.Qt.ToolTipRole)
82
+ if not key:
83
+ key = self.get_by_idx(ix.row(), self.items_available)
84
+ if key and key not in selected_ids:
85
+ selected_ids.append(key)
86
+
87
+ self.selected_available_ids = selected_ids
88
+ if not selected_ids:
75
89
  self.selected_available = None
76
90
  self.window.ui.nodes["models.importer.add"].setEnabled(False)
77
- else:
78
- self.selected_available = self.get_by_idx(idx, self.items_available)
79
- if self.items_available.get(self.selected_available) is None:
80
- self.selected_available = None
81
- self.window.ui.nodes["models.importer.add"].setEnabled(False)
82
- else:
83
- # if not in current then enable add button
84
- if not self.in_current(self.selected_available):
85
- self.window.ui.nodes["models.importer.add"].setEnabled(True)
86
- else:
87
- self.window.ui.nodes["models.importer.add"].setEnabled(False)
91
+ return
92
+
93
+ # keep a single reference for backward compatibility (last selected)
94
+ self.selected_available = selected_ids[-1]
95
+
96
+ # enable add if any of selected can be added (not already in current)
97
+ can_add = False
98
+ for key in selected_ids:
99
+ if key is None:
100
+ continue
101
+ if not self.in_current(key) and self.items_available.get(key) is not None:
102
+ can_add = True
103
+ break
104
+ self.window.ui.nodes["models.importer.add"].setEnabled(can_add)
88
105
 
89
106
  def change_current(self):
90
107
  """On change current model selection"""
91
108
  if self.provider not in self.items_current:
92
109
  self.items_current[self.provider] = {}
93
- val = self.window.ui.nodes["models.importer.current"].selectionModel().currentIndex()
94
- idx = val.row()
95
- if idx < 0:
110
+ view = self.window.ui.nodes["models.importer.current"]
111
+ sel_model = view.selectionModel()
112
+ rows = sel_model.selectedRows() if sel_model else []
113
+ # collect selected model keys by reading stored tooltip role (stable ID)
114
+ selected_ids = []
115
+ for ix in rows:
116
+ key = ix.data(QtCore.Qt.ToolTipRole)
117
+ if not key:
118
+ key = self.get_by_idx(ix.row(), self.items_current[self.provider])
119
+ if key and key not in selected_ids:
120
+ selected_ids.append(key)
121
+
122
+ self.selected_current_ids = selected_ids
123
+ if not selected_ids:
96
124
  self.selected_current = None
97
125
  self.window.ui.nodes["models.importer.remove"].setEnabled(False)
98
- else:
99
- self.selected_current = self.get_by_idx(idx, self.items_current[self.provider])
100
- if self.items_current[self.provider].get(self.selected_current) is None:
101
- self.selected_current = None
102
- self.window.ui.nodes["models.importer.remove"].setEnabled(False)
103
- else:
104
- if (self.selected_current in self.items_current[self.provider]
105
- and self.in_available(self.selected_current)):
106
- self.window.ui.nodes["models.importer.remove"].setEnabled(True)
107
- else:
108
- self.window.ui.nodes["models.importer.remove"].setEnabled(False)
126
+ return
127
+
128
+ # keep a single reference for backward compatibility (last selected)
129
+ self.selected_current = selected_ids[-1]
130
+
131
+ # enable remove if any of selected can be removed
132
+ can_remove = False
133
+ for key in selected_ids:
134
+ if key is None:
135
+ continue
136
+ if (key in self.items_current[self.provider]) and self.in_available(key):
137
+ can_remove = True
138
+ break
139
+ self.window.ui.nodes["models.importer.remove"].setEnabled(can_remove)
109
140
 
110
141
  def in_available(self, model: str) -> bool:
111
142
  """
@@ -122,43 +153,111 @@ class Importer:
122
153
  return True
123
154
  return False
124
155
 
156
+ def _selected_keys_from_view(self, node_id: str, items: Dict) -> List[str]:
157
+ """
158
+ Resolve selected model keys from a given list view using stored tooltip role,
159
+ falling back to index->dict mapping when needed.
160
+
161
+ :param node_id: ui node id
162
+ :param items: dict used to build the view's model
163
+ :return: list of selected keys
164
+ """
165
+ keys: List[str] = []
166
+ try:
167
+ view = self.window.ui.nodes[node_id]
168
+ sel_model = view.selectionModel()
169
+ rows = sel_model.selectedRows() if sel_model else []
170
+ for ix in rows:
171
+ key = ix.data(QtCore.Qt.ToolTipRole)
172
+ if not key:
173
+ key = self.get_by_idx(ix.row(), items)
174
+ if key and key not in keys:
175
+ keys.append(key)
176
+ except Exception:
177
+ pass
178
+ return keys
179
+
125
180
  def add(self):
126
- """Add model to current list"""
181
+ """Add model(s) to current list"""
127
182
  if self.provider not in self.items_current:
128
183
  self.items_current[self.provider] = {}
129
- if self.selected_available is None:
184
+
185
+ # collect multi-selection; fallback to single selection
186
+ keys = self._selected_keys_from_view(
187
+ 'models.importer.available',
188
+ self.items_available,
189
+ )
190
+ if not keys and self.selected_available is not None:
191
+ keys = [self.selected_available]
192
+
193
+ if not keys:
130
194
  self.set_status(trans('models.importer.error.add.no_model'))
131
195
  return
132
- if self.in_current(self.selected_available):
196
+
197
+ any_added = False
198
+ for key in list(keys):
199
+ if key is None:
200
+ continue
201
+ if self.in_current(key):
202
+ continue
203
+ model = self.items_available.get(key)
204
+ if model is None:
205
+ continue
206
+ self.items_current[self.provider][key] = model
207
+ if key not in self.pending:
208
+ self.pending[key] = model
209
+ if key in self.removed:
210
+ del self.removed[key]
211
+ if not self.all and key in self.items_available:
212
+ del self.items_available[key]
213
+ any_added = True
214
+
215
+ if not any_added:
133
216
  self.set_status(trans('models.importer.error.add.not_exists'))
134
217
  return
135
- model = self.items_available[self.selected_available]
136
- self.items_current[self.provider][self.selected_available] = model
137
- if self.selected_available not in self.pending:
138
- self.pending[self.selected_available] = model
139
- if self.selected_available in self.removed:
140
- del self.removed[self.selected_available]
141
- if not self.all:
142
- del self.items_available[self.selected_available]
218
+
143
219
  self.refresh()
144
220
 
145
221
  def remove(self):
146
- """Remove model from current list"""
222
+ """Remove model(s) from current list"""
147
223
  if self.provider not in self.items_current:
148
224
  self.items_current[self.provider] = {}
149
- if self.selected_current is None:
225
+
226
+ # collect multi-selection; fallback to single selection
227
+ keys = self._selected_keys_from_view(
228
+ 'models.importer.current',
229
+ self.items_current[self.provider],
230
+ )
231
+ if not keys and self.selected_current is not None:
232
+ keys = [self.selected_current]
233
+
234
+ if not keys:
150
235
  self.set_status(trans('models.importer.error.remove.no_model'))
151
236
  return
152
- if not self.in_current(self.selected_current):
237
+
238
+ any_removed = False
239
+ for key in list(keys):
240
+ if key is None:
241
+ continue
242
+ if not self.in_current(key):
243
+ continue
244
+ model = self.items_current[self.provider].get(key)
245
+ if model is None:
246
+ continue
247
+ # return to available list
248
+ self.items_available[key] = model
249
+ if key not in self.removed:
250
+ self.removed[key] = model
251
+ if key in self.items_current[self.provider]:
252
+ del self.items_current[self.provider][key]
253
+ if key in self.pending:
254
+ del self.pending[key]
255
+ any_removed = True
256
+
257
+ if not any_removed:
153
258
  self.set_status(trans('models.importer.error.remove.not_exists'))
154
259
  return
155
- model = self.items_current[self.provider][self.selected_current]
156
- self.items_available[self.selected_current] = model
157
- if self.selected_current not in self.removed:
158
- self.removed[self.selected_current] = model
159
- del self.items_current[self.provider][self.selected_current]
160
- if self.selected_current in self.pending:
161
- del self.pending[self.selected_current]
260
+
162
261
  self.refresh()
163
262
 
164
263
  def setup(self):
@@ -328,7 +427,7 @@ class Importer:
328
427
  name = model.get('id')
329
428
  if "name" in model:
330
429
  name = model.get('name')
331
- m = self.window.core.models.create_empty(append=False)
430
+ m, _ = self.window.core.models.create_empty(append=False)
332
431
  m.id = id
333
432
  m.name = name
334
433
  m.mode = [
@@ -504,7 +603,7 @@ class Importer:
504
603
  else:
505
604
  for model in ollama_models:
506
605
  name = model.get('name')
507
- m = self.window.core.models.create_empty(append=False)
606
+ m, _ = self.window.core.models.create_empty(append=False)
508
607
  m.id = name
509
608
  m.name = name
510
609
  m.mode = [
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.02 15:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -53,7 +53,7 @@ class Painter:
53
53
  """
54
54
  self.open(path)
55
55
  if not self.is_active():
56
- self.window.controller.ui.switch_tab('draw')
56
+ self.window.controller.ui.tabs.switch_tab(Tab.TAB_TOOL_PAINTER)
57
57
 
58
58
  def save(self):
59
59
  """Store current image"""
@@ -6,9 +6,11 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.14 13:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ from PySide6 import QtCore
13
+
12
14
  from pygpt_net.core.types import (
13
15
  MODE_EXPERT,
14
16
  )
@@ -23,6 +25,8 @@ class Experts:
23
25
  :param window: Window instance
24
26
  """
25
27
  self.window = window
28
+ self.selected_available_uuids = [] # multi-selected available experts
29
+ self.selected_selected_uuids = [] # multi-selected selected experts
26
30
 
27
31
  def refresh(self):
28
32
  """Refresh presets"""
@@ -78,13 +82,43 @@ class Experts:
78
82
 
79
83
  self.update_tab()
80
84
 
85
+ def _selected_uuids_from_view(self, node_id: str, fallback_idx_resolver) -> list[str]:
86
+ """
87
+ Resolve selected expert UUIDs from a given list view using stored tooltip role,
88
+ falling back to idx -> uuid resolver when needed.
89
+
90
+ :param node_id: ui node id
91
+ :param fallback_idx_resolver: callable that maps row index -> uuid
92
+ :return: list of selected uuids
93
+ """
94
+ uuids = []
95
+ try:
96
+ view = self.window.ui.nodes[node_id]
97
+ sel_model = view.selectionModel()
98
+ rows = sel_model.selectedRows() if sel_model else []
99
+ for ix in rows:
100
+ uuid = ix.data(QtCore.Qt.ToolTipRole)
101
+ if not uuid:
102
+ uuid = fallback_idx_resolver(ix.row())
103
+ if uuid and uuid not in uuids:
104
+ uuids.append(uuid)
105
+ except Exception:
106
+ pass
107
+ return uuids
108
+
81
109
  def change_available(self):
82
- """Change selected expert"""
83
- pass
110
+ """Change selected expert(s) in available list"""
111
+ self.selected_available_uuids = self._selected_uuids_from_view(
112
+ "preset.experts.available",
113
+ self.get_available_by_idx
114
+ )
84
115
 
85
116
  def change_selected(self):
86
- """Change selected expert"""
87
- pass
117
+ """Change selected expert(s) in selected list"""
118
+ self.selected_selected_uuids = self._selected_uuids_from_view(
119
+ "preset.experts.selected",
120
+ self.get_selected_by_idx
121
+ )
88
122
 
89
123
  def get_current_available(self) -> str:
90
124
  """
@@ -138,27 +172,47 @@ class Experts:
138
172
  i += 1
139
173
 
140
174
  def add_expert(self):
141
- """Add expert"""
175
+ """Add expert(s)"""
142
176
  agent_uuid = self.get_current_agent_id()
143
177
  if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
144
178
  self.window.controller.presets.editor.save(close=False)
145
179
  return
146
- expert_uuid = self.get_current_available()
147
- if expert_uuid is None:
180
+
181
+ uuids = self.selected_available_uuids[:]
182
+ if not uuids:
183
+ one = self.get_current_available()
184
+ if one:
185
+ uuids = [one]
186
+
187
+ if not uuids:
148
188
  return
149
- self.window.core.presets.add_expert(agent_uuid, expert_uuid)
189
+
190
+ for expert_uuid in uuids:
191
+ if expert_uuid and not self.is_active(expert_uuid):
192
+ self.window.core.presets.add_expert(agent_uuid, expert_uuid)
193
+
150
194
  self.update_list()
151
195
 
152
196
  def remove_expert(self):
153
- """Remove expert"""
197
+ """Remove expert(s)"""
154
198
  agent_uuid = self.get_current_agent_id()
155
199
  if agent_uuid is None or not self.window.core.presets.exists_uuid(agent_uuid):
156
200
  self.window.controller.presets.editor.save(close=False)
157
201
  return
158
- expert_uuid = self.get_current_selected()
159
- if expert_uuid is None:
202
+
203
+ uuids = self.selected_selected_uuids[:]
204
+ if not uuids:
205
+ one = self.get_current_selected()
206
+ if one:
207
+ uuids = [one]
208
+
209
+ if not uuids:
160
210
  return
161
- self.window.core.presets.remove_expert(agent_uuid, expert_uuid)
211
+
212
+ for expert_uuid in uuids:
213
+ if expert_uuid and self.is_active(expert_uuid):
214
+ self.window.core.presets.remove_expert(agent_uuid, expert_uuid)
215
+
162
216
  self.update_list()
163
217
 
164
218
  def update_tab(self):
@@ -174,5 +228,4 @@ class Experts:
174
228
  if num == 0:
175
229
  tabs.setTabText(idx, trans("preset.tab.experts"))
176
230
  else:
177
- tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")
178
-
231
+ tabs.setTabText(idx, trans("preset.tab.experts") + f" ({num})")
@@ -6,11 +6,11 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.12.25 20:00:00 #
9
+ # Updated Date: 2025.12.27 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
13
- from typing import Optional, List, Dict
13
+ from typing import Optional, List, Dict, Union
14
14
 
15
15
  from PySide6.QtCore import QTimer
16
16
  from PySide6.QtGui import QTextCursor
@@ -536,25 +536,39 @@ class Presets:
536
536
  """
537
537
  return _FILENAME_SANITIZE_RE.sub('_', name.lower())
538
538
 
539
- def duplicate(self, idx: Optional[int] = None):
539
+ def duplicate(self, idx: Optional[Union[int, list]] = None):
540
540
  """
541
541
  Duplicate preset
542
542
 
543
- :param idx: preset index (row index)
543
+ :param idx: preset ID (or list of IDs)
544
544
  """
545
- if idx is not None:
546
- w = self.window
547
- mode = w.core.config.get('mode')
548
- preset = w.core.presets.get_by_idx(idx, mode)
549
- if preset:
550
- if preset in w.core.presets.items:
551
- new_id = w.core.presets.duplicate(preset)
552
- self.update_list()
553
- self.refresh(no_scroll=True)
554
- idx = w.core.presets.get_idx_by_id(mode, new_id)
555
- self.editor.edit(idx)
556
- self.select(idx) # switch to the new preset
557
- w.update_status(trans('status.preset.duplicated'))
545
+ ids = idx if isinstance(idx, list) else [idx]
546
+ last_id = None
547
+ updated = False
548
+ w = self.window
549
+ mode = w.core.config.get('mode')
550
+ for idx in ids:
551
+ if idx is not None:
552
+ if len(ids) > 1:
553
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
554
+ preset_id = preset.filename if preset else None
555
+ else:
556
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
557
+ preset_id = preset
558
+ if preset_id:
559
+ new_id = w.core.presets.duplicate(preset_id)
560
+ last_id = new_id
561
+ updated = True
562
+
563
+ if updated:
564
+ self.update_list()
565
+ self.refresh(no_scroll=True)
566
+ if len(ids) == 1 and idx is not None:
567
+ idx = w.core.presets.get_idx_by_id(mode, last_id)
568
+ self.editor.edit(idx)
569
+ self.select(idx) # switch to the new preset if only one was duplicated
570
+ w.update_status(trans('status.preset.duplicated'))
571
+
558
572
 
559
573
  def enable(self, idx: Optional[int] = None):
560
574
  """
@@ -654,30 +668,49 @@ class Presets:
654
668
 
655
669
  def delete(
656
670
  self,
657
- idx: Optional[int] = None,
671
+ idx: Optional[Union[int, list]] = None,
658
672
  force: bool = False
659
673
  ):
660
674
  """
661
675
  Delete preset
662
676
 
663
- :param idx: preset index (row index)
677
+ :param idx: preset index (row index) or list of indices
664
678
  :param force: force delete without confirmation
665
679
  """
666
- if idx is not None:
667
- w = self.window
668
- mode = w.core.config.get('mode')
669
- preset_id = w.core.presets.get_by_idx(idx, mode)
670
- if preset_id and preset_id in w.core.presets.items:
671
- if not force:
672
- w.ui.dialogs.confirm(
673
- type='preset_delete',
674
- id=idx,
675
- msg=trans('confirm.preset.delete'),
676
- )
677
- return
678
-
679
- # Determine neighbor only if the deleted preset is currently active.
680
- # This keeps API semantics untouched and prevents unexpected selection changes.
680
+ ids = idx if isinstance(idx, list) else [idx]
681
+ w = self.window
682
+ mode = w.core.config.get('mode')
683
+ is_preset = False
684
+ updated = False
685
+ for tmp_id in ids:
686
+ if len(ids) > 1:
687
+ preset = w.core.presets.get_by_id(mode, tmp_id) # by ID, PresetItem
688
+ else:
689
+ preset = w.core.presets.get_by_idx(tmp_id, mode) # by index, str
690
+ if preset:
691
+ is_preset = True
692
+ break
693
+
694
+ if ids and is_preset:
695
+ if not force:
696
+ w.ui.dialogs.confirm(
697
+ type='preset_delete',
698
+ id=idx,
699
+ msg=trans('confirm.preset.delete'),
700
+ )
701
+ return
702
+
703
+ # Determine neighbor only if the deleted preset is currently active.
704
+ # This keeps API semantics untouched and prevents unexpected selection changes.
705
+ for idx in ids:
706
+ if len(ids) > 1:
707
+ preset = w.core.presets.get_by_id(mode, idx) # by ID, PresetItem
708
+ preset_id = preset.filename if preset else None
709
+ else:
710
+ preset = w.core.presets.get_by_idx(idx, mode) # by index, str
711
+ preset_id = preset
712
+ if not preset:
713
+ continue
681
714
  is_current = (preset_id == w.core.config.get('preset'))
682
715
  target_id = None
683
716
  if is_current:
@@ -700,8 +733,11 @@ class Presets:
700
733
  w.core.config.set('preset', None)
701
734
  w.ui.nodes['preset.prompt'].setPlainText("")
702
735
 
703
- self.refresh(no_scroll=True)
704
- w.update_status(trans('status.preset.deleted'))
736
+ updated = True
737
+
738
+ if updated:
739
+ self.refresh(no_scroll=True)
740
+ w.update_status(trans('status.preset.deleted'))
705
741
 
706
742
  def restore(self, force: bool = False):
707
743
  """