pygpt-net 2.6.21__py3-none-any.whl → 2.6.23__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 (160) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/__init__.py +4 -8
  5. pygpt_net/controller/access/voice.py +2 -2
  6. pygpt_net/controller/agent/llama.py +3 -0
  7. pygpt_net/controller/assistant/batch.py +2 -3
  8. pygpt_net/controller/assistant/editor.py +2 -2
  9. pygpt_net/controller/assistant/files.py +2 -3
  10. pygpt_net/controller/assistant/store.py +2 -2
  11. pygpt_net/controller/audio/audio.py +2 -2
  12. pygpt_net/controller/chat/response.py +4 -0
  13. pygpt_net/controller/ctx/ctx.py +2 -1
  14. pygpt_net/controller/files/files.py +24 -55
  15. pygpt_net/controller/idx/indexer.py +85 -76
  16. pygpt_net/controller/lang/lang.py +52 -34
  17. pygpt_net/controller/model/importer.py +2 -2
  18. pygpt_net/controller/notepad/notepad.py +86 -84
  19. pygpt_net/controller/plugins/settings.py +3 -4
  20. pygpt_net/controller/settings/profile.py +105 -124
  21. pygpt_net/controller/theme/menu.py +154 -57
  22. pygpt_net/controller/theme/nodes.py +51 -44
  23. pygpt_net/controller/theme/theme.py +33 -9
  24. pygpt_net/controller/tools/tools.py +2 -2
  25. pygpt_net/controller/ui/tabs.py +2 -3
  26. pygpt_net/core/agents/observer/evaluation.py +2 -2
  27. pygpt_net/core/agents/runners/loop.py +1 -0
  28. pygpt_net/core/bridge/bridge.py +2 -0
  29. pygpt_net/core/ctx/container.py +13 -12
  30. pygpt_net/core/ctx/output.py +7 -4
  31. pygpt_net/core/debug/console/console.py +2 -2
  32. pygpt_net/core/filesystem/actions.py +1 -2
  33. pygpt_net/core/filesystem/opener.py +261 -0
  34. pygpt_net/core/filesystem/url.py +13 -10
  35. pygpt_net/core/platforms/platforms.py +5 -4
  36. pygpt_net/core/render/plain/helpers.py +2 -5
  37. pygpt_net/core/render/plain/renderer.py +26 -30
  38. pygpt_net/core/render/web/body.py +1 -1
  39. pygpt_net/core/settings/settings.py +43 -13
  40. pygpt_net/core/tabs/tabs.py +20 -13
  41. pygpt_net/data/config/config.json +4 -4
  42. pygpt_net/data/config/models.json +3 -3
  43. pygpt_net/data/css/web-blocks.dark.css +7 -1
  44. pygpt_net/data/css/web-blocks.light.css +5 -2
  45. pygpt_net/data/css/web-chatgpt.dark.css +7 -1
  46. pygpt_net/data/css/web-chatgpt.light.css +3 -0
  47. pygpt_net/data/css/web-chatgpt_wide.dark.css +7 -1
  48. pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
  49. pygpt_net/data/locale/locale.de.ini +5 -1
  50. pygpt_net/data/locale/locale.en.ini +5 -1
  51. pygpt_net/data/locale/locale.es.ini +5 -1
  52. pygpt_net/data/locale/locale.fr.ini +5 -1
  53. pygpt_net/data/locale/locale.it.ini +5 -1
  54. pygpt_net/data/locale/locale.pl.ini +6 -4
  55. pygpt_net/data/locale/locale.uk.ini +5 -1
  56. pygpt_net/data/locale/locale.zh.ini +5 -1
  57. pygpt_net/plugin/twitter/plugin.py +2 -2
  58. pygpt_net/provider/core/config/patch.py +12 -1
  59. pygpt_net/tools/audio_transcriber/ui/dialogs.py +44 -54
  60. pygpt_net/tools/code_interpreter/body.py +1 -2
  61. pygpt_net/tools/code_interpreter/tool.py +7 -4
  62. pygpt_net/tools/code_interpreter/ui/html.py +1 -3
  63. pygpt_net/tools/code_interpreter/ui/widgets.py +2 -3
  64. pygpt_net/tools/html_canvas/ui/widgets.py +1 -3
  65. pygpt_net/tools/image_viewer/ui/dialogs.py +40 -37
  66. pygpt_net/tools/indexer/ui/widgets.py +2 -4
  67. pygpt_net/tools/media_player/tool.py +2 -5
  68. pygpt_net/tools/media_player/ui/widgets.py +60 -36
  69. pygpt_net/tools/text_editor/ui/widgets.py +18 -19
  70. pygpt_net/tools/translator/ui/widgets.py +39 -35
  71. pygpt_net/ui/base/context_menu.py +9 -4
  72. pygpt_net/ui/dialog/db.py +1 -3
  73. pygpt_net/ui/dialog/models.py +1 -3
  74. pygpt_net/ui/dialog/models_importer.py +2 -4
  75. pygpt_net/ui/dialogs.py +34 -30
  76. pygpt_net/ui/layout/chat/attachments.py +72 -84
  77. pygpt_net/ui/layout/chat/attachments_ctx.py +40 -44
  78. pygpt_net/ui/layout/chat/attachments_uploaded.py +36 -39
  79. pygpt_net/ui/layout/chat/calendar.py +100 -70
  80. pygpt_net/ui/layout/chat/chat.py +23 -17
  81. pygpt_net/ui/layout/chat/input.py +95 -118
  82. pygpt_net/ui/layout/chat/output.py +100 -162
  83. pygpt_net/ui/layout/chat/painter.py +89 -61
  84. pygpt_net/ui/layout/ctx/ctx_list.py +43 -52
  85. pygpt_net/ui/layout/status.py +23 -14
  86. pygpt_net/ui/layout/toolbox/agent.py +27 -38
  87. pygpt_net/ui/layout/toolbox/agent_llama.py +41 -45
  88. pygpt_net/ui/layout/toolbox/assistants.py +42 -38
  89. pygpt_net/ui/layout/toolbox/computer_env.py +32 -23
  90. pygpt_net/ui/layout/toolbox/footer.py +13 -16
  91. pygpt_net/ui/layout/toolbox/image.py +18 -21
  92. pygpt_net/ui/layout/toolbox/indexes.py +46 -89
  93. pygpt_net/ui/layout/toolbox/mode.py +20 -7
  94. pygpt_net/ui/layout/toolbox/model.py +12 -10
  95. pygpt_net/ui/layout/toolbox/presets.py +68 -52
  96. pygpt_net/ui/layout/toolbox/prompt.py +31 -58
  97. pygpt_net/ui/layout/toolbox/toolbox.py +25 -21
  98. pygpt_net/ui/layout/toolbox/vision.py +20 -22
  99. pygpt_net/ui/main.py +2 -4
  100. pygpt_net/ui/menu/about.py +64 -84
  101. pygpt_net/ui/menu/audio.py +87 -63
  102. pygpt_net/ui/menu/config.py +121 -127
  103. pygpt_net/ui/menu/debug.py +69 -76
  104. pygpt_net/ui/menu/file.py +32 -35
  105. pygpt_net/ui/menu/menu.py +2 -3
  106. pygpt_net/ui/menu/plugins.py +69 -33
  107. pygpt_net/ui/menu/theme.py +45 -46
  108. pygpt_net/ui/menu/tools.py +56 -60
  109. pygpt_net/ui/menu/video.py +20 -25
  110. pygpt_net/ui/tray.py +1 -2
  111. pygpt_net/ui/widget/audio/bar.py +1 -3
  112. pygpt_net/ui/widget/audio/input_button.py +3 -4
  113. pygpt_net/ui/widget/calendar/select.py +1 -2
  114. pygpt_net/ui/widget/dialog/base.py +12 -9
  115. pygpt_net/ui/widget/dialog/editor_file.py +20 -23
  116. pygpt_net/ui/widget/dialog/find.py +25 -24
  117. pygpt_net/ui/widget/dialog/profile.py +57 -53
  118. pygpt_net/ui/widget/draw/painter.py +62 -93
  119. pygpt_net/ui/widget/element/button.py +42 -30
  120. pygpt_net/ui/widget/element/checkbox.py +23 -15
  121. pygpt_net/ui/widget/element/group.py +6 -5
  122. pygpt_net/ui/widget/element/labels.py +1 -2
  123. pygpt_net/ui/widget/filesystem/explorer.py +93 -102
  124. pygpt_net/ui/widget/image/display.py +1 -2
  125. pygpt_net/ui/widget/lists/assistant.py +1 -2
  126. pygpt_net/ui/widget/lists/attachment.py +1 -2
  127. pygpt_net/ui/widget/lists/attachment_ctx.py +1 -2
  128. pygpt_net/ui/widget/lists/context.py +2 -4
  129. pygpt_net/ui/widget/lists/index.py +1 -2
  130. pygpt_net/ui/widget/lists/model.py +1 -2
  131. pygpt_net/ui/widget/lists/model_editor.py +1 -2
  132. pygpt_net/ui/widget/lists/model_importer.py +1 -2
  133. pygpt_net/ui/widget/lists/preset.py +1 -2
  134. pygpt_net/ui/widget/lists/preset_plugins.py +1 -2
  135. pygpt_net/ui/widget/lists/profile.py +1 -2
  136. pygpt_net/ui/widget/lists/uploaded.py +1 -2
  137. pygpt_net/ui/widget/option/checkbox.py +2 -4
  138. pygpt_net/ui/widget/option/checkbox_list.py +1 -4
  139. pygpt_net/ui/widget/option/cmd.py +1 -4
  140. pygpt_net/ui/widget/option/dictionary.py +25 -28
  141. pygpt_net/ui/widget/option/input.py +1 -3
  142. pygpt_net/ui/widget/tabs/Input.py +16 -12
  143. pygpt_net/ui/widget/tabs/body.py +5 -3
  144. pygpt_net/ui/widget/tabs/layout.py +41 -28
  145. pygpt_net/ui/widget/tabs/output.py +442 -85
  146. pygpt_net/ui/widget/textarea/calendar_note.py +1 -2
  147. pygpt_net/ui/widget/textarea/editor.py +41 -73
  148. pygpt_net/ui/widget/textarea/find.py +11 -10
  149. pygpt_net/ui/widget/textarea/html.py +3 -6
  150. pygpt_net/ui/widget/textarea/input.py +134 -69
  151. pygpt_net/ui/widget/textarea/notepad.py +54 -38
  152. pygpt_net/ui/widget/textarea/output.py +65 -54
  153. pygpt_net/ui/widget/textarea/search_input.py +5 -4
  154. pygpt_net/ui/widget/textarea/web.py +2 -4
  155. pygpt_net/ui/widget/vision/camera.py +2 -31
  156. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/METADATA +38 -174
  157. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/RECORD +160 -159
  158. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/LICENSE +0 -0
  159. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/WHEEL +0 -0
  160. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/entry_points.txt +0 -0
