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.
Files changed (159) hide show
  1. pygpt_net/CHANGELOG.txt +15 -0
  2. pygpt_net/__init__.py +4 -4
  3. pygpt_net/app_core.py +4 -2
  4. pygpt_net/controller/__init__.py +5 -1
  5. pygpt_net/controller/assistant/assistant.py +1 -4
  6. pygpt_net/controller/assistant/batch.py +5 -504
  7. pygpt_net/controller/assistant/editor.py +5 -5
  8. pygpt_net/controller/assistant/files.py +16 -16
  9. pygpt_net/controller/chat/handler/google_stream.py +307 -1
  10. pygpt_net/controller/chat/handler/worker.py +10 -25
  11. pygpt_net/controller/chat/handler/xai_stream.py +621 -52
  12. pygpt_net/controller/chat/image.py +2 -2
  13. pygpt_net/controller/debug/fixtures.py +3 -2
  14. pygpt_net/controller/dialogs/confirm.py +73 -101
  15. pygpt_net/controller/files/files.py +65 -4
  16. pygpt_net/controller/lang/mapping.py +9 -9
  17. pygpt_net/controller/painter/capture.py +50 -1
  18. pygpt_net/controller/presets/presets.py +2 -1
  19. pygpt_net/controller/remote_store/__init__.py +12 -0
  20. pygpt_net/{provider/core/assistant_file/db_sqlite → controller/remote_store/google}/__init__.py +2 -2
  21. pygpt_net/controller/remote_store/google/batch.py +402 -0
  22. pygpt_net/controller/remote_store/google/store.py +615 -0
  23. pygpt_net/controller/remote_store/openai/__init__.py +12 -0
  24. pygpt_net/controller/remote_store/openai/batch.py +524 -0
  25. pygpt_net/controller/{assistant → remote_store/openai}/store.py +63 -60
  26. pygpt_net/controller/remote_store/remote_store.py +35 -0
  27. pygpt_net/controller/ui/ui.py +20 -1
  28. pygpt_net/core/assistants/assistants.py +3 -15
  29. pygpt_net/core/db/database.py +5 -3
  30. pygpt_net/core/filesystem/url.py +4 -1
  31. pygpt_net/core/locale/placeholder.py +35 -0
  32. pygpt_net/core/remote_store/__init__.py +12 -0
  33. pygpt_net/core/remote_store/google/__init__.py +11 -0
  34. pygpt_net/core/remote_store/google/files.py +224 -0
  35. pygpt_net/core/remote_store/google/store.py +248 -0
  36. pygpt_net/core/remote_store/openai/__init__.py +11 -0
  37. pygpt_net/core/{assistants → remote_store/openai}/files.py +26 -19
  38. pygpt_net/core/{assistants → remote_store/openai}/store.py +32 -15
  39. pygpt_net/core/remote_store/remote_store.py +24 -0
  40. pygpt_net/core/render/web/body.py +3 -2
  41. pygpt_net/core/types/chunk.py +27 -0
  42. pygpt_net/data/config/config.json +8 -4
  43. pygpt_net/data/config/models.json +77 -3
  44. pygpt_net/data/config/settings.json +45 -0
  45. pygpt_net/data/js/app/template.js +1 -1
  46. pygpt_net/data/js/app.min.js +2 -2
  47. pygpt_net/data/locale/locale.de.ini +44 -41
  48. pygpt_net/data/locale/locale.en.ini +56 -43
  49. pygpt_net/data/locale/locale.es.ini +44 -41
  50. pygpt_net/data/locale/locale.fr.ini +44 -41
  51. pygpt_net/data/locale/locale.it.ini +44 -41
  52. pygpt_net/data/locale/locale.pl.ini +45 -42
  53. pygpt_net/data/locale/locale.uk.ini +44 -41
  54. pygpt_net/data/locale/locale.zh.ini +44 -41
  55. pygpt_net/data/locale/plugin.cmd_history.de.ini +1 -1
  56. pygpt_net/data/locale/plugin.cmd_history.en.ini +1 -1
  57. pygpt_net/data/locale/plugin.cmd_history.es.ini +1 -1
  58. pygpt_net/data/locale/plugin.cmd_history.fr.ini +1 -1
  59. pygpt_net/data/locale/plugin.cmd_history.it.ini +1 -1
  60. pygpt_net/data/locale/plugin.cmd_history.pl.ini +1 -1
  61. pygpt_net/data/locale/plugin.cmd_history.uk.ini +1 -1
  62. pygpt_net/data/locale/plugin.cmd_history.zh.ini +1 -1
  63. pygpt_net/data/locale/plugin.cmd_mouse_control.en.ini +14 -0
  64. pygpt_net/data/locale/plugin.cmd_web.de.ini +1 -1
  65. pygpt_net/data/locale/plugin.cmd_web.en.ini +1 -1
  66. pygpt_net/data/locale/plugin.cmd_web.es.ini +1 -1
  67. pygpt_net/data/locale/plugin.cmd_web.fr.ini +1 -1
  68. pygpt_net/data/locale/plugin.cmd_web.it.ini +1 -1
  69. pygpt_net/data/locale/plugin.cmd_web.pl.ini +1 -1
  70. pygpt_net/data/locale/plugin.cmd_web.uk.ini +1 -1
  71. pygpt_net/data/locale/plugin.cmd_web.zh.ini +1 -1
  72. pygpt_net/data/locale/plugin.idx_llama_index.de.ini +2 -2
  73. pygpt_net/data/locale/plugin.idx_llama_index.en.ini +2 -2
  74. pygpt_net/data/locale/plugin.idx_llama_index.es.ini +2 -2
  75. pygpt_net/data/locale/plugin.idx_llama_index.fr.ini +2 -2
  76. pygpt_net/data/locale/plugin.idx_llama_index.it.ini +2 -2
  77. pygpt_net/data/locale/plugin.idx_llama_index.pl.ini +2 -2
  78. pygpt_net/data/locale/plugin.idx_llama_index.uk.ini +2 -2
  79. pygpt_net/data/locale/plugin.idx_llama_index.zh.ini +2 -2
  80. pygpt_net/item/assistant.py +1 -211
  81. pygpt_net/item/ctx.py +3 -3
  82. pygpt_net/item/store.py +238 -0
  83. pygpt_net/js_rc.py +2449 -2447
  84. pygpt_net/migrations/Version20260102190000.py +35 -0
  85. pygpt_net/migrations/__init__.py +3 -1
  86. pygpt_net/plugin/cmd_mouse_control/config.py +471 -1
  87. pygpt_net/plugin/cmd_mouse_control/plugin.py +487 -22
  88. pygpt_net/plugin/cmd_mouse_control/worker.py +464 -87
  89. pygpt_net/plugin/cmd_mouse_control/worker_sandbox.py +729 -0
  90. pygpt_net/plugin/idx_llama_index/config.py +2 -2
  91. pygpt_net/provider/api/anthropic/__init__.py +10 -8
  92. pygpt_net/provider/api/google/__init__.py +21 -58
  93. pygpt_net/provider/api/google/chat.py +545 -129
  94. pygpt_net/provider/api/google/computer.py +190 -0
  95. pygpt_net/provider/api/google/realtime/realtime.py +2 -2
  96. pygpt_net/provider/api/google/remote_tools.py +93 -0
  97. pygpt_net/provider/api/google/store.py +546 -0
  98. pygpt_net/provider/api/google/worker/__init__.py +0 -0
  99. pygpt_net/provider/api/google/worker/importer.py +392 -0
  100. pygpt_net/provider/api/openai/__init__.py +7 -3
  101. pygpt_net/provider/api/openai/computer.py +10 -1
  102. pygpt_net/provider/api/openai/responses.py +0 -0
  103. pygpt_net/provider/api/openai/store.py +6 -6
  104. pygpt_net/provider/api/openai/worker/importer.py +24 -24
  105. pygpt_net/provider/api/x_ai/__init__.py +10 -9
  106. pygpt_net/provider/api/x_ai/chat.py +272 -102
  107. pygpt_net/provider/core/config/patch.py +16 -1
  108. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +3 -3
  109. pygpt_net/provider/core/model/patch.py +17 -3
  110. pygpt_net/provider/core/preset/json_file.py +13 -7
  111. pygpt_net/provider/core/{assistant_file → remote_file}/__init__.py +1 -1
  112. pygpt_net/provider/core/{assistant_file → remote_file}/base.py +9 -9
  113. pygpt_net/provider/core/remote_file/db_sqlite/__init__.py +12 -0
  114. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/patch.py +1 -1
  115. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/provider.py +23 -20
  116. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/storage.py +35 -27
  117. pygpt_net/provider/core/{assistant_file → remote_file}/db_sqlite/utils.py +5 -4
  118. pygpt_net/provider/core/{assistant_store → remote_store}/__init__.py +1 -1
  119. pygpt_net/provider/core/{assistant_store → remote_store}/base.py +10 -10
  120. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/__init__.py +1 -1
  121. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/patch.py +1 -1
  122. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/provider.py +16 -15
  123. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/storage.py +30 -23
  124. pygpt_net/provider/core/{assistant_store → remote_store}/db_sqlite/utils.py +5 -4
  125. pygpt_net/provider/core/{assistant_store → remote_store}/json_file.py +9 -9
  126. pygpt_net/provider/llms/google.py +2 -2
  127. pygpt_net/tools/image_viewer/ui/dialogs.py +298 -12
  128. pygpt_net/tools/text_editor/ui/widgets.py +5 -1
  129. pygpt_net/ui/base/config_dialog.py +3 -2
  130. pygpt_net/ui/base/context_menu.py +44 -1
  131. pygpt_net/ui/dialog/assistant.py +3 -3
  132. pygpt_net/ui/dialog/plugins.py +3 -1
  133. pygpt_net/ui/dialog/remote_store_google.py +539 -0
  134. pygpt_net/ui/dialog/{assistant_store.py → remote_store_openai.py} +95 -95
  135. pygpt_net/ui/dialogs.py +5 -3
  136. pygpt_net/ui/layout/chat/attachments_uploaded.py +3 -3
  137. pygpt_net/ui/layout/toolbox/computer_env.py +26 -8
  138. pygpt_net/ui/layout/toolbox/indexes.py +22 -19
  139. pygpt_net/ui/layout/toolbox/model.py +28 -5
  140. pygpt_net/ui/menu/tools.py +13 -5
  141. pygpt_net/ui/widget/dialog/remote_store_google.py +56 -0
  142. pygpt_net/ui/widget/dialog/{assistant_store.py → remote_store_openai.py} +9 -9
  143. pygpt_net/ui/widget/element/button.py +4 -4
  144. pygpt_net/ui/widget/image/display.py +25 -8
  145. pygpt_net/ui/widget/lists/remote_store_google.py +248 -0
  146. pygpt_net/ui/widget/lists/{assistant_store.py → remote_store_openai.py} +21 -21
  147. pygpt_net/ui/widget/option/checkbox_list.py +47 -9
  148. pygpt_net/ui/widget/option/combo.py +39 -3
  149. pygpt_net/ui/widget/tabs/output.py +9 -1
  150. pygpt_net/ui/widget/textarea/editor.py +14 -1
  151. pygpt_net/ui/widget/textarea/input.py +20 -7
  152. pygpt_net/ui/widget/textarea/notepad.py +24 -1
  153. pygpt_net/ui/widget/textarea/output.py +23 -1
  154. pygpt_net/ui/widget/textarea/web.py +16 -1
  155. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/METADATA +41 -2
  156. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/RECORD +158 -132
  157. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/LICENSE +0 -0
  158. {pygpt_net-2.7.4.dist-info → pygpt_net-2.7.6.dist-info}/WHEEL +0 -0
  159. {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: 2025.08.24 23:00:00 #
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(pixmap)
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
- if key != self._last_src_key or target_size != self._last_target_size:
104
- self.pixmap.setPixmap(
105
- src.scaled(
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
- self._last_src_key = key
112
- self._last_target_size = target_size
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: 2025.08.24 23:00:00 #
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: 2025.09.04 00:00:00 #
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: 2025.08.24 23:00:00 #
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,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.07.19 17:00:00 #
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.assistant.store'))
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.assistant.store.toggle_editor()
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)
@@ -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.09.16 22:00:00 #
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: