pygpt-net 2.6.15__py3-none-any.whl → 2.6.17__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 (57) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/__init__.py +8 -2
  4. pygpt_net/controller/chat/command.py +18 -6
  5. pygpt_net/controller/ctx/ctx.py +2 -2
  6. pygpt_net/controller/mode/mode.py +3 -2
  7. pygpt_net/controller/plugins/plugins.py +31 -15
  8. pygpt_net/controller/presets/editor.py +11 -32
  9. pygpt_net/controller/settings/profile.py +16 -3
  10. pygpt_net/controller/settings/workdir.py +184 -124
  11. pygpt_net/controller/theme/theme.py +11 -5
  12. pygpt_net/core/agents/observer/evaluation.py +3 -14
  13. pygpt_net/core/agents/runners/llama_workflow.py +7 -6
  14. pygpt_net/core/command/command.py +5 -3
  15. pygpt_net/core/experts/experts.py +58 -13
  16. pygpt_net/core/plugins/plugins.py +12 -1
  17. pygpt_net/core/render/plain/body.py +10 -19
  18. pygpt_net/core/render/plain/renderer.py +27 -27
  19. pygpt_net/data/config/config.json +6 -6
  20. pygpt_net/data/config/models.json +3 -3
  21. pygpt_net/data/locale/locale.en.ini +2 -2
  22. pygpt_net/data/locale/plugin.openai_dalle.de.ini +1 -1
  23. pygpt_net/data/locale/plugin.openai_dalle.en.ini +1 -1
  24. pygpt_net/data/locale/plugin.openai_dalle.es.ini +1 -1
  25. pygpt_net/data/locale/plugin.openai_dalle.fr.ini +1 -1
  26. pygpt_net/data/locale/plugin.openai_dalle.it.ini +1 -1
  27. pygpt_net/data/locale/plugin.openai_dalle.pl.ini +1 -1
  28. pygpt_net/data/locale/plugin.openai_dalle.uk.ini +1 -1
  29. pygpt_net/data/locale/plugin.openai_dalle.zh.ini +1 -1
  30. pygpt_net/data/locale/plugin.openai_vision.de.ini +1 -1
  31. pygpt_net/data/locale/plugin.openai_vision.en.ini +1 -1
  32. pygpt_net/data/locale/plugin.openai_vision.es.ini +1 -1
  33. pygpt_net/data/locale/plugin.openai_vision.fr.ini +1 -1
  34. pygpt_net/data/locale/plugin.openai_vision.it.ini +1 -1
  35. pygpt_net/data/locale/plugin.openai_vision.pl.ini +1 -1
  36. pygpt_net/data/locale/plugin.openai_vision.uk.ini +1 -1
  37. pygpt_net/data/locale/plugin.openai_vision.zh.ini +1 -1
  38. pygpt_net/item/ctx.py +5 -4
  39. pygpt_net/plugin/idx_llama_index/plugin.py +9 -5
  40. pygpt_net/plugin/idx_llama_index/worker.py +5 -2
  41. pygpt_net/plugin/openai_dalle/plugin.py +1 -1
  42. pygpt_net/tools/translator/ui/dialogs.py +1 -0
  43. pygpt_net/tools/translator/ui/widgets.py +1 -2
  44. pygpt_net/ui/__init__.py +12 -10
  45. pygpt_net/ui/base/config_dialog.py +15 -10
  46. pygpt_net/ui/dialog/about.py +26 -18
  47. pygpt_net/ui/dialog/plugins.py +6 -4
  48. pygpt_net/ui/dialog/settings.py +75 -87
  49. pygpt_net/ui/dialog/workdir.py +7 -2
  50. pygpt_net/ui/main.py +5 -1
  51. pygpt_net/ui/widget/textarea/editor.py +1 -2
  52. pygpt_net/ui/widget/textarea/web.py +22 -16
  53. {pygpt_net-2.6.15.dist-info → pygpt_net-2.6.17.dist-info}/METADATA +26 -14
  54. {pygpt_net-2.6.15.dist-info → pygpt_net-2.6.17.dist-info}/RECORD +57 -57
  55. {pygpt_net-2.6.15.dist-info → pygpt_net-2.6.17.dist-info}/LICENSE +0 -0
  56. {pygpt_net-2.6.15.dist-info → pygpt_net-2.6.17.dist-info}/WHEEL +0 -0
  57. {pygpt_net-2.6.15.dist-info → pygpt_net-2.6.17.dist-info}/entry_points.txt +0 -0