@@ -6,21 +6,301 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.14 07:00:00 #
9
+ # Updated Date: 2025.08.25 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtWidgets import QTabWidget, QMenu, QPushButton
13
- from PySide6.QtCore import Qt, Slot
14
- from PySide6.QtGui import QAction, QIcon
12
+ from PySide6.QtWidgets import QTabWidget, QMenu, QPushButton, QToolButton, QTabBar
13
+ from PySide6.QtCore import Qt, Slot, QTimer, QEvent
14
+ from PySide6.QtGui import QAction, QIcon, QGuiApplication
15
15
 
16
16
  from pygpt_net.core.tabs.tab import Tab
17
17
  from pygpt_net.utils import trans
18
- import pygpt_net.icons_rc
18
+
19
+ _ICON_CACHE = {}
20
+
21
+
22
+ def icon(path: str) -> QIcon:
23
+ if QGuiApplication.instance() is None:
24
+ return QIcon()
25
+ cached = _ICON_CACHE.get(path)
26
+ if cached is None:
27
+ cached = QIcon(path)
28
+ _ICON_CACHE[path] = cached
29
+ return cached
30
+
31
+
32
+ ICON_PATH_ADD = ':/icons/add.svg'
33
+ ICON_PATH_EDIT = ':/icons/edit.svg'
34
+ ICON_PATH_CLOSE = ':/icons/close.svg'
35
+ ICON_PATH_RELOAD = ':/icons/reload.svg'
36
+ ICON_PATH_FORWARD = ':/icons/forward'
37
+ ICON_PATH_BACK = ':/icons/back'
38
+
39
+
40
+ class OutputTabBar(QTabBar):
41
+ def __init__(
42
+ self,
43
+ window=None,
44
+ column=None,
45
+ tabs=None,
46
+ corner_button=None,
47
+ parent=None
48
+ ):
49
+ super().__init__(parent)
50
+ self.window = window
51
+ self.column = column
52
+ self.tabs = tabs
53
+ self.corner_button = corner_button
54
+
55
+ # inline [+] just after the last tab (only when there is real free space)
56
+ self.add_btn_inline = AddButton(window, column, tabs)
57
+ self.add_btn_inline.setParent(self)
58
+ self.add_btn_inline.setVisible(False)
59
+ self.add_btn_inline.raise_()
60
+
61
+ # visual gap between last tab and [+]
62
+ self._spacing = 3
63
+
64
+ # add button vertical offset (to align with text)
65
+ self._inline_y_offset = 2 # px up
66
+
67
+ # keep tabs natural width (do not stretch)
68
+ self.setExpanding(False)
69
+
70
+ # allow scroll buttons when needed
71
+ self.setUsesScrollButtons(True)
72
+
73
+ # ensure the tab bar stays visible even with 0 tabs
74
+ self._min_bar_height = max(self.add_btn_inline.sizeHint().height() + 6, 28)
75
+ self.setMinimumHeight(self._min_bar_height)
76
+
77
+ if hasattr(self.tabs, "setTabBarAutoHide"):
78
+ self.tabs.setTabBarAutoHide(False)
79
+
80
+ # state
81
+ self._inline_mode = False
82
+ self._corner_current = None # track where the corner [+] currently is
83
+ self._last_inline_pos = None # track last inline position to avoid useless moves
84
+
85
+ # coalesced updates (debounce to 1 per event-loop turn)
86
+ self._update_timer = QTimer(self)
87
+ self._update_timer.setSingleShot(True)
88
+ self._update_timer.timeout.connect(self.updateAddButtonPlacement)
89
+
90
+ # re-layout updates
91
+ self.currentChanged.connect(lambda _: self._queue_update())
92
+ self.tabMoved.connect(lambda _from, _to: self._queue_update())
93
+
94
+ # initial placement
95
+ QTimer.singleShot(0, self.updateAddButtonPlacement)
96
+
97
+ def sizeHint(self):
98
+ """
99
+ Override sizeHint to
100
+
101
+ :return: QSize
102
+ """
103
+ # keep a non-zero height even with 0 tabs
104
+ sh = super().sizeHint()
105
+ sh.setHeight(max(sh.height(), self._min_bar_height))
106
+ return sh
107
+
108
+ def minimumSizeHint(self):
109
+ """
110
+ Override minimumSizeHint
111
+
112
+ :return: QSize
113
+ """
114
+ m = super().minimumSizeHint()
115
+ m.setHeight(max(m.height(), self._min_bar_height))
116
+ return m
117
+
118
+ def _queue_update(self):
119
+ """Queue an update to recompute [+] placement (debounced)."""
120
+ # Coalesce many triggers into a single call at the end of the event loop.
121
+ self._update_timer.start(0)
122
+
123
+ def _visible_scroll_buttons(self):
124
+ """
125
+ Find the left and right scroll buttons if they are visible.
126
+
127
+ :return: (left_button, right_button) or (None, None) if not found
128
+ """
129
+ # find the scroll arrow buttons created by QTabBar
130
+ left = right = None
131
+ for btn in self.findChildren(QToolButton):
132
+ if not btn.isVisible():
133
+ continue
134
+ if not btn.autoRepeat():
135
+ continue
136
+ if left is None or btn.x() < left.x():
137
+ left = btn
138
+ if right is None or btn.x() > right.x():
139
+ right = btn
140
+ return left, right
141
+
142
+ def _set_corner_target(self, corner: Qt.Corner | None) -> bool:
143
+ """
144
+ Move the corner_button to a given corner (or detach it) only when it changes.
145
+
146
+ :param corner: target corner or None to detach
147
+ :return: True if changed, False if no change was needed
148
+ """
149
+ if self.corner_button is None:
150
+ return False
151
+
152
+ if self._corner_current == corner:
153
+ return False # nothing to do
154
+
155
+ # detach only from the previously used corner
156
+ if self._corner_current is not None:
157
+ self.tabs.setCornerWidget(None, self._corner_current)
158
+
159
+ if corner is not None:
160
+ self.tabs.setCornerWidget(self.corner_button, corner)
161
+
162
+ self._corner_current = corner
163
+ return True
164
+
165
+ def _column_index(self) -> int:
166
+ """
167
+ Get the column index this tab bar belongs to (0 or 1).
168
+
169
+ :return: Column index
170
+ """
171
+ idx = 0
172
+ if self.column is None:
173
+ return idx
174
+ return int(self.column.get_idx())
175
+
176
+ def updateAddButtonPlacement(self):
177
+ """
178
+ Recompute where the [+] button should be placed (inline or corner).
179
+
180
+ This method is called automatically on relevant events.
181
+ 1. If there are no tabs, show [+] in the left or right corner based on column index.
182
+ 2. If there are tabs but scroll buttons are visible (overflow), show [+] in the top-right corner.
183
+ 3. If there are tabs and no scroll buttons, show [+] inline after the last tab if there's room.
184
+ 4. Otherwise, show [+] in the top-right corner.
185
+ """
186
+ # CASE 1: no tabs -> show [+] in left or right corner based on column index
187
+ if self.count() == 0:
188
+ idx = self._column_index()
189
+ corner = Qt.TopLeftCorner if idx == 0 else Qt.TopRightCorner
190
+
191
+ changed = False
192
+ changed |= self._set_corner_target(corner)
193
+
194
+ if self._inline_mode:
195
+ self._inline_mode = False
196
+ changed = True
197
+
198
+ if self.add_btn_inline.isVisible():
199
+ self.add_btn_inline.setVisible(False)
200
+ changed = True
201
+
202
+ if self.corner_button is not None and not self.corner_button.isVisible():
203
+ self.corner_button.setVisible(True)
204
+ changed = True
205
+ return
206
+
207
+ # CASE 2: tabs exist
208
+ # if scroll buttons are visible we are in overflow -> use top-right corner [+]
209
+ left_sb, right_sb = self._visible_scroll_buttons()
210
+ if left_sb or right_sb:
211
+ changed = False
212
+ if self._inline_mode:
213
+ self._inline_mode = False
214
+ changed = True
215
+ if self.add_btn_inline.isVisible():
216
+ self.add_btn_inline.setVisible(False)
217
+ changed = True
218
+ changed |= self._set_corner_target(Qt.TopRightCorner)
219
+ if self.corner_button is not None and not self.corner_button.isVisible():
220
+ self.corner_button.setVisible(True)
221
+ changed = True
222
+ return
223
+
224
+ # otherwise, put [+] inline (right after the last tab) if there's real room
225
+ last_rect = self.tabRect(self.count() - 1)
226
+ x = last_rect.right() + 1 + self._spacing
227
+ want_inline = (x + self.add_btn_inline.width()) <= (self.width() - 1)
228
+
229
+ if want_inline:
230
+ changed = False
231
+
232
+ if not self._inline_mode:
233
+ self._inline_mode = True
234
+ changed = True
235
+
236
+ # hide any corner [+]
237
+ if self.corner_button is not None and self.corner_button.isVisible():
238
+ self.corner_button.setVisible(False)
239
+ changed = True
240
+ changed |= self._set_corner_target(None)
241
+
242
+ # compute position
243
+ y = (self.height() - self.add_btn_inline.height()) // 2
244
+ # NOTE: lift inline [+] slightly to align with tabs
245
+ y = max(0, y - self._inline_y_offset) # clamp to avoid negative
246
+ x = min(x, self.width() - self.add_btn_inline.width() - 1) # clamp inside the bar
247
+ new_pos = (x, y)
248
+ if self._last_inline_pos != new_pos:
249
+ self.add_btn_inline.move(x, y)
250
+ self._last_inline_pos = new_pos
251
+ changed = True
252
+
253
+ if not self.add_btn_inline.isVisible():
254
+ self.add_btn_inline.setVisible(True)
255
+ changed = True
256
+
257
+ self.add_btn_inline.raise_()
258
+ else:
259
+ # not enough room -> top-right corner
260
+ changed = False
261
+ if self._inline_mode:
262
+ self._inline_mode = False
263
+ changed = True
264
+ if self.add_btn_inline.isVisible():
265
+ self.add_btn_inline.setVisible(False)
266
+ changed = True
267
+ changed |= self._set_corner_target(Qt.TopRightCorner)
268
+ if self.corner_button is not None and not self.corner_button.isVisible():
269
+ self.corner_button.setVisible(True)
270
+ changed = True
271
+
272
+ def resizeEvent(self, event):
273
+ """Resize event handler to recompute [+] placement."""
274
+ super().resizeEvent(event)
275
+ self._queue_update()
276
+
277
+ def showEvent(self, event):
278
+ """Show event handler to recompute [+] placement."""
279
+ super().showEvent(event)
280
+ self._queue_update()
281
+
282
+ def tabInserted(self, index):
283
+ """Tab inserted event handler to recompute [+] placement."""
284
+ super().tabInserted(index)
285
+ self._queue_update()
286
+
287
+ def tabRemoved(self, index):
288
+ """Tab removed event handler to recompute [+] placement."""
289
+ super().tabRemoved(index)
290
+ self._queue_update()
291
+
292
+ def event(self, e):
293
+ """Event handler to catch layout/style changes that may affect [+] placement."""
294
+ res = super().event(e)
295
+ # only queue updates
296
+ if e.type() in (QEvent.LayoutRequest, QEvent.StyleChange, QEvent.PolishRequest, QEvent.FontChange):
297
+ self._queue_update()
298
+ return res
19
299
 
