pygpt-net 2.6.36__py3-none-any.whl → 2.6.38__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 (96) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/handler/anthropic_stream.py +164 -0
  4. pygpt_net/controller/chat/handler/google_stream.py +181 -0
  5. pygpt_net/controller/chat/handler/langchain_stream.py +24 -0
  6. pygpt_net/controller/chat/handler/llamaindex_stream.py +47 -0
  7. pygpt_net/controller/chat/handler/openai_stream.py +260 -0
  8. pygpt_net/controller/chat/handler/utils.py +210 -0
  9. pygpt_net/controller/chat/handler/worker.py +570 -0
  10. pygpt_net/controller/chat/handler/xai_stream.py +135 -0
  11. pygpt_net/controller/chat/stream.py +1 -1
  12. pygpt_net/controller/ctx/ctx.py +1 -1
  13. pygpt_net/controller/debug/debug.py +6 -6
  14. pygpt_net/controller/model/editor.py +3 -0
  15. pygpt_net/controller/model/importer.py +9 -2
  16. pygpt_net/controller/plugins/plugins.py +11 -3
  17. pygpt_net/controller/presets/presets.py +2 -2
  18. pygpt_net/core/bridge/context.py +35 -35
  19. pygpt_net/core/bridge/worker.py +40 -16
  20. pygpt_net/core/ctx/bag.py +7 -2
  21. pygpt_net/core/ctx/reply.py +17 -2
  22. pygpt_net/core/db/viewer.py +19 -34
  23. pygpt_net/core/render/plain/pid.py +12 -1
  24. pygpt_net/core/render/web/body.py +30 -39
  25. pygpt_net/core/tabs/tab.py +24 -1
  26. pygpt_net/data/config/config.json +10 -3
  27. pygpt_net/data/config/models.json +3 -3
  28. pygpt_net/data/config/settings.json +105 -0
  29. pygpt_net/data/css/style.dark.css +2 -3
  30. pygpt_net/data/css/style.light.css +2 -3
  31. pygpt_net/data/locale/locale.de.ini +3 -1
  32. pygpt_net/data/locale/locale.en.ini +19 -1
  33. pygpt_net/data/locale/locale.es.ini +3 -1
  34. pygpt_net/data/locale/locale.fr.ini +3 -1
  35. pygpt_net/data/locale/locale.it.ini +3 -1
  36. pygpt_net/data/locale/locale.pl.ini +4 -2
  37. pygpt_net/data/locale/locale.uk.ini +3 -1
  38. pygpt_net/data/locale/locale.zh.ini +3 -1
  39. pygpt_net/item/assistant.py +51 -2
  40. pygpt_net/item/attachment.py +21 -20
  41. pygpt_net/item/calendar_note.py +19 -2
  42. pygpt_net/item/ctx.py +115 -2
  43. pygpt_net/item/index.py +9 -2
  44. pygpt_net/item/mode.py +9 -6
  45. pygpt_net/item/model.py +20 -3
  46. pygpt_net/item/notepad.py +14 -2
  47. pygpt_net/item/preset.py +42 -2
  48. pygpt_net/item/prompt.py +8 -2
  49. pygpt_net/plugin/cmd_files/plugin.py +2 -2
  50. pygpt_net/provider/api/__init__.py +5 -3
  51. pygpt_net/provider/api/anthropic/__init__.py +190 -29
  52. pygpt_net/provider/api/anthropic/audio.py +30 -0
  53. pygpt_net/provider/api/anthropic/chat.py +341 -0
  54. pygpt_net/provider/api/anthropic/image.py +25 -0
  55. pygpt_net/provider/api/anthropic/tools.py +266 -0
  56. pygpt_net/provider/api/anthropic/vision.py +142 -0
  57. pygpt_net/provider/api/google/chat.py +2 -2
  58. pygpt_net/provider/api/google/realtime/client.py +2 -2
  59. pygpt_net/provider/api/google/tools.py +58 -48
  60. pygpt_net/provider/api/google/vision.py +7 -1
  61. pygpt_net/provider/api/openai/chat.py +1 -0
  62. pygpt_net/provider/api/openai/vision.py +6 -0
  63. pygpt_net/provider/api/x_ai/__init__.py +247 -0
  64. pygpt_net/provider/api/x_ai/audio.py +32 -0
  65. pygpt_net/provider/api/x_ai/chat.py +968 -0
  66. pygpt_net/provider/api/x_ai/image.py +208 -0
  67. pygpt_net/provider/api/x_ai/remote.py +262 -0
  68. pygpt_net/provider/api/x_ai/tools.py +120 -0
  69. pygpt_net/provider/api/x_ai/vision.py +119 -0
  70. pygpt_net/provider/core/attachment/json_file.py +2 -2
  71. pygpt_net/provider/core/config/patch.py +28 -0
  72. pygpt_net/provider/llms/anthropic.py +4 -2
  73. pygpt_net/tools/text_editor/tool.py +4 -1
  74. pygpt_net/tools/text_editor/ui/dialogs.py +1 -1
  75. pygpt_net/ui/base/config_dialog.py +5 -11
  76. pygpt_net/ui/dialog/db.py +177 -59
  77. pygpt_net/ui/dialog/dictionary.py +57 -59
  78. pygpt_net/ui/dialog/editor.py +3 -2
  79. pygpt_net/ui/dialog/image.py +1 -1
  80. pygpt_net/ui/dialog/logger.py +3 -2
  81. pygpt_net/ui/dialog/models.py +16 -16
  82. pygpt_net/ui/dialog/plugins.py +63 -60
  83. pygpt_net/ui/layout/ctx/ctx_list.py +3 -4
  84. pygpt_net/ui/layout/toolbox/__init__.py +2 -2
  85. pygpt_net/ui/layout/toolbox/assistants.py +8 -9
  86. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  87. pygpt_net/ui/main.py +9 -4
  88. pygpt_net/ui/widget/element/labels.py +20 -4
  89. pygpt_net/ui/widget/textarea/editor.py +0 -4
  90. pygpt_net/ui/widget/textarea/web.py +1 -1
  91. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/METADATA +18 -6
  92. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/RECORD +95 -76
  93. pygpt_net/controller/chat/handler/stream_worker.py +0 -1136
  94. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/LICENSE +0 -0
  95. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/WHEEL +0 -0
  96. {pygpt_net-2.6.36.dist-info → pygpt_net-2.6.38.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.09.05 01:00:00 #
10
+ # ================================================== #
11
+
12
+ import os
13
+ from typing import Optional, Dict, List
14
+
15
+ from pygpt_net.item.attachment import AttachmentItem
16
+ from pygpt_net.item.ctx import CtxItem
17
+
18
+
19
+ class Vision:
20
+ def __init__(self, window=None):
21
+ """
22
+ Vision helpers for xAI (image inputs as data: URIs).
23
+
24
+ :param window: Window instance
25
+ """
26
+ self.window = window
27
+ self.attachments: Dict[str, str] = {}
28
+ self.urls: List[str] = []
29
+ self.input_tokens = 0
30
+ self.allowed_mimes = {"image/jpeg", "image/png"}
31
+
32
+ def build_images_for_chat(self, attachments: Optional[Dict[str, AttachmentItem]]) -> List[str]:
33
+ """
34
+ Build image inputs for xai_sdk.chat.image(...).
35
+ Returns list of image sources (URLs or data: URIs).
36
+
37
+ :param attachments: Attachments dict (id -> AttachmentItem)
38
+ :return: List of image sources
39
+ """
40
+ import base64
41
+
42
+ images: List[str] = []
43
+ self.attachments = {}
44
+ self.urls = []
45
+
46
+ if not attachments:
47
+ return images
48
+
49
+ for id_, att in (attachments or {}).items():
50
+ try:
51
+ if att.path and self.window.core.api.xai.vision.is_image(att.path):
52
+ mime = self.window.core.api.xai.vision.guess_mime(att.path)
53
+ # Accept only JPEG/PNG for SDK too (for consistency)
54
+ #if mime not in self.allowed_mimes:
55
+ # continue
56
+ with open(att.path, "rb") as f:
57
+ b64 = base64.b64encode(f.read()).decode("utf-8")
58
+ images.append(f"data:{mime};base64,{b64}")
59
+ self.attachments[id_] = att.path
60
+ att.consumed = True
61
+ except Exception:
62
+ continue
63
+ return images
64
+
65
+ def is_image(self, path: str) -> bool:
66
+ """
67
+ Return True if path looks like an image file.
68
+
69
+ :param path: File path
70
+ """
71
+ return path.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif', '.webp'))
72
+
73
+ def guess_mime(self, path: str) -> str:
74
+ """Guess MIME by extension.
75
+
76
+ :param path: File path
77
+ :return: MIME type string
78
+ """
79
+ ext = os.path.splitext(path)[1].lower().lstrip(".")
80
+ if ext in ("jpg", "jpeg"):
81
+ return "image/jpeg"
82
+ if ext == "png":
83
+ return "image/png"
84
+ if ext == "gif":
85
+ return "image/gif"
86
+ if ext == "bmp":
87
+ return "image/bmp"
88
+ if ext == "webp":
89
+ return "image/webp"
90
+ if ext == "tiff":
91
+ return "image/tiff"
92
+ return "image/jpeg"
93
+
94
+ def append_images(self, ctx: CtxItem):
95
+ """
96
+ Append sent images list to context for UI/history.
97
+
98
+ :param ctx: CtxItem
99
+ """
100
+ images = self.get_attachments()
101
+ if len(images) > 0:
102
+ ctx.images = self.window.core.filesystem.make_local_list(list(images.values()))
103
+
104
+ def get_attachments(self) -> Dict[str, str]:
105
+ return self.attachments
106
+
107
+ def get_urls(self) -> List[str]:
108
+ return self.urls
109
+
110
+ def reset_tokens(self):
111
+ self.input_tokens = 0
112
+
113
+ def get_used_tokens(self) -> int:
114
+ return self.input_tokens
115
+
116
+ def reset(self):
117
+ self.attachments = {}
118
+ self.urls = []
119
+ self.input_tokens = 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.12.16 01:00:00 #
9
+ # Updated Date: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -162,7 +162,7 @@ class JsonFileProvider(BaseProvider):
162
162
  attachment.name = data['name']
163
163
  if 'path' in data:
164
164
  attachment.path = data['path']
165
- if 'remote_id' in data:
165
+ if 'remote' in data:
166
166
  attachment.remote = data['remote']
167
167
  if 'send' in data:
168
168
  attachment.send = data['send']
@@ -2447,6 +2447,34 @@ class Patch:
2447
2447
  patch_css('web-blocks.css', True)
2448
2448
  updated = True
2449
2449
 
2450
+ # < 2.6.37
2451
+ if old < parse_version("2.6.37"):
2452
+ print("Migrating config from < 2.6.37...")
2453
+
2454
+ # add: label-desc CSS
2455
+ patch_css('style.dark.css', True)
2456
+ patch_css('style.light.css', True)
2457
+
2458
+ # add: Anthropic SDK
2459
+ if "api_native_anthropic" not in data:
2460
+ data["api_native_anthropic"] = True
2461
+ if "remote_tools.anthropic.web_search" not in data:
2462
+ data["remote_tools.anthropic.web_search"] = True
2463
+
2464
+ # add: xAI SDK
2465
+ if "api_native_xai" not in data:
2466
+ data["api_native_xai"] = True
2467
+ if "remote_tools.xai.mode" not in data:
2468
+ data["remote_tools.xai.mode"] = "auto"
2469
+ if "remote_tools.xai.sources.web" not in data:
2470
+ data["remote_tools.xai.sources.web"] = True
2471
+ if "remote_tools.xai.sources.x" not in data:
2472
+ data["remote_tools.xai.sources.x"] = True
2473
+ if "remote_tools.xai.sources.news" not in data:
2474
+ data["remote_tools.xai.sources.news"] = False
2475
+
2476
+ updated = True
2477
+
2450
2478
  # update file
2451
2479
  migrated = False
2452
2480
  if updated:
@@ -15,7 +15,7 @@ from llama_index.core.base.embeddings.base import BaseEmbedding
15
15
  from llama_index.core.llms.llm import BaseLLM as LlamaBaseLLM
16
16
 
17
17
  from pygpt_net.core.types import (
18
- MODE_LLAMA_INDEX,
18
+ MODE_LLAMA_INDEX, MODE_CHAT,
19
19
  )
20
20
  from pygpt_net.provider.llms.base import BaseLLM
21
21
  from pygpt_net.item.model import ModelItem
@@ -93,7 +93,9 @@ class AnthropicLLM(BaseLLM):
93
93
  :param window: window instance
94
94
  :return: list of models
95
95
  """
96
- client = window.core.api.anthropic.get_client()
96
+ model = ModelItem()
97
+ model.provider = "anthropic"
98
+ client = window.core.api.anthropic.get_client(MODE_CHAT, model)
97
99
  models_list = client.models.list()
98
100
  items = []
99
101
  if models_list.data:
@@ -183,16 +183,19 @@ class TextEditor(BaseTool):
183
183
  self.window.core.filesystem.editor.destroy(id) # unregister from memory
184
184
  self.window.ui.dialogs.close(id)
185
185
 
186
- def save(self, id: str) -> str:
186
+ def save(self, id: str, close: bool = False) -> str:
187
187
  """
188
188
  Save content to current file
189
189
 
190
190
  :param id: editor id
191
+ :param close: close editor after save
191
192
  :return: new editor id
192
193
  """
193
194
  file = self.window.ui.dialog[id].file
194
195
  if file:
195
196
  self.window.core.filesystem.editor.save(id)
197
+ if close:
198
+ self.close(id)
196
199
  else:
197
200
  id = self.save_as_file(id)
198
201
  return id
@@ -47,7 +47,7 @@ class DialogSpawner:
47
47
  lambda: self.window.tools.get("editor").restore(id)
48
48
  )
49
49
  self.window.ui.nodes['editor.custom.btn.save'].clicked.connect(
50
- lambda: self.window.tools.get("editor").save(id)
50
+ lambda: self.window.tools.get("editor").save(id, close=True)
51
51
  )
52
52
 
53
53
  bottom_layout = QHBoxLayout()
@@ -6,14 +6,13 @@
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.22 10:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
13
13
  from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QSizePolicy, QWidget, QFrame
14
- from PySide6.QtGui import QFont
15
14
 
16
- from pygpt_net.ui.widget.element.labels import TitleLabel, UrlLabel
15
+ from pygpt_net.ui.widget.element.labels import TitleLabel, UrlLabel, DescLabel, BaseLabel
17
16
  from pygpt_net.ui.widget.option.checkbox import OptionCheckbox
18
17
  from pygpt_net.ui.widget.option.checkbox_list import OptionCheckboxList
19
18
  from pygpt_net.ui.widget.option.combo import OptionCombo
@@ -123,7 +122,7 @@ class BaseConfigDialog:
123
122
  if extra.get('bold'):
124
123
  nodes[label_key] = TitleLabel(txt)
125
124
  else:
126
- nodes[label_key] = QLabel(txt)
125
+ nodes[label_key] = BaseLabel(txt)
127
126
  nodes[label_key].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
128
127
  nodes[label_key].setMinimumWidth(120)
129
128
  nodes[label_key].setWordWrap(True)
@@ -169,7 +168,7 @@ class BaseConfigDialog:
169
168
  if extra.get('bold'):
170
169
  nodes[label_key] = TitleLabel(txt)
171
170
  else:
172
- nodes[label_key] = QLabel(txt)
171
+ nodes[label_key] = BaseLabel(txt)
173
172
  nodes[label_key].setMinimumHeight(30)
174
173
  nodes[label_key].setWordWrap(True)
175
174
 
@@ -234,12 +233,7 @@ class BaseConfigDialog:
234
233
  :param text: text (to translate)
235
234
  :return: QLabel
236
235
  """
237
- desc = trans(text)
238
- label = QLabel(desc)
239
- label.setWordWrap(True)
240
- label.setMaximumHeight(80)
241
- label.setStyleSheet("font-size: 10px;")
242
- return label
236
+ return DescLabel(trans(text))
243
237
 
244
238
  def add_urls(self, urls, align=Qt.AlignLeft) -> QWidget:
245
239
  """
pygpt_net/ui/dialog/db.py CHANGED
@@ -6,13 +6,13 @@
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: 2025.09.05 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from PySide6.QtCore import Qt
13
- from PySide6.QtGui import QAction, QIcon
12
+ from PySide6.QtCore import Qt, QTimer, QSignalBlocker
13
+ from PySide6.QtGui import QAction, QIcon, QIntValidator
14
14
  from PySide6.QtWidgets import QGridLayout, QScrollArea, QSplitter, QComboBox, QLineEdit, QPushButton, \
15
- QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar
15
+ QHBoxLayout, QVBoxLayout, QLabel, QWidget, QSizePolicy, QCheckBox, QMenuBar, QAbstractItemView, QHeaderView, QStyledItemDelegate
16
16
 
17
17
  from pygpt_net.ui.widget.dialog.db import DatabaseDialog
18
18
  from pygpt_net.ui.widget.lists.db import DatabaseList, DatabaseTableModel
@@ -20,6 +20,18 @@ from pygpt_net.ui.widget.textarea.editor import CodeEditor
20
20
  from pygpt_net.utils import trans
21
21
 
22
22
 
23
+ class _FastTextDelegate(QStyledItemDelegate):
24
+ def __init__(self, max_chars=1200, parent=None):
25
+ super().__init__(parent)
26
+ self.max_chars = max_chars
27
+
28
+ def displayText(self, value, locale):
29
+ s = "" if value is None else str(value)
30
+ if len(s) > self.max_chars:
31
+ return s[:self.max_chars] + "…"
32
+ return s
33
+
34
+
23
35
  class Database:
24
36
  def __init__(self, window=None):
25
37
  """
@@ -29,6 +41,8 @@ class Database:
29
41
  """
30
42
  self.window = window
31
43
  self.viewer = None
44
+ self._splitter_timer = None
45
+ self._text_viewer = None
32
46
 
33
47
  def setup(self, id: str = "db"):
34
48
  """
@@ -45,6 +59,7 @@ class Database:
45
59
  # data viewer
46
60
  text_viewer = CodeEditor(self.window)
47
61
  text_viewer.setReadOnly(False)
62
+ self._text_viewer = text_viewer
48
63
 
49
64
  self.window.ui.debug[id].browser = self.viewer
50
65
  self.window.ui.debug[id].viewer = text_viewer
@@ -63,6 +78,12 @@ class Database:
63
78
  splitter.addWidget(editor_widget) # Value viewer
64
79
  splitter.setStretchFactor(0, 3)
65
80
  splitter.setStretchFactor(1, 1)
81
+ splitter.setOpaqueResize(False)
82
+
83
+ self._splitter_timer = QTimer(self.window)
84
+ self._splitter_timer.setSingleShot(True)
85
+ self._splitter_timer.timeout.connect(self._finish_splitter_move)
86
+ splitter.splitterMoved.connect(self._on_splitter_moved)
66
87
 
67
88
  self.menu_bar = QMenuBar()
68
89
  self.batch_actions_menu = self.menu_bar.addMenu("Actions")
@@ -81,6 +102,18 @@ class Database:
81
102
  self.window.ui.dialog['debug.db'].setLayout(layout)
82
103
  self.window.ui.dialog['debug.db'].setWindowTitle("Debug: Database (SQLite)")
83
104
 
105
+ def _on_splitter_moved(self, pos, index):
106
+ if self._text_viewer is not None:
107
+ self._text_viewer.setUpdatesEnabled(False)
108
+ self._splitter_timer.start(120)
109
+
110
+ def _finish_splitter_move(self):
111
+ if self._text_viewer is not None:
112
+ self._text_viewer.setUpdatesEnabled(True)
113
+ if hasattr(self._text_viewer, 'viewport'):
114
+ self._text_viewer.viewport().update()
115
+
116
+
84
117
  class DataBrowser(QWidget):
85
118
  def __init__(self, window=None):
86
119
  super().__init__()
@@ -111,7 +144,7 @@ class DataBrowser(QWidget):
111
144
 
112
145
  # buttons
113
146
  self.refresh_button = QPushButton(trans("db.refresh"))
114
- self.refresh_button.clicked.connect(self.update_table_view)
147
+ self.refresh_button.clicked.connect(self.force_refresh)
115
148
  self.prev_button = QPushButton(trans("db.prev"))
116
149
  self.next_button = QPushButton(trans("db.next"))
117
150
  self.limit_input = QLineEdit("100")
@@ -126,7 +159,7 @@ class DataBrowser(QWidget):
126
159
  self.page_input_label = QLabel(trans("db.page") + ":")
127
160
  self.page_input_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
128
161
  self.page_input.editingFinished.connect(self.on_page_input_change)
129
- self.page_info_label = QLabel(" / 1") # total pages
162
+ self.page_info_label = QLabel(" / 1")
130
163
  self.page_info_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
131
164
 
132
165
  # checkboxes
@@ -155,18 +188,30 @@ class DataBrowser(QWidget):
155
188
  self.table_select.addItems(self.get_table_names())
156
189
  self.sort_order_select.addItems(['ASC', 'DESC'])
157
190
 
191
+ self._search_timer = QTimer(self)
192
+ self._search_timer.setSingleShot(True)
193
+ self._search_timer.timeout.connect(self.update_table_view)
194
+ self._last_fetch_sig = None
195
+ self._last_count_sig = None
196
+ self._cached_total_rows = 0
197
+ self._last_columns_sig = None
198
+
199
+ v = QIntValidator(1, 1000000000, self)
200
+ self.limit_input.setValidator(v)
201
+ self.page_input.setValidator(v)
202
+
158
203
  # signals
159
204
  self.table_select.currentIndexChanged.connect(self.on_table_select_changed)
160
205
  self.sort_by_select.currentIndexChanged.connect(self.update_table_view)
161
206
  self.sort_order_select.currentIndexChanged.connect(self.update_table_view)
162
- self.search_input.textChanged.connect(self.update_table_view)
207
+ self.search_input.textChanged.connect(self.on_search_text_changed)
163
208
  self.search_column_select.currentIndexChanged.connect(self.update_table_view)
164
209
 
165
210
  self.prev_button.clicked.connect(self.prev_page)
166
211
  self.next_button.clicked.connect(self.next_page)
167
212
 
168
213
  self.table_select.setCurrentText(self.get_default_table())
169
- self.on_table_select_changed() # load data
214
+ self.on_table_select_changed()
170
215
 
171
216
  # setup layouts
172
217
  if not self.is_inline():
@@ -221,6 +266,27 @@ class DataBrowser(QWidget):
221
266
 
222
267
  self.setLayout(main_layout)
223
268
 
269
+ view = self.get_list_widget()
270
+ if hasattr(view, "setUniformRowHeights"):
271
+ view.setUniformRowHeights(True)
272
+ if hasattr(view, "setWordWrap"):
273
+ view.setWordWrap(False)
274
+ if hasattr(view, "setTextElideMode"):
275
+ view.setTextElideMode(Qt.ElideRight)
276
+ if hasattr(view, "setVerticalScrollMode"):
277
+ view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
278
+ if hasattr(view, "setHorizontalScrollMode"):
279
+ view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
280
+ if hasattr(view, "setViewportUpdateMode"):
281
+ view.setViewportUpdateMode(QAbstractItemView.MinimalViewportUpdate)
282
+ if hasattr(view, "horizontalHeader"):
283
+ hh = view.horizontalHeader()
284
+ if hh is not None:
285
+ hh.setSectionResizeMode(QHeaderView.Interactive)
286
+ self._fast_delegate = _FastTextDelegate(parent=view)
287
+ if hasattr(view, "setItemDelegate"):
288
+ view.setItemDelegate(self._fast_delegate)
289
+
224
290
  def is_inline(self) -> bool:
225
291
  return False
226
292
 
@@ -339,6 +405,8 @@ class DataBrowser(QWidget):
339
405
  self.limit_input.setText(str(current_limit))
340
406
  return
341
407
  self.current_offset = 0
408
+ self._last_fetch_sig = None
409
+ self._last_count_sig = None
342
410
  self.update_table_view()
343
411
 
344
412
  def get_page_size(self) -> int:
@@ -358,27 +426,18 @@ class DataBrowser(QWidget):
358
426
 
359
427
  :param update: Update table view
360
428
  """
361
- page = int(self.page_input.text()) - 1
362
- limit = int(self.limit_input.text())
363
- search_query = self.search_input.text()
364
- search_column = self.search_column_select.currentText()
365
- if search_column == "*":
366
- search_column = None
367
- current_table = self.get_table_name(self.table_select.currentText())
368
- total_rows = self.get_viewer().count_rows(
369
- current_table,
370
- search_query=search_query,
371
- search_column=search_column,
372
- filters=self.get_filters(),
373
- )
429
+ try:
430
+ page = int(self.page_input.text()) - 1
431
+ except ValueError:
432
+ page = 0
433
+ limit = self.get_page_size()
434
+ total_rows = self._get_total_rows()
374
435
 
375
- # check if page is in range
376
436
  if page * limit < total_rows and page >= 0:
377
437
  if update:
378
438
  self.current_offset = page * limit
379
439
  self.update_table_view()
380
440
  else:
381
- # reset page
382
441
  current_page = self.current_offset // limit + 1
383
442
  self.page_input.setText(str(current_page))
384
443
 
@@ -395,38 +454,33 @@ class DataBrowser(QWidget):
395
454
  """Table select change event"""
396
455
  tables = self.get_tables()
397
456
  current_table = self.get_table_name(self.table_select.currentText())
398
- self.sort_by_select.clear()
399
- if current_table in tables:
400
- self.sort_by_select.addItems(tables[current_table]['sort_by'])
401
- self.sort_by_select.setCurrentText(tables[current_table]['default_sort'])
402
- self.sort_order_select.setCurrentText(tables[current_table]['default_order'])
403
- self.page_input.setText("1") # reset page
404
- self.on_page_input_change()
405
- self.update_table_view() # update view
406
- self.update_search_columns() # update search columns
457
+ with QSignalBlocker(self.sort_by_select), QSignalBlocker(self.sort_order_select):
458
+ self.sort_by_select.clear()
459
+ if current_table in tables:
460
+ self.sort_by_select.addItems(tables[current_table]['sort_by'])
461
+ self.sort_by_select.setCurrentText(tables[current_table]['default_sort'])
462
+ self.sort_order_select.setCurrentText(tables[current_table]['default_order'])
463
+ self.page_input.setText("1")
464
+ self.current_offset = 0
465
+ self.update_search_columns()
466
+ self._last_fetch_sig = None
467
+ self._last_count_sig = None
468
+ self._last_columns_sig = None
469
+ self.update_table_view()
407
470
 
408
471
  def update_search_columns(self):
409
472
  """Update search columns"""
410
473
  tables = self.get_tables()
411
474
  current_table = self.get_table_name(self.table_select.currentText())
412
- self.search_column_select.clear()
413
- self.search_column_select.addItem("*", None)
414
- self.search_column_select.addItems(tables[current_table]['columns'])
475
+ with QSignalBlocker(self.search_column_select):
476
+ self.search_column_select.clear()
477
+ self.search_column_select.addItem("*", None)
478
+ self.search_column_select.addItems(tables[current_table]['columns'])
415
479
 
416
480
  def update_pagination_info(self):
417
481
  """Update pagination info"""
418
- limit = int(self.limit_input.text())
419
- search_query = self.search_input.text()
420
- search_column = self.search_column_select.currentText()
421
- if search_column == "*":
422
- search_column = None
423
- current_table = self.get_table_name(self.table_select.currentText())
424
- total_rows = self.get_viewer().count_rows(
425
- current_table,
426
- search_query=search_query,
427
- search_column=search_column,
428
- filters=self.get_filters(),
429
- )
482
+ limit = self.get_page_size()
483
+ total_rows = self._get_total_rows()
430
484
  total_pages = (total_rows - 1) // limit + 1
431
485
  self.page_info_label.setText(f" / {total_pages} ({total_rows} rows)")
432
486
 
@@ -435,21 +489,24 @@ class DataBrowser(QWidget):
435
489
  self.prev_button.setEnabled(self.current_offset > 0)
436
490
  self.next_button.setEnabled(self.current_offset + limit < total_rows)
437
491
 
438
- def update_table_view(self):
492
+ def update_table_view(self, *args, force: bool = False):
439
493
  """Update table view"""
440
494
  tables = self.get_tables()
441
495
  current_table = self.get_table_name(self.table_select.currentText())
442
496
  sort_by = self.sort_by_select.currentText()
443
497
  sort_order = self.sort_order_select.currentText()
444
498
  search_query = self.search_input.text()
445
- search_column = self.search_column_select.currentText()
446
- if search_column == "*":
447
- search_column = None
448
- limit = int(self.limit_input.text())
499
+ search_column = self.search_column_select.currentData()
500
+ limit = self.get_page_size()
449
501
 
450
502
  if current_table not in tables or sort_by == '' or sort_order == '' or limit <= 0:
451
503
  return
452
504
 
505
+ sig = self._params_signature_fetch()
506
+ if not force and self._last_fetch_sig == sig:
507
+ self.update_pagination_info()
508
+ return
509
+
453
510
  data = self.get_viewer().fetch_data(
454
511
  table=current_table,
455
512
  columns=tables[current_table]['columns'],
@@ -462,11 +519,12 @@ class DataBrowser(QWidget):
462
519
  filters=self.get_filters(),
463
520
  )
464
521
  self.load_data(data, tables[current_table])
522
+ self._last_fetch_sig = sig
465
523
  self.update_pagination_info()
466
524
 
467
525
  def prev_page(self):
468
526
  """Previous page event"""
469
- limit = int(self.limit_input.text())
527
+ limit = self.get_page_size()
470
528
  if self.current_offset - limit >= 0:
471
529
  self.current_offset -= limit
472
530
  else:
@@ -475,10 +533,8 @@ class DataBrowser(QWidget):
475
533
 
476
534
  def next_page(self):
477
535
  """Next page event"""
478
- limit = int(self.limit_input.text())
479
- current_table = self.get_table_name(self.table_select.currentText())
480
- total_rows = self.get_viewer().count_rows(current_table)
481
-
536
+ limit = self.get_page_size()
537
+ total_rows = self._get_total_rows()
482
538
  if self.current_offset + limit < total_rows:
483
539
  self.current_offset += limit
484
540
  self.update_table_view()
@@ -498,11 +554,73 @@ class DataBrowser(QWidget):
498
554
  self.db_path_label.setText(self.window.core.db.db_path)
499
555
  if not self.is_inline():
500
556
  convert_timestamps = self.convert_timestamps_checkbox.isChecked()
557
+ view = self.get_list_widget()
558
+ if hasattr(view, 'setSortingEnabled'):
559
+ view.setSortingEnabled(False)
560
+ view.setUpdatesEnabled(False)
501
561
  model = DatabaseTableModel(
502
562
  data,
503
563
  table['columns'],
504
564
  timestamp_columns=table.get('timestamp_columns', []),
505
565
  convert_timestamps=convert_timestamps,
506
566
  )
507
- self.get_list_widget().setModel(model)
508
- self.get_list_widget().adjustColumns()
567
+ if hasattr(model, 'setParent'):
568
+ model.setParent(view)
569
+ view.setModel(model)
570
+ if self._last_columns_sig != tuple(table['columns']):
571
+ if hasattr(view, 'adjustColumns'):
572
+ view.adjustColumns()
573
+ if hasattr(view, "horizontalHeader"):
574
+ hh = view.horizontalHeader()
575
+ if hh is not None:
576
+ hh.setSectionResizeMode(QHeaderView.Interactive)
577
+ self._last_columns_sig = tuple(table['columns'])
578
+ view.setUpdatesEnabled(True)
579
+
580
+ def on_search_text_changed(self, text):
581
+ self._search_timer.start(200)
582
+
583
+ def _params_signature_base(self):
584
+ table = self.get_table_name(self.table_select.currentText())
585
+ search_query = self.search_input.text()
586
+ search_column = self.search_column_select.currentData()
587
+ filters = self.get_filters()
588
+ if isinstance(filters, dict):
589
+ filters_key = tuple(sorted(filters.items()))
590
+ else:
591
+ filters_key = str(filters)
592
+ return table, search_query, search_column, filters_key
593
+
594
+ def _params_signature_fetch(self):
595
+ table, search_query, search_column, filters_key = self._params_signature_base()
596
+ tables = self.get_tables()
597
+ columns = tuple(tables[table]['columns']) if table in tables else tuple()
598
+ sort_by = self.sort_by_select.currentText()
599
+ sort_order = self.sort_order_select.currentText()
600
+ limit = self.get_page_size()
601
+ offset = self.current_offset
602
+ return table, columns, sort_by, sort_order, search_query, search_column, filters_key, limit, offset
603
+
604
+ def _params_signature_count(self):
605
+ return self._params_signature_base()
606
+
607
+ def _get_total_rows(self):
608
+ sig = self._params_signature_count()
609
+ if self._last_count_sig == sig:
610
+ return self._cached_total_rows
611
+ table, search_query, search_column, _ = sig
612
+ total_rows = self.get_viewer().count_rows(
613
+ table,
614
+ search_query=search_query,
615
+ search_column=search_column,
616
+ filters=self.get_filters(),
617
+ )
618
+ self._last_count_sig = sig
619
+ self._cached_total_rows = total_rows
620
+ return total_rows
621
+
622
+ def force_refresh(self):
623
+ self._last_fetch_sig = None
624
+ self._last_count_sig = None
625
+ self._last_columns_sig = None
626
+ self.update_table_view()