pygpt-net 2.4.37__py3-none-any.whl → 2.4.39__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 (81) hide show
  1. CHANGELOG.md +11 -0
  2. README.md +24 -5
  3. pygpt_net/CHANGELOG.txt +11 -0
  4. pygpt_net/__init__.py +3 -3
  5. pygpt_net/controller/__init__.py +3 -1
  6. pygpt_net/controller/calendar/__init__.py +3 -1
  7. pygpt_net/controller/chat/render.py +8 -5
  8. pygpt_net/controller/config/placeholder.py +29 -0
  9. pygpt_net/controller/ctx/__init__.py +32 -24
  10. pygpt_net/controller/ctx/common.py +3 -2
  11. pygpt_net/controller/dialogs/confirm.py +2 -2
  12. pygpt_net/controller/lang/custom.py +2 -7
  13. pygpt_net/controller/lang/mapping.py +2 -2
  14. pygpt_net/controller/layout.py +2 -2
  15. pygpt_net/controller/notepad.py +8 -5
  16. pygpt_net/controller/settings/editor.py +6 -0
  17. pygpt_net/controller/theme/__init__.py +33 -8
  18. pygpt_net/controller/theme/common.py +22 -1
  19. pygpt_net/controller/theme/markdown.py +26 -14
  20. pygpt_net/controller/theme/menu.py +26 -5
  21. pygpt_net/controller/ui/tabs.py +201 -58
  22. pygpt_net/core/attachments/context.py +14 -12
  23. pygpt_net/core/audio/__init__.py +59 -1
  24. pygpt_net/core/ctx/__init__.py +11 -1
  25. pygpt_net/core/ctx/container.py +16 -9
  26. pygpt_net/core/ctx/output.py +86 -67
  27. pygpt_net/core/debug/tabs.py +3 -2
  28. pygpt_net/core/filesystem/__init__.py +5 -19
  29. pygpt_net/core/filesystem/url.py +7 -3
  30. pygpt_net/core/render/base.py +14 -3
  31. pygpt_net/core/render/markdown/renderer.py +3 -1
  32. pygpt_net/core/render/plain/renderer.py +3 -3
  33. pygpt_net/core/render/web/body.py +39 -17
  34. pygpt_net/core/render/web/renderer.py +7 -5
  35. pygpt_net/core/tabs/__init__.py +180 -71
  36. pygpt_net/core/tabs/tab.py +13 -4
  37. pygpt_net/data/config/config.json +14 -4
  38. pygpt_net/data/config/models.json +3 -3
  39. pygpt_net/data/config/modes.json +3 -3
  40. pygpt_net/data/config/settings.json +55 -10
  41. pygpt_net/data/config/settings_section.json +3 -0
  42. pygpt_net/data/css/style.light.css +1 -0
  43. pygpt_net/data/css/{web.css → web-blocks.css} +144 -133
  44. pygpt_net/data/css/web-chatgpt.css +342 -0
  45. pygpt_net/data/css/web-chatgpt.dark.css +64 -0
  46. pygpt_net/data/css/web-chatgpt.light.css +75 -0
  47. pygpt_net/data/css/web-chatgpt_wide.css +342 -0
  48. pygpt_net/data/css/web-chatgpt_wide.dark.css +64 -0
  49. pygpt_net/data/css/web-chatgpt_wide.light.css +75 -0
  50. pygpt_net/data/locale/locale.de.ini +12 -0
  51. pygpt_net/data/locale/locale.en.ini +14 -1
  52. pygpt_net/data/locale/locale.es.ini +12 -0
  53. pygpt_net/data/locale/locale.fr.ini +12 -0
  54. pygpt_net/data/locale/locale.it.ini +12 -0
  55. pygpt_net/data/locale/locale.pl.ini +12 -0
  56. pygpt_net/data/locale/locale.uk.ini +12 -0
  57. pygpt_net/data/locale/locale.zh.ini +12 -0
  58. pygpt_net/plugin/audio_input/simple.py +21 -5
  59. pygpt_net/provider/core/config/patch.py +22 -1
  60. pygpt_net/provider/core/ctx/base.py +4 -1
  61. pygpt_net/provider/core/ctx/db_sqlite/__init__.py +10 -1
  62. pygpt_net/provider/core/ctx/db_sqlite/storage.py +22 -1
  63. pygpt_net/ui/layout/chat/input.py +10 -18
  64. pygpt_net/ui/layout/chat/output.py +26 -44
  65. pygpt_net/ui/layout/toolbox/footer.py +18 -2
  66. pygpt_net/ui/menu/config.py +7 -11
  67. pygpt_net/ui/menu/theme.py +9 -2
  68. pygpt_net/ui/widget/lists/context.py +1 -0
  69. pygpt_net/ui/widget/tabs/layout.py +195 -0
  70. pygpt_net/ui/widget/tabs/output.py +124 -35
  71. pygpt_net/ui/widget/textarea/html.py +11 -1
  72. pygpt_net/ui/widget/textarea/output.py +10 -1
  73. pygpt_net/ui/widget/textarea/search_input.py +4 -1
  74. pygpt_net/ui/widget/textarea/web.py +49 -9
  75. {pygpt_net-2.4.37.dist-info → pygpt_net-2.4.39.dist-info}/METADATA +25 -6
  76. {pygpt_net-2.4.37.dist-info → pygpt_net-2.4.39.dist-info}/RECORD +81 -74
  77. /pygpt_net/data/css/{web.dark.css → web-blocks.dark.css} +0 -0
  78. /pygpt_net/data/css/{web.light.css → web-blocks.light.css} +0 -0
  79. {pygpt_net-2.4.37.dist-info → pygpt_net-2.4.39.dist-info}/LICENSE +0 -0
  80. {pygpt_net-2.4.37.dist-info → pygpt_net-2.4.39.dist-info}/WHEEL +0 -0
  81. {pygpt_net-2.4.37.dist-info → pygpt_net-2.4.39.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.30 15:00:00 #
9
+ # Updated Date: 2024.12.07 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -156,3 +156,24 @@ class Common:
156
156
  with open(path) as file:
157
157
  content += file.read()
158
158
  return content
159
+
160
+ def get_styles_list(self) -> list:
161
+ """
162
+ Return a list of available styles
163
+
164
+ :return: list of styles names
165
+ """
166
+ styles = []
167
+ app_dir = os.path.join(self.window.core.config.get_app_path(), 'data', 'css')
168
+ user_dir = os.path.join(self.window.core.config.path, 'css')
169
+ for path in [app_dir, user_dir]:
170
+ if not os.path.exists(path):
171
+ continue
172
+ for file in os.listdir(path):
173
+ if file.startswith("web-") and file.endswith('.css'):
174
+ to_replace = ['web-', '.css', '.light', '.dark']
175
+ for item in to_replace:
176
+ file = file.replace(item, '')
177
+ if file not in styles:
178
+ styles.append(file)
179
+ return sorted(styles)
@@ -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.11.20 21:00:00 #
9
+ # Updated Date: 2024.12.07 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -23,6 +23,7 @@ class Markdown:
23
23
  """
24
24
  self.window = window
25
25
  self.css = {} # external styles
26
+ self.web_style = ""
26
27
 
27
28
  def update(self, force: bool = False):
28
29
  """
@@ -60,7 +61,8 @@ class Markdown:
60
61
 
61
62
  :return: stylesheet
62
63
  """
63
- if "web" not in self.css:
64
+ web_style = self.window.core.config.get("theme.style", "blocks")
65
+ if "web" not in self.css or self.web_style != web_style:
64
66
  self.load()
65
67
  if "web" in self.css:
66
68
  return self.css["web"]
@@ -73,31 +75,41 @@ class Markdown:
73
75
  self.window.dispatch(event) # per current engine
74
76
  self.window.controller.ctx.refresh()
75
77
  self.window.controller.ctx.refresh_output()
76
- data = {
78
+ event = RenderEvent(RenderEvent.END, {
77
79
  "meta": meta,
78
- }
79
- event = RenderEvent(RenderEvent.END, data)
80
+ })
80
81
  self.window.dispatch(event)
81
82
 
82
83
  def load(self):
83
84
  """Load markdown styles"""
84
- parents = ["markdown", "web"]
85
+ parents = [
86
+ "markdown",
87
+ "web",
88
+ ]
89
+ web_style = self.window.core.config.get("theme.style", "blocks")
85
90
  for base_name in parents:
91
+ suffix = ""
92
+ if base_name == 'web':
93
+ suffix = "-" + web_style
94
+ self.web_style = web_style
86
95
  theme = self.window.core.config.get('theme')
87
96
  name = str(base_name)
88
- color_name = str(base_name)
89
97
  if theme.startswith('light'):
90
- color_name += '.light'
98
+ color = '.light'
91
99
  else:
92
- color_name += '.dark'
100
+ color = '.dark'
101
+
102
+ # load CSS, app + user
103
+ file_base = name + suffix + '.css'
104
+ file_color = name + suffix + color + '.css'
93
105
  paths = []
94
- paths.append(os.path.join(self.window.core.config.get_app_path(), 'data', 'css', name + '.css'))
95
- paths.append(os.path.join(self.window.core.config.get_app_path(), 'data', 'css', color_name + '.css'))
96
- paths.append(os.path.join(self.window.core.config.get_user_path(), 'css', name + '.css'))
97
- paths.append(os.path.join(self.window.core.config.get_user_path(), 'css', color_name + '.css'))
106
+ paths.append(os.path.join(self.window.core.config.get_app_path(), 'data', 'css', file_base))
107
+ paths.append(os.path.join(self.window.core.config.get_app_path(), 'data', 'css', file_color))
108
+ paths.append(os.path.join(self.window.core.config.get_user_path(), 'css', file_base))
109
+ paths.append(os.path.join(self.window.core.config.get_user_path(), 'css', file_color))
98
110
  content = ''
99
111
  for path in paths:
100
- if os.path.exists(path):
112
+ if os.path.exists(path) and os.path.isfile(path):
101
113
  with open(path, 'r') as file:
102
114
  content += file.read()
103
115
 
@@ -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.11.05 23:00:00 #
9
+ # Updated Date: 2024.12.07 21:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtGui import QAction
@@ -30,6 +30,18 @@ class Menu:
30
30
  # setup themes list menu
31
31
  if self.loaded:
32
32
  return
33
+
34
+ # styles
35
+ styles = self.window.controller.theme.common.get_styles_list()
36
+ for style in styles:
37
+ style_id = style.lower()
38
+ title = style.replace('_', ' ').title()
39
+ self.window.ui.menu['theme_style'][style_id] = QAction(title, self.window, checkable=True)
40
+ self.window.ui.menu['theme_style'][style_id].triggered.connect(
41
+ lambda checked=None, style=style_id: self.window.controller.theme.toggle_style(style))
42
+ self.window.ui.menu['theme.style'].addAction(self.window.ui.menu['theme_style'][style_id])
43
+
44
+ # color themes
33
45
  themes = self.window.controller.theme.common.get_themes_list()
34
46
  for theme in themes:
35
47
  name = self.window.controller.theme.common.translate(theme)
@@ -42,6 +54,7 @@ class Menu:
42
54
  self.window.ui.menu['theme.dark'].addAction(self.window.ui.menu['theme'][theme])
43
55
  elif theme.startswith('light'):
44
56
  self.window.ui.menu['theme.light'].addAction(self.window.ui.menu['theme'][theme])
57
+
45
58
  self.loaded = True
46
59
 
47
60
  def setup_syntax(self):
@@ -86,14 +99,22 @@ class Menu:
86
99
 
87
100
  def update_list(self):
88
101
  """Update theme list menu"""
89
- current = self.window.core.config.get('theme')
102
+ # styles
103
+ current_style = self.window.core.config.get('theme.style')
104
+ for style in self.window.ui.menu['theme_style']:
105
+ self.window.ui.menu['theme_style'][style].setChecked(False)
106
+ if current_style in self.window.ui.menu['theme_style']:
107
+ self.window.ui.menu['theme_style'][current_style].setChecked(True)
108
+
109
+ # color themes
110
+ current_theme = self.window.core.config.get('theme')
90
111
  for theme in self.window.ui.menu['theme']:
91
112
  self.window.ui.menu['theme'][theme].setChecked(False)
92
- if current in self.window.ui.menu['theme']:
93
- self.window.ui.menu['theme'][current].setChecked(True)
113
+ if current_theme in self.window.ui.menu['theme']:
114
+ self.window.ui.menu['theme'][current_theme].setChecked(True)
94
115
 
95
116
  def update_syntax(self):
96
- """Update syntax menu"""
117
+ """Update code syntax highlight menu"""
97
118
  current = self.window.core.config.get('render.code_syntax')
98
119
  for style in self.window.ui.menu['theme_syntax']:
99
120
  self.window.ui.menu['theme_syntax'][style].setChecked(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: 2024.11.20 21:00:00 #
9
+ # Updated Date: 2024.12.09 03:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QTimer
@@ -29,35 +29,66 @@ class Tabs:
29
29
  self.initialized = False
30
30
  self.appended = False
31
31
  self.current = 0
32
+ self.column_idx = 0
33
+ self.tmp_column_idx = 0
34
+ self.locked = False
32
35
 
33
36
  def setup(self):
34
37
  """Setup tabs"""
35
38
  self.window.core.tabs.load()
36
39
  self.window.controller.notepad.load()
40
+ self.setup_options()
37
41
  self.initialized = True
38
42
 
39
- def add(self, type: int, title: str, icon=None, reference=None, data_id=None):
43
+ def setup_options(self):
44
+ """Setup options"""
45
+ state = self.window.core.config.get("layout.split", False)
46
+ self.window.ui.nodes['layout.split'].setChecked(state)
47
+ if not state:
48
+ self.window.ui.splitters['columns'].setSizes([1, 0])
49
+
50
+ def add(
51
+ self,
52
+ type: int,
53
+ title: str,
54
+ icon=None,
55
+ child=None,
56
+ data_id=None
57
+ ):
40
58
  """
41
- Add tab
59
+ Add a new tab
42
60
 
43
61
  :param type: Tab type
44
62
  :param title: Tab title
45
63
  :param icon: Tab icon
46
- :param reference: Tab reference
47
- :param data_id: Tab data ID
48
- """
49
- self.window.core.tabs.add(type, title, icon, reference, data_id)
50
-
51
- def append(self, type: int, idx: int):
52
- """
53
- Append tab in place
64
+ :param child: Tab child (child widget)
65
+ :param data_id: Tab data ID (child data ID)
66
+ """
67
+ self.window.core.tabs.add(
68
+ type=type,
69
+ title=title,
70
+ icon=icon,
71
+ child=child,
72
+ data_id=data_id
73
+ )
74
+
75
+ def append(
76
+ self,
77
+ type: int,
78
+ idx: int,
79
+ column_idx: int = 0
80
+ ):
81
+ """
82
+ Append tab at tab index
54
83
 
55
84
  :param type: Tab type
56
85
  :param idx: Tab index
86
+ :param column_idx: Column index
57
87
  """
58
88
  self.appended = True # lock reload in previous tab
59
- self.window.core.tabs.append(type, idx)
60
- self.switch_tab_by_idx(idx + 1) # switch to new tab
89
+ self.column_idx = column_idx # switch to column
90
+ self.window.core.tabs.append(type, idx, column_idx)
91
+ self.switch_tab_by_idx(idx + 1, column_idx) # switch to new tab
61
92
 
62
93
  def reload_titles(self):
63
94
  """Reload tab titles"""
@@ -78,15 +109,16 @@ class Tabs:
78
109
  else:
79
110
  self.window.ui.nodes['output_plain'][pid].setVisible(False)
80
111
  self.window.ui.nodes['output'][pid].setVisible(True)
81
- self.switch_tab(Tab.TAB_CHAT)
112
+ #self.switch_tab(Tab.TAB_CHAT)
82
113
 
83
- def on_tab_changed(self, idx: int):
114
+ def on_tab_changed(self, idx: int, column_idx: int = 0):
84
115
  """
85
116
  Output tab changed
86
117
 
87
118
  :param idx: tab index
119
+ :param column_idx: column index
88
120
  """
89
- tab = self.window.core.tabs.get_tab_by_index(idx)
121
+ tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
90
122
  if tab is None:
91
123
  self.appended = False
92
124
  return
@@ -101,41 +133,53 @@ class Tabs:
101
133
 
102
134
 
103
135
  prev_tab = self.current
136
+ prev_column = self.column_idx
104
137
  self.current = idx
138
+ self.column_idx = column_idx
105
139
  self.window.controller.ui.mode.update()
106
140
  self.window.controller.ui.vision.update()
107
141
 
108
142
  # check type
109
143
  if tab.type == Tab.TAB_NOTEPAD:
110
144
  self.window.controller.notepad.opened_once = True
111
- self.window.controller.notepad.on_open(idx)
145
+ self.window.controller.notepad.on_open(idx, column_idx)
112
146
  elif tab.type == Tab.TAB_CHAT:
113
- pid_meta = self.window.core.ctx.output.get_meta(tab.pid)
114
- meta = self.window.core.ctx.get_meta_by_id(pid_meta)
147
+ # get meta for selected tab, if not loaded yet then append meta here
148
+ meta_id = self.window.core.ctx.output.prepare_meta(tab)
149
+ meta = self.window.core.ctx.get_meta_by_id(meta_id)
115
150
  if meta is not None:
116
- self.window.controller.ctx.load(pid_meta) # reload renderer
151
+ self.window.controller.ctx.load(meta.id) # reload renderer
117
152
  elif tab.type == Tab.TAB_TOOL_PAINTER:
118
153
  if self.window.core.config.get('vision.capture.enabled'):
119
154
  self.window.controller.camera.enable_capture()
120
155
 
121
- if prev_tab != idx:
156
+ if prev_tab != idx or prev_column != column_idx:
122
157
  self.window.dispatch(AppEvent(AppEvent.TAB_SELECTED)) # app event
123
158
 
124
- def get_current_idx(self) -> int:
159
+ def get_current_idx(self, column_idx: int = 0) -> int:
125
160
  """
126
161
  Get current tab index
127
162
 
163
+ :param column_idx: column index
128
164
  :return: tab index
129
165
  """
130
166
  return self.current
131
167
 
168
+ def get_current_column_idx(self) -> int:
169
+ """
170
+ Get current column index
171
+
172
+ :return: column index
173
+ """
174
+ return self.column_idx
175
+
132
176
  def get_current_tab(self) -> Tab or None:
133
177
  """
134
178
  Get current tab
135
179
 
136
180
  :return: tab
137
181
  """
138
- return self.window.core.tabs.get_tab_by_index(self.current)
182
+ return self.window.core.tabs.get_tab_by_index(self.get_current_idx(), self.column_idx)
139
183
 
140
184
  def get_current_type(self) -> int or None:
141
185
  """
@@ -143,7 +187,7 @@ class Tabs:
143
187
 
144
188
  :return: tab type
145
189
  """
146
- tab = self.window.core.tabs.get_tab_by_index(self.current)
190
+ tab = self.window.core.tabs.get_tab_by_index(self.get_current_idx(), self.column_idx)
147
191
  if tab is None:
148
192
  return None
149
193
  return tab.type
@@ -154,7 +198,7 @@ class Tabs:
154
198
 
155
199
  :return: tab PID
156
200
  """
157
- tab = self.window.core.tabs.get_tab_by_index(self.current)
201
+ tab = self.window.core.tabs.get_tab_by_index(self.get_current_idx(), self.column_idx)
158
202
  if tab is None:
159
203
  return None
160
204
  return tab.pid
@@ -166,7 +210,7 @@ class Tabs:
166
210
  :param idx: tab index
167
211
  :return: tab type
168
212
  """
169
- tab = self.window.core.tabs.get_tab_by_index(idx)
213
+ tab = self.window.core.tabs.get_tab_by_index(idx, self.column_idx)
170
214
  if tab is None:
171
215
  return None
172
216
  return tab.type
@@ -178,68 +222,105 @@ class Tabs:
178
222
  :param type: tab type
179
223
  :return: tab index
180
224
  """
181
- return self.window.core.tabs.get_min_idx_by_type(type)
225
+ return self.window.core.tabs.get_min_idx_by_type(type, self.column_idx)
182
226
 
183
- def on_tab_clicked(self, idx: int):
227
+ def on_column_changed(self):
228
+ """Column changed event"""
229
+ if self.locked:
230
+ return
231
+ tab = self.window.core.tabs.get_tab_by_index(self.current, self.column_idx)
232
+ if tab is None:
233
+ return
234
+ current_ctx = self.window.core.ctx.get_current()
235
+ if current_ctx is not None and current_ctx != tab.data_id:
236
+ self.window.controller.ctx.select_on_list_only(tab.data_id)
237
+
238
+ def on_tab_clicked(self, idx: int, column_idx: int = 0):
184
239
  """
185
240
  Tab click event
186
241
 
187
242
  :param idx: tab index
243
+ :param column_idx: column index
244
+ """
245
+ self.current = idx
246
+ self.column_idx = column_idx
247
+ self.on_column_changed()
248
+
249
+ def on_column_focus(self, idx: int):
250
+ """
251
+ Column focus event
252
+
253
+ :param idx: column index
188
254
  """
189
- pass
255
+ self.column_idx = idx
256
+ self.on_column_changed()
190
257
 
191
- def on_tab_dbl_clicked(self, idx: int):
258
+ def on_tab_dbl_clicked(self, idx: int, column_idx: int = 0):
192
259
  """
193
260
  Tab double click event
194
261
 
195
262
  :param idx: tab index
263
+ :param column_idx: column index
196
264
  """
197
- pass
265
+ self.column_idx = column_idx
266
+ self.on_tab_changed(idx, column_idx)
198
267
 
199
- def on_tab_closed(self, idx: int):
268
+ def on_tab_closed(self, idx: int, column_idx: int = 0):
200
269
  """
201
270
  Tab close event
202
271
 
203
272
  :param idx: tab index
273
+ :param column_idx: column index
204
274
  """
205
- self.window.core.tabs.remove_tab_by_idx(idx)
275
+ self.window.core.tabs.remove_tab_by_idx(idx, column_idx)
206
276
 
207
- def on_tab_moved(self, idx: int):
277
+ def on_tab_moved(self, idx: int, column_idx: int = 0):
208
278
  """
209
279
  Tab moved event
210
280
 
211
281
  :param idx: tab index
282
+ :param column_idx: column index
212
283
  """
213
284
  self.window.core.tabs.update()
214
285
 
215
- def close(self, idx: int):
286
+ def close(self, idx: int, column_idx: int = 0):
216
287
  """
217
288
  Close tab
218
289
 
219
290
  :param idx: tab index
291
+ :param column_idx: column index
220
292
  """
221
- self.on_tab_closed(idx)
293
+ self.on_tab_closed(idx, column_idx)
222
294
 
223
- def close_all(self, type: int, force: bool = False):
295
+ def close_all(
296
+ self,
297
+ type: int,
298
+ column_idx: int = 0,
299
+ force: bool = False
300
+ ):
224
301
  """
225
302
  Close all tabs
226
303
 
227
304
  :param type: tab type
305
+ :param column_idx: column index
228
306
  :param force: force close
229
307
  """
230
308
  if not force:
309
+ self.tmp_column_idx = column_idx
231
310
  self.window.ui.dialogs.confirm(
232
311
  type='tab.close_all',
233
312
  id=type,
234
313
  msg=trans('tab.close_all.confirm'),
235
314
  )
236
315
  return
237
- self.window.core.tabs.remove_all_by_type(type)
316
+ column_idx = self.tmp_column_idx
317
+ self.window.core.tabs.remove_all_by_type(type, column_idx)
238
318
 
239
319
  def next_tab(self):
240
320
  """Switch to next tab"""
241
- current = self.window.ui.tabs['output'].currentIndex()
242
- all = len(self.window.ui.tabs['output'].children())
321
+ tabs = self.window.ui.layout.get_active_tabs()
322
+ current = tabs.currentIndex()
323
+ all = len(tabs.children())
243
324
  next = current + 1
244
325
  if next >= all:
245
326
  next = 0
@@ -247,8 +328,9 @@ class Tabs:
247
328
 
248
329
  def prev_tab(self):
249
330
  """Switch to previous tab"""
250
- current = self.window.ui.tabs['output'].currentIndex()
251
- all = len(self.window.ui.tabs['output'].children())
331
+ tabs = self.window.ui.layout.get_active_tabs()
332
+ current = tabs.currentIndex()
333
+ all = len(tabs.children())
252
334
  prev = current - 1
253
335
  if prev < 0:
254
336
  prev = all - 1
@@ -264,14 +346,16 @@ class Tabs:
264
346
  if idx is not None:
265
347
  self.switch_tab_by_idx(idx)
266
348
 
267
- def switch_tab_by_idx(self, idx: int):
349
+ def switch_tab_by_idx(self, idx: int, column_idx: int = 0):
268
350
  """
269
351
  Switch tab by index
270
352
 
271
353
  :param idx: tab index
354
+ :param column_idx: column index
272
355
  """
273
- self.window.ui.tabs['output'].setCurrentIndex(idx)
274
- self.on_tab_changed(idx)
356
+ tabs = self.window.ui.layout.get_tabs_by_idx(column_idx)
357
+ tabs.setCurrentIndex(idx)
358
+ self.on_tab_changed(idx, column_idx)
275
359
 
276
360
  def get_current_tab_name(self) -> str:
277
361
  """
@@ -279,7 +363,8 @@ class Tabs:
279
363
 
280
364
  :return: tab name
281
365
  """
282
- return self.window.ui.tabs['output'].tabText(self.current)
366
+ tabs = self.window.ui.layout.get_active_tabs()
367
+ return tabs.tabText(self.current)
283
368
 
284
369
  def get_current_tab_name_for_audio(self) -> str:
285
370
  """
@@ -309,18 +394,20 @@ class Tabs:
309
394
  """
310
395
  Update tab tooltip
311
396
 
312
- :param tooltip: tooltip
397
+ :param tooltip: tooltip text
313
398
  """
314
- self.window.ui.tabs['output'].setTabToolTip(self.current, tooltip)
399
+ tabs = self.window.ui.layout.get_active_tabs()
400
+ tabs.setTabToolTip(self.current, tooltip)
315
401
 
316
- def rename(self, idx: int):
402
+ def rename(self, idx: int, column_idx: int = 0):
317
403
  """
318
- Rename tab
404
+ Rename tab (show dialog)
319
405
 
320
406
  :param idx: tab idx
407
+ :param column_idx: column idx
321
408
  """
322
409
  # get tab
323
- tab = self.window.core.tabs.get_tab_by_index(idx)
410
+ tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
324
411
  if tab is None:
325
412
  return
326
413
  # set dialog and show
@@ -329,7 +416,12 @@ class Tabs:
329
416
  self.window.ui.dialog['rename'].current = idx
330
417
  self.window.ui.dialog['rename'].show()
331
418
 
332
- def update_name(self, idx: int, name: str, close: bool = True):
419
+ def update_name(
420
+ self,
421
+ idx: int,
422
+ name: str,
423
+ close: bool = True
424
+ ):
333
425
  """
334
426
  Update tab title
335
427
 
@@ -349,7 +441,11 @@ class Tabs:
349
441
  """
350
442
  self.update_name(self.current, name)
351
443
 
352
- def update_title(self, idx: int, title: str):
444
+ def update_title(
445
+ self,
446
+ idx: int,
447
+ title: str
448
+ ):
353
449
  """
354
450
  Update tab title
355
451
 
@@ -359,8 +455,9 @@ class Tabs:
359
455
  # check if current tab is chat
360
456
  if self.get_current_type() != Tab.TAB_CHAT:
361
457
  return
458
+ tabs = self.window.ui.layout.get_active_tabs()
362
459
  tooltip = title
363
- self.window.ui.tabs['output'].setTabToolTip(idx, tooltip)
460
+ tabs.setTabToolTip(idx, tooltip)
364
461
  if len(title) > 8:
365
462
  title = title[:8] + '...' # truncate to max 8 chars
366
463
  self.window.core.tabs.update_title(idx, title, tooltip)
@@ -383,7 +480,53 @@ class Tabs:
383
480
  if idx is not None:
384
481
  self.switch_tab_by_idx(idx)
385
482
 
386
- def new_tab(self):
387
- """Handle [+} button"""
388
- idx = self.get_current_idx()
389
- self.append(Tab.TAB_CHAT, idx)
483
+ def new_tab(self, column_idx: int = 0):
484
+ """
485
+ Handle [+] button
486
+
487
+ :param column_idx: column index
488
+ """
489
+ idx = self.get_current_idx(column_idx)
490
+ self.append(Tab.TAB_CHAT, idx, column_idx)
491
+
492
+ def restore_data(self):
493
+ """Restore tab data"""
494
+ data = self.window.core.config.get("tabs.opened", [])
495
+ if not data:
496
+ self.switch_tab_by_idx(0, 0)
497
+ return
498
+ for col_idx in data:
499
+ tab_idx = data[col_idx]
500
+ self.switch_tab_by_idx(int(tab_idx), int(col_idx))
501
+
502
+ # set default column to 0
503
+ self.column_idx = 0
504
+ self.on_column_changed()
505
+
506
+ def move_tab(self, idx: int, column_idx: int, new_column_idx: int):
507
+ """
508
+ Move tab to another column
509
+
510
+ :param idx: tab index
511
+ :param column_idx: column index
512
+ :param new_column_idx: new column index
513
+ """
514
+ self.locked = True
515
+ tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
516
+ self.window.core.tabs.move_tab(tab, new_column_idx)
517
+ self.locked = False
518
+
519
+ def toggle_split_screen(self, state):
520
+ """
521
+ Toggle split screen
522
+
523
+ :param state: state
524
+ """
525
+ if state:
526
+ #self.rightWidget.show()
527
+ self.window.ui.splitters['columns'].setSizes([1, 1])
528
+ else:
529
+ #self.rightWidget.hide()
530
+ self.window.ui.splitters['columns'].setSizes([1, 0])
531
+ self.window.core.config.set("layout.split", state)
532
+ self.window.core.config.save()