20
300
 
21
301
  class AddButton(QPushButton):
22
302
  def __init__(self, window=None, column=None, tabs=None):
23
- super(AddButton, self).__init__(QIcon(":/icons/add.svg"), "", window)
303
+ super(AddButton, self).__init__(icon(ICON_PATH_ADD), "", window)
24
304
  self.window = window
25
305
  self.column = column
26
306
  self.tabs = tabs
@@ -65,26 +345,25 @@ class AddButton(QPushButton):
65
345
  :return: menu
66
346
  """
67
347
  menu = QMenu(self)
348
+ menu.setAttribute(Qt.WA_DeleteOnClose, True)
68
349
 
69
- actions = {}
70
- actions['add_chat'] = QAction(QIcon(":/icons/add.svg"), trans('action.tab.add.chat'), self)
71
- actions['add_chat'].triggered.connect(
72
- lambda: self.tabs.add_tab(index, column_idx, Tab.TAB_CHAT)
350
+ add_chat = QAction(icon(ICON_PATH_ADD), trans('action.tab.add.chat'), menu)
351
+ add_chat.triggered.connect(
352
+ lambda: self.tabs.add_tab(-2, column_idx, Tab.TAB_CHAT)
73
353
  )
74
- actions['add_notepad'] = QAction(QIcon(":/icons/add.svg"), trans('action.tab.add.notepad'), self)
75
- actions['add_notepad'].triggered.connect(
76
- lambda: self.tabs.add_tab(index, column_idx, Tab.TAB_NOTEPAD)
354
+ add_notepad = QAction(icon(ICON_PATH_ADD), trans('action.tab.add.notepad'), menu)
355
+ add_notepad.triggered.connect(
356
+ lambda: self.tabs.add_tab(-2, column_idx, Tab.TAB_NOTEPAD)
77
357
  )
78
358
 
79
- # add chat, add notepad
80
- menu.addAction(actions['add_chat'])
81
- menu.addAction(actions['add_notepad'])
359
+ menu.addAction(add_chat)
360
+ menu.addAction(add_notepad)
82
361
 
83
- # add tools submenu
84
- self.window.controller.tools.append_tab_menu(self, menu, index, column_idx, self.tabs)
362
+ self.window.controller.tools.append_tab_menu(self, menu, -2, column_idx, self.tabs)
85
363
 
86
364
  return menu
87
365
 
366
+
88
367
  class OutputTabs(QTabWidget):
89
368
  def __init__(self, window=None, column=None):
90
369
  super(OutputTabs, self).__init__(window)
@@ -96,14 +375,6 @@ class OutputTabs(QTabWidget):
96
375
  self.setMovable(True)
97
376
  self.init()
98
377
 
99
- def set_active(self, active: bool):
100
- """Set the active state of the tab bar."""
101
- self.active = active
102
- if self.active:
103
- self.setStyleSheet("QTabBar::tab { border-bottom-width: 2px; }")
104
- else:
105
- self.setStyleSheet("QTabBar::tab { border-bottom-width: 0px; }")
106
-
107
378
  def init(self):
108
379
  """Initialize"""
109
380
  # create the [+] button
@@ -112,22 +383,85 @@ class OutputTabs(QTabWidget):
112
383
  # add the button to the top right corner of the tab bar
113
384
  self.setCornerWidget(add_button, corner=Qt.TopRightCorner)
114
385
 
115
- # connect signals
116
- self.currentChanged.connect(
117
- lambda: self.window.controller.ui.tabs.on_tab_changed(self.currentIndex(), self.column.get_idx())
118
- )
119
- self.tabBarClicked.connect(
120
- lambda: self.window.controller.ui.tabs.on_tab_clicked(self.currentIndex(), self.column.get_idx())
121
- )
122
- self.tabBarDoubleClicked.connect(
123
- lambda: self.window.controller.ui.tabs.on_tab_dbl_clicked(self.currentIndex(), self.column.get_idx())
124
- )
125
- self.tabCloseRequested.connect(
126
- lambda: self.window.controller.ui.tabs.on_tab_closed(self.currentIndex(), self.column.get_idx())
127
- )
128
- self.tabBar().tabMoved.connect(
129
- lambda: self.window.controller.ui.tabs.on_tab_moved(self.currentIndex(), self.column.get_idx())
386
+ self.setDocumentMode(True)
387
+
388
+ # use a custom tab bar that shows an inline [+] right after the tabs
389
+ tab_bar = OutputTabBar(
390
+ window=self.window,
391
+ column=self.column,
392
+ tabs=self,
393
+ corner_button=add_button,
394
+ parent=self,
130
395
  )
396
+ self.setTabBar(tab_bar)
397
+ self.setMovable(True)
398
+ self.tabBar().setMovable(True)
399
+
400
+ self.setDocumentMode(True) # QT Material fix
401
+ self.tabBar().setDrawBase(False) # QT Material fix
402
+
403
+ # the custom tab bar decides when to show inline or corner [+]
404
+ add_button.setVisible(False)
405
+
406
+ # ensure initial recompute happens after the first layout pass
407
+ QTimer.singleShot(0, self._refresh_plus_button)
408
+
409
+ # tab bar visible even when empty
410
+ if hasattr(self, "setTabBarAutoHide"):
411
+ self.setTabBarAutoHide(False)
412
+
413
+ # IMPORTANT: reserve vertical space for the bar even with 0 tabs
414
+ # (prevents the whole widget from collapsing)
415
+ mh = max(self.tabBar().minimumSizeHint().height() + 2, 30) # +2
416
+ self.setMinimumHeight(mh)
417
+
418
+ # connect signals
419
+ self.currentChanged.connect(self._on_current_changed)
420
+ self.tabBarClicked.connect(self._on_tabbar_clicked)
421
+ self.tabBarDoubleClicked.connect(self._on_tabbar_dbl_clicked)
422
+ self.tabCloseRequested.connect(self._on_tab_close_requested)
423
+ self.tabBar().tabMoved.connect(self._on_tab_moved)
424
+
425
+ def set_active(self, active: bool):
426
+ """
427
+ Set the active state of the tab bar.
428
+
429
+ :param active: True to activate, False to deactivate
430
+ """
431
+ self.active = active
432
+
433
+ def _refresh_plus_button(self):
434
+ """Force the tab bar to recompute [+] placement after tab changes."""
435
+ tb = self.tabBar()
436
+ if hasattr(tb, "updateAddButtonPlacement"):
437
+ tb.updateAddButtonPlacement()
438
+
439
+ def addTab(self, *args, **kwargs):
440
+ """Add a new tab and refresh [+] placement."""
441
+ idx = super().addTab(*args, **kwargs)
442
+ QTimer.singleShot(0, self._refresh_plus_button) # defer until layout is done
443
+ return idx
444
+
445
+ def insertTab(self, *args, **kwargs):
446
+ """Insert a new tab at a specific index and refresh [+] placement."""
447
+ idx = super().insertTab(*args, **kwargs)
448
+ QTimer.singleShot(0, self._refresh_plus_button)
449
+ return idx
450
+
451
+ def removeTab(self, index):
452
+ """Remove a tab and refresh [+] placement."""
453
+ super().removeTab(index)
454
+ QTimer.singleShot(0, self._refresh_plus_button)
455
+
456
+ def setTabText(self, index: int, text: str):
457
+ """Set tab text and refresh [+] placement."""
458
+ super().setTabText(index, text)
459
+ QTimer.singleShot(0, self._refresh_plus_button)
460
+
461
+ def clear(self):
462
+ """Clear all tabs and refresh [+] placement."""
463
+ super().clear()
464
+ QTimer.singleShot(0, self._refresh_plus_button)
131
465
 
132
466
  def get_column(self):
133
467
  """
@@ -165,7 +499,7 @@ class OutputTabs(QTabWidget):
165
499
  elif tab.type == Tab.TAB_TOOL:
166
500
  self.show_tool_menu(idx, column_idx, event.globalPos()) # tool
167
501
  else:
168
- self.show_default_menu(idx, column_idx, event.globalPos()) # default
502
+ self.show_default_menu(idx, column_idx, event.globalPos()) # default
169
503
  super(OutputTabs, self).mousePressEvent(event)
170
504
 
171
505
  def prepare_menu(self, index: int, column_idx: int) -> QMenu:
@@ -177,44 +511,40 @@ class OutputTabs(QTabWidget):
177
511
  :return: menu
178
512
  """
179
513
  menu = QMenu(self)
514
+ menu.setAttribute(Qt.WA_DeleteOnClose, True)
180
515
 
181
- actions = {}
182
- actions['add_chat'] = QAction(QIcon(":/icons/add.svg"), trans('action.tab.add.chat'), self)
183
- actions['add_chat'].triggered.connect(
516
+ add_chat = QAction(icon(ICON_PATH_ADD), trans('action.tab.add.chat'), menu)
517
+ add_chat.triggered.connect(
184
518
  lambda: self.add_tab(index, column_idx, Tab.TAB_CHAT)
185
519
  )
186
- actions['add_notepad'] = QAction(QIcon(":/icons/add.svg"), trans('action.tab.add.notepad'), self)
187
- actions['add_notepad'].triggered.connect(
520
+ add_notepad = QAction(icon(ICON_PATH_ADD), trans('action.tab.add.notepad'), menu)
521
+ add_notepad.triggered.connect(
188
522
  lambda: self.add_tab(index, column_idx, Tab.TAB_NOTEPAD)
189
523
  )
190
- actions['edit'] = QAction(QIcon(":/icons/edit.svg"), trans('action.rename'), self)
191
- actions['edit'].triggered.connect(
524
+ edit = QAction(icon(ICON_PATH_EDIT), trans('action.rename'), menu)
525
+ edit.triggered.connect(
192
526
  lambda: self.rename_tab(index, column_idx)
193
527
  )
194
- actions['move_right'] = QAction(QIcon(":/icons/forward"), trans('action.tab.move.right'), self)
195
- actions['move_right'].triggered.connect(
528
+ move_right = QAction(icon(ICON_PATH_FORWARD), trans('action.tab.move.right'), menu)
529
+ move_right.triggered.connect(
196
530
  lambda: self.window.controller.ui.tabs.move_tab(index, column_idx, 1)
197
531
  )
198
- actions['move_left'] = QAction(QIcon(":/icons/back"), trans('action.tab.move.left'), self)
199
- actions['move_left'].triggered.connect(
532
+ move_left = QAction(icon(ICON_PATH_BACK), trans('action.tab.move.left'), menu)
533
+ move_left.triggered.connect(
200
534
  lambda: self.window.controller.ui.tabs.move_tab(index, column_idx, 0)
201
535
  )
202
536
 
203
- # add chat, add notepad
204
- menu.addAction(actions['add_chat'])
205
- menu.addAction(actions['add_notepad'])
537
+ menu.addAction(add_chat)
538
+ menu.addAction(add_notepad)
206
539
 
207
- # add tools submenu
208
540
  self.window.controller.tools.append_tab_menu(self, menu, index, column_idx, self)
209
541
 
210
- # rename tab
211
- menu.addAction(actions['edit'])
542
+ menu.addAction(edit)
212
543
 
213
- # move tab left, move tab right
214
544
  if column_idx != 0:
215
- menu.addAction(actions['move_left'])
545
+ menu.addAction(move_left)
216
546
  if column_idx != 1:
217
- menu.addAction(actions['move_right'])
547
+ menu.addAction(move_right)
218
548
 
219
549
  return menu
220
550
 
@@ -227,19 +557,18 @@ class OutputTabs(QTabWidget):
227
557
  :param global_pos: global position
228
558
  """
