pygpt-net 2.7.4__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 +15 -0
- pygpt_net/__init__.py +4 -4
- pygpt_net/app_core.py +4 -2
- pygpt_net/controller/__init__.py +5 -1
- pygpt_net/controller/assistant/assistant.py +1 -4
- pygpt_net/controller/assistant/batch.py +5 -504
- pygpt_net/controller/assistant/editor.py +5 -5
- pygpt_net/controller/assistant/files.py +16 -16
- pygpt_net/controller/chat/handler/google_stream.py +307 -1
- pygpt_net/controller/chat/handler/worker.py +10 -25
- pygpt_net/controller/chat/handler/xai_stream.py +621 -52
- pygpt_net/controller/chat/image.py +2 -2
- pygpt_net/controller/debug/fixtures.py +3 -2
- pygpt_net/controller/dialogs/confirm.py +73 -101
- pygpt_net/controller/files/files.py +65 -4
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/painter/capture.py +50 -1
- pygpt_net/controller/presets/presets.py +2 -1
- pygpt_net/controller/remote_store/__init__.py +12 -0
- pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
- pygpt_net/controller/remote_store/google/batch.py +402 -0
- pygpt_net/controller/remote_store/google/store.py +615 -0
- pygpt_net/controller/remote_store/openai/__init__.py +12 -0
- pygpt_net/controller/remote_store/openai/batch.py +524 -0
- pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
- pygpt_net/controller/remote_store/remote_store.py +35 -0
- pygpt_net/controller/ui/ui.py +20 -1
- pygpt_net/core/assistants/assistants.py +3 -15
- pygpt_net/core/db/database.py +5 -3
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/locale/placeholder.py +35 -0
- pygpt_net/core/remote_store/__init__.py +12 -0
- pygpt_net/core/remote_store/google/__init__.py +11 -0
- pygpt_net/core/remote_store/google/files.py +224 -0
- pygpt_net/core/remote_store/google/store.py +248 -0
- pygpt_net/core/remote_store/openai/__init__.py +11 -0
- pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
- pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
- pygpt_net/core/remote_store/remote_store.py +24 -0
- pygpt_net/core/render/web/body.py +3 -2
- pygpt_net/core/types/chunk.py +27 -0
- pygpt_net/data/config/config.json +8 -4
- pygpt_net/data/config/models.json +77 -3
- pygpt_net/data/config/settings.json +45 -0
- 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 +44 -41
- pygpt_net/data/locale/locale.en.ini +56 -43
- pygpt_net/data/locale/locale.es.ini +44 -41
- pygpt_net/data/locale/locale.fr.ini +44 -41
- pygpt_net/data/locale/locale.it.ini +44 -41
- pygpt_net/data/locale/locale.pl.ini +45 -42
- pygpt_net/data/locale/locale.uk.ini +44 -41
- pygpt_net/data/locale/locale.zh.ini +44 -41
- pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
- pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
- pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
- pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
- pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
- pygpt_net/item/assistant.py +1 -211
- pygpt_net/item/ctx.py +3 -3
- pygpt_net/item/store.py +238 -0
- pygpt_net/js_rc.py +2449 -2447
- pygpt_net/migrations/Version20260102190000.py +35 -0
- pygpt_net/migrations/__init__.py +3 -1
- pygpt_net/plugin/cmd_mouse_control/config.py +471 -1
- pygpt_net/plugin/cmd_mouse_control/plugin.py +487 -22
- pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
- pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
- pygpt_net/plugin/idx_llama_index/config.py +2 -2
- pygpt_net/provider/api/anthropic/__init__.py +10 -8
- pygpt_net/provider/api/google/__init__.py +21 -58
- pygpt_net/provider/api/google/chat.py +545 -129
- pygpt_net/provider/api/google/computer.py +190 -0
- pygpt_net/provider/api/google/realtime/realtime.py +2 -2
- pygpt_net/provider/api/google/remote_tools.py +93 -0
- pygpt_net/provider/api/google/store.py +546 -0
- pygpt_net/provider/api/google/worker/__init__.py +0 -0
- pygpt_net/provider/api/google/worker/importer.py +392 -0
- pygpt_net/provider/api/openai/__init__.py +7 -3
- pygpt_net/provider/api/openai/computer.py +10 -1
- pygpt_net/provider/api/openai/responses.py +0 -0
- pygpt_net/provider/api/openai/store.py +6 -6
- pygpt_net/provider/api/openai/worker/importer.py +24 -24
- pygpt_net/provider/api/x_ai/__init__.py +10 -9
- pygpt_net/provider/api/x_ai/chat.py +272 -102
- pygpt_net/provider/core/config/patch.py +16 -1
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
- pygpt_net/provider/core/model/patch.py +17 -3
- pygpt_net/provider/core/preset/json_file.py +13 -7
- pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
- pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
- pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
- pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
- pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
- pygpt_net/provider/llms/google.py +2 -2
- pygpt_net/tools/image_viewer/ui/dialogs.py +298 -12
- pygpt_net/tools/text_editor/ui/widgets.py +5 -1
- pygpt_net/ui/base/config_dialog.py +3 -2
- pygpt_net/ui/base/context_menu.py +44 -1
- pygpt_net/ui/dialog/assistant.py +3 -3
- pygpt_net/ui/dialog/plugins.py +3 -1
- pygpt_net/ui/dialog/remote_store_google.py +539 -0
- pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
- pygpt_net/ui/dialogs.py +5 -3
- pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
- pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
- pygpt_net/ui/layout/toolbox/indexes.py +22 -19
- pygpt_net/ui/layout/toolbox/model.py +28 -5
- pygpt_net/ui/menu/tools.py +13 -5
- pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
- pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/image/display.py +25 -8
- pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
- pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
- pygpt_net/ui/widget/option/checkbox_list.py +47 -9
- pygpt_net/ui/widget/option/combo.py +39 -3
- 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.4.dist-info → pygpt_net-2.7.6.dist-info}/METADATA +41 -2
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/RECORD +158 -132
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.4.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,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.01 15:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -20,6 +20,7 @@ from pygpt_net.ui.widget.option.dictionary import OptionDict
|
|
|
20
20
|
from pygpt_net.ui.widget.option.input import OptionInput, PasswordInput
|
|
21
21
|
from pygpt_net.ui.widget.option.slider import OptionSlider
|
|
22
22
|
from pygpt_net.ui.widget.option.textarea import OptionTextarea
|
|
23
|
+
from pygpt_net.core.locale.placeholder import apply as trans_placeholder_apply
|
|
23
24
|
from pygpt_net.utils import trans
|
|
24
25
|
|
|
25
26
|
|
|
@@ -233,7 +234,7 @@ class BaseConfigDialog:
|
|
|
233
234
|
:param text: text (to translate)
|
|
234
235
|
:return: QLabel
|
|
235
236
|
"""
|
|
236
|
-
return DescLabel(trans(text))
|
|
237
|
+
return DescLabel(trans_placeholder_apply(trans(text)))
|
|
237
238
|
|
|
238
239
|
def add_urls(self, urls, align=Qt.AlignLeft) -> QWidget:
|
|
239
240
|
"""
|
|
@@ -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,
|
pygpt_net/ui/dialog/assistant.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.02 19:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -44,9 +44,9 @@ class Assistant(BaseConfigDialog):
|
|
|
44
44
|
|
|
45
45
|
# store
|
|
46
46
|
self.window.ui.nodes['assistant.btn.store.editor'] = QPushButton(QIcon(":/icons/db.svg"), "")
|
|
47
|
-
self.window.ui.nodes['assistant.btn.store.editor'].setToolTip(trans('dialog.
|
|
47
|
+
self.window.ui.nodes['assistant.btn.store.editor'].setToolTip(trans('dialog.remote_store.openai'))
|
|
48
48
|
self.window.ui.nodes['assistant.btn.store.editor'].clicked.connect(
|
|
49
|
-
lambda: self.window.controller.
|
|
49
|
+
lambda: self.window.controller.remote_store.openai.toggle_editor()
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
self.window.ui.nodes['assistant.btn.store.editor'].setAutoDefault(False)
|
pygpt_net/ui/dialog/plugins.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2026.01.01 15:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -27,6 +27,7 @@ from pygpt_net.ui.widget.option.dictionary import OptionDict
|
|
|
27
27
|
from pygpt_net.ui.widget.option.input import OptionInput, PasswordInput
|
|
28
28
|
from pygpt_net.ui.widget.option.slider import OptionSlider
|
|
29
29
|
from pygpt_net.ui.widget.option.textarea import OptionTextarea
|
|
30
|
+
from pygpt_net.core.locale.placeholder import apply as trans_placeholder_apply
|
|
30
31
|
from pygpt_net.utils import trans
|
|
31
32
|
|
|
32
33
|
|
|
@@ -430,6 +431,7 @@ class Plugins:
|
|
|
430
431
|
# if txt_tooltip == f"{key}.tooltip":
|
|
431
432
|
# txt_tooltip = txt_desc
|
|
432
433
|
|
|
434
|
+
txt_desc = trans_placeholder_apply(txt_desc)
|
|
433
435
|
|
|
434
436
|
"""
|
|
435
437
|
if option['type'] not in no_desc_types:
|