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.
- pygpt_net/CHANGELOG.txt +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +3 -1
- pygpt_net/controller/__init__.py +4 -8
- pygpt_net/controller/access/voice.py +2 -2
- pygpt_net/controller/agent/llama.py +3 -0
- pygpt_net/controller/assistant/batch.py +2 -3
- pygpt_net/controller/assistant/editor.py +2 -2
- pygpt_net/controller/assistant/files.py +2 -3
- pygpt_net/controller/assistant/store.py +2 -2
- pygpt_net/controller/audio/audio.py +2 -2
- pygpt_net/controller/chat/response.py +4 -0
- pygpt_net/controller/ctx/ctx.py +2 -1
- pygpt_net/controller/files/files.py +24 -55
- pygpt_net/controller/idx/indexer.py +85 -76
- pygpt_net/controller/lang/lang.py +52 -34
- pygpt_net/controller/model/importer.py +2 -2
- pygpt_net/controller/notepad/notepad.py +86 -84
- pygpt_net/controller/plugins/settings.py +3 -4
- pygpt_net/controller/settings/profile.py +105 -124
- pygpt_net/controller/theme/menu.py +154 -57
- pygpt_net/controller/theme/nodes.py +51 -44
- pygpt_net/controller/theme/theme.py +33 -9
- pygpt_net/controller/tools/tools.py +2 -2
- pygpt_net/controller/ui/tabs.py +2 -3
- pygpt_net/core/agents/observer/evaluation.py +2 -2
- pygpt_net/core/agents/runners/loop.py +1 -0
- pygpt_net/core/bridge/bridge.py +2 -0
- pygpt_net/core/ctx/container.py +13 -12
- pygpt_net/core/ctx/output.py +7 -4
- pygpt_net/core/debug/console/console.py +2 -2
- pygpt_net/core/filesystem/actions.py +1 -2
- pygpt_net/core/filesystem/opener.py +261 -0
- pygpt_net/core/filesystem/url.py +13 -10
- pygpt_net/core/platforms/platforms.py +5 -4
- pygpt_net/core/render/plain/helpers.py +2 -5
- pygpt_net/core/render/plain/renderer.py +26 -30
- pygpt_net/core/render/web/body.py +1 -1
- pygpt_net/core/settings/settings.py +43 -13
- pygpt_net/core/tabs/tabs.py +20 -13
- pygpt_net/data/config/config.json +4 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/css/web-blocks.dark.css +7 -1
- pygpt_net/data/css/web-blocks.light.css +5 -2
- pygpt_net/data/css/web-chatgpt.dark.css +7 -1
- pygpt_net/data/css/web-chatgpt.light.css +3 -0
- pygpt_net/data/css/web-chatgpt_wide.dark.css +7 -1
- pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
- pygpt_net/data/locale/locale.de.ini +5 -1
- pygpt_net/data/locale/locale.en.ini +5 -1
- pygpt_net/data/locale/locale.es.ini +5 -1
- pygpt_net/data/locale/locale.fr.ini +5 -1
- pygpt_net/data/locale/locale.it.ini +5 -1
- pygpt_net/data/locale/locale.pl.ini +6 -4
- pygpt_net/data/locale/locale.uk.ini +5 -1
- pygpt_net/data/locale/locale.zh.ini +5 -1
- pygpt_net/plugin/twitter/plugin.py +2 -2
- pygpt_net/provider/core/config/patch.py +12 -1
- pygpt_net/tools/audio_transcriber/ui/dialogs.py +44 -54
- pygpt_net/tools/code_interpreter/body.py +1 -2
- pygpt_net/tools/code_interpreter/tool.py +7 -4
- pygpt_net/tools/code_interpreter/ui/html.py +1 -3
- pygpt_net/tools/code_interpreter/ui/widgets.py +2 -3
- pygpt_net/tools/html_canvas/ui/widgets.py +1 -3
- pygpt_net/tools/image_viewer/ui/dialogs.py +40 -37
- pygpt_net/tools/indexer/ui/widgets.py +2 -4
- pygpt_net/tools/media_player/tool.py +2 -5
- pygpt_net/tools/media_player/ui/widgets.py +60 -36
- pygpt_net/tools/text_editor/ui/widgets.py +18 -19
- pygpt_net/tools/translator/ui/widgets.py +39 -35
- pygpt_net/ui/base/context_menu.py +9 -4
- pygpt_net/ui/dialog/db.py +1 -3
- pygpt_net/ui/dialog/models.py +1 -3
- pygpt_net/ui/dialog/models_importer.py +2 -4
- pygpt_net/ui/dialogs.py +34 -30
- pygpt_net/ui/layout/chat/attachments.py +72 -84
- pygpt_net/ui/layout/chat/attachments_ctx.py +40 -44
- pygpt_net/ui/layout/chat/attachments_uploaded.py +36 -39
- pygpt_net/ui/layout/chat/calendar.py +100 -70
- pygpt_net/ui/layout/chat/chat.py +23 -17
- pygpt_net/ui/layout/chat/input.py +95 -118
- pygpt_net/ui/layout/chat/output.py +100 -162
- pygpt_net/ui/layout/chat/painter.py +89 -61
- pygpt_net/ui/layout/ctx/ctx_list.py +43 -52
- pygpt_net/ui/layout/status.py +23 -14
- pygpt_net/ui/layout/toolbox/agent.py +27 -38
- pygpt_net/ui/layout/toolbox/agent_llama.py +41 -45
- pygpt_net/ui/layout/toolbox/assistants.py +42 -38
- pygpt_net/ui/layout/toolbox/computer_env.py +32 -23
- pygpt_net/ui/layout/toolbox/footer.py +13 -16
- pygpt_net/ui/layout/toolbox/image.py +18 -21
- pygpt_net/ui/layout/toolbox/indexes.py +46 -89
- pygpt_net/ui/layout/toolbox/mode.py +20 -7
- pygpt_net/ui/layout/toolbox/model.py +12 -10
- pygpt_net/ui/layout/toolbox/presets.py +68 -52
- pygpt_net/ui/layout/toolbox/prompt.py +31 -58
- pygpt_net/ui/layout/toolbox/toolbox.py +25 -21
- pygpt_net/ui/layout/toolbox/vision.py +20 -22
- pygpt_net/ui/main.py +2 -4
- pygpt_net/ui/menu/about.py +64 -84
- pygpt_net/ui/menu/audio.py +87 -63
- pygpt_net/ui/menu/config.py +121 -127
- pygpt_net/ui/menu/debug.py +69 -76
- pygpt_net/ui/menu/file.py +32 -35
- pygpt_net/ui/menu/menu.py +2 -3
- pygpt_net/ui/menu/plugins.py +69 -33
- pygpt_net/ui/menu/theme.py +45 -46
- pygpt_net/ui/menu/tools.py +56 -60
- pygpt_net/ui/menu/video.py +20 -25
- pygpt_net/ui/tray.py +1 -2
- pygpt_net/ui/widget/audio/bar.py +1 -3
- pygpt_net/ui/widget/audio/input_button.py +3 -4
- pygpt_net/ui/widget/calendar/select.py +1 -2
- pygpt_net/ui/widget/dialog/base.py +12 -9
- pygpt_net/ui/widget/dialog/editor_file.py +20 -23
- pygpt_net/ui/widget/dialog/find.py +25 -24
- pygpt_net/ui/widget/dialog/profile.py +57 -53
- pygpt_net/ui/widget/draw/painter.py +62 -93
- pygpt_net/ui/widget/element/button.py +42 -30
- pygpt_net/ui/widget/element/checkbox.py +23 -15
- pygpt_net/ui/widget/element/group.py +6 -5
- pygpt_net/ui/widget/element/labels.py +1 -2
- pygpt_net/ui/widget/filesystem/explorer.py +93 -102
- pygpt_net/ui/widget/image/display.py +1 -2
- pygpt_net/ui/widget/lists/assistant.py +1 -2
- pygpt_net/ui/widget/lists/attachment.py +1 -2
- pygpt_net/ui/widget/lists/attachment_ctx.py +1 -2
- pygpt_net/ui/widget/lists/context.py +2 -4
- pygpt_net/ui/widget/lists/index.py +1 -2
- pygpt_net/ui/widget/lists/model.py +1 -2
- pygpt_net/ui/widget/lists/model_editor.py +1 -2
- pygpt_net/ui/widget/lists/model_importer.py +1 -2
- pygpt_net/ui/widget/lists/preset.py +1 -2
- pygpt_net/ui/widget/lists/preset_plugins.py +1 -2
- pygpt_net/ui/widget/lists/profile.py +1 -2
- pygpt_net/ui/widget/lists/uploaded.py +1 -2
- pygpt_net/ui/widget/option/checkbox.py +2 -4
- pygpt_net/ui/widget/option/checkbox_list.py +1 -4
- pygpt_net/ui/widget/option/cmd.py +1 -4
- pygpt_net/ui/widget/option/dictionary.py +25 -28
- pygpt_net/ui/widget/option/input.py +1 -3
- pygpt_net/ui/widget/tabs/Input.py +16 -12
- pygpt_net/ui/widget/tabs/body.py +5 -3
- pygpt_net/ui/widget/tabs/layout.py +41 -28
- pygpt_net/ui/widget/tabs/output.py +442 -85
- pygpt_net/ui/widget/textarea/calendar_note.py +1 -2
- pygpt_net/ui/widget/textarea/editor.py +41 -73
- pygpt_net/ui/widget/textarea/find.py +11 -10
- pygpt_net/ui/widget/textarea/html.py +3 -6
- pygpt_net/ui/widget/textarea/input.py +134 -69
- pygpt_net/ui/widget/textarea/notepad.py +54 -38
- pygpt_net/ui/widget/textarea/output.py +65 -54
- pygpt_net/ui/widget/textarea/search_input.py +5 -4
- pygpt_net/ui/widget/textarea/web.py +2 -4
- pygpt_net/ui/widget/vision/camera.py +2 -31
- {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/METADATA +38 -174
- {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/RECORD +160 -159
- {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/WHEEL +0 -0
- {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:
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
lambda: self.tabs.add_tab(
|
|
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
|
-
|
|
80
|
-
menu.addAction(
|
|
81
|
-
menu.addAction(actions['add_notepad'])
|
|
359
|
+
menu.addAction(add_chat)
|
|
360
|
+
menu.addAction(add_notepad)
|
|
82
361
|
|
|
83
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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())
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
menu.addAction(
|
|
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
|
-
|
|
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(
|
|
545
|
+
menu.addAction(move_left)
|
|
216
546
|
if column_idx != 1:
|
|
217
|
-
menu.addAction(
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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(
|
|
268
|
-
context_menu.addAction(
|
|
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
|
-
|
|
282
|
-
|
|
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(
|
|
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
|
-
|
|
299
|
-
|
|
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(
|
|
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.
|
|
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):
|