229
559
  context_menu = self.prepare_menu(index, column_idx)
230
- actions = {}
231
- actions['close'] = QAction(QIcon(":/icons/close.svg"), trans('action.tab.close'), self)
232
- actions['close'].triggered.connect(
560
+ close_act = QAction(icon(ICON_PATH_CLOSE), trans('action.tab.close'), context_menu)
561
+ close_act.triggered.connect(
233
562
  lambda: self.close_tab(index, column_idx)
234
563
  )
235
- actions['close_all'] = QAction(QIcon(":/icons/close.svg"), trans('action.tab.close_all.notepad'), self)
236
- actions['close_all'].triggered.connect(
564
+ close_all_act = QAction(icon(ICON_PATH_CLOSE), trans('action.tab.close_all.notepad'), context_menu)
565
+ close_all_act.triggered.connect(
237
566
  lambda: self.close_all(Tab.TAB_NOTEPAD, column_idx)
238
567
  )
239
- context_menu.addAction(actions['close'])
568
+ context_menu.addAction(close_act)
240
569
 
241
570
  if self.window.core.tabs.count_by_type(Tab.TAB_NOTEPAD) > 1:
242
- context_menu.addAction(actions['close_all'])
571
+ context_menu.addAction(close_all_act)
243
572
 
244
573
  context_menu.exec(global_pos)
245
574
 
@@ -252,20 +581,19 @@ class OutputTabs(QTabWidget):
252
581
  :param global_pos: global position
253
582
  """
254
583
  context_menu = self.prepare_menu(index, column_idx)
255
- actions = {}
256
- actions['close'] = QAction(QIcon(":/icons/close.svg"), trans('action.tab.close'), self)
257
- actions['close'].triggered.connect(
584
+ close_act = QAction(icon(ICON_PATH_CLOSE), trans('action.tab.close'), context_menu)
585
+ close_act.triggered.connect(
258
586
  lambda: self.close_tab(index, column_idx)
259
587
  )
260
- actions['close_all'] = QAction(QIcon(":/icons/close.svg"), trans('action.tab.close_all.chat'), self)
261
- actions['close_all'].triggered.connect(
588
+ close_all_act = QAction(icon(ICON_PATH_CLOSE), trans('action.tab.close_all.chat'), context_menu)
589
+ close_all_act.triggered.connect(
262
590
  lambda: self.close_all(Tab.TAB_CHAT, column_idx)
263
591
  )
264
592
 
265
593
  # at least one chat tab must be open
266
594
  if self.window.core.tabs.count_by_type(Tab.TAB_CHAT) > 1:
267
- context_menu.addAction(actions['close'])
268
- context_menu.addAction(actions['close_all'])
595
+ context_menu.addAction(close_act)
596
+ context_menu.addAction(close_all_act)
269
597
 
270
598
  context_menu.exec(global_pos)
271
599
 
@@ -278,12 +606,11 @@ class OutputTabs(QTabWidget):
278
606
  :param global_pos: global position
279
607
  """
280
608
  context_menu = self.prepare_menu(index, column_idx)
281
- actions = {}
282
- actions['refresh'] = QAction(QIcon(":/icons/reload.svg"), trans('action.refresh'), self)
283
- actions['refresh'].triggered.connect(
609
+ refresh = QAction(icon(ICON_PATH_RELOAD), trans('action.refresh'), context_menu)
610
+ refresh.triggered.connect(
284
611
  lambda: self.window.controller.files.update_explorer()
285
612
  )
286
- context_menu.addAction(actions['refresh'])
613
+ context_menu.addAction(refresh)
287
614
  context_menu.exec(global_pos)
288
615
 
289
616
  def show_tool_menu(self, index: int, column_idx: int, global_pos):
@@ -295,12 +622,11 @@ class OutputTabs(QTabWidget):
295
622
  :param global_pos: global position
296
623
  """
297
624
  context_menu = self.prepare_menu(index, column_idx)
298
- actions = {}
299
- actions['close'] = QAction(QIcon(":/icons/close.svg"), trans('action.tab.close'), self)
300
- actions['close'].triggered.connect(
625
+ close_act = QAction(icon(ICON_PATH_CLOSE), trans('action.tab.close'), context_menu)
626
+ close_act.triggered.connect(
301
627
  lambda: self.close_tab(index, column_idx)
302
628
  )
303
- context_menu.addAction(actions['close'])
629
+ context_menu.addAction(close_act)
304
630
  context_menu.exec(global_pos)
305
631
 
306
632
  def show_default_menu(self, index: int, column_idx: int, global_pos):
@@ -314,6 +640,32 @@ class OutputTabs(QTabWidget):
314
640
  context_menu = self.prepare_menu(index, column_idx)
315
641
  context_menu.exec(global_pos)
316
642
 
643
+ @Slot(int)
644
+ def _on_current_changed(self, _idx: int):
645
+ """On current tab changed"""
646
+ self.window.controller.ui.tabs.on_tab_changed(self.currentIndex(), self.column.get_idx())
647
+
648
+ @Slot(int)
649
+ def _on_tabbar_clicked(self, _idx: int):
650
+ """On tab bar clicked"""
651
+ self.window.controller.ui.tabs.on_tab_clicked(self.currentIndex(), self.column.get_idx())
652
+
653
+ @Slot(int)
654
+ def _on_tabbar_dbl_clicked(self, _idx: int):
655
+ """On tab bar double clicked"""
656
+ self.window.controller.ui.tabs.on_tab_dbl_clicked(self.currentIndex(), self.column.get_idx())
657
+
658
+ @Slot(int)
659
+ def _on_tab_close_requested(self, _idx: int):
660
+ """On tab close requested"""
661
+ self.window.controller.ui.tabs.on_tab_closed(self.currentIndex(), self.column.get_idx())
662
+ QTimer.singleShot(0, self._refresh_plus_button) # defer until layout is done
663
+
664
+ @Slot(int, int)
665
+ def _on_tab_moved(self, _from: int, _to: int):
666
+ """On tab moved"""
667
+ self.window.controller.ui.tabs.on_tab_moved(self.currentIndex(), self.column.get_idx())
668
+
317
669
  @Slot()
318
670
  def rename_tab(self, index: int, column_idx: int):
319
671
  """
@@ -354,9 +706,14 @@ class OutputTabs(QTabWidget):
354
706
  :param type: type
355
707
  :param tool_id: tool id
356
708
  """
709
+ if index == -2: # new btn
710
+ index = self.window.core.tabs.get_max_idx_by_column(column_idx)
711
+ if index == -1:
712
+ index = 0
713
+
357
714
  self.window.controller.ui.tabs.append(
358
715
  type=type,
359
716
  tool_id=tool_id,
360
717
  idx=index,
361
718
  column_idx=column_idx,
362
- )
719
+ )
@@ -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 02:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
@@ -16,7 +16,6 @@ from PySide6.QtWidgets import QTextEdit
16
16
  from pygpt_net.core.tabs.tab import Tab
17
17
  from pygpt_net.core.text.finder import Finder
18
18
  from pygpt_net.utils import trans
19
- import pygpt_net.icons_rc
20
19
 
21
20
 
22
21
  class CalendarNote(QTextEdit):