pygpt-net 2.6.21__py3-none-any.whl → 2.6.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/controller/__init__.py +4 -8
  5. pygpt_net/controller/access/voice.py +2 -2
  6. pygpt_net/controller/agent/llama.py +3 -0
  7. pygpt_net/controller/assistant/batch.py +2 -3
  8. pygpt_net/controller/assistant/editor.py +2 -2
  9. pygpt_net/controller/assistant/files.py +2 -3
  10. pygpt_net/controller/assistant/store.py +2 -2
  11. pygpt_net/controller/audio/audio.py +2 -2
  12. pygpt_net/controller/chat/response.py +4 -0
  13. pygpt_net/controller/ctx/ctx.py +2 -1
  14. pygpt_net/controller/files/files.py +24 -55
  15. pygpt_net/controller/idx/indexer.py +85 -76
  16. pygpt_net/controller/lang/lang.py +52 -34
  17. pygpt_net/controller/model/importer.py +2 -2
  18. pygpt_net/controller/notepad/notepad.py +86 -84
  19. pygpt_net/controller/plugins/settings.py +3 -4
  20. pygpt_net/controller/settings/profile.py +105 -124
  21. pygpt_net/controller/theme/menu.py +154 -57
  22. pygpt_net/controller/theme/nodes.py +51 -44
  23. pygpt_net/controller/theme/theme.py +33 -9
  24. pygpt_net/controller/tools/tools.py +2 -2
  25. pygpt_net/controller/ui/tabs.py +2 -3
  26. pygpt_net/core/agents/observer/evaluation.py +2 -2
  27. pygpt_net/core/agents/runners/loop.py +1 -0
  28. pygpt_net/core/bridge/bridge.py +2 -0
  29. pygpt_net/core/ctx/container.py +13 -12
  30. pygpt_net/core/ctx/output.py +7 -4
  31. pygpt_net/core/debug/console/console.py +2 -2
  32. pygpt_net/core/filesystem/actions.py +1 -2
  33. pygpt_net/core/filesystem/opener.py +261 -0
  34. pygpt_net/core/filesystem/url.py +13 -10
  35. pygpt_net/core/platforms/platforms.py +5 -4
  36. pygpt_net/core/render/plain/helpers.py +2 -5
  37. pygpt_net/core/render/plain/renderer.py +26 -30
  38. pygpt_net/core/render/web/body.py +1 -1
  39. pygpt_net/core/settings/settings.py +43 -13
  40. pygpt_net/core/tabs/tabs.py +20 -13
  41. pygpt_net/data/config/config.json +4 -4
  42. pygpt_net/data/config/models.json +3 -3
  43. pygpt_net/data/css/web-blocks.dark.css +7 -1
  44. pygpt_net/data/css/web-blocks.light.css +5 -2
  45. pygpt_net/data/css/web-chatgpt.dark.css +7 -1
  46. pygpt_net/data/css/web-chatgpt.light.css +3 -0
  47. pygpt_net/data/css/web-chatgpt_wide.dark.css +7 -1
  48. pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
  49. pygpt_net/data/locale/locale.de.ini +5 -1
  50. pygpt_net/data/locale/locale.en.ini +5 -1
  51. pygpt_net/data/locale/locale.es.ini +5 -1
  52. pygpt_net/data/locale/locale.fr.ini +5 -1
  53. pygpt_net/data/locale/locale.it.ini +5 -1
  54. pygpt_net/data/locale/locale.pl.ini +6 -4
  55. pygpt_net/data/locale/locale.uk.ini +5 -1
  56. pygpt_net/data/locale/locale.zh.ini +5 -1
  57. pygpt_net/plugin/twitter/plugin.py +2 -2
  58. pygpt_net/provider/core/config/patch.py +12 -1
  59. pygpt_net/tools/audio_transcriber/ui/dialogs.py +44 -54
  60. pygpt_net/tools/code_interpreter/body.py +1 -2
  61. pygpt_net/tools/code_interpreter/tool.py +7 -4
  62. pygpt_net/tools/code_interpreter/ui/html.py +1 -3
  63. pygpt_net/tools/code_interpreter/ui/widgets.py +2 -3
  64. pygpt_net/tools/html_canvas/ui/widgets.py +1 -3
  65. pygpt_net/tools/image_viewer/ui/dialogs.py +40 -37
  66. pygpt_net/tools/indexer/ui/widgets.py +2 -4
  67. pygpt_net/tools/media_player/tool.py +2 -5
  68. pygpt_net/tools/media_player/ui/widgets.py +60 -36
  69. pygpt_net/tools/text_editor/ui/widgets.py +18 -19
  70. pygpt_net/tools/translator/ui/widgets.py +39 -35
  71. pygpt_net/ui/base/context_menu.py +9 -4
  72. pygpt_net/ui/dialog/db.py +1 -3
  73. pygpt_net/ui/dialog/models.py +1 -3
  74. pygpt_net/ui/dialog/models_importer.py +2 -4
  75. pygpt_net/ui/dialogs.py +34 -30
  76. pygpt_net/ui/layout/chat/attachments.py +72 -84
  77. pygpt_net/ui/layout/chat/attachments_ctx.py +40 -44
  78. pygpt_net/ui/layout/chat/attachments_uploaded.py +36 -39
  79. pygpt_net/ui/layout/chat/calendar.py +100 -70
  80. pygpt_net/ui/layout/chat/chat.py +23 -17
  81. pygpt_net/ui/layout/chat/input.py +95 -118
  82. pygpt_net/ui/layout/chat/output.py +100 -162
  83. pygpt_net/ui/layout/chat/painter.py +89 -61
  84. pygpt_net/ui/layout/ctx/ctx_list.py +43 -52
  85. pygpt_net/ui/layout/status.py +23 -14
  86. pygpt_net/ui/layout/toolbox/agent.py +27 -38
  87. pygpt_net/ui/layout/toolbox/agent_llama.py +41 -45
  88. pygpt_net/ui/layout/toolbox/assistants.py +42 -38
  89. pygpt_net/ui/layout/toolbox/computer_env.py +32 -23
  90. pygpt_net/ui/layout/toolbox/footer.py +13 -16
  91. pygpt_net/ui/layout/toolbox/image.py +18 -21
  92. pygpt_net/ui/layout/toolbox/indexes.py +46 -89
  93. pygpt_net/ui/layout/toolbox/mode.py +20 -7
  94. pygpt_net/ui/layout/toolbox/model.py +12 -10
  95. pygpt_net/ui/layout/toolbox/presets.py +68 -52
  96. pygpt_net/ui/layout/toolbox/prompt.py +31 -58
  97. pygpt_net/ui/layout/toolbox/toolbox.py +25 -21
  98. pygpt_net/ui/layout/toolbox/vision.py +20 -22
  99. pygpt_net/ui/main.py +2 -4
  100. pygpt_net/ui/menu/about.py +64 -84
  101. pygpt_net/ui/menu/audio.py +87 -63
  102. pygpt_net/ui/menu/config.py +121 -127
  103. pygpt_net/ui/menu/debug.py +69 -76
  104. pygpt_net/ui/menu/file.py +32 -35
  105. pygpt_net/ui/menu/menu.py +2 -3
  106. pygpt_net/ui/menu/plugins.py +69 -33
  107. pygpt_net/ui/menu/theme.py +45 -46
  108. pygpt_net/ui/menu/tools.py +56 -60
  109. pygpt_net/ui/menu/video.py +20 -25
  110. pygpt_net/ui/tray.py +1 -2
  111. pygpt_net/ui/widget/audio/bar.py +1 -3
  112. pygpt_net/ui/widget/audio/input_button.py +3 -4
  113. pygpt_net/ui/widget/calendar/select.py +1 -2
  114. pygpt_net/ui/widget/dialog/base.py +12 -9
  115. pygpt_net/ui/widget/dialog/editor_file.py +20 -23
  116. pygpt_net/ui/widget/dialog/find.py +25 -24
  117. pygpt_net/ui/widget/dialog/profile.py +57 -53
  118. pygpt_net/ui/widget/draw/painter.py +62 -93
  119. pygpt_net/ui/widget/element/button.py +42 -30
  120. pygpt_net/ui/widget/element/checkbox.py +23 -15
  121. pygpt_net/ui/widget/element/group.py +6 -5
  122. pygpt_net/ui/widget/element/labels.py +1 -2
  123. pygpt_net/ui/widget/filesystem/explorer.py +93 -102
  124. pygpt_net/ui/widget/image/display.py +1 -2
  125. pygpt_net/ui/widget/lists/assistant.py +1 -2
  126. pygpt_net/ui/widget/lists/attachment.py +1 -2
  127. pygpt_net/ui/widget/lists/attachment_ctx.py +1 -2
  128. pygpt_net/ui/widget/lists/context.py +2 -4
  129. pygpt_net/ui/widget/lists/index.py +1 -2
  130. pygpt_net/ui/widget/lists/model.py +1 -2
  131. pygpt_net/ui/widget/lists/model_editor.py +1 -2
  132. pygpt_net/ui/widget/lists/model_importer.py +1 -2
  133. pygpt_net/ui/widget/lists/preset.py +1 -2
  134. pygpt_net/ui/widget/lists/preset_plugins.py +1 -2
  135. pygpt_net/ui/widget/lists/profile.py +1 -2
  136. pygpt_net/ui/widget/lists/uploaded.py +1 -2
  137. pygpt_net/ui/widget/option/checkbox.py +2 -4
  138. pygpt_net/ui/widget/option/checkbox_list.py +1 -4
  139. pygpt_net/ui/widget/option/cmd.py +1 -4
  140. pygpt_net/ui/widget/option/dictionary.py +25 -28
  141. pygpt_net/ui/widget/option/input.py +1 -3
  142. pygpt_net/ui/widget/tabs/Input.py +16 -12
  143. pygpt_net/ui/widget/tabs/body.py +5 -3
  144. pygpt_net/ui/widget/tabs/layout.py +41 -28
  145. pygpt_net/ui/widget/tabs/output.py +442 -85
  146. pygpt_net/ui/widget/textarea/calendar_note.py +1 -2
  147. pygpt_net/ui/widget/textarea/editor.py +41 -73
  148. pygpt_net/ui/widget/textarea/find.py +11 -10
  149. pygpt_net/ui/widget/textarea/html.py +3 -6
  150. pygpt_net/ui/widget/textarea/input.py +134 -69
  151. pygpt_net/ui/widget/textarea/notepad.py +54 -38
  152. pygpt_net/ui/widget/textarea/output.py +65 -54
  153. pygpt_net/ui/widget/textarea/search_input.py +5 -4
  154. pygpt_net/ui/widget/textarea/web.py +2 -4
  155. pygpt_net/ui/widget/vision/camera.py +2 -31
  156. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/METADATA +38 -174
  157. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/RECORD +160 -159
  158. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/LICENSE +0 -0
  159. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/WHEEL +0 -0
  160. {pygpt_net-2.6.21.dist-info → pygpt_net-2.6.23.dist-info}/entry_points.txt +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: 2024.04.19 01:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6 import QtCore
@@ -15,7 +15,6 @@ from PySide6.QtWidgets import QDialog, QPushButton, QHBoxLayout, QLabel, QVBoxLa
15
15
 
16
16
  from pygpt_net.utils import trans
17
17
  from .base import BaseDialog
18
- from ..element.labels import HelpLabel
19
18
  from ..option.input import DirectoryInput
20
19
 
21
20
 
@@ -27,7 +26,7 @@ class ProfileDialog(BaseDialog):
27
26
  :param window: main window
28
27
  :param id: settings id
29
28
  """
30
- super(ProfileDialog, self).__init__(window, id)
29
+ super().__init__(window, id)
31
30
  self.window = window
32
31
  self.id = id
33
32
 
@@ -39,7 +38,7 @@ class ProfileDialog(BaseDialog):
39
38
  """
40
39
  self.window.controller.settings.profile.dialog = False
41
40
  self.window.controller.settings.profile.update_menu()
42
- super(ProfileDialog, self).closeEvent(event)
41
+ super().closeEvent(event)
43
42
 
44
43
  def keyPressEvent(self, event):
45
44
  """
@@ -48,10 +47,9 @@ class ProfileDialog(BaseDialog):
48
47
  :param event: key press event
49
48
  """
50
49
  if event.key() == Qt.Key_Escape:
51
- self.cleanup()
52
- self.close() # close dialog when the Esc key is pressed.
50
+ self.close()
53
51
  else:
54
- super(ProfileDialog, self).keyPressEvent(event)
52
+ super().keyPressEvent(event)
55
53
 
56
54
  def cleanup(self):
57
55
  """
@@ -61,7 +59,6 @@ class ProfileDialog(BaseDialog):
61
59
  self.window.controller.settings.profile.update_menu()
62
60
 
63
61
 
64
-
65
62
  class ProfileEditDialog(QDialog):
66
63
  def __init__(self, window=None, id=None):
67
64
  """
@@ -69,7 +66,7 @@ class ProfileEditDialog(QDialog):
69
66
 
70
67
  :param window: main window
71
68
  """
72
- super(ProfileEditDialog, self).__init__(window)
69
+ super().__init__(window)
73
70
  self.window = window
74
71
  self.id = id
75
72
  self.uuid = None
@@ -77,29 +74,25 @@ class ProfileEditDialog(QDialog):
77
74
  self.name = ""
78
75
  self.mode = "create" # create | update
79
76
  self.input = ProfileNameInput(window, id)
77
+ self.input.setParent(self)
80
78
  self.input.setMinimumWidth(400)
81
79
 
82
- self.window.ui.nodes['dialog.profile.item.btn.update'] = QPushButton(trans('dialog.profile.item.btn.update'))
83
- self.window.ui.nodes['dialog.profile.item.btn.update'].clicked.connect(
84
- lambda: self.window.controller.settings.profile.handle_update(
85
- self.mode,
86
- self.input.text(),
87
- self.workdir.text(),
88
- self.uuid,
89
- )
90
- )
80
+ nodes = self.window.ui.nodes
91
81
 
92
- self.window.ui.nodes['dialog.profile.item.btn.dismiss'] = QPushButton(trans('dialog.profile.item.btn.dismiss'))
93
- self.window.ui.nodes['dialog.profile.item.btn.dismiss'].clicked.connect(
94
- lambda: self.window.controller.settings.profile.dismiss_update()
95
- )
82
+ self.btn_update = QPushButton(trans('dialog.profile.item.btn.update'), self)
83
+ self.btn_update.clicked.connect(self._on_update_clicked)
84
+ nodes['dialog.profile.item.btn.update'] = self.btn_update
85
+
86
+ self.btn_dismiss = QPushButton(trans('dialog.profile.item.btn.dismiss'), self)
87
+ self.btn_dismiss.clicked.connect(self._on_dismiss_clicked)
88
+ nodes['dialog.profile.item.btn.dismiss'] = self.btn_dismiss
96
89
 
97
90
  bottom = QHBoxLayout()
98
- bottom.addWidget(self.window.ui.nodes['dialog.profile.item.btn.dismiss'])
99
- bottom.addWidget(self.window.ui.nodes['dialog.profile.item.btn.update'])
91
+ bottom.addWidget(nodes['dialog.profile.item.btn.dismiss'])
92
+ bottom.addWidget(nodes['dialog.profile.item.btn.update'])
100
93
 
101
- self.window.ui.nodes['dialog.profile.name.label'] = QLabel(trans("dialog.profile.name.label"))
102
- self.window.ui.nodes['dialog.profile.workdir.label'] = QLabel(trans("dialog.profile.workdir.label"))
94
+ nodes['dialog.profile.name.label'] = QLabel(trans("dialog.profile.name.label"), self)
95
+ nodes['dialog.profile.workdir.label'] = QLabel(trans("dialog.profile.workdir.label"), self)
103
96
 
104
97
  option = {
105
98
  'type': 'text',
@@ -107,42 +100,56 @@ class ProfileEditDialog(QDialog):
107
100
  'value': "",
108
101
  }
109
102
  self.workdir = DirectoryInput(self.window, 'profile', 'item', option)
103
+ self.workdir.setParent(self)
110
104
 
111
- self.window.ui.nodes['dialog.profile.checkbox.switch'] = QCheckBox(trans("dialog.profile.checkbox.switch"))
112
- self.window.ui.nodes['dialog.profile.checkbox.switch'].setChecked(True)
113
- self.window.ui.nodes['dialog.profile.checkbox.db'] = QCheckBox(trans("dialog.profile.checkbox.include_db"))
114
- self.window.ui.nodes['dialog.profile.checkbox.db'].setChecked(True)
115
- self.window.ui.nodes['dialog.profile.checkbox.data'] = QCheckBox(trans("dialog.profile.checkbox.include_datadir"))
116
- self.window.ui.nodes['dialog.profile.checkbox.data'].setChecked(True)
105
+ nodes['dialog.profile.checkbox.switch'] = QCheckBox(trans("dialog.profile.checkbox.switch"), self)
106
+ nodes['dialog.profile.checkbox.switch'].setChecked(True)
117
107
 
118
- # checkboxes layout
108
+ self.checkboxes = QWidget(self)
119
109
  checkboxes_layout = QHBoxLayout()
120
110
  checkboxes_layout.setContentsMargins(0, 0, 0, 0)
121
- checkboxes_layout.addWidget(self.window.ui.nodes['dialog.profile.checkbox.db'])
122
- checkboxes_layout.addWidget(self.window.ui.nodes['dialog.profile.checkbox.data'])
123
- self.checkboxes = QWidget()
111
+
112
+ nodes['dialog.profile.checkbox.db'] = QCheckBox(trans("dialog.profile.checkbox.include_db"), self.checkboxes)
113
+ nodes['dialog.profile.checkbox.db'].setChecked(True)
114
+ nodes['dialog.profile.checkbox.data'] = QCheckBox(trans("dialog.profile.checkbox.include_datadir"), self.checkboxes)
115
+ nodes['dialog.profile.checkbox.data'].setChecked(True)
116
+
117
+ checkboxes_layout.addWidget(nodes['dialog.profile.checkbox.db'])
118
+ checkboxes_layout.addWidget(nodes['dialog.profile.checkbox.data'])
124
119
  self.checkboxes.setLayout(checkboxes_layout)
125
120
 
126
121
  layout = QVBoxLayout()
127
- layout.addWidget(self.window.ui.nodes['dialog.profile.name.label'])
122
+ layout.addWidget(nodes['dialog.profile.name.label'])
128
123
  layout.addWidget(self.input)
129
- layout.addWidget(self.window.ui.nodes['dialog.profile.workdir.label'])
124
+ layout.addWidget(nodes['dialog.profile.workdir.label'])
130
125
  layout.addWidget(self.workdir)
131
126
  layout.addWidget(self.checkboxes)
132
- layout.addWidget(self.window.ui.nodes['dialog.profile.checkbox.switch'])
127
+ layout.addWidget(nodes['dialog.profile.checkbox.switch'])
133
128
  layout.addLayout(bottom)
134
129
 
135
130
  self.setLayout(layout)
136
131
 
132
+ def _on_update_clicked(self):
133
+ self.window.controller.settings.profile.handle_update(
134
+ self.mode,
135
+ self.input.text(),
136
+ self.workdir.text(),
137
+ self.uuid,
138
+ )
139
+
140
+ def _on_dismiss_clicked(self):
141
+ self.window.controller.settings.profile.dismiss_update()
142
+
137
143
  def prepare(self):
138
144
  """Prepare dialog before show"""
139
- if self.mode == 'create':
140
- self.window.ui.nodes['dialog.profile.item.btn.update'].setText(trans('dialog.profile.item.btn.create'))
141
- elif self.mode == 'edit':
142
- self.window.ui.nodes['dialog.profile.item.btn.update'].setText(trans("dialog.profile.item.btn.update"))
143
- elif self.mode == 'duplicate':
144
- self.window.ui.nodes['dialog.profile.item.btn.update'].setText(trans("dialog.profile.item.btn.duplicate"))
145
-
145
+ mapping = {
146
+ 'create': 'dialog.profile.item.btn.create',
147
+ 'edit': 'dialog.profile.item.btn.update',
148
+ 'duplicate': 'dialog.profile.item.btn.duplicate',
149
+ }
150
+ key = mapping.get(self.mode)
151
+ if key:
152
+ self.window.ui.nodes['dialog.profile.item.btn.update'].setText(trans(key))
146
153
  self.workdir.setText(self.path)
147
154
 
148
155
 
@@ -154,8 +161,7 @@ class ProfileNameInput(QLineEdit):
154
161
  :param window: main window
155
162
  :param id: info window id
156
163
  """
157
- super(ProfileNameInput, self).__init__(window)
158
-
164
+ super().__init__(window)
159
165
  self.window = window
160
166
  self.id = id
161
167
 
@@ -165,13 +171,11 @@ class ProfileNameInput(QLineEdit):
165
171
 
166
172
  :param event: key event
167
173
  """
168
- super(ProfileNameInput, self).keyPressEvent(event)
169
-
170
- # save on Enter
171
- if event.key() == QtCore.Qt.Key_Return or event.key() == QtCore.Qt.Key_Enter:
174
+ super().keyPressEvent(event)
175
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
172
176
  self.window.controller.settings.profile.handle_update(
173
177
  self.window.ui.dialog['profile.item'].mode,
174
178
  self.window.ui.dialog['profile.item'].input.text(),
175
179
  self.window.ui.dialog['profile.item'].workdir.text(),
176
180
  self.window.ui.dialog['profile.item'].uuid,
177
- )
181
+ )
@@ -6,18 +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.01.19 02:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
13
+ from collections import deque
13
14
 
14
15
  from PySide6.QtCore import Qt, QPoint
15
- from PySide6.QtGui import QImage, QPainter, QPen, QAction, QIcon, QKeySequence
16
+ from PySide6.QtGui import QImage, QPainter, QPen, QAction, QIcon
16
17
  from PySide6.QtWidgets import QMenu, QWidget, QFileDialog, QMessageBox, QApplication
17
18
 
18
19
  from pygpt_net.core.tabs.tab import Tab
19
20
  from pygpt_net.utils import trans
20
- import pygpt_net.icons_rc
21
21
 
22
22
 
23
23
  class PainterWidget(QWidget):
@@ -31,13 +31,50 @@ class PainterWidget(QWidget):
31
31
  self.brushColor = Qt.black
32
32
  self.lastPoint = QPoint()
33
33
  self.originalImage = None
34
- self.undoStack = []
35
- self.redoStack = []
36
34
  self.undoLimit = 10
35
+ self.undoStack = deque(maxlen=self.undoLimit)
36
+ self.redoStack = deque()
37
37
  self.setFocusPolicy(Qt.StrongFocus)
38
38
  self.setFocus()
39
39
  self.installEventFilter(self)
40
40
  self.tab = None
41
+ self._pen = QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
42
+ self.setAttribute(Qt.WA_OpaquePaintEvent, True)
43
+ self.setAttribute(Qt.WA_StaticContents, True)
44
+
45
+ self._act_undo = QAction(QIcon(":/icons/undo.svg"), trans('action.undo'), self)
46
+ self._act_undo.triggered.connect(self.undo)
47
+
48
+ self._act_redo = QAction(QIcon(":/icons/redo.svg"), trans('action.redo'), self)
49
+ self._act_redo.triggered.connect(self.redo)
50
+
51
+ self._act_copy = QAction(QIcon(":/icons/copy.svg"), trans('action.copy'), self)
52
+ self._act_copy.triggered.connect(self.handle_copy)
53
+
54
+ self._act_paste = QAction(QIcon(":/icons/paste.svg"), trans('action.paste'), self)
55
+ self._act_paste.triggered.connect(self.handle_paste)
56
+
57
+ self._act_open = QAction(QIcon(":/icons/folder_filled.svg"), trans('action.open'), self)
58
+ self._act_open.triggered.connect(self.action_open)
59
+
60
+ self._act_capture = QAction(QIcon(":/icons/fullscreen.svg"), trans('painter.btn.capture'), self)
61
+ self._act_capture.triggered.connect(self.action_capture)
62
+
63
+ self._act_save = QAction(QIcon(":/icons/save.svg"), trans('img.action.save'), self)
64
+ self._act_save.triggered.connect(self.action_save)
65
+
66
+ self._act_clear = QAction(QIcon(":/icons/close.svg"), trans('painter.btn.clear'), self)
67
+ self._act_clear.triggered.connect(self.action_clear)
68
+
69
+ self._ctx_menu = QMenu(self)
70
+ self._ctx_menu.addAction(self._act_undo)
71
+ self._ctx_menu.addAction(self._act_redo)
72
+ self._ctx_menu.addAction(self._act_open)
73
+ self._ctx_menu.addAction(self._act_capture)
74
+ self._ctx_menu.addAction(self._act_copy)
75
+ self._ctx_menu.addAction(self._act_paste)
76
+ self._ctx_menu.addAction(self._act_save)
77
+ self._ctx_menu.addAction(self._act_clear)
41
78
 
42
79
  def set_tab(self, tab: Tab):
43
80
  """
@@ -67,66 +104,14 @@ class PainterWidget(QWidget):
67
104
 
68
105
  :param event: Event
69
106
  """
70
- actions = {}
71
- actions['undo'] = QAction(QIcon(":/icons/undo.svg"), trans('action.undo'), self)
72
- actions['undo'].triggered.connect(
73
- lambda: self.undo())
74
- if self.has_undo():
75
- actions['undo'].setEnabled(True)
76
- else:
77
- actions['undo'].setEnabled(False)
78
-
79
- actions['redo'] = QAction(QIcon(":/icons/redo.svg"), trans('action.redo'), self)
80
- actions['redo'].triggered.connect(
81
- lambda: self.redo())
82
- if self.has_redo():
83
- actions['redo'].setEnabled(True)
84
- else:
85
- actions['redo'].setEnabled(False)
86
-
87
- actions['copy'] = QAction(QIcon(":/icons/copy.svg"), trans('action.copy'), self)
88
- actions['copy'].triggered.connect(
89
- lambda: self.handle_copy()
90
- )
107
+ self._act_undo.setEnabled(self.has_undo())
108
+ self._act_redo.setEnabled(self.has_redo())
91
109
 
92
- is_paste = False
93
110
  clipboard = QApplication.clipboard()
94
111
  mime_data = clipboard.mimeData()
95
- if mime_data.hasImage():
96
- is_paste = True
112
+ self._act_paste.setEnabled(bool(mime_data.hasImage()))
97
113
 
98
- actions['paste'] = QAction(QIcon(":/icons/paste.svg"), trans('action.paste'), self)
99
- actions['paste'].triggered.connect(
100
- lambda: self.handle_paste()
101
- )
102
- if is_paste:
103
- actions['paste'].setEnabled(True)
104
- else:
105
- actions['paste'].setEnabled(False)
106
-
107
- actions['open'] = QAction(QIcon(":/icons/folder_filled.svg"), trans('action.open'), self)
108
- actions['open'].triggered.connect(
109
- lambda: self.action_open())
110
- actions['capture'] = QAction(QIcon(":/icons/fullscreen.svg"), trans('painter.btn.capture'), self)
111
- actions['capture'].triggered.connect(
112
- lambda: self.action_capture())
113
- actions['save'] = QAction(QIcon(":/icons/save.svg"), trans('img.action.save'), self)
114
- actions['save'].triggered.connect(
115
- lambda: self.action_save())
116
- actions['clear'] = QAction(QIcon(":/icons/close.svg"), trans('painter.btn.clear'), self)
117
- actions['clear'].triggered.connect(
118
- lambda: self.action_clear())
119
-
120
- menu = QMenu(self)
121
- menu.addAction(actions['undo'])
122
- menu.addAction(actions['redo'])
123
- menu.addAction(actions['open'])
124
- menu.addAction(actions['capture'])
125
- menu.addAction(actions['copy'])
126
- menu.addAction(actions['paste'])
127
- menu.addAction(actions['save'])
128
- menu.addAction(actions['clear'])
129
- menu.exec_(event.globalPos())
114
+ self._ctx_menu.exec(event.globalPos())
130
115
 
131
116
  def action_open(self):
132
117
  """Open the image"""
@@ -195,7 +180,6 @@ class PainterWidget(QWidget):
195
180
  if image.width() == self.width():
196
181
  new = image
197
182
  else:
198
- # to fit the width
199
183
  if image.width() > image.height():
200
184
  width = self.width()
201
185
  height = (image.height() * self.width()) / image.width()
@@ -215,11 +199,8 @@ class PainterWidget(QWidget):
215
199
  Qt.SmoothTransformation,
216
200
  )
217
201
 
218
- self.image = QImage(
219
- self.width(),
220
- self.height(),
221
- QImage.Format_RGB32,
222
- )
202
+ if self.image.size() != self.size():
203
+ self.image = QImage(self.size(), QImage.Format_RGB32)
223
204
  self.image.fill(Qt.white)
224
205
  painter = QPainter(self.image)
225
206
  painter.drawImage(0, 0, new)
@@ -229,22 +210,20 @@ class PainterWidget(QWidget):
229
210
 
230
211
  def saveForUndo(self):
231
212
  """Save current state for undo"""
232
- if len(self.undoStack) >= self.undoLimit:
233
- self.undoStack.pop(0)
234
- self.undoStack.append(self.image.copy())
235
- self.redoStack.clear() # clear redo on new action
213
+ self.undoStack.append(QImage(self.image))
214
+ self.redoStack.clear()
236
215
 
237
216
  def undo(self):
238
217
  """Undo the last action"""
239
218
  if self.undoStack:
240
- self.redoStack.append(self.image.copy())
219
+ self.redoStack.append(QImage(self.image))
241
220
  self.image = self.undoStack.pop()
242
221
  self.update()
243
222
 
244
223
  def redo(self):
245
224
  """Redo the last undo action"""
246
225
  if self.redoStack:
247
- self.undoStack.append(self.image.copy())
226
+ self.undoStack.append(QImage(self.image))
248
227
  self.image = self.redoStack.pop()
249
228
  self.update()
250
229
 
@@ -263,6 +242,7 @@ class PainterWidget(QWidget):
263
242
  :param color: Color
264
243
  """