pygpt_net/item/ctx.py CHANGED
@@ -13,6 +13,7 @@ import copy
13
13
  import datetime
14
14
  import json
15
15
  import time
16
+ from typing import Optional
16
17
 
17
18
 
18
19
  class CtxItem:
@@ -94,7 +95,7 @@ class CtxItem:
94
95
 
95
96
 
96
97
  @property
97
- def final_input(self) -> str:
98
+ def final_input(self) -> Optional[str]:
98
99
  """
99
100
  Final input
100
101
 
@@ -103,11 +104,11 @@ class CtxItem:
103
104
  if self.input is None:
104
105
  return None
105
106
  if self.hidden_input:
106
- return "\n\n".join(self.input, self.hidden_input)
107
+ return "\n\n".join([self.input, self.hidden_input])
107
108
  return self.input
108
109
 
109
110
  @property
110
- def final_output(self) -> str:
111
+ def final_output(self) -> Optional[str]:
111
112
  """
112
113
  Final output
113
114
 
@@ -116,7 +117,7 @@ class CtxItem:
116
117
  if self.output is None:
117
118
  return None
118
119
  if self.hidden_output:
119
- return "\n\n".join(self.output, self.hidden_output)
120
+ return "\n\n".join([self.output, self.hidden_output])
120
121
  return self.output
121
122
 
122
123
  def clear_reply(self):
@@ -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.08.15 23:00:00 #
9
+ # Updated Date: 2025.08.20 09:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -174,14 +174,16 @@ class Plugin(BasePlugin):
174
174
  prepared_question = response
175
175
  return prepared_question
176
176
 
177
- def get_from_retrieval(self, query: str) -> str:
177
+ def get_from_retrieval(self, query: str, idx: str = None) -> str:
178
178
  """
179
179
  Get response from retrieval
180
180
 
181
181
  :param query: query
182
+ :param idx: index to query, if None then use default index
182
183
  :return: response
183
184
  """
184
- idx = self.get_option_value("idx")
185
+ if idx is None:
186
+ idx = self.get_option_value("idx")
185
187
  indexes = idx.split(",")
186
188
  response = ""
187
189
  for index in indexes:
@@ -226,16 +228,18 @@ class Plugin(BasePlugin):
226
228
  prompt += "\nADDITIONAL CONTEXT: " + response
227
229
  return prompt
228
230
 
229
- def query(self, question: str) -> Tuple[str, list, list]:
231
+ def query(self, question: str, idx: str = None) -> Tuple[str, list, list]:
230
232
  """
231
233
  Query Llama-index
232
234
 
233
235
  :param question: question
236
+ :param idx: index to query, if None then use default index
234
237
  :return: response, list of document ids, list of metadata
235
238
  """
236
239
  doc_ids = []
237
240
  metas = []
238
- idx = self.get_option_value("idx")
241
+ if idx is None:
242
+ idx = self.get_option_value("idx")
239
243
  model = self.window.core.models.from_defaults()
240
244
 
241
245
  if self.get_option_value("model_query") is not None:
@@ -72,9 +72,12 @@ class Worker(BaseWorker):
72
72
  :return: response item
73
73
  """
74
74
  question = self.get_param(item, "query")
75
+ idx = None
76
+ if self.has_param(item, "idx"):
77
+ idx = self.get_param(item, "idx")
75
78
  self.status("Please wait... querying: {}...".format(question))
76
79
  # at first, try to get from retrieval
77
- response = self.plugin.get_from_retrieval(question)
80
+ response = self.plugin.get_from_retrieval(question, idx=idx) # get response from retrieval
78
81
  if response is not None and response != "":
79
82
  self.log("Found using retrieval...")
80
83
  context = "ADDITIONAL CONTEXT (response from DB):\n--------------------------------\n" + response
@@ -83,7 +86,7 @@ class Worker(BaseWorker):
83
86
  }
84
87
  return self.make_response(item, response, extra=extra)
85
88
 
86
- content, doc_ids, metas = self.plugin.query(question) # send question to Llama-index
89
+ content, doc_ids, metas = self.plugin.query(question, idx=idx) # send question to Llama-index
87
90
  result = content
88
91
  context = "ADDITIONAL CONTEXT (response from DB):\n--------------------------------\n" + content
89
92
  if doc_ids:
@@ -34,7 +34,7 @@ class Plugin(BasePlugin):
34
34
  def __init__(self, *args, **kwargs):
35
35
  super(Plugin, self).__init__(*args, **kwargs)
36
36
  self.id = "openai_dalle"
37
- self.name = "DALL-E 3: Image generation"
37
+ self.name = "Image generation"
38
38
  self.description = "Integrates DALL-E 3 image generation with any chat"
39
39
  self.prefix = "DALL-E"
40
40
  self.type = [
@@ -109,6 +109,7 @@ class ToolDialog(BaseDialog):
109
109
 
110
110
  :param window: main window
111
111
  :param id: logger id
112
+ :param tool: Tool instance
112
113
  """
113
114
  super(ToolDialog, self).__init__(window, id)
114
115
  self.window = window
@@ -488,8 +488,7 @@ class TextareaField(QTextEdit):
488
488
  if self.value > self.min_font_size:
489
489
  self.value -= 1
490
490
 
491
- size_str = f"{self.value}px"
492
- self.update_stylesheet(f"font-size: {size_str};")
491
+ self.update_stylesheet(f"QTextEdit {{ font-size: {self.value}px }};")
493
492
  event.accept()
494
493
  else:
495
494
  super(TextareaField, self).wheelEvent(event)
pygpt_net/ui/__init__.py CHANGED
@@ -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.01.19 03:00:00 #
9
+ # Updated Date: 2025.08.20 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -89,12 +89,6 @@ class UI:
89
89
  self.splitters['main'].addWidget(self.parts['chat']) # chat box
90
90
  self.splitters['main'].addWidget(self.parts['toolbox']) # toolbox
91
91
 
92
- # FIRST RUN: initial sizes if not set yet
93
- if not self.window.core.config.has("layout.splitters") \
94
- or self.window.core.config.get("layout.splitters") == {}\
95
- or self.window.core.config.get("license.accepted") == False:
96
- self.set_initial_size()
97
-
98
92
  # menus
99
93
  self.menus.setup()
100
94
 
@@ -107,6 +101,14 @@ class UI:
107
101
  # set window title
108
102
  self.update_title()
109
103
 
104
+ def on_show(self):
105
+ """Called after MainWindow onShow() event"""
106
+ # FIRST RUN: initial sizes if not set yet
107
+ if not self.window.core.config.has("layout.splitters") \
108
+ or self.window.core.config.get("layout.splitters") == {} \
109
+ or self.window.core.config.get("license.accepted") == False:
110
+ self.set_initial_size()
111
+
110
112
  def set_initial_size(self):
111
113
  """Set default sizes"""
112
114
  def set_initial_splitter_height():
@@ -118,19 +120,19 @@ class UI:
118
120
  self.window.ui.splitters['main.output'].setSizes([size_output, size_input])
119
121
  else:
120
122
  QTimer.singleShot(0, set_initial_splitter_height)
121
- QTimer.singleShot(0, set_initial_splitter_height)
123
+ QTimer.singleShot(10, set_initial_splitter_height)
122
124
 
123
125
  def set_initial_splitter_width():
124
126
  """Set initial splitter width"""
125
127
  total_width = self.window.ui.splitters['main'].size().width()
126
128
  if total_width > 0:
127
- size_output = int(total_width * 0.7)
129
+ size_output = int(total_width * 0.75)
128
130
  size_ctx = (total_width - size_output) / 2
129
131
  size_toolbox = (total_width - size_output) / 2
130
132
  self.window.ui.splitters['main'].setSizes([size_ctx, size_output, size_toolbox])
131
133
  else:
132
134
  QTimer.singleShot(0, set_initial_splitter_width)
133
- QTimer.singleShot(0, set_initial_splitter_width)
135
+ QTimer.singleShot(10, set_initial_splitter_width)
134
136
 
135
137
  def update_title(self):
136
138
  """Update window title"""
@@ -11,6 +11,7 @@
11
11
 
12
12
  from PySide6.QtCore import Qt
13
13
  from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QSizePolicy, QWidget, QFrame
14
+ from PySide6.QtGui import QFont
14
15
 
15
16
  from pygpt_net.ui.widget.element.labels import TitleLabel, UrlLabel
16
17
  from pygpt_net.ui.widget.option.checkbox import OptionCheckbox
@@ -57,7 +58,9 @@ class BaseConfigDialog:
57
58
  if slider and t in ('int', 'float'):
58
59
  widgets[key] = OptionSlider(self.window, id, key, option)
59
60
  else:
60
- widgets[key] = PasswordInput(self.window, id, key, option) if secret else OptionInput(self.window, id, key, option)
61
+ widgets[key] = PasswordInput(self.window, id, key, option) if secret else OptionInput(self.window,
62
+ id, key,
63
+ option)
61
64
 
62
65
  elif t == 'textarea':
63
66
  w = OptionTextarea(self.window, id, key, option)
@@ -99,20 +102,21 @@ class BaseConfigDialog:
99
102
  label = option['label']
100
103
  desc = option.get('description')
101
104
  extra = option.get('extra') or {}
102
- label_key = label + '.label'
105
+ label_key = f'{label}.label'
103
106
  nodes = self.window.ui.nodes
104
107
 
108
+ txt = trans(label)
105
109
  if extra.get('bold'):
106
- nodes[label_key] = TitleLabel(trans(label))
110
+ nodes[label_key] = TitleLabel(txt)
107
111
  else:
108
- nodes[label_key] = QLabel(trans(label))
112
+ nodes[label_key] = QLabel(txt)
109
113
  nodes[label_key].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
110
114
  nodes[label_key].setMinimumWidth(120)
111
115
  nodes[label_key].setWordWrap(True)
112
116
 
113
117
  desc_key = None
114
118
  if desc is not None:
115
- desc_key = label + '.desc'
119
+ desc_key = f'{label}.desc'
116
120
  nodes[desc_key] = self.add_description(desc)
117
121
 
118
122
  if option.get('type') == 'textarea':
@@ -142,21 +146,22 @@ class BaseConfigDialog:
142
146
  :return: QHBoxLayout
143
147
  """
144
148
  label = option['label']
145
- label_key = label + '.label'
149
+ label_key = f'{label}.label'
146
150
  desc = option.get('description')
147
151
  extra = option.get('extra') or {}
148
152
  nodes = self.window.ui.nodes
149
153
 
154
+ txt = trans(label)
150
155
  if extra.get('bold'):
151
- nodes[label_key] = TitleLabel(trans(label))
156
+ nodes[label_key] = TitleLabel(txt)
152
157
  else:
153
- nodes[label_key] = QLabel(trans(label))
158
+ nodes[label_key] = QLabel(txt)
154
159
  nodes[label_key].setMinimumHeight(30)
155
160
  nodes[label_key].setWordWrap(True)
156
161
 
157
162
  desc_key = None
158
163
  if desc is not None:
159
- desc_key = label + '.desc'
164
+ desc_key = f'{label}.desc'
160
165
  nodes[desc_key] = self.add_description(desc)
161
166
 
162
167
  layout = QVBoxLayout()
@@ -191,7 +196,7 @@ class BaseConfigDialog:
191
196
 
192
197
  desc_key = None
193
198
  if desc is not None:
194
- desc_key = label + '.desc'
199
+ desc_key = f'{label}.desc'
195
200
  nodes[desc_key] = self.add_description(desc)
196
201
 
197
202
  urls = extra.get('urls')
@@ -114,18 +114,22 @@ class About:
114
114
  pixmap = QPixmap(path)
115
115
  logo_label.setPixmap(pixmap)
116
116
 
117
- self.window.ui.nodes['dialog.about.btn.website'] = QPushButton(QIcon(":/icons/home.svg"), trans('about.btn.website'))
118
- self.window.ui.nodes['dialog.about.btn.website'].clicked.connect(lambda: self.window.controller.dialogs.info.goto_website())
119
- self.window.ui.nodes['dialog.about.btn.website'].setCursor(Qt.PointingHandCursor)
120
- self.window.ui.nodes['dialog.about.btn.website'].setStyleSheet("font-size: 11px;")
117
+ btn_web = QPushButton(QIcon(":/icons/home.svg"), trans('about.btn.website'))
118
+ btn_web.clicked.connect(lambda: self.window.controller.dialogs.info.goto_website())
119
+ btn_web.setCursor(Qt.PointingHandCursor)
120
+ btn_web.setStyleSheet("font-size: 11px;")
121
+ self.window.ui.nodes['dialog.about.btn.website'] = btn_web
121
122
 
122
- self.window.ui.nodes['dialog.about.btn.github'] = QPushButton(QIcon(":/icons/code.svg"), trans('about.btn.github'))
123
- self.window.ui.nodes['dialog.about.btn.github'].clicked.connect(lambda: self.window.controller.dialogs.info.goto_github())
124
- self.window.ui.nodes['dialog.about.btn.github'].setCursor(Qt.PointingHandCursor)
123
+ btn_git = QPushButton(QIcon(":/icons/code.svg"), trans('about.btn.github'))
124
+ btn_git.clicked.connect(lambda: self.window.controller.dialogs.info.goto_github())
125
+ btn_git.setCursor(Qt.PointingHandCursor)
126
+ self.window.ui.nodes['dialog.about.btn.github'] = btn_git
125
127
 
126
- self.window.ui.nodes['dialog.about.btn.support'] = QPushButton(QIcon(":/icons/favorite.svg"), trans('about.btn.support'))
127
- self.window.ui.nodes['dialog.about.btn.support'].clicked.connect(lambda: self.window.controller.dialogs.info.goto_donate())
128
- self.window.ui.nodes['dialog.about.btn.support'].setCursor(Qt.PointingHandCursor)
128
+
129
+ btn_support = QPushButton(QIcon(":/icons/favorite.svg"), trans('about.btn.support'))
130
+ btn_support.clicked.connect(lambda: self.window.controller.dialogs.info.goto_donate())
131
+ btn_support.setCursor(Qt.PointingHandCursor)
132
+ self.window.ui.nodes['dialog.about.btn.support'] = btn_support
129
133
 
130
134
  buttons_layout = QHBoxLayout()
131
135
  buttons_layout.addWidget(self.window.ui.nodes['dialog.about.btn.support'])
@@ -133,10 +137,13 @@ class About:
133
137
  buttons_layout.addWidget(self.window.ui.nodes['dialog.about.btn.github'])
134
138
 
135
139
  string = self.prepare_content()
136
- self.window.ui.nodes['dialog.about.content'] = QLabel(string)
137
- self.window.ui.nodes['dialog.about.content'].setTextInteractionFlags(Qt.TextSelectableByMouse)
138
- self.window.ui.nodes['dialog.about.thanks'] = QLabel(trans('about.thanks'))
139
- self.window.ui.nodes['dialog.about.thanks'].setAlignment(Qt.AlignCenter)
140
+ content = QLabel(string)
141
+ content.setTextInteractionFlags(Qt.TextSelectableByMouse)
142
+ self.window.ui.nodes['dialog.about.content'] = content
143
+
144
+ thanks = QLabel(trans('about.thanks'))
145
+ thanks.setAlignment(Qt.AlignCenter)
146
+ self.window.ui.nodes['dialog.about.thanks'] = thanks
140
147
 
141
148
  title = QLabel("PyGPT")
142
149
  title.setContentsMargins(0, 0, 0, 0)
@@ -150,10 +157,11 @@ class About:
150
157
  )
151
158
  title.setAlignment(Qt.AlignCenter)
152
159
 
153
- self.window.ui.nodes['dialog.about.thanks.content'] = QPlainTextEdit()
154
- self.window.ui.nodes['dialog.about.thanks.content'].setReadOnly(True)
155
- self.window.ui.nodes['dialog.about.thanks.content'].setPlainText("")
156
- self.window.ui.nodes['dialog.about.thanks.content'].setStyleSheet("font-size: 11px;")
160
+ thanks_content = QPlainTextEdit()
161
+ thanks_content.setReadOnly(True)
162
+ thanks_content.setPlainText("")
163
+ thanks_content.setStyleSheet("font-size: 11px;")
164
+ self.window.ui.nodes['dialog.about.thanks.content'] = thanks_content
157
165
 
158
166
  layout = QVBoxLayout()
159
167
  layout.addWidget(logo_label)
@@ -76,12 +76,14 @@ class Plugins:
76
76
  self.window.ui.tabs['plugin.settings'] = QTabWidget()
77
77
  self.window.ui.tabs['plugin.settings.tabs'] = {}
78
78
 
79
+ sorted_ids = self.window.core.plugins.get_ids(sort=True)
80
+
79
81
  # build plugin settings tabs
80
- for id in self.window.core.plugins.plugins:
82
+ for id in sorted_ids:
81
83
  content_tabs = {}
82
84
  scroll_tabs = {}
83
85
 
84
- plugin = self.window.core.plugins.plugins[id]
86
+ plugin = self.window.core.plugins.get(id)
85
87
  parent_id = "plugin." + id
86
88
 
87
89
  # create plugin options entry if not exists
@@ -231,8 +233,8 @@ class Plugins:
231
233
  )
232
234
 
233
235
  data = {}
234
- for plugin_id in self.window.core.plugins.plugins:
235
- plugin = self.window.core.plugins.plugins[plugin_id]
236
+ for plugin_id in sorted_ids:
237
+ plugin = self.window.core.plugins.get(plugin_id)
236
238
  data[plugin_id] = plugin
237
239
 
238
240
  # plugins list
@@ -65,6 +65,7 @@ class Settings(BaseConfigDialog):
65
65
 
66
66
  # settings section tabs
67
67
  self.window.ui.tabs['settings.section'] = QTabWidget()
68
+ options_get = self.window.controller.settings.editor.get_options
68
69
 
69
70
  # build settings tabs
70
71
  for section_id in sections:
@@ -74,21 +75,26 @@ class Settings(BaseConfigDialog):
74
75
  first_tab = "general"
75
76
 
76
77
  # get settings options for section
77
- fields = self.window.controller.settings.editor.get_options(section_id)
78
+ fields = options_get(section_id)
78
79
  is_general = False
79
- for key in fields:
80
- if 'tab' in fields[key]:
81
- tab = fields[key]['tab']
80
+
81
+ tab_by_key = {}
82
+ for key, field in fields.items():
83
+ if 'tab' in field:
84
+ tab = field['tab']
82
85
  if tab is not None:
83
86
  if first_tab == "general":
84
87
  first_tab = tab
85
- if tab.lower() == "general":
88
+ if isinstance(tab, str) and tab.lower() == "general":
86
89
  is_general = True
87
- break
88
90
  else:
89
91
  is_general = True
90
- break
91
92
 
93
+ tab_id = field['tab'] if field.get('tab') not in (None, "") else "general"
94
+ tab_by_key[key] = tab_id
95
+
96
+ if field.get('advanced'):
97
+ advanced_keys.setdefault(tab_id, []).append(key)
92
98
 
93
99
  # extract tab ids, general is default
94
100
  tab_ids = self.extract_option_tabs(fields)
@@ -97,109 +103,85 @@ class Settings(BaseConfigDialog):
97
103
  tab_ids += extra_tabs
98
104
  for tab_id in tab_ids:
99
105
  content_tabs[tab_id] = QVBoxLayout()
100
- scroll_tabs[tab_id] = QScrollArea()
101
- scroll_tabs[tab_id].setWidgetResizable(True)
102
-
103
- # prepare advanced options keys
104
- for key in fields:
105
- if 'advanced' in fields[key] and fields[key]['advanced']:
106
- tab_id = "general"
107
- if 'tab' in fields[key]:
108
- tab = fields[key]['tab']
109
- if tab is not None and tab != "":
110
- tab_id = tab
111
- if tab_id not in advanced_keys:
112
- advanced_keys[tab_id] = []
113
- advanced_keys[tab_id].append(key)
106
+ s = QScrollArea()
107
+ s.setWidgetResizable(True)
108
+ scroll_tabs[tab_id] = s
114
109
 
115
110
  # build settings widgets
116
111
  widgets = self.build_widgets(self.id, fields)
117
112
 
118
113
  # apply settings widgets
119
- for key in widgets:
120
- self.window.ui.config[self.id][key] = widgets[key]
114
+ self.window.ui.config[self.id].update(widgets)
121
115
 
122
116
  # apply widgets to layouts
123
117
  options = {}
124
- for key in widgets:
125
- if fields[key]["type"] == 'text' or fields[key]["type"] == 'int' or fields[key]["type"] == 'float':
126
- options[key] = self.add_option(widgets[key], fields[key])
127
- elif fields[key]["type"] == 'textarea':
128
- options[key] = self.add_row_option(widgets[key], fields[key])
129
- elif fields[key]["type"] == 'bool':
130
- options[key] = self.add_raw_option(widgets[key], fields[key])
131
- elif fields[key]['type'] == 'dict':
132
- options[key] = self.add_row_option(widgets[key], fields[key]) # dict
133
- # register dict to editor:
134
- self.window.ui.dialogs.register_dictionary(
135
- key,
136
- parent="config",
137
- option=fields[key],
138
- )
139
- elif fields[key]['type'] == 'combo':
140
- options[key] = self.add_option(widgets[key], fields[key]) # combobox
141
- elif fields[key]['type'] == 'bool_list':
142
- options[key] = self.add_option(widgets[key], fields[key]) # bool list
143
-
144
- #self.window.ui.nodes['settings.api_key.label'].setMinimumHeight(60)
118
+ add_option = self.add_option
119
+ add_row_option = self.add_row_option
120
+ add_raw_option = self.add_raw_option
121
+ register_dictionary = self.window.ui.dialogs.register_dictionary
122
+
123
+ for key, widget in widgets.items():
124
+ f = fields[key]
125
+ t = f['type']
126
+ if t in ('text', 'int', 'float', 'combo', 'bool_list'):
127
+ options[key] = add_option(widget, f)
128
+ elif t in ('textarea', 'dict'):
129
+ options[key] = add_row_option(widget, f)
130
+ if t == 'dict':
131
+ # register dict to editor:
132
+ register_dictionary(
133
+ key,
134
+ parent="config",
135
+ option=f,
136
+ )
137
+ elif t == 'bool':
138
+ options[key] = add_raw_option(widget, f)
139
+
140
+ # self.window.ui.nodes['settings.api_key.label'].setMinimumHeight(60)
145
141
 
146
142
  # append widgets options layouts to scroll area
147
- for key in options:
148
- option = options[key]
149
- tab_id = "general"
150
- if 'tab' in fields[key]:
151
- tab = fields[key]['tab']
152
- if tab is not None and tab != "":
153
- tab_id = tab
143
+ advanced_membership = {tid: set(keys) for tid, keys in advanced_keys.items()}
144
+ last_option_key = next(reversed(options)) if options else None
145
+
146
+ for key, option in options.items():
147
+ tab_id = tab_by_key.get(key, "general")
154
148
 
155
149
  # hide advanced options
156
- if tab_id in advanced_keys and key in advanced_keys[tab_id]:
150
+ if tab_id in advanced_membership and key in advanced_membership[tab_id]:
157
151
  continue
158
152
 
159
- content_tabs[tab_id].addLayout(option) # add
153
+ content_tabs[tab_id].addLayout(option)
160
154
 
161
155
  # append URLs
162
156
  if 'urls' in fields[key]:
163
157
  content_tabs[tab_id].addWidget(self.add_urls(fields[key]['urls']))
164
158
 
165
159
  # check if not last option
166
- if key != list(options.keys())[-1] or tab_id in advanced_keys:
160
+ if key != last_option_key or tab_id in advanced_keys:
167
161
  content_tabs[tab_id].addWidget(self.add_line())
168
162
 
169
163
  # append advanced options at the end
170
164
  if len(advanced_keys) > 0:
171
165
  groups = {}
172
- for key in options:
173
- tab_id = "general"
174
- if 'tab' in fields[key]:
175
- tab = fields[key]['tab']
176
- if tab is not None and tab != "":
177
- tab_id = tab
178
-
179
- if tab_id not in advanced_keys:
180
- continue
181
-
182
- # ignore non-advanced options
183
- if key not in advanced_keys[tab_id]:
166
+ for tab_id, adv_list in advanced_keys.items():
167
+ if not adv_list:
184
168
  continue
185
169
 
186
170
  group_id = 'settings.advanced.' + section_id + '.' + tab_id
171
+ groups[tab_id] = CollapsedGroup(self.window, group_id, None, False, None)
172
+ groups[tab_id].box.setText(trans('settings.advanced.collapse'))
187
173
 
188
- if tab_id not in groups:
189
- groups[tab_id] = CollapsedGroup(self.window, group_id, None, False, None)
190
- groups[tab_id].box.setText(trans('settings.advanced.collapse'))
191
-
192
- groups[tab_id].add_layout(options[key]) # add option to group
193
-
194
- # add line if not last option
195
- if key != advanced_keys[tab_id][-1]:
196
- groups[tab_id].add_widget(self.add_line())
174
+ last_idx = len(adv_list) - 1
175
+ for idx, key in enumerate(adv_list):
176
+ groups[tab_id].add_layout(options[key])
177
+ if idx != last_idx:
178
+ groups[tab_id].add_widget(self.add_line())
197
179
 
198
180
  # add advanced options group to scrolls
199
- for tab_id in groups:
181
+ for tab_id, group in groups.items():
200
182
  group_id = 'settings.advanced.' + section_id + '.' + tab_id
201
- content_tabs[tab_id].addWidget(groups[tab_id])
202
- self.window.ui.groups[group_id] = groups[tab_id]
183
+ content_tabs[tab_id].addWidget(group)
184
+ self.window.ui.groups[group_id] = group
203
185
 
204
186
  # add extra features buttons
205
187
  self.append_extra(content_tabs, section_id, options, fields)
@@ -214,10 +196,10 @@ class Settings(BaseConfigDialog):
214
196
  tab_widget = QTabWidget()
215
197
 
216
198
  # sort to make general tab first if exists
217
- if "general" in content_tabs:
218
- content_tabs = {"general": content_tabs.pop("general")} | content_tabs
199
+ tab_order = (["general"] + [tid for tid in content_tabs if
200
+ tid != "general"]) if "general" in content_tabs else list(content_tabs)
219
201
 
220
- for tab_id in content_tabs:
202
+ for tab_id in tab_order:
221
203
  if tab_id == "general":
222
204
  name_key = trans("settings.section.tab.general")
223
205
  else:
@@ -315,21 +297,27 @@ class Settings(BaseConfigDialog):
315
297
  :return: list with keys
316
298
  """
317
299
  keys = []
300
+ seen = set()
318
301
  is_default = False
319
- for key in options:
320
- option = options[key]
302
+
303
+ for option in options.values():
321
304
  if 'tab' in option:
322
305
  tab = option['tab']
323
306
  if tab == "" or tab is None:
324
307
  is_default = True
325
- if tab not in keys:
326
- keys.append(tab)
308
+ try:
309
+ if tab not in seen:
310
+ seen.add(tab)
311
+ keys.append(tab)
312
+ except TypeError:
313
+ if tab not in keys:
314
+ keys.append(tab)
327
315
  else:
328
316
  is_default = True
329
317
 
330
- # add default general tab if not exists
331
- if len(keys) == 0 or (is_default and "general" not in keys):
318
+ if not keys or (is_default and "general" not in keys):
332
319
  keys.append("general")
320
+
333
321
  return keys
334
322
 
335
323
  def append_extra(self, content: dict, section_id: str, widgets: dict, options: dict):