pygpt-net 2.7.7__py3-none-any.whl → 2.7.8__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 +7 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/assistant/batch.py +2 -2
- pygpt_net/controller/assistant/files.py +7 -6
- pygpt_net/controller/assistant/threads.py +0 -0
- pygpt_net/controller/chat/command.py +0 -0
- pygpt_net/controller/dialogs/confirm.py +35 -58
- pygpt_net/controller/lang/mapping.py +9 -9
- pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
- pygpt_net/controller/remote_store/remote_store.py +982 -13
- pygpt_net/core/command/command.py +0 -0
- pygpt_net/core/db/viewer.py +1 -1
- pygpt_net/core/realtime/worker.py +3 -1
- pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
- pygpt_net/core/remote_store/anthropic/files.py +211 -0
- pygpt_net/core/remote_store/anthropic/store.py +208 -0
- pygpt_net/core/remote_store/openai/store.py +5 -4
- pygpt_net/core/remote_store/remote_store.py +5 -1
- pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
- pygpt_net/core/remote_store/xai/files.py +225 -0
- pygpt_net/core/remote_store/xai/store.py +219 -0
- pygpt_net/data/config/config.json +9 -6
- pygpt_net/data/config/models.json +5 -4
- pygpt_net/data/config/settings.json +54 -1
- pygpt_net/data/icons/folder_eye.svg +1 -0
- pygpt_net/data/icons/folder_eye_filled.svg +1 -0
- pygpt_net/data/icons/folder_open.svg +1 -0
- pygpt_net/data/icons/folder_open_filled.svg +1 -0
- pygpt_net/data/locale/locale.de.ini +4 -3
- pygpt_net/data/locale/locale.en.ini +14 -4
- pygpt_net/data/locale/locale.es.ini +4 -3
- pygpt_net/data/locale/locale.fr.ini +4 -3
- pygpt_net/data/locale/locale.it.ini +4 -3
- pygpt_net/data/locale/locale.pl.ini +5 -4
- pygpt_net/data/locale/locale.uk.ini +4 -3
- pygpt_net/data/locale/locale.zh.ini +4 -3
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +282 -138
- pygpt_net/provider/api/anthropic/__init__.py +2 -0
- pygpt_net/provider/api/anthropic/chat.py +84 -1
- pygpt_net/provider/api/anthropic/store.py +307 -0
- pygpt_net/provider/api/anthropic/stream.py +75 -0
- pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
- pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
- pygpt_net/provider/api/google/chat.py +59 -2
- pygpt_net/provider/api/google/store.py +124 -3
- pygpt_net/provider/api/google/stream.py +91 -24
- pygpt_net/provider/api/google/worker/importer.py +16 -28
- pygpt_net/provider/api/openai/assistants.py +2 -2
- pygpt_net/provider/api/openai/store.py +4 -1
- pygpt_net/provider/api/openai/worker/importer.py +19 -61
- pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
- pygpt_net/provider/api/x_ai/__init__.py +30 -6
- pygpt_net/provider/api/x_ai/audio.py +43 -11
- pygpt_net/provider/api/x_ai/chat.py +92 -4
- pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
- pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
- pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
- pygpt_net/provider/api/x_ai/remote_tools.py +19 -1
- pygpt_net/provider/api/x_ai/store.py +610 -0
- pygpt_net/provider/api/x_ai/stream.py +30 -9
- pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
- pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
- pygpt_net/provider/audio_output/xai_tts.py +325 -0
- pygpt_net/provider/core/config/patch.py +18 -3
- pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
- pygpt_net/provider/core/model/patch.py +13 -0
- pygpt_net/tools/image_viewer/tool.py +334 -34
- pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
- pygpt_net/ui/dialog/assistant.py +1 -1
- pygpt_net/ui/dialog/plugins.py +13 -5
- pygpt_net/ui/dialog/remote_store.py +552 -0
- pygpt_net/ui/dialogs.py +3 -5
- pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
- pygpt_net/ui/menu/tools.py +6 -13
- pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
- pygpt_net/ui/widget/element/button.py +4 -4
- pygpt_net/ui/widget/image/display.py +2 -2
- pygpt_net/ui/widget/lists/context.py +2 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +9 -2
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +82 -70
- pygpt_net/controller/remote_store/google/store.py +0 -615
- pygpt_net/controller/remote_store/openai/batch.py +0 -524
- pygpt_net/controller/remote_store/openai/store.py +0 -699
- pygpt_net/ui/dialog/remote_store_google.py +0 -539
- pygpt_net/ui/dialog/remote_store_openai.py +0 -539
- pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
- pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
- pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# dialog.py
|
|
2
|
+
|
|
3
|
+
# dialog.py
|
|
4
|
+
|
|
1
5
|
#!/usr/bin/env python3
|
|
2
6
|
# -*- coding: utf-8 -*-
|
|
3
7
|
# ================================================== #
|
|
@@ -6,12 +10,24 @@
|
|
|
6
10
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
11
|
# MIT License #
|
|
8
12
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2026.01.
|
|
13
|
+
# Updated Date: 2026.01.06 19:00:00 #
|
|
10
14
|
# ================================================== #
|
|
11
15
|
|
|
12
16
|
from PySide6.QtCore import Qt, QPoint, QSize, QEvent
|
|
13
17
|
from PySide6.QtGui import QAction, QIcon
|
|
14
|
-
from PySide6.QtWidgets import
|
|
18
|
+
from PySide6.QtWidgets import (
|
|
19
|
+
QMenuBar,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
QHBoxLayout,
|
|
22
|
+
QSizePolicy,
|
|
23
|
+
QScrollArea,
|
|
24
|
+
QToolButton,
|
|
25
|
+
QPushButton,
|
|
26
|
+
QWidget,
|
|
27
|
+
QFrame,
|
|
28
|
+
QStyle,
|
|
29
|
+
QLabel,
|
|
30
|
+
)
|
|
15
31
|
|
|
16
32
|
from pygpt_net.ui.widget.dialog.base import BaseDialog
|
|
17
33
|
from pygpt_net.ui.widget.image.display import ImageLabel
|
|
@@ -56,7 +72,18 @@ class DialogSpawner:
|
|
|
56
72
|
row.addWidget(scroll)
|
|
57
73
|
|
|
58
74
|
layout = QVBoxLayout()
|
|
75
|
+
# remove extra outer margins to bring the toolbar closer to the top/menu bar
|
|
76
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
77
|
+
layout.setSpacing(0)
|
|
78
|
+
|
|
79
|
+
# full-width toolbar frame; icons cluster inside stays compact on the left
|
|
80
|
+
toolbar = dialog.setup_toolbar()
|
|
81
|
+
layout.addWidget(toolbar)
|
|
82
|
+
# image area
|
|
59
83
|
layout.addLayout(row)
|
|
84
|
+
# bottom status bar
|
|
85
|
+
statusbar = dialog.setup_statusbar()
|
|
86
|
+
layout.addWidget(statusbar)
|
|
60
87
|
|
|
61
88
|
dialog.append_layout(layout)
|
|
62
89
|
dialog.source = source
|
|
@@ -98,6 +125,12 @@ class ImageViewerDialog(BaseDialog):
|
|
|
98
125
|
self._icon_save = QIcon(":/icons/save.svg")
|
|
99
126
|
self._icon_logout = QIcon(":/icons/logout.svg")
|
|
100
127
|
|
|
128
|
+
# toolbar icons
|
|
129
|
+
self._icon_prev = QIcon(":/icons/back.svg")
|
|
130
|
+
self._icon_next = QIcon(":/icons/forward.svg")
|
|
131
|
+
self._icon_copy = QIcon(":/icons/copy.svg")
|
|
132
|
+
self._icon_fullscreen = QIcon(":/icons/fullscreen.svg")
|
|
133
|
+
|
|
101
134
|
# zoom / pan state
|
|
102
135
|
self._zoom_mode = 'fit' # 'fit' or 'manual'
|
|
103
136
|
self._zoom_factor = 1.0 # current manual factor (image space)
|
|
@@ -112,6 +145,21 @@ class ImageViewerDialog(BaseDialog):
|
|
|
112
145
|
self._max_widget_dim = 32768 # max single dimension (px) for the view widget
|
|
113
146
|
self._max_total_pixels = 80_000_000 # max total pixels of the view widget (about 80MP)
|
|
114
147
|
|
|
148
|
+
# buttons map to keep enabled state in sync with actions
|
|
149
|
+
self._tb_btns = {}
|
|
150
|
+
|
|
151
|
+
# status bar widgets and metadata
|
|
152
|
+
self.status_bar = None
|
|
153
|
+
self.lbl_index = None
|
|
154
|
+
self.lbl_resolution = None
|
|
155
|
+
self.lbl_zoom = None
|
|
156
|
+
self.lbl_extra = None
|
|
157
|
+
|
|
158
|
+
self._meta_index = 0 # 1-based index of file in dir
|
|
159
|
+
self._meta_total = 0 # total files in dir
|
|
160
|
+
self._meta_img_size = QSize() # original image size
|
|
161
|
+
self._meta_file_size = "" # formatted file size string
|
|
162
|
+
|
|
115
163
|
def append_layout(self, layout):
|
|
116
164
|
"""
|
|
117
165
|
Update layout
|
|
@@ -153,6 +201,8 @@ class ImageViewerDialog(BaseDialog):
|
|
|
153
201
|
self._fit_factor = self._compute_fit_factor(src.size(), target_size)
|
|
154
202
|
self._last_src_key = key
|
|
155
203
|
self._last_target_size = target_size
|
|
204
|
+
# update status bar to reflect new zoom and display size
|
|
205
|
+
self._refresh_statusbar()
|
|
156
206
|
super(ImageViewerDialog, self).resizeEvent(event)
|
|
157
207
|
|
|
158
208
|
def setup_menu(self) -> QMenuBar:
|
|
@@ -197,6 +247,196 @@ class ImageViewerDialog(BaseDialog):
|
|
|
197
247
|
|
|
198
248
|
return self.menu_bar
|
|
199
249
|
|
|
250
|
+
def setup_toolbar(self) -> QFrame:
|
|
251
|
+
"""
|
|
252
|
+
Setup top toolbar.
|
|
253
|
+
Full-width frame (Expanding), with a fixed-size icons strip on the left that does not stretch.
|
|
254
|
+
"""
|
|
255
|
+
bar = QFrame(self)
|
|
256
|
+
bar.setObjectName("image_viewer_toolbar")
|
|
257
|
+
bar.setFrameShape(QFrame.NoFrame)
|
|
258
|
+
bar.setMinimumHeight(35)
|
|
259
|
+
bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # full width, fixed height
|
|
260
|
+
|
|
261
|
+
# scoped minimal style for icon-only QPushButtons
|
|
262
|
+
bar.setStyleSheet(
|
|
263
|
+
"""
|
|
264
|
+
#image_viewer_toolbar {
|
|
265
|
+
border: none;
|
|
266
|
+
}
|
|
267
|
+
#image_viewer_toolbar QPushButton {
|
|
268
|
+
border: none;
|
|
269
|
+
padding: 0;
|
|
270
|
+
}
|
|
271
|
+
#image_viewer_toolbar QPushButton:disabled {
|
|
272
|
+
opacity: 0.5;
|
|
273
|
+
}
|
|
274
|
+
"""
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# main container layout that fills the width
|
|
278
|
+
lay = QHBoxLayout(bar)
|
|
279
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
280
|
+
lay.setSpacing(0)
|
|
281
|
+
|
|
282
|
+
# a compact, non-expanding strip that holds the icons
|
|
283
|
+
strip = QWidget(bar)
|
|
284
|
+
strip.setObjectName("image_viewer_toolbar_strip")
|
|
285
|
+
strip.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
286
|
+
|
|
287
|
+
strip_lay = QHBoxLayout(strip)
|
|
288
|
+
strip_lay.setContentsMargins(6, 6, 6, 6)
|
|
289
|
+
strip_lay.setSpacing(6)
|
|
290
|
+
|
|
291
|
+
# DPI-aware icon and button sizes; explicit fixed size prevents vertical squashing
|
|
292
|
+
pm = self.style().pixelMetric(QStyle.PM_ToolBarIconSize)
|
|
293
|
+
if not isinstance(pm, int) or pm <= 0:
|
|
294
|
+
pm = self.style().pixelMetric(QStyle.PM_SmallIconSize)
|
|
295
|
+
if not isinstance(pm, int) or pm <= 0:
|
|
296
|
+
pm = 20
|
|
297
|
+
# reduce icon height by ~5px (and width to keep square) as requested
|
|
298
|
+
icon_px = max(16, pm - 5)
|
|
299
|
+
btn_px = max(29, icon_px + 10) # keep compact square buttons; min reduced by 5px
|
|
300
|
+
icon_size = QSize(icon_px, icon_px)
|
|
301
|
+
|
|
302
|
+
def bind_button_to_action(btn: QPushButton, action: QAction):
|
|
303
|
+
"""Keep button in sync with the given action."""
|
|
304
|
+
btn.setIcon(action.icon())
|
|
305
|
+
btn.setToolTip(action.toolTip())
|
|
306
|
+
btn.setEnabled(action.isEnabled())
|
|
307
|
+
action.changed.connect(lambda: (
|
|
308
|
+
btn.setIcon(action.icon()),
|
|
309
|
+
btn.setToolTip(action.toolTip()),
|
|
310
|
+
btn.setEnabled(action.isEnabled())
|
|
311
|
+
))
|
|
312
|
+
btn.clicked.connect(action.trigger)
|
|
313
|
+
|
|
314
|
+
def mk_btn(key: str, icon: QIcon, tooltip: str) -> QPushButton:
|
|
315
|
+
"""Create a fixed-size square button with icon-only look."""
|
|
316
|
+
action = QAction(icon, "", self)
|
|
317
|
+
action.setToolTip(tooltip)
|
|
318
|
+
self.actions[key] = action
|
|
319
|
+
|
|
320
|
+
btn = QPushButton(strip)
|
|
321
|
+
btn.setObjectName("ivtb")
|
|
322
|
+
btn.setCursor(Qt.PointingHandCursor)
|
|
323
|
+
btn.setFocusPolicy(Qt.NoFocus)
|
|
324
|
+
btn.setFlat(True)
|
|
325
|
+
btn.setText("")
|
|
326
|
+
btn.setIcon(icon)
|
|
327
|
+
btn.setIconSize(icon_size)
|
|
328
|
+
btn.setFixedSize(btn_px, btn_px)
|
|
329
|
+
|
|
330
|
+
bind_button_to_action(btn, action)
|
|
331
|
+
self._tb_btns[key] = btn
|
|
332
|
+
|
|
333
|
+
strip_lay.addWidget(btn)
|
|
334
|
+
return btn
|
|
335
|
+
|
|
336
|
+
def add_sep():
|
|
337
|
+
sep = QFrame(strip)
|
|
338
|
+
sep.setFrameShape(QFrame.VLine)
|
|
339
|
+
sep.setFrameShadow(QFrame.Sunken)
|
|
340
|
+
sep.setFixedHeight(btn_px - 4)
|
|
341
|
+
strip_lay.addWidget(sep)
|
|
342
|
+
|
|
343
|
+
# prev
|
|
344
|
+
mk_btn("tb_prev", self._icon_prev, "Previous image")
|
|
345
|
+
self.actions["tb_prev"].triggered.connect(
|
|
346
|
+
lambda checked=False: self.window.tools.get("viewer").prev_by_id(self.id)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# next
|
|
350
|
+
mk_btn("tb_next", self._icon_next, "Next image")
|
|
351
|
+
self.actions["tb_next"].triggered.connect(
|
|
352
|
+
lambda checked=False: self.window.tools.get("viewer").next_by_id(self.id)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
add_sep()
|
|
356
|
+
|
|
357
|
+
# open in directory
|
|
358
|
+
mk_btn("tb_open_dir", self._icon_folder, "Open in directory")
|
|
359
|
+
self.actions["tb_open_dir"].triggered.connect(
|
|
360
|
+
lambda checked=False: self.window.tools.get("viewer").open_dir_by_id(self.id)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# save as...
|
|
364
|
+
mk_btn("tb_save_as", self._icon_save, "Save as...")
|
|
365
|
+
self.actions["tb_save_as"].triggered.connect(
|
|
366
|
+
lambda checked=False: self.window.tools.get("viewer").save_by_id(self.id)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# copy
|
|
370
|
+
mk_btn("tb_copy", self._icon_copy, "Copy to clipboard")
|
|
371
|
+
self.actions["tb_copy"].triggered.connect(
|
|
372
|
+
lambda checked=False: self.window.tools.get("viewer").copy_by_id(self.id)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
add_sep()
|
|
376
|
+
|
|
377
|
+
# fullscreen
|
|
378
|
+
mk_btn("tb_fullscreen", self._icon_fullscreen, "Toggle fullscreen")
|
|
379
|
+
self.actions["tb_fullscreen"].triggered.connect(
|
|
380
|
+
lambda checked=False: self.window.tools.get("viewer").toggle_fullscreen_by_id(self.id)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# initial enabled state; actual state adjusted on image load
|
|
384
|
+
for k in ("tb_prev", "tb_next", "tb_open_dir", "tb_save_as", "tb_copy", "tb_fullscreen"):
|
|
385
|
+
if k in self.actions:
|
|
386
|
+
self.actions[k].setEnabled(k == "tb_fullscreen")
|
|
387
|
+
|
|
388
|
+
# compose: icons strip on the left, stretch fills the rest so frame spans full width
|
|
389
|
+
lay.addWidget(strip)
|
|
390
|
+
lay.addStretch(1)
|
|
391
|
+
|
|
392
|
+
return bar
|
|
393
|
+
|
|
394
|
+
def setup_statusbar(self) -> QFrame:
|
|
395
|
+
"""
|
|
396
|
+
Setup bottom status bar with four columns:
|
|
397
|
+
1) file index in directory (e.g., 1/13)
|
|
398
|
+
2) displayed size (e.g., 1280x720 px)
|
|
399
|
+
3) current zoom in percent
|
|
400
|
+
4) original image resolution and file size on the right
|
|
401
|
+
"""
|
|
402
|
+
bar = QFrame(self)
|
|
403
|
+
bar.setObjectName("image_viewer_statusbar")
|
|
404
|
+
bar.setFrameShape(QFrame.NoFrame)
|
|
405
|
+
bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
406
|
+
bar.setStyleSheet("""
|
|
407
|
+
#image_viewer_statusbar {
|
|
408
|
+
border: none;
|
|
409
|
+
}
|
|
410
|
+
#image_viewer_statusbar QLabel {
|
|
411
|
+
color: #686868;
|
|
412
|
+
font-size: 12px;
|
|
413
|
+
padding: 4px 8px;
|
|
414
|
+
}
|
|
415
|
+
""")
|
|
416
|
+
|
|
417
|
+
lay = QHBoxLayout(bar)
|
|
418
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
419
|
+
lay.setSpacing(0)
|
|
420
|
+
|
|
421
|
+
self.lbl_index = QLabel("-", bar)
|
|
422
|
+
self.lbl_resolution = QLabel("-", bar)
|
|
423
|
+
self.lbl_zoom = QLabel("-", bar)
|
|
424
|
+
self.lbl_extra = QLabel("-", bar)
|
|
425
|
+
|
|
426
|
+
# make columns distribute evenly; order updated to show current (displayed) on the left,
|
|
427
|
+
# and original with file size on the right: index | displayed | zoom | original (+ size)
|
|
428
|
+
for w in (self.lbl_index, self.lbl_extra, self.lbl_zoom, self.lbl_resolution):
|
|
429
|
+
w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
430
|
+
|
|
431
|
+
lay.addWidget(self.lbl_index, 1, Qt.AlignLeft)
|
|
432
|
+
lay.addWidget(self.lbl_extra, 1, Qt.AlignLeft)
|
|
433
|
+
lay.addWidget(self.lbl_zoom, 1, Qt.AlignCenter)
|
|
434
|
+
lay.addWidget(self.lbl_resolution, 1, Qt.AlignRight)
|
|
435
|
+
|
|
436
|
+
self.status_bar = bar
|
|
437
|
+
self._refresh_statusbar()
|
|
438
|
+
return bar
|
|
439
|
+
|
|
200
440
|
# =========================
|
|
201
441
|
# Zoom / pan implementation
|
|
202
442
|
# =========================
|
|
@@ -266,10 +506,8 @@ class ImageViewerDialog(BaseDialog):
|
|
|
266
506
|
)
|
|
267
507
|
self._zoom_factor = self._fit_factor
|
|
268
508
|
self._zoom_mode = 'manual'
|
|
269
|
-
# switch to manual rendering path: original pixmap + scaled contents
|
|
270
509
|
self.scroll_area.setWidgetResizable(False)
|
|
271
510
|
self.pixmap.setScaledContents(True)
|
|
272
|
-
# ensure we display original image for better performance (no giant intermediate pixmaps)
|
|
273
511
|
self.pixmap.setPixmap(self.source.pixmap())
|
|
274
512
|
|
|
275
513
|
old_w = max(1, self.pixmap.width())
|
|
@@ -287,10 +525,8 @@ class ImageViewerDialog(BaseDialog):
|
|
|
287
525
|
return False
|
|
288
526
|
|
|
289
527
|
step = self._zoom_step if angle > 0 else (1.0 / self._zoom_step)
|
|
290
|
-
# compute tentative new factor and clamp by hard min/max and size guards
|
|
291
528
|
tentative = self._zoom_factor * step
|
|
292
529
|
tentative = max(self._min_zoom, min(self._max_zoom, tentative))
|
|
293
|
-
# apply size-based guards to avoid extremely huge widget sizes
|
|
294
530
|
tentative = self._clamp_factor_by_size(tentative)
|
|
295
531
|
|
|
296
532
|
if abs(tentative - self._zoom_factor) < 1e-9:
|
|
@@ -301,7 +537,6 @@ class ImageViewerDialog(BaseDialog):
|
|
|
301
537
|
hbar = self.scroll_area.horizontalScrollBar()
|
|
302
538
|
vbar = self.scroll_area.verticalScrollBar()
|
|
303
539
|
|
|
304
|
-
# position in content coords before zoom (keep point under cursor stable)
|
|
305
540
|
content_x = hbar.value() + vp_pos.x()
|
|
306
541
|
content_y = vbar.value() + vp_pos.y()
|
|
307
542
|
rx = content_x / float(old_w)
|
|
@@ -365,23 +600,19 @@ class ImageViewerDialog(BaseDialog):
|
|
|
365
600
|
iw = max(1, src.width())
|
|
366
601
|
ih = max(1, src.height())
|
|
367
602
|
|
|
368
|
-
# only guard when zooming in; zooming out should not be limited by these caps
|
|
369
603
|
if factor <= 1.0:
|
|
370
604
|
return factor
|
|
371
605
|
|
|
372
|
-
# desired size
|
|
373
606
|
dw = iw * factor
|
|
374
607
|
dh = ih * factor
|
|
375
608
|
|
|
376
609
|
scale = 1.0
|
|
377
610
|
|
|
378
|
-
# total pixel cap
|
|
379
611
|
total = dw * dh
|
|
380
612
|
if total > self._max_total_pixels:
|
|
381
613
|
from math import sqrt
|
|
382
614
|
scale = min(scale, sqrt(self._max_total_pixels / float(total)))
|
|
383
615
|
|
|
384
|
-
# dimension caps
|
|
385
616
|
if dw * scale > self._max_widget_dim:
|
|
386
617
|
scale = min(scale, self._max_widget_dim / float(dw))
|
|
387
618
|
if dh * scale > self._max_widget_dim:
|
|
@@ -394,10 +625,6 @@ class ImageViewerDialog(BaseDialog):
|
|
|
394
625
|
def _set_scaled_pixmap_by_factor(self, factor: float):
|
|
395
626
|
"""
|
|
396
627
|
Scale and display the image using provided factor relative to the original image.
|
|
397
|
-
In manual mode this avoids allocating giant intermediate QPixmaps by:
|
|
398
|
-
- drawing the original pixmap;
|
|
399
|
-
- enabling scaled contents;
|
|
400
|
-
- resizing the label to the required size.
|
|
401
628
|
"""
|
|
402
629
|
if not self._has_image():
|
|
403
630
|
return
|
|
@@ -406,31 +633,30 @@ class ImageViewerDialog(BaseDialog):
|
|
|
406
633
|
iw = max(1, src.width())
|
|
407
634
|
ih = max(1, src.height())
|
|
408
635
|
|
|
409
|
-
# target size based on factor (KeepAspectRatio preserved by proportional math)
|
|
410
636
|
new_w = max(1, int(round(iw * factor)))
|
|
411
637
|
new_h = max(1, int(round(ih * factor)))
|
|
412
638
|
|
|
413
|
-
# enforce guards once more to be safe
|
|
414
639
|
guarded_factor = self._clamp_factor_by_size(factor)
|
|
415
640
|
if abs(guarded_factor - factor) > 1e-9:
|
|
416
641
|
new_w = max(1, int(round(iw * guarded_factor)))
|
|
417
642
|
new_h = max(1, int(round(ih * guarded_factor)))
|
|
418
|
-
self._zoom_factor = guarded_factor
|
|
643
|
+
self._zoom_factor = guarded_factor
|
|
419
644
|
|
|
420
645
|
if self._zoom_mode == 'manual':
|
|
421
|
-
# ensure manual path uses original pixmap and scaled contents
|
|
422
646
|
if self.pixmap.pixmap() is None or self.pixmap.pixmap().cacheKey() != src.cacheKey():
|
|
423
647
|
self.pixmap.setPixmap(src)
|
|
424
648
|
self.pixmap.setScaledContents(True)
|
|
425
649
|
self.pixmap.resize(new_w, new_h)
|
|
426
650
|
else:
|
|
427
|
-
# fallback (not expected here): keep classic high-quality scaling
|
|
428
651
|
scaled = src.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
429
652
|
self.pixmap.setScaledContents(False)
|
|
430
653
|
self.pixmap.setPixmap(scaled)
|
|
431
654
|
if self.scroll_area is not None:
|
|
432
655
|
self.scroll_area.setWidgetResizable(True)
|
|
433
656
|
|
|
657
|
+
# keep status bar in sync with zoom and current display size
|
|
658
|
+
self._refresh_statusbar()
|
|
659
|
+
|
|
434
660
|
def _event_pos(self, event) -> QPoint:
|
|
435
661
|
"""
|
|
436
662
|
Extract integer QPoint from mouse/touchpad event position (supports QPointF in 6.9+).
|
|
@@ -439,4 +665,74 @@ class ImageViewerDialog(BaseDialog):
|
|
|
439
665
|
return event.position().toPoint()
|
|
440
666
|
if hasattr(event, "pos"):
|
|
441
667
|
return event.pos()
|
|
442
|
-
return QPoint(0, 0)
|
|
668
|
+
return QPoint(0, 0)
|
|
669
|
+
|
|
670
|
+
# =========================
|
|
671
|
+
# Status bar helpers
|
|
672
|
+
# =========================
|
|
673
|
+
|
|
674
|
+
def set_status_meta(self, index: int = None, total: int = None, img_size: QSize = None, file_size_str: str = None):
|
|
675
|
+
"""
|
|
676
|
+
Update persistent metadata shown in status bar (index/total, original image size, file size).
|
|
677
|
+
"""
|
|
678
|
+
if index is not None:
|
|
679
|
+
self._meta_index = max(0, int(index))
|
|
680
|
+
if total is not None:
|
|
681
|
+
self._meta_total = max(0, int(total))
|
|
682
|
+
if img_size is not None:
|
|
683
|
+
self._meta_img_size = QSize(max(0, img_size.width()), max(0, img_size.height()))
|
|
684
|
+
if file_size_str is not None:
|
|
685
|
+
self._meta_file_size = file_size_str or ""
|
|
686
|
+
self._refresh_statusbar()
|
|
687
|
+
|
|
688
|
+
def _current_zoom_percent(self) -> int:
|
|
689
|
+
"""
|
|
690
|
+
Return current zoom in percent, based on mode.
|
|
691
|
+
"""
|
|
692
|
+
factor = self._fit_factor if self._zoom_mode == 'fit' else self._zoom_factor
|
|
693
|
+
try:
|
|
694
|
+
return max(1, int(round(factor * 100.0)))
|
|
695
|
+
except Exception:
|
|
696
|
+
return 100
|
|
697
|
+
|
|
698
|
+
def _displayed_size(self) -> QSize:
|
|
699
|
+
"""
|
|
700
|
+
Return the currently displayed image size in pixels.
|
|
701
|
+
"""
|
|
702
|
+
if self._zoom_mode == 'manual' and self.pixmap is not None:
|
|
703
|
+
return QSize(max(0, self.pixmap.width()), max(0, self.pixmap.height()))
|
|
704
|
+
# in fit mode or when using a scaled pixmap
|
|
705
|
+
if self.pixmap is not None and self.pixmap.pixmap() is not None:
|
|
706
|
+
pm = self.pixmap.pixmap()
|
|
707
|
+
return QSize(max(0, pm.width()), max(0, pm.height()))
|
|
708
|
+
return QSize(0, 0)
|
|
709
|
+
|
|
710
|
+
def _refresh_statusbar(self):
|
|
711
|
+
"""
|
|
712
|
+
Refresh all status bar labels from current metadata and state.
|
|
713
|
+
"""
|
|
714
|
+
if self.lbl_index is None:
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
# 1) index/total
|
|
718
|
+
if self._meta_index > 0 and self._meta_total > 0:
|
|
719
|
+
self.lbl_index.setText(f"{self._meta_index}/{self._meta_total}")
|
|
720
|
+
else:
|
|
721
|
+
self.lbl_index.setText("-")
|
|
722
|
+
|
|
723
|
+
# 2) left: displayed size only
|
|
724
|
+
dsz = self._displayed_size()
|
|
725
|
+
dsz_txt = f"{dsz.width()}x{dsz.height()} px" if dsz.width() > 0 and dsz.height() > 0 else "-"
|
|
726
|
+
self.lbl_extra.setText(dsz_txt)
|
|
727
|
+
|
|
728
|
+
# 3) zoom percent (center)
|
|
729
|
+
self.lbl_zoom.setText(f"{self._current_zoom_percent()}%")
|
|
730
|
+
|
|
731
|
+
# 4) right: original resolution with file size appended
|
|
732
|
+
if self._meta_img_size.width() > 0 and self._meta_img_size.height() > 0:
|
|
733
|
+
right_txt = f"{self._meta_img_size.width()}x{self._meta_img_size.height()} px"
|
|
734
|
+
if self._meta_file_size:
|
|
735
|
+
right_txt += f" | {self._meta_file_size}"
|
|
736
|
+
self.lbl_resolution.setText(right_txt)
|
|
737
|
+
else:
|
|
738
|
+
self.lbl_resolution.setText("-")
|
pygpt_net/ui/dialog/assistant.py
CHANGED
|
@@ -46,7 +46,7 @@ class Assistant(BaseConfigDialog):
|
|
|
46
46
|
self.window.ui.nodes['assistant.btn.store.editor'] = QPushButton(QIcon(":/icons/db.svg"), "")
|
|
47
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.remote_store.
|
|
49
|
+
lambda: self.window.controller.remote_store.toggle_editor(provider="openai")
|
|
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: 2026.01.
|
|
9
|
+
# Updated Date: 2026.01.06 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
@@ -176,8 +176,12 @@ class Plugins:
|
|
|
176
176
|
desc_txt = plugin.description
|
|
177
177
|
if plugin.use_locale:
|
|
178
178
|
domain = f"plugin.{plugin.id}"
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
translated_name = trans('plugin.name', False, domain)
|
|
180
|
+
translated_desc = trans('plugin.description', False, domain)
|
|
181
|
+
if translated_name != 'plugin.name':
|
|
182
|
+
name_txt = translated_name
|
|
183
|
+
if translated_desc != 'plugin.description':
|
|
184
|
+
desc_txt = translated_desc
|
|
181
185
|
|
|
182
186
|
self.window.ui.nodes[desc_key] = DescLabel(desc_txt)
|
|
183
187
|
self.window.ui.nodes[desc_key].setAlignment(Qt.AlignCenter)
|
|
@@ -423,8 +427,12 @@ class Plugins:
|
|
|
423
427
|
# translate if localization is enabled
|
|
424
428
|
if plugin.use_locale and allow_locale:
|
|
425
429
|
domain = f"plugin.{plugin.id}"
|
|
426
|
-
|
|
427
|
-
|
|
430
|
+
translated_label = trans(f"{key}.label", False, domain)
|
|
431
|
+
translated_description = trans(f"{key}.description", False, domain)
|
|
432
|
+
if translated_label != f"{key}.label":
|
|
433
|
+
txt_title = translated_label
|
|
434
|
+
if translated_description != f"{key}.description":
|
|
435
|
+
txt_desc = translated_description
|
|
428
436
|
# txt_tooltip = trans(f"{key}.tooltip", False, domain)
|
|
429
437
|
|
|
430
438
|
# if empty tooltip then use description
|