265
244
  self.brushColor = color
245
+ self._pen.setColor(color)
266
246
 
267
247
  def set_brush_size(self, size):
268
248
  """
@@ -271,6 +251,7 @@ class PainterWidget(QWidget):
271
251
  :param size: Brush size
272
252
  """
273
253
  self.brushSize = size
254
+ self._pen.setWidth(size)
274
255
 
275
256
  def clear_image(self):
276
257
  """Clear the image"""
@@ -289,16 +270,9 @@ class PainterWidget(QWidget):
289
270
  self.lastPoint = event.pos()
290
271
  self.saveForUndo()
291
272
  painter = QPainter(self.image)
292
- painter.setPen(
293
- QPen(
294
- self.brushColor,
295
- self.brushSize,
296
- Qt.SolidLine,
297
- Qt.RoundCap,
298
- Qt.RoundJoin,
299
- )
300
- )
273
+ painter.setPen(self._pen)
301
274
  painter.drawPoint(self.lastPoint)
275
+ painter.end()
302
276
  self.update()
303
277
 
304
278
  def mouseMoveEvent(self, event):
@@ -309,16 +283,9 @@ class PainterWidget(QWidget):
309
283
  """
310
284
  if (event.buttons() & Qt.LeftButton) and self.drawing:
311
285
  painter = QPainter(self.image)
312
- painter.setPen(
313
- QPen(
314
- self.brushColor,
315
- self.brushSize,
316
- Qt.SolidLine,
317
- Qt.RoundCap,
318
- Qt.RoundJoin,
319
- )
320
- )
286
+ painter.setPen(self._pen)
321
287
  painter.drawLine(self.lastPoint, event.pos())
288
+ painter.end()
322
289
  self.lastPoint = event.pos()
323
290
  self.update()
324
291
 
@@ -350,12 +317,13 @@ class PainterWidget(QWidget):
350
317
  """
351
318
  painter = QPainter(self)
352
319
  painter.drawImage(self.rect(), self.image, self.image.rect())
320
+ painter.end()
353
321
  self.originalImage = self.image
354
322
 
355
323
  def resizeEvent(self, event):
356
324
  """
357
325
  Update coords on resize
358
-
326
+
359
327
  :param event: Event
360
328
  """
361
329
  if self.image.size() != self.size():
@@ -363,6 +331,7 @@ class PainterWidget(QWidget):
363
331
  new.fill(Qt.white)
364
332
  painter = QPainter(new)
365
333
  painter.drawImage(QPoint(0, 0), self.image)
334
+ painter.end()
366
335
  self.image = new
367
336
 
368
337
  def eventFilter(self, source, event):
@@ -376,4 +345,4 @@ class PainterWidget(QWidget):
376
345
  if self.tab is not None:
377
346
  col_idx = self.tab.column_idx
378
347
  self.window.controller.ui.tabs.on_column_focus(col_idx)
379
- return super().eventFilter(source, event)
348
+ return super().eventFilter(source, event)
@@ -6,11 +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: 2024.04.29 16:00:00 #
9
+ # Updated Date: 2025.08.24 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
13
- from PySide6.QtGui import QAction, QIcon
13
+ from PySide6.QtGui import QIcon
14
14
  from PySide6.QtWidgets import QPushButton, QMenu
15
15
 
16
16
  from pygpt_net.utils import trans
@@ -22,13 +22,17 @@ class ContextMenuButton(QPushButton):
22
22
  self.action = action
23
23
 
24
24
  def mousePressEvent(self, event):
25
- if event.button() == Qt.LeftButton or event.button() == Qt.RightButton:
25
+ btn = event.button()
26
+ if btn == Qt.LeftButton or btn == Qt.RightButton:
26
27
  self.action(self, event.pos())
27
28
  else:
28
29
  super().mousePressEvent(event)
29
30
 
30
31
 
31
32
  class NewCtxButton(QPushButton):
33
+ _icon_add = None
34
+ _icon_folder_filled = None
35
+
32
36
  def __init__(self, title: str = None, window=None):
33
37
  super().__init__(title)
34
38
  self.window = window
@@ -36,6 +40,12 @@ class NewCtxButton(QPushButton):
36
40
  lambda: self.window.controller.ctx.new()
37
41
  )
38
42
 
43
+ @classmethod
44
+ def _ensure_icons(cls):
45
+ if cls._icon_add is None:
46
+ cls._icon_add = QIcon(":/icons/add.svg")
47
+ cls._icon_folder_filled = QIcon(":/icons/folder_filled.svg")
48
+
39
49
  def mousePressEvent(self, event):
40
50
  if event.button() == Qt.RightButton:
41
51
  self.new_context_menu(self, event.pos())
@@ -49,32 +59,30 @@ class NewCtxButton(QPushButton):
49
59
  :param parent: parent widget
50
60
  :param pos: mouse position
51
61
  """
62
+ type(self)._ensure_icons()
52
63
  group_id = self.window.controller.ctx.group_id
53
- menu = QMenu(self)
54
- actions = {}
55
- actions['new'] = QAction(QIcon(":/icons/add.svg"), trans('action.ctx.new'), self)
56
- actions['new'].triggered.connect(
57
- lambda: self.window.controller.ctx.new_ungrouped()
58
- )
64
+ menu = QMenu(parent)
59
65
  if group_id is not None and group_id > 0:
60
66
  group_name = self.window.controller.ctx.get_group_name(group_id)
61
- actions['new_in_group'] = QAction(QIcon(":/icons/add.svg"), trans('action.ctx.new.in_group').format(group=group_name), self)
62
- actions['new_in_group'].triggered.connect(
67
+ act_new_in_group = menu.addAction(type(self)._icon_add, trans('action.ctx.new.in_group').format(group=group_name))
68
+ act_new_in_group.triggered.connect(
63
69
  lambda checked=False, id=group_id: self.window.controller.ctx.new(force=False, group_id=id)
64
70
  )
65
- actions['new_group'] = QAction(QIcon(":/icons/folder_filled.svg"), trans('menu.file.group.new'), self)
66
- actions['new_group'].triggered.connect(
67
- lambda: self.window.controller.ctx.new_group()
71
+ act_new = menu.addAction(type(self)._icon_add, trans('action.ctx.new'))
72
+ act_new.triggered.connect(
73
+ lambda checked=False: self.window.controller.ctx.new_ungrouped()
74
+ )
75
+ act_new_group = menu.addAction(type(self)._icon_folder_filled, trans('menu.file.group.new'))
76
+ act_new_group.triggered.connect(
77
+ lambda checked=False: self.window.controller.ctx.new_group()
68
78
  )
69
-
70
- if group_id is not None and group_id > 0:
71
- menu.addAction(actions['new_in_group'])
72
- menu.addAction(actions['new'])
73
- menu.addAction(actions['new_group'])
74
79
  menu.exec_(parent.mapToGlobal(pos))
80
+ menu.deleteLater()
75
81
 
76
82
 
77
83
  class SyncButton(QPushButton):
84
+ _icon_download = None
85
+
78
86
  def __init__(self, title: str = None, window=None):
79
87
  super().__init__(title)
80
88
  self.window = window
@@ -82,6 +90,11 @@ class SyncButton(QPushButton):
82
90
  lambda: self.window.controller.assistant.batch.import_files_current()
83
91
  )
84
92
 
93
+ @classmethod
94
+ def _ensure_icons(cls):
95
+ if cls._icon_download is None:
96
+ cls._icon_download = QIcon(":/icons/download.svg")
97
+
85
98
  def mousePressEvent(self, event):
86
99
  if event.button() == Qt.RightButton:
87
100
  self.new_context_menu(self, event.pos())
@@ -95,16 +108,15 @@ class SyncButton(QPushButton):
95
108
  :param parent: parent widget
96
109
  :param pos: mouse position
97
110
  """
98
- menu = QMenu(self)
99
- actions = {}
100
- actions['current'] = QAction(QIcon(":/icons/download.svg"), trans('attachments_uploaded.btn.sync.current'), self)
101
- actions['current'].triggered.connect(
102
- lambda: self.window.controller.assistant.batch.import_files_current()
111
+ type(self)._ensure_icons()
112
+ menu = QMenu(parent)
113
+ act_current = menu.addAction(type(self)._icon_download, trans('attachments_uploaded.btn.sync.current'))
114
+ act_current.triggered.connect(
115
+ lambda checked=False: self.window.controller.assistant.batch.import_files_current()
103
116
  )
104
- actions['all'] = QAction(QIcon(":/icons/download.svg"), trans('attachments_uploaded.btn.sync.all'), self)
105
- actions['all'].triggered.connect(
106
- lambda: self.window.controller.assistant.batch.import_files()
117
+ act_all = menu.addAction(type(self)._icon_download, trans('attachments_uploaded.btn.sync.all'))
118
+ act_all.triggered.connect(
119
+ lambda checked=False: self.window.controller.assistant.batch.import_files()
107
120
  )
108
- menu.addAction(actions['current'])
109
- menu.addAction(actions['all'])
110
- menu.exec_(parent.mapToGlobal(pos))
121
+ menu.exec_(parent.mapToGlobal(pos))
122
+ menu.deleteLater()