omninative-ui 0.1.0__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.
@@ -0,0 +1,89 @@
1
+ # omninative_ui/__init__.py
2
+ """
3
+ OmniNative UI — PySide6 component library with the OmniNative dark theme.
4
+
5
+ Usage:
6
+ from omninative_ui import OButton, OWindow, OMNINATIVE
7
+ from omninative_ui.chat import OChatView # granular import
8
+ """
9
+
10
+ from .tokens import (
11
+ OMNINATIVE,
12
+ _FONT_FAMILY,
13
+ _FONT_SIZE_SM,
14
+ _FONT_SIZE_LG,
15
+ _CORNER,
16
+ _PAD,
17
+ )
18
+
19
+ from .icons import (
20
+ _icon_cache,
21
+ _audio_icon_cache,
22
+ _get_cached_checkbox,
23
+ _get_cached_chevron,
24
+ _get_cached_plus,
25
+ _get_cached_arrow,
26
+ _get_cached_audio_icon,
27
+ )
28
+
29
+ from ._utils import (
30
+ get_global_stylesheet,
31
+ _pixmap_to_data_url,
32
+ )
33
+
34
+ from .core import (
35
+ OWindow,
36
+ OGroup,
37
+ OLabel,
38
+ OElidedLabel,
39
+ OSectionHeader,
40
+ OSeparator,
41
+ OButton,
42
+ OOptionButton,
43
+ OComboBox,
44
+ ORadioButton,
45
+ OCheckBoxBase,
46
+ OCheckBox,
47
+ OTableCheckBox,
48
+ OStatusBar,
49
+ OOptionRow,
50
+ )
51
+
52
+ from .inputs import (
53
+ OLineEdit,
54
+ OTextBox,
55
+ OSpinBox,
56
+ OSlider,
57
+ _WheelIgnoredSlider,
58
+ OProgressBar,
59
+ )
60
+
61
+ from .containers import (
62
+ OScrollArea,
63
+ OTreeWidget,
64
+ OTabs,
65
+ OVirtualTable,
66
+ OTableTextEdit,
67
+ OTableItemDelegate,
68
+ )
69
+
70
+ from .media import (
71
+ OAudioButton,
72
+ OAudioWaveform,
73
+ OAudioPlayer,
74
+ OImageViewer,
75
+ OFullscreenViewer,
76
+ )
77
+
78
+ from .chat import (
79
+ OChatMessage,
80
+ OChatView,
81
+ OChatInput,
82
+ OActionMenuItem,
83
+ OActionMenu,
84
+ )
85
+
86
+ # Re-export QTreeWidgetItem for convenience (used in demo)
87
+ from PySide6.QtWidgets import QTreeWidgetItem
88
+
89
+ __version__ = "0.1.0"
@@ -0,0 +1,119 @@
1
+ # omninative_ui/_utils.py
2
+ from typing import TYPE_CHECKING
3
+ from PySide6.QtCore import QByteArray, QBuffer, QIODevice
4
+
5
+ if TYPE_CHECKING:
6
+ from PySide6.QtGui import QPixmap
7
+
8
+ from .tokens import OMNINATIVE, _FONT_FAMILY, _FONT_SIZE_SM, _CORNER
9
+
10
+
11
+ def _pixmap_to_data_url(pixmap: 'QPixmap') -> str:
12
+ byte_array = QByteArray()
13
+ buffer = QBuffer(byte_array)
14
+ buffer.open(QIODevice.WriteOnly)
15
+ pixmap.save(buffer, "PNG")
16
+ return f"data:image/png;base64,{byte_array.toBase64().data().decode()}"
17
+
18
+ def get_global_stylesheet() -> str:
19
+ bg_color = OMNINATIVE["background"]
20
+ widget_bg = OMNINATIVE["background"]
21
+ scroll_bg = OMNINATIVE["background"]
22
+ return f"""
23
+ QWidget {{
24
+ background-color: transparent;
25
+ color: {OMNINATIVE["bright"]};
26
+ font-family: "{_FONT_FAMILY}";
27
+ font-size: {_FONT_SIZE_SM}pt;
28
+ }}
29
+ QWidget#content_wrapper {{
30
+ background-color: transparent;
31
+ }}
32
+ QWidget#central_widget {{
33
+ background-color: {OMNINATIVE["background"]};
34
+ }}
35
+ QLabel {{
36
+ background-color: transparent;
37
+ }}
38
+ QScrollArea {{
39
+ background-color: transparent;
40
+ }}
41
+ QScrollArea > QWidget > QWidget {{
42
+ background-color: transparent;
43
+ }}
44
+ QScrollBar:vertical {{
45
+ border: none;
46
+ background: transparent;
47
+ width: 8px;
48
+ margin: 0px;
49
+ }}
50
+ QScrollBar::handle:vertical {{
51
+ background: {OMNINATIVE["accent"]};
52
+ min-height: 20px;
53
+ margin-left: 3px;
54
+ margin-right: 4px;
55
+ margin-top: 2px;
56
+ margin-bottom: 2px;
57
+ border-radius: 0px;
58
+ }}
59
+ QScrollBar::handle:vertical:hover {{
60
+ background: {OMNINATIVE["accent"]};
61
+ }}
62
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
63
+ border: none;
64
+ background: none;
65
+ height: 0px;
66
+ width: 0px;
67
+ }}
68
+ QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
69
+ background: none;
70
+ }}
71
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
72
+ background: none;
73
+ }}
74
+ QComboBox {{
75
+ background-color: {OMNINATIVE["background"]};
76
+ color: {OMNINATIVE["bright"]};
77
+ border: 1px solid {OMNINATIVE["gray"]};
78
+ border-radius: {_CORNER}px;
79
+ padding: 2px 8px;
80
+ }}
81
+ QComboBox::drop-down {{
82
+ subcontrol-origin: padding;
83
+ subcontrol-position: top right;
84
+ width: 20px;
85
+ border-left-width: 0px;
86
+ }}
87
+ QComboBox::down-arrow {{
88
+ image: none;
89
+ }}
90
+ QComboBox QAbstractItemView {{
91
+ background-color: {OMNINATIVE["background"]};
92
+ color: {OMNINATIVE["accent"]};
93
+ border: 1px solid {OMNINATIVE["gray"]};
94
+ selection-background-color: {OMNINATIVE["dark"]};
95
+ selection-color: {OMNINATIVE["bright"]};
96
+ }}
97
+ QLineEdit, QTextEdit, #RInput {{
98
+ background-color: {OMNINATIVE["dark"]};
99
+ color: {OMNINATIVE["bright"]};
100
+ border: 1px solid {OMNINATIVE["gray"]};
101
+ border-radius: {_CORNER}px;
102
+ padding: 2px 4px;
103
+ }}
104
+ QLineEdit:focus, QTextEdit:focus, #RInput:focus {{
105
+ border: 1px solid {OMNINATIVE["primary"]};
106
+ }}
107
+ QLineEdit[readOnly="true"], QTextEdit[readOnly="true"] {{
108
+ color: {OMNINATIVE["accent"]};
109
+ background-color: transparent;
110
+ border: 1px dashed {OMNINATIVE["gray"]};
111
+ }}
112
+ QSpinBox {{
113
+ background-color: {OMNINATIVE["dark"]};
114
+ color: {OMNINATIVE["bright"]};
115
+ border: 1px solid {OMNINATIVE["gray"]};
116
+ border-radius: {_CORNER}px;
117
+ }}
118
+ """
119
+
omninative_ui/chat.py ADDED
@@ -0,0 +1,305 @@
1
+ # omninative_ui/chat.py
2
+ import time
3
+ from typing import Optional, List, Any
4
+
5
+ from PySide6.QtWidgets import (
6
+ QWidget,
7
+ QVBoxLayout,
8
+ QHBoxLayout,
9
+ QLabel,
10
+ QFrame,
11
+ QPushButton,
12
+ QLineEdit,
13
+ )
14
+ from PySide6.QtGui import QFont, QIcon
15
+ from PySide6.QtCore import Qt, Signal, QTimer, QSize, QPoint
16
+
17
+ from .tokens import OMNINATIVE, _FONT_FAMILY, _FONT_SIZE_SM, _CORNER
18
+ from .icons import _get_cached_plus, _get_cached_arrow, _get_cached_chevron
19
+ from .containers import OScrollArea
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # OChatMessage
24
+ # ---------------------------------------------------------------------------
25
+ class OChatMessage(QFrame):
26
+ def __init__(self, role: str, content: str, parent: Optional[QWidget] = None) -> None:
27
+ super().__init__(parent)
28
+ self.role = role
29
+ self.content = content
30
+
31
+ self.setObjectName("chat_msg")
32
+
33
+ self.layout_ = QHBoxLayout(self)
34
+ self.layout_.setContentsMargins(12, 12, 12, 12)
35
+ self.layout_.setSpacing(12)
36
+
37
+ self.text_lbl = QLabel(content)
38
+ self.text_lbl.setFont(QFont(_FONT_FAMILY, _FONT_SIZE_SM))
39
+ self.text_lbl.setWordWrap(True)
40
+ self.text_lbl.setTextFormat(Qt.MarkdownText)
41
+ self.text_lbl.setTextInteractionFlags(Qt.TextBrowserInteraction)
42
+ self.text_lbl.setOpenExternalLinks(True)
43
+
44
+ if role == "user":
45
+ self.setStyleSheet(f"#chat_msg {{ background-color: {OMNINATIVE['dark']}; border-radius: {_CORNER}px; }}")
46
+ self.text_lbl.setStyleSheet(f"color: {OMNINATIVE['bright']}; background: transparent; border: none;")
47
+ self.layout_.addWidget(self.text_lbl)
48
+ else:
49
+ self.setStyleSheet(f"#chat_msg {{ background: transparent; border: none; }}")
50
+ self.text_lbl.setStyleSheet(f"color: {OMNINATIVE['bright']}; background: transparent; border: none;")
51
+ self.layout_.addWidget(self.text_lbl)
52
+
53
+ def append_text(self, text: str) -> None:
54
+ self.content += text
55
+ self.text_lbl.setText(self.content)
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # OChatView
59
+ # ---------------------------------------------------------------------------
60
+ class OChatView(OScrollArea):
61
+ def __init__(self, master: Optional[QWidget], **kwargs: Any) -> None:
62
+ super().__init__(master, **kwargs)
63
+
64
+ # Remove the outer border while preserving any scrollbar styles
65
+ self.setStyleSheet(self.styleSheet() + " QScrollArea { border: none; background: transparent; }")
66
+
67
+ self.container = QWidget()
68
+ self.container.setStyleSheet("background: transparent;")
69
+ self.container_layout = QVBoxLayout(self.container)
70
+ self.container_layout.setAlignment(Qt.AlignTop)
71
+ self.container_layout.setContentsMargins(10, 10, 10, 10)
72
+ self.container_layout.setSpacing(15)
73
+
74
+ self.setWidget(self.container)
75
+ self.setWidgetResizable(True)
76
+
77
+ self.messages: List[OChatMessage] = []
78
+ self._last_assistant_msg: Optional[OChatMessage] = None
79
+
80
+ def add_message(self, role: str, content: str) -> None:
81
+ msg = OChatMessage(role, content)
82
+ self.container_layout.addWidget(msg)
83
+ self.messages.append(msg)
84
+ if role == "assistant":
85
+ self._last_assistant_msg = msg
86
+ self._scroll_to_bottom()
87
+
88
+ def append_chunk(self, text: str) -> None:
89
+ if self._last_assistant_msg:
90
+ self._last_assistant_msg.append_text(text)
91
+ self._scroll_to_bottom()
92
+
93
+ def clear_chat(self) -> None:
94
+ for i in reversed(range(self.container_layout.count())):
95
+ widget = self.container_layout.itemAt(i).widget()
96
+ if widget:
97
+ widget.setParent(None)
98
+ widget.deleteLater()
99
+ self.messages.clear()
100
+ self._last_assistant_msg = None
101
+
102
+ def _scroll_to_bottom(self) -> None:
103
+ # Allow layout to compute sizes before scrolling
104
+ QTimer.singleShot(10, self._do_scroll)
105
+
106
+ def _do_scroll(self) -> None:
107
+ sb = self.verticalScrollBar()
108
+ sb.setValue(sb.maximum())
109
+
110
+ def pack(self, **kwargs: Any) -> None: pass
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # OChatInput
114
+ # ---------------------------------------------------------------------------
115
+ class OChatInput(QFrame):
116
+ submitted = Signal(str)
117
+ add_clicked = Signal()
118
+
119
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
120
+ super().__init__(parent)
121
+ self.setObjectName("chat_input")
122
+ self.setFixedHeight(50)
123
+ self.setStyleSheet(f"#chat_input {{ background-color: {OMNINATIVE['dark']}; border-radius: 25px; }}")
124
+
125
+ self.layout_ = QHBoxLayout(self)
126
+ self.layout_.setContentsMargins(15, 0, 10, 0)
127
+ self.layout_.setSpacing(10)
128
+
129
+ # Add button (+)
130
+ self.btn_add = QPushButton()
131
+ self.btn_add.setFixedSize(30, 30)
132
+ self.btn_add.setIcon(QIcon(_get_cached_plus(size=24, color=OMNINATIVE['accent'], weight=1.5)))
133
+ self.btn_add.setIconSize(QSize(24, 24))
134
+ self.btn_add.setStyleSheet(f"background: transparent; border: none;")
135
+ self.btn_add.setCursor(Qt.PointingHandCursor)
136
+
137
+ def _add_enter(event: Any) -> None:
138
+ self.btn_add.setIcon(QIcon(_get_cached_plus(size=24, color=OMNINATIVE['bright'], weight=1.5)))
139
+ QPushButton.enterEvent(self.btn_add, event)
140
+
141
+ def _add_leave(event: Any) -> None:
142
+ self.btn_add.setIcon(QIcon(_get_cached_plus(size=24, color=OMNINATIVE['accent'], weight=1.5)))
143
+ QPushButton.leaveEvent(self.btn_add, event)
144
+
145
+ self.btn_add.enterEvent = _add_enter
146
+ self.btn_add.leaveEvent = _add_leave
147
+
148
+ self.btn_add.clicked.connect(self.add_clicked.emit)
149
+
150
+ # Input Field
151
+ self.input_field = QLineEdit()
152
+ self.input_field.setPlaceholderText("Pregunta lo que quieras")
153
+ self.input_field.setFont(QFont(_FONT_FAMILY, 10))
154
+ self.input_field.setStyleSheet(f"color: {OMNINATIVE['bright']}; background: transparent; border: none;")
155
+ self.input_field.returnPressed.connect(self._submit)
156
+
157
+ # Action button (Arrow)
158
+ self.btn_action = QPushButton()
159
+ self.btn_action.setFixedSize(34, 34)
160
+ self.btn_action.setIcon(QIcon(_get_cached_arrow(size=24, color=OMNINATIVE['gray'], direction="up", weight=2.0)))
161
+ self.btn_action.setIconSize(QSize(20, 20))
162
+ self.btn_action.setStyleSheet(f"QPushButton {{ background-color: {OMNINATIVE['bright']}; border-radius: 17px; border: none; }}")
163
+ self.btn_action.setCursor(Qt.PointingHandCursor)
164
+
165
+ def _action_enter(event: Any) -> None:
166
+ self.btn_action.setIcon(QIcon(_get_cached_arrow(size=24, color=OMNINATIVE['background'], direction="up", weight=2.0)))
167
+ QPushButton.enterEvent(self.btn_action, event)
168
+
169
+ def _action_leave(event: Any) -> None:
170
+ self.btn_action.setIcon(QIcon(_get_cached_arrow(size=24, color=OMNINATIVE['gray'], direction="up", weight=2.0)))
171
+ QPushButton.leaveEvent(self.btn_action, event)
172
+
173
+ self.btn_action.enterEvent = _action_enter
174
+ self.btn_action.leaveEvent = _action_leave
175
+
176
+ self.btn_action.clicked.connect(self._on_action_clicked)
177
+
178
+ self.layout_.addWidget(self.btn_add)
179
+ self.layout_.addWidget(self.input_field)
180
+ self.layout_.addWidget(self.btn_action)
181
+
182
+ def _on_action_clicked(self) -> None:
183
+ self._submit()
184
+
185
+ def _submit(self) -> None:
186
+ text = self.input_field.text().strip()
187
+ if text:
188
+ self.submitted.emit(text)
189
+ self.input_field.clear()
190
+
191
+ def clear(self) -> None:
192
+ self.input_field.clear()
193
+
194
+ def text(self) -> str:
195
+ return self.input_field.text()
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # OActionMenu
199
+ # ---------------------------------------------------------------------------
200
+ class OActionMenuItem(QFrame):
201
+ clicked = Signal(str)
202
+
203
+ def __init__(self, text: str, icon_char: Optional[str] = None, has_chevron: bool = False, parent: Optional[QWidget] = None) -> None:
204
+ super().__init__(parent)
205
+ self.text_val = text
206
+ self.setObjectName("action_item")
207
+ self.setStyleSheet(f"#action_item {{ background-color: transparent; border-radius: 6px; }}")
208
+ self.setFixedHeight(36)
209
+ self.setCursor(Qt.PointingHandCursor)
210
+
211
+ self.layout_ = QHBoxLayout(self)
212
+ self.layout_.setContentsMargins(10, 0, 10, 0)
213
+ self.layout_.setSpacing(12)
214
+
215
+ # Icon
216
+ if icon_char:
217
+ self.icon_lbl = QLabel(icon_char)
218
+ self.icon_lbl.setFont(QFont(_FONT_FAMILY, 14))
219
+ self.icon_lbl.setStyleSheet(f"color: {OMNINATIVE['bright']}; background: transparent;")
220
+ self.icon_lbl.setFixedWidth(24)
221
+ self.icon_lbl.setAlignment(Qt.AlignCenter)
222
+ self.layout_.addWidget(self.icon_lbl)
223
+
224
+ # Text
225
+ self.text_lbl = QLabel(text)
226
+ self.text_lbl.setFont(QFont(_FONT_FAMILY, 10))
227
+ self.text_lbl.setStyleSheet(f"color: {OMNINATIVE['bright']}; background: transparent;")
228
+ self.layout_.addWidget(self.text_lbl)
229
+
230
+ self.layout_.addStretch()
231
+
232
+ # Chevron
233
+ if has_chevron:
234
+ self.chevron_lbl = QLabel()
235
+ self.chevron_lbl.setPixmap(_get_cached_chevron(size=14, color=OMNINATIVE['bright'], direction="right"))
236
+ self.chevron_lbl.setStyleSheet("background: transparent;")
237
+ self.layout_.addWidget(self.chevron_lbl)
238
+
239
+ def enterEvent(self, event: Any) -> None:
240
+ self.setStyleSheet(f"#action_item {{ background-color: {OMNINATIVE['dark']}; border-radius: 6px; }}")
241
+ super().enterEvent(event)
242
+
243
+ def leaveEvent(self, event: Any) -> None:
244
+ self.setStyleSheet(f"#action_item {{ background-color: transparent; border-radius: 6px; }}")
245
+ super().leaveEvent(event)
246
+
247
+ def mousePressEvent(self, event: Any) -> None:
248
+ if event.button() == Qt.LeftButton:
249
+ self.clicked.emit(self.text_val)
250
+ # Find the popup and close it
251
+ p = self.window()
252
+ if isinstance(p, OActionMenu):
253
+ p.close()
254
+ super().mousePressEvent(event)
255
+
256
+ class OActionMenu(QWidget):
257
+ action_triggered = Signal(str)
258
+
259
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
260
+ super().__init__(parent)
261
+ self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)
262
+ self.setAttribute(Qt.WA_TranslucentBackground)
263
+ self._last_hide_time = 0.0
264
+
265
+ self.main_layout = QVBoxLayout(self)
266
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
267
+
268
+ self.container = QFrame()
269
+ self.container.setObjectName("menu_container")
270
+ self.container.setStyleSheet(f"#menu_container {{ background-color: #2F2F2F; border-radius: 12px; border: 1px solid #3F3F3F; }}")
271
+ self.main_layout.addWidget(self.container)
272
+
273
+ self.layout_ = QVBoxLayout(self.container)
274
+ self.layout_.setContentsMargins(6, 6, 6, 6)
275
+ self.layout_.setSpacing(2)
276
+
277
+ self.setFixedWidth(280)
278
+
279
+ def add_action(self, text: str, icon_char: Optional[str] = None, has_chevron: bool = False) -> OActionMenuItem:
280
+ item = OActionMenuItem(text, icon_char, has_chevron)
281
+ item.clicked.connect(self.action_triggered.emit)
282
+ self.layout_.addWidget(item)
283
+ return item
284
+
285
+ def add_separator(self) -> None:
286
+ sep = QFrame()
287
+ sep.setFixedHeight(1)
288
+ sep.setStyleSheet(f"background-color: {OMNINATIVE['dark']}; margin: 4px 10px;")
289
+ self.layout_.addWidget(sep)
290
+
291
+ def hideEvent(self, event: Any) -> None:
292
+ self._last_hide_time = time.time()
293
+ super().hideEvent(event)
294
+
295
+ def show_above(self, widget: QWidget) -> None:
296
+ # Prevent immediate reopening if it was just closed by a click on the toggle button
297
+ if time.time() - self._last_hide_time < 0.15:
298
+ return
299
+
300
+ self.adjustSize()
301
+ pos = widget.mapToGlobal(QPoint(0, 0))
302
+ x = pos.x()
303
+ y = pos.y() - self.height() - 10
304
+ self.move(x, y)
305
+ self.show()