pygpt-net 2.7.5__py3-none-any.whl → 2.7.6__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 +8 -0
- pygpt_net/__init__.py +2 -2
- pygpt_net/controller/chat/handler/worker.py +9 -31
- pygpt_net/controller/chat/handler/xai_stream.py +621 -52
- pygpt_net/controller/debug/fixtures.py +3 -2
- pygpt_net/controller/files/files.py +65 -4
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/render/web/body.py +3 -2
- pygpt_net/core/types/chunk.py +27 -0
- pygpt_net/data/config/config.json +2 -2
- pygpt_net/data/config/models.json +2 -2
- pygpt_net/data/config/settings.json +1 -1
- pygpt_net/data/js/app/template.js +1 -1
- pygpt_net/data/js/app.min.js +2 -2
- pygpt_net/data/locale/locale.de.ini +3 -0
- pygpt_net/data/locale/locale.en.ini +3 -0
- pygpt_net/data/locale/locale.es.ini +3 -0
- pygpt_net/data/locale/locale.fr.ini +3 -0
- pygpt_net/data/locale/locale.it.ini +3 -0
- pygpt_net/data/locale/locale.pl.ini +3 -0
- pygpt_net/data/locale/locale.uk.ini +3 -0
- pygpt_net/data/locale/locale.zh.ini +3 -0
- pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +2 -2
- pygpt_net/item/ctx.py +3 -5
- pygpt_net/js_rc.py +2449 -2447
- pygpt_net/plugin/cmd_mouse_control/config.py +8 -7
- pygpt_net/plugin/cmd_mouse_control/plugin.py +3 -4
- pygpt_net/provider/api/anthropic/__init__.py +10 -8
- pygpt_net/provider/api/google/__init__.py +6 -5
- pygpt_net/provider/api/google/chat.py +1 -2
- pygpt_net/provider/api/openai/__init__.py +7 -3
- pygpt_net/provider/api/openai/responses.py +0 -0
- pygpt_net/provider/api/x_ai/__init__.py +10 -9
- pygpt_net/provider/api/x_ai/chat.py +272 -102
- pygpt_net/tools/image_viewer/ui/dialogs.py +298 -12
- pygpt_net/tools/text_editor/ui/widgets.py +5 -1
- pygpt_net/ui/base/context_menu.py +44 -1
- pygpt_net/ui/layout/toolbox/indexes.py +22 -19
- pygpt_net/ui/layout/toolbox/model.py +28 -5
- pygpt_net/ui/widget/image/display.py +25 -8
- pygpt_net/ui/widget/tabs/output.py +9 -1
- pygpt_net/ui/widget/textarea/editor.py +14 -1
- pygpt_net/ui/widget/textarea/input.py +20 -7
- pygpt_net/ui/widget/textarea/notepad.py +24 -1
- pygpt_net/ui/widget/textarea/output.py +23 -1
- pygpt_net/ui/widget/textarea/web.py +16 -1
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/METADATA +10 -2
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/RECORD +50 -49
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.5.dist-info → pygpt_net-2.7.6.dist-info}/entry_points.txt +0 -0
|
@@ -6,17 +6,18 @@
|
|
|
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: 2026.01.03 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtCore import Qt, QPoint, QSize, QEvent
|
|
13
13
|
from PySide6.QtGui import QAction, QIcon
|
|
14
|
-
from PySide6.QtWidgets import QMenuBar, QVBoxLayout, QHBoxLayout, QSizePolicy
|
|
14
|
+
from PySide6.QtWidgets import QMenuBar, QVBoxLayout, QHBoxLayout, QSizePolicy, QScrollArea
|
|
15
15
|
|
|
16
16
|
from pygpt_net.ui.widget.dialog.base import BaseDialog
|
|
17
17
|
from pygpt_net.ui.widget.image.display import ImageLabel
|
|
18
18
|
from pygpt_net.utils import trans
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class DialogSpawner:
|
|
21
22
|
def __init__(self, window=None):
|
|
22
23
|
"""
|
|
@@ -44,8 +45,14 @@ class DialogSpawner:
|
|
|
44
45
|
pixmap = ImageLabel(dialog, self.path)
|
|
45
46
|
pixmap.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored))
|
|
46
47
|
|
|
48
|
+
# scrollable container for pixmap
|
|
49
|
+
scroll = QScrollArea(dialog)
|
|
50
|
+
scroll.setWidgetResizable(True)
|
|
51
|
+
scroll.setAlignment(Qt.AlignCenter)
|
|
52
|
+
scroll.setWidget(pixmap)
|
|
53
|
+
|
|
47
54
|
row = QHBoxLayout()
|
|
48
|
-
row.addWidget(
|
|
55
|
+
row.addWidget(scroll)
|
|
49
56
|
|
|
50
57
|
layout = QVBoxLayout()
|
|
51
58
|
layout.addLayout(row)
|
|
@@ -53,6 +60,10 @@ class DialogSpawner:
|
|
|
53
60
|
dialog.append_layout(layout)
|
|
54
61
|
dialog.source = source
|
|
55
62
|
dialog.pixmap = pixmap
|
|
63
|
+
dialog.scroll_area = scroll
|
|
64
|
+
|
|
65
|
+
# install event filter for wheel zoom and panning and cursor changes
|
|
66
|
+
dialog.scroll_area.viewport().installEventFilter(dialog)
|
|
56
67
|
|
|
57
68
|
return dialog
|
|
58
69
|
|
|
@@ -74,13 +85,32 @@ class ImageViewerDialog(BaseDialog):
|
|
|
74
85
|
self.actions = {}
|
|
75
86
|
self.source = None
|
|
76
87
|
self.pixmap = None
|
|
88
|
+
self.scroll_area = None
|
|
89
|
+
|
|
90
|
+
# cache for image change / resize handling
|
|
77
91
|
self._last_src_key = 0
|
|
78
92
|
self._last_target_size = None
|
|
93
|
+
|
|
94
|
+
# icons
|
|
79
95
|
self._icon_add = QIcon(":/icons/add.svg")
|
|
80
96
|
self._icon_folder = QIcon(":/icons/folder.svg")
|
|
81
97
|
self._icon_save = QIcon(":/icons/save.svg")
|
|
82
98
|
self._icon_logout = QIcon(":/icons/logout.svg")
|
|
83
99
|
|
|
100
|
+
# zoom / pan state
|
|
101
|
+
self._zoom_mode = 'fit' # 'fit' or 'manual'
|
|
102
|
+
self._zoom_factor = 1.0 # current manual factor (image space)
|
|
103
|
+
self._fit_factor = 1.0 # computed on-the-fly for fit mode
|
|
104
|
+
self._min_zoom = 0.05
|
|
105
|
+
self._max_zoom = 16.0
|
|
106
|
+
self._zoom_step = 1.25
|
|
107
|
+
self._drag_active = False
|
|
108
|
+
self._drag_last_pos = QPoint()
|
|
109
|
+
|
|
110
|
+
# performance guards to prevent extremely huge widget sizes
|
|
111
|
+
self._max_widget_dim = 32768 # max single dimension (px) for the view widget
|
|
112
|
+
self._max_total_pixels = 80_000_000 # max total pixels of the view widget (about 80MP)
|
|
113
|
+
|
|
84
114
|
def append_layout(self, layout):
|
|
85
115
|
"""
|
|
86
116
|
Update layout
|
|
@@ -98,18 +128,30 @@ class ImageViewerDialog(BaseDialog):
|
|
|
98
128
|
"""
|
|
99
129
|
src = self.source.pixmap() if self.source is not None else None
|
|
100
130
|
if src and not src.isNull() and self.pixmap is not None:
|
|
101
|
-
target_size = self.pixmap.size()
|
|
102
131
|
key = src.cacheKey()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
# reset to fit mode on new image
|
|
133
|
+
if key != self._last_src_key:
|
|
134
|
+
self._zoom_mode = 'fit'
|
|
135
|
+
self._zoom_factor = 1.0
|
|
136
|
+
if self.scroll_area is not None:
|
|
137
|
+
self.scroll_area.setWidgetResizable(True)
|
|
138
|
+
# ensure no distortion in fit mode
|
|
139
|
+
if self.pixmap is not None:
|
|
140
|
+
self.pixmap.setScaledContents(False)
|
|
141
|
+
|
|
142
|
+
if self._zoom_mode == 'fit':
|
|
143
|
+
target_size = self._viewport_size()
|
|
144
|
+
if key != self._last_src_key or target_size != self._last_target_size:
|
|
145
|
+
# scale to viewport while keeping aspect ratio, smooth transform
|
|
146
|
+
scaled = src.scaled(
|
|
106
147
|
target_size,
|
|
107
148
|
Qt.KeepAspectRatio,
|
|
108
149
|
Qt.SmoothTransformation
|
|
109
150
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
self.pixmap.setPixmap(scaled)
|
|
152
|
+
self._fit_factor = self._compute_fit_factor(src.size(), target_size)
|
|
153
|
+
self._last_src_key = key
|
|
154
|
+
self._last_target_size = target_size
|
|
113
155
|
super(ImageViewerDialog, self).resizeEvent(event)
|
|
114
156
|
|
|
115
157
|
def setup_menu(self) -> QMenuBar:
|
|
@@ -152,4 +194,248 @@ class ImageViewerDialog(BaseDialog):
|
|
|
152
194
|
self.file_menu.addAction(self.actions["save_as"])
|
|
153
195
|
self.file_menu.addAction(self.actions["exit"])
|
|
154
196
|
|
|
155
|
-
return self.menu_bar
|
|
197
|
+
return self.menu_bar
|
|
198
|
+
|
|
199
|
+
# =========================
|
|
200
|
+
# Zoom / pan implementation
|
|
201
|
+
# =========================
|
|
202
|
+
|
|
203
|
+
def eventFilter(self, obj, event):
|
|
204
|
+
"""
|
|
205
|
+
Handle wheel zoom, panning and cursor changes on the scroll area viewport.
|
|
206
|
+
"""
|
|
207
|
+
if self.scroll_area is None or obj is not self.scroll_area.viewport():
|
|
208
|
+
return super(ImageViewerDialog, self).eventFilter(obj, event)
|
|
209
|
+
|
|
210
|
+
et = event.type()
|
|
211
|
+
|
|
212
|
+
# Always show grab cursor when mouse is over the image area
|
|
213
|
+
if et == QEvent.Enter:
|
|
214
|
+
if self._has_image():
|
|
215
|
+
self.scroll_area.viewport().setCursor(Qt.OpenHandCursor)
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
if et == QEvent.Leave:
|
|
219
|
+
self.scroll_area.viewport().unsetCursor()
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
if et == QEvent.MouseButtonPress:
|
|
223
|
+
if event.button() == Qt.LeftButton and self._can_drag():
|
|
224
|
+
self._drag_active = True
|
|
225
|
+
self._drag_last_pos = self._event_pos(event)
|
|
226
|
+
self.scroll_area.viewport().setCursor(Qt.ClosedHandCursor)
|
|
227
|
+
event.accept()
|
|
228
|
+
return True
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
if et == QEvent.MouseMove:
|
|
232
|
+
if self._drag_active:
|
|
233
|
+
pos = self._event_pos(event)
|
|
234
|
+
delta = pos - self._drag_last_pos
|
|
235
|
+
self._drag_last_pos = pos
|
|
236
|
+
# move scrollbars opposite to mouse movement
|
|
237
|
+
hbar = self.scroll_area.horizontalScrollBar()
|
|
238
|
+
vbar = self.scroll_area.verticalScrollBar()
|
|
239
|
+
hbar.setValue(hbar.value() - delta.x())
|
|
240
|
+
vbar.setValue(vbar.value() - delta.y())
|
|
241
|
+
event.accept()
|
|
242
|
+
return True
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
if et == QEvent.MouseButtonRelease:
|
|
246
|
+
if event.button() == Qt.LeftButton and self._drag_active:
|
|
247
|
+
self._drag_active = False
|
|
248
|
+
# back to grab cursor
|
|
249
|
+
if self._has_image():
|
|
250
|
+
self.scroll_area.viewport().setCursor(Qt.OpenHandCursor)
|
|
251
|
+
event.accept()
|
|
252
|
+
return True
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
if et == QEvent.Wheel:
|
|
256
|
+
# zoom in/out with mouse wheel
|
|
257
|
+
if not self._has_image():
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# start manual zoom from current fit factor if needed
|
|
261
|
+
if self._zoom_mode == 'fit':
|
|
262
|
+
self._fit_factor = self._compute_fit_factor(
|
|
263
|
+
self.source.pixmap().size(),
|
|
264
|
+
self._viewport_size()
|
|
265
|
+
)
|
|
266
|
+
self._zoom_factor = self._fit_factor
|
|
267
|
+
self._zoom_mode = 'manual'
|
|
268
|
+
# switch to manual rendering path: original pixmap + scaled contents
|
|
269
|
+
self.scroll_area.setWidgetResizable(False)
|
|
270
|
+
self.pixmap.setScaledContents(True)
|
|
271
|
+
# ensure we display original image for better performance (no giant intermediate pixmaps)
|
|
272
|
+
self.pixmap.setPixmap(self.source.pixmap())
|
|
273
|
+
|
|
274
|
+
old_w = max(1, self.pixmap.width())
|
|
275
|
+
old_h = max(1, self.pixmap.height())
|
|
276
|
+
|
|
277
|
+
angle = 0
|
|
278
|
+
try:
|
|
279
|
+
angle = event.angleDelta().y()
|
|
280
|
+
if angle == 0:
|
|
281
|
+
angle = event.angleDelta().x()
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
if angle == 0:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
step = self._zoom_step if angle > 0 else (1.0 / self._zoom_step)
|
|
289
|
+
# compute tentative new factor and clamp by hard min/max and size guards
|
|
290
|
+
tentative = self._zoom_factor * step
|
|
291
|
+
tentative = max(self._min_zoom, min(self._max_zoom, tentative))
|
|
292
|
+
# apply size-based guards to avoid extremely huge widget sizes
|
|
293
|
+
tentative = self._clamp_factor_by_size(tentative)
|
|
294
|
+
|
|
295
|
+
if abs(tentative - self._zoom_factor) < 1e-9:
|
|
296
|
+
event.accept()
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
vp_pos = self._event_pos(event)
|
|
300
|
+
hbar = self.scroll_area.horizontalScrollBar()
|
|
301
|
+
vbar = self.scroll_area.verticalScrollBar()
|
|
302
|
+
|
|
303
|
+
# position in content coords before zoom (keep point under cursor stable)
|
|
304
|
+
content_x = hbar.value() + vp_pos.x()
|
|
305
|
+
content_y = vbar.value() + vp_pos.y()
|
|
306
|
+
rx = content_x / float(old_w)
|
|
307
|
+
ry = content_y / float(old_h)
|
|
308
|
+
|
|
309
|
+
self._zoom_factor = tentative
|
|
310
|
+
self._set_scaled_pixmap_by_factor(self._zoom_factor)
|
|
311
|
+
|
|
312
|
+
new_w = max(1, self.pixmap.width())
|
|
313
|
+
new_h = max(1, self.pixmap.height())
|
|
314
|
+
|
|
315
|
+
hbar.setValue(int(rx * new_w - vp_pos.x()))
|
|
316
|
+
vbar.setValue(int(ry * new_h - vp_pos.y()))
|
|
317
|
+
|
|
318
|
+
event.accept()
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
return super(ImageViewerDialog, self).eventFilter(obj, event)
|
|
322
|
+
|
|
323
|
+
def _has_image(self) -> bool:
|
|
324
|
+
"""Check if source image is available."""
|
|
325
|
+
return (
|
|
326
|
+
self.source is not None
|
|
327
|
+
and self.source.pixmap() is not None
|
|
328
|
+
and not self.source.pixmap().isNull()
|
|
329
|
+
and self.pixmap is not None
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _can_drag(self) -> bool:
|
|
333
|
+
"""Allow dragging only when image does not fit into the viewport."""
|
|
334
|
+
if not self._has_image():
|
|
335
|
+
return False
|
|
336
|
+
if self._zoom_mode != 'manual':
|
|
337
|
+
return False
|
|
338
|
+
vp = self._viewport_size()
|
|
339
|
+
return self.pixmap.width() > vp.width() or self.pixmap.height() > vp.height()
|
|
340
|
+
|
|
341
|
+
def _viewport_size(self) -> QSize:
|
|
342
|
+
"""Get current viewport size."""
|
|
343
|
+
if self.scroll_area is not None:
|
|
344
|
+
return self.scroll_area.viewport().size()
|
|
345
|
+
return self.size()
|
|
346
|
+
|
|
347
|
+
def _compute_fit_factor(self, img_size: QSize, target: QSize) -> float:
|
|
348
|
+
"""Compute factor for fitting image into target size."""
|
|
349
|
+
iw = max(1, img_size.width())
|
|
350
|
+
ih = max(1, img_size.height())
|
|
351
|
+
tw = max(1, target.width())
|
|
352
|
+
th = max(1, target.height())
|
|
353
|
+
return min(tw / float(iw), th / float(ih))
|
|
354
|
+
|
|
355
|
+
def _clamp_factor_by_size(self, factor: float) -> float:
|
|
356
|
+
"""
|
|
357
|
+
Clamp zoom factor to avoid creating extremely large widget sizes.
|
|
358
|
+
This keeps interactivity smooth by limiting max resulting dimensions and total pixels.
|
|
359
|
+
"""
|
|
360
|
+
src = self.source.pixmap()
|
|
361
|
+
if not src or src.isNull():
|
|
362
|
+
return factor
|
|
363
|
+
|
|
364
|
+
iw = max(1, src.width())
|
|
365
|
+
ih = max(1, src.height())
|
|
366
|
+
|
|
367
|
+
# only guard when zooming in; zooming out should not be limited by these caps
|
|
368
|
+
if factor <= 1.0:
|
|
369
|
+
return factor
|
|
370
|
+
|
|
371
|
+
# desired size
|
|
372
|
+
dw = iw * factor
|
|
373
|
+
dh = ih * factor
|
|
374
|
+
|
|
375
|
+
scale = 1.0
|
|
376
|
+
|
|
377
|
+
# total pixel cap
|
|
378
|
+
total = dw * dh
|
|
379
|
+
if total > self._max_total_pixels:
|
|
380
|
+
from math import sqrt
|
|
381
|
+
scale = min(scale, sqrt(self._max_total_pixels / float(total)))
|
|
382
|
+
|
|
383
|
+
# dimension caps
|
|
384
|
+
if dw * scale > self._max_widget_dim:
|
|
385
|
+
scale = min(scale, self._max_widget_dim / float(dw))
|
|
386
|
+
if dh * scale > self._max_widget_dim:
|
|
387
|
+
scale = min(scale, self._max_widget_dim / float(dh))
|
|
388
|
+
|
|
389
|
+
if scale < 1.0:
|
|
390
|
+
return max(self._min_zoom, factor * scale)
|
|
391
|
+
return factor
|
|
392
|
+
|
|
393
|
+
def _set_scaled_pixmap_by_factor(self, factor: float):
|
|
394
|
+
"""
|
|
395
|
+
Scale and display the image using provided factor relative to the original image.
|
|
396
|
+
In manual mode this avoids allocating giant intermediate QPixmaps by:
|
|
397
|
+
- drawing the original pixmap;
|
|
398
|
+
- enabling scaled contents;
|
|
399
|
+
- resizing the label to the required size.
|
|
400
|
+
"""
|
|
401
|
+
if not self._has_image():
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
src = self.source.pixmap()
|
|
405
|
+
iw = max(1, src.width())
|
|
406
|
+
ih = max(1, src.height())
|
|
407
|
+
|
|
408
|
+
# target size based on factor (KeepAspectRatio preserved by proportional math)
|
|
409
|
+
new_w = max(1, int(round(iw * factor)))
|
|
410
|
+
new_h = max(1, int(round(ih * factor)))
|
|
411
|
+
|
|
412
|
+
# enforce guards once more to be safe
|
|
413
|
+
guarded_factor = self._clamp_factor_by_size(factor)
|
|
414
|
+
if abs(guarded_factor - factor) > 1e-9:
|
|
415
|
+
new_w = max(1, int(round(iw * guarded_factor)))
|
|
416
|
+
new_h = max(1, int(round(ih * guarded_factor)))
|
|
417
|
+
self._zoom_factor = guarded_factor # keep internal factor in sync
|
|
418
|
+
|
|
419
|
+
if self._zoom_mode == 'manual':
|
|
420
|
+
# ensure manual path uses original pixmap and scaled contents
|
|
421
|
+
if self.pixmap.pixmap() is None or self.pixmap.pixmap().cacheKey() != src.cacheKey():
|
|
422
|
+
self.pixmap.setPixmap(src)
|
|
423
|
+
self.pixmap.setScaledContents(True)
|
|
424
|
+
self.pixmap.resize(new_w, new_h)
|
|
425
|
+
else:
|
|
426
|
+
# fallback (not expected here): keep classic high-quality scaling
|
|
427
|
+
scaled = src.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
428
|
+
self.pixmap.setScaledContents(False)
|
|
429
|
+
self.pixmap.setPixmap(scaled)
|
|
430
|
+
if self.scroll_area is not None:
|
|
431
|
+
self.scroll_area.setWidgetResizable(True)
|
|
432
|
+
|
|
433
|
+
def _event_pos(self, event) -> QPoint:
|
|
434
|
+
"""
|
|
435
|
+
Extract integer QPoint from mouse/touchpad event position (supports QPointF in 6.9+).
|
|
436
|
+
"""
|
|
437
|
+
if hasattr(event, "position"):
|
|
438
|
+
return event.position().toPoint()
|
|
439
|
+
if hasattr(event, "pos"):
|
|
440
|
+
return event.pos()
|
|
441
|
+
return QPoint(0, 0)
|
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.03 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtGui import QAction, QIcon, QKeySequence
|
|
@@ -67,6 +67,10 @@ class TextFileEditor(BaseCodeEditor):
|
|
|
67
67
|
)
|
|
68
68
|
menu.addAction(action)
|
|
69
69
|
|
|
70
|
+
# Add zoom submenu
|
|
71
|
+
zoom_menu = self.window.ui.context_menu.get_zoom_menu(self, "editor", self.value, self.on_zoom_changed)
|
|
72
|
+
menu.addMenu(zoom_menu)
|
|
73
|
+
|
|
70
74
|
action = QAction(self._icon_search, trans('text.context_menu.find'), menu)
|
|
71
75
|
action.triggered.connect(self.find_open)
|
|
72
76
|
action.setShortcut(QKeySequence.StandardKey.Find)
|
|
@@ -6,8 +6,9 @@
|
|
|
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: 2026.01.03 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
|
+
from typing import Union
|
|
11
12
|
|
|
12
13
|
from PySide6.QtWidgets import QMenu
|
|
13
14
|
from PySide6.QtGui import QAction, QIcon
|
|
@@ -24,6 +25,8 @@ class ContextMenu:
|
|
|
24
25
|
_ICON_CODE = QIcon(":/icons/code.svg")
|
|
25
26
|
_ICON_TEXT = QIcon(":/icons/text.svg")
|
|
26
27
|
_ICON_TRANSLATOR = QIcon(":/icons/translate.svg")
|
|
28
|
+
_ICON_ZOOM_IN = QIcon(":/icons/zoom_in.svg")
|
|
29
|
+
_ICON_ZOOM_OUT = QIcon(":/icons/zoom_out.svg")
|
|
27
30
|
|
|
28
31
|
def __init__(self, window=None):
|
|
29
32
|
"""
|
|
@@ -33,6 +36,46 @@ class ContextMenu:
|
|
|
33
36
|
"""
|
|
34
37
|
self.window = window
|
|
35
38
|
|
|
39
|
+
def get_zoom_menu(self, parent, parent_type: str, current_zoom: Union[int, float], callback = None) -> QMenu:
|
|
40
|
+
"""
|
|
41
|
+
Get zoom menu (Zoom In/Out)
|
|
42
|
+
|
|
43
|
+
:param parent: Parent menu
|
|
44
|
+
:param parent_type: Type of textarea ('chat', 'notepad', etc.)
|
|
45
|
+
:param current_zoom: Current zoom level
|
|
46
|
+
:param callback: Callback function on zoom change
|
|
47
|
+
:return: Menu
|
|
48
|
+
"""
|
|
49
|
+
menu = QMenu(trans('context_menu.zoom'), parent)
|
|
50
|
+
if parent_type in ("font_size.input", "font_size", "editor"):
|
|
51
|
+
action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
|
|
52
|
+
new_zoom = current_zoom + 2
|
|
53
|
+
action_zoom_in.triggered.connect(
|
|
54
|
+
lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
|
|
55
|
+
)
|
|
56
|
+
menu.addAction(action_zoom_in)
|
|
57
|
+
action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
|
|
58
|
+
new_zoom = current_zoom - 2
|
|
59
|
+
action_zoom_out.triggered.connect(
|
|
60
|
+
lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
|
|
61
|
+
)
|
|
62
|
+
menu.addAction(action_zoom_out)
|
|
63
|
+
elif parent_type in ("zoom"):
|
|
64
|
+
action_zoom_in = QAction(self._ICON_ZOOM_IN, trans('context_menu.zoom.in'), menu)
|
|
65
|
+
new_zoom = current_zoom + 0.1
|
|
66
|
+
action_zoom_in.triggered.connect(
|
|
67
|
+
lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
|
|
68
|
+
)
|
|
69
|
+
menu.addAction(action_zoom_in)
|
|
70
|
+
action_zoom_out = QAction(self._ICON_ZOOM_OUT, trans('context_menu.zoom.out'), menu)
|
|
71
|
+
new_zoom = current_zoom - 0.1
|
|
72
|
+
action_zoom_out.triggered.connect(
|
|
73
|
+
lambda checked=False, new_zoom=new_zoom: callback(new_zoom)
|
|
74
|
+
)
|
|
75
|
+
menu.addAction(action_zoom_out)
|
|
76
|
+
|
|
77
|
+
return menu
|
|
78
|
+
|
|
36
79
|
def get_copy_to_menu(
|
|
37
80
|
self,
|
|
38
81
|
parent,
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.03 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.
|
|
12
|
+
from PySide6.QtCore import Qt, QSize
|
|
13
|
+
from PySide6.QtGui import QStandardItemModel, QIcon
|
|
13
14
|
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QCheckBox, QSizePolicy
|
|
14
15
|
|
|
15
16
|
from pygpt_net.ui.widget.element.labels import HelpLabel, TitleLabel
|
|
@@ -54,6 +55,14 @@ class Indexes:
|
|
|
54
55
|
"""
|
|
55
56
|
nodes = self.window.ui.nodes
|
|
56
57
|
nodes['indexes.new'] = QPushButton(self._settings_icon, "")
|
|
58
|
+
# Configure compact, borderless settings button aligned to the right
|
|
59
|
+
icon_size = 20
|
|
60
|
+
nodes['indexes.new'].setFlat(True)
|
|
61
|
+
nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
|
|
62
|
+
nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
|
|
63
|
+
nodes['indexes.new'].setFixedSize(icon_size, icon_size)
|
|
64
|
+
nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
65
|
+
nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
|
|
57
66
|
nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
|
|
58
67
|
|
|
59
68
|
nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
|
|
@@ -119,6 +128,14 @@ class Indexes:
|
|
|
119
128
|
nodes['llama_index.mode.select'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
120
129
|
|
|
121
130
|
nodes['indexes.new'] = QPushButton(self._settings_icon, "")
|
|
131
|
+
# Configure compact, borderless settings button for options row
|
|
132
|
+
icon_size = 20
|
|
133
|
+
nodes['indexes.new'].setFlat(True)
|
|
134
|
+
nodes['indexes.new'].setStyleSheet("QPushButton { border: none; background: transparent; padding: 0; }")
|
|
135
|
+
nodes['indexes.new'].setIconSize(QSize(icon_size, icon_size))
|
|
136
|
+
nodes['indexes.new'].setFixedSize(icon_size, icon_size)
|
|
137
|
+
nodes['indexes.new'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
138
|
+
nodes['indexes.new'].setFocusPolicy(Qt.NoFocus)
|
|
122
139
|
nodes['indexes.new'].clicked.connect(self._open_llama_index_settings)
|
|
123
140
|
|
|
124
141
|
nodes['indexes.label'] = TitleLabel(trans("toolbox.indexes.label"))
|
|
@@ -129,6 +146,8 @@ class Indexes:
|
|
|
129
146
|
idx_layout.addWidget(nodes['indexes.select'])
|
|
130
147
|
idx_layout.addWidget(nodes['indexes.new'], alignment=Qt.AlignRight)
|
|
131
148
|
idx_layout.setContentsMargins(0, 0, 0, 10)
|
|
149
|
+
# Ensure the combo takes maximum horizontal space
|
|
150
|
+
idx_layout.setStretch(1, 1)
|
|
132
151
|
idx_widget = QWidget()
|
|
133
152
|
idx_widget.setLayout(idx_layout)
|
|
134
153
|
idx_widget.setMinimumHeight(55)
|
|
@@ -138,6 +157,7 @@ class Indexes:
|
|
|
138
157
|
mode_layout.addWidget(nodes['llama_index.mode.label'])
|
|
139
158
|
mode_layout.addWidget(nodes['llama_index.mode.select'])
|
|
140
159
|
mode_layout.setContentsMargins(0, 0, 0, 10)
|
|
160
|
+
mode_layout.setStretch(1, 1)
|
|
141
161
|
mode_widget = QWidget()
|
|
142
162
|
mode_widget.setLayout(mode_layout)
|
|
143
163
|
mode_widget.setMinimumHeight(55)
|
|
@@ -173,23 +193,6 @@ class Indexes:
|
|
|
173
193
|
if self._last_combo_signature != signature:
|
|
174
194
|
self.window.ui.nodes['indexes.select'].set_keys(combo_keys)
|
|
175
195
|
self._last_combo_signature = signature
|
|
176
|
-
"""
|
|
177
|
-
# store previous selection
|
|
178
|
-
self.window.ui.nodes[self.id].backup_selection()
|
|
179
|
-
|
|
180
|
-
self.window.ui.models[self.id].removeRows(0, self.window.ui.models[self.id].rowCount())
|
|
181
|
-
i = 0
|
|
182
|
-
for item in data:
|
|
183
|
-
self.window.ui.models[self.id].insertRow(i)
|
|
184
|
-
name = item['name']
|
|
185
|
-
index = self.window.ui.models[self.id].index(i, 0)
|
|
186
|
-
self.window.ui.models[self.id].setData(index, "ID: " + item['id'], QtCore.Qt.ToolTipRole)
|
|
187
|
-
self.window.ui.models[self.id].setData(self.window.ui.models[self.id].index(i, 0), name)
|
|
188
|
-
i += 1
|
|
189
|
-
|
|
190
|
-
# restore previous selection
|
|
191
|
-
self.window.ui.nodes[self.id].restore_selection()
|
|
192
|
-
"""
|
|
193
196
|
|
|
194
197
|
def _open_llama_index_settings(self):
|
|
195
198
|
self.window.controller.settings.open_section('llama-index')
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.03 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
|
-
|
|
12
|
-
from PySide6.
|
|
11
|
+
from PySide6.QtCore import Qt, QSize
|
|
12
|
+
from PySide6.QtGui import QIcon
|
|
13
|
+
from PySide6.QtWidgets import QVBoxLayout, QWidget, QPushButton, QHBoxLayout, QSizePolicy
|
|
13
14
|
|
|
14
15
|
from pygpt_net.ui.widget.element.labels import TitleLabel
|
|
15
16
|
from pygpt_net.ui.widget.lists.model_combo import ModelCombo
|
|
@@ -27,6 +28,7 @@ class Model:
|
|
|
27
28
|
self.window = window
|
|
28
29
|
self.id = 'prompt.model'
|
|
29
30
|
self.label_key = f'{self.id}.label'
|
|
31
|
+
self._settings_icon = QIcon(":/icons/settings.svg")
|
|
30
32
|
|
|
31
33
|
def setup(self) -> QWidget:
|
|
32
34
|
"""
|
|
@@ -51,12 +53,33 @@ class Model:
|
|
|
51
53
|
nodes[self.label_key] = label
|
|
52
54
|
|
|
53
55
|
combo = ModelCombo(self.window, self.id)
|
|
56
|
+
# Ensure combo takes maximum horizontal space
|
|
57
|
+
combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
54
58
|
nodes[self.id] = combo
|
|
55
59
|
|
|
60
|
+
nodes['prompt.model.settings'] = QPushButton(self._settings_icon, "")
|
|
61
|
+
# Configure compact, borderless settings button aligned to the right
|
|
62
|
+
icon_size = 20
|
|
63
|
+
nodes['prompt.model.settings'].setFlat(True)
|
|
64
|
+
nodes['prompt.model.settings'].setStyleSheet("QPushButton { border: none; padding: 0; }")
|
|
65
|
+
nodes['prompt.model.settings'].setIconSize(QSize(icon_size, icon_size))
|
|
66
|
+
nodes['prompt.model.settings'].setFixedSize(icon_size, icon_size)
|
|
67
|
+
nodes['prompt.model.settings'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
68
|
+
nodes['prompt.model.settings'].setFocusPolicy(Qt.NoFocus)
|
|
69
|
+
nodes['prompt.model.settings'].clicked.connect(self._open_settings)
|
|
70
|
+
|
|
71
|
+
model_cols = QHBoxLayout()
|
|
72
|
+
model_cols.addWidget(combo, 1) # stretch to take remaining space
|
|
73
|
+
model_cols.addWidget(nodes['prompt.model.settings'], alignment=Qt.AlignRight)
|
|
74
|
+
model_cols.setContentsMargins(0, 0, 0, 0)
|
|
75
|
+
|
|
56
76
|
layout = QVBoxLayout()
|
|
57
77
|
layout.addWidget(label)
|
|
58
|
-
layout.
|
|
78
|
+
layout.addLayout(model_cols)
|
|
59
79
|
layout.addStretch()
|
|
60
80
|
layout.setContentsMargins(2, 5, 5, 5)
|
|
61
81
|
|
|
62
|
-
return layout
|
|
82
|
+
return layout
|
|
83
|
+
|
|
84
|
+
def _open_settings(self):
|
|
85
|
+
self.window.controller.model.editor.open()
|
|
@@ -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:
|
|
9
|
+
# Updated Date: 2026.01.03 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtGui import QAction, QIcon
|
|
@@ -27,6 +27,17 @@ class ImageLabel(QLabel):
|
|
|
27
27
|
self.window = window
|
|
28
28
|
self.path = path
|
|
29
29
|
|
|
30
|
+
def _get_window(self):
|
|
31
|
+
"""
|
|
32
|
+
Get main window instance
|
|
33
|
+
|
|
34
|
+
:return: Window instance
|
|
35
|
+
"""
|
|
36
|
+
if self.window is not None:
|
|
37
|
+
if hasattr(self.window, 'window'):
|
|
38
|
+
return self.window.window
|
|
39
|
+
return self.window
|
|
40
|
+
|
|
30
41
|
def contextMenuEvent(self, event):
|
|
31
42
|
"""
|
|
32
43
|
Context menu event
|
|
@@ -36,6 +47,8 @@ class ImageLabel(QLabel):
|
|
|
36
47
|
if not self.path:
|
|
37
48
|
return
|
|
38
49
|
|
|
50
|
+
win = self._get_window()
|
|
51
|
+
|
|
39
52
|
actions = {}
|
|
40
53
|
use_actions = []
|
|
41
54
|
actions['open'] = QAction(QIcon(":/icons/fullscreen.svg"), trans('img.action.open'), self)
|
|
@@ -64,13 +77,13 @@ class ImageLabel(QLabel):
|
|
|
64
77
|
self,
|
|
65
78
|
)
|
|
66
79
|
actions['use_attachment'].triggered.connect(
|
|
67
|
-
lambda:
|
|
80
|
+
lambda: win.controller.files.use_attachment(self.path),
|
|
68
81
|
)
|
|
69
82
|
use_actions.append(actions['use_attachment'])
|
|
70
83
|
|
|
71
84
|
# use by filetype
|
|
72
|
-
if
|
|
73
|
-
extra_use_actions =
|
|
85
|
+
if win.core.filesystem.actions.has_use(self.path):
|
|
86
|
+
extra_use_actions = win.core.filesystem.actions.get_use(self, self.path)
|
|
74
87
|
for action in extra_use_actions:
|
|
75
88
|
use_actions.append(action)
|
|
76
89
|
|
|
@@ -97,7 +110,8 @@ class ImageLabel(QLabel):
|
|
|
97
110
|
|
|
98
111
|
:param event: mouse event
|
|
99
112
|
"""
|
|
100
|
-
self.
|
|
113
|
+
win = self._get_window()
|
|
114
|
+
win.tools.get("viewer").open(self.path)
|
|
101
115
|
|
|
102
116
|
def action_open_dir(self, event):
|
|
103
117
|
"""
|
|
@@ -105,7 +119,8 @@ class ImageLabel(QLabel):
|
|
|
105
119
|
|
|
106
120
|
:param event: mouse event
|
|
107
121
|
"""
|
|
108
|
-
self.
|
|
122
|
+
win = self._get_window()
|
|
123
|
+
win.tools.get("viewer").open_dir(self.path)
|
|
109
124
|
|
|
110
125
|
def action_save(self, event):
|
|
111
126
|
"""
|
|
@@ -113,7 +128,8 @@ class ImageLabel(QLabel):
|
|
|
113
128
|
|
|
114
129
|
:param event: mouse event
|
|
115
130
|
"""
|
|
116
|
-
self.
|
|
131
|
+
win = self._get_window()
|
|
132
|
+
win.tools.get("viewer").save(self.path)
|
|
117
133
|
|
|
118
134
|
def action_delete(self, event):
|
|
119
135
|
"""
|
|
@@ -121,4 +137,5 @@ class ImageLabel(QLabel):
|
|
|
121
137
|
|
|
122
138
|
:param event: mouse event
|
|
123
139
|
"""
|
|
124
|
-
self.
|
|
140
|
+
win = self._get_window()
|
|
141
|
+
win.tools.get("viewer").delete(self.path)
|