tfbscript 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.
- tfbscript/__init__.py +21 -0
- tfbscript/__main__.py +52 -0
- tfbscript/ansi.py +203 -0
- tfbscript/arena.py +119 -0
- tfbscript/binary.py +84 -0
- tfbscript/debug.py +14 -0
- tfbscript/editor/__init__.py +3 -0
- tfbscript/editor/app.py +141 -0
- tfbscript/editor/fonts/ms-sans-serif/MS Sans Serif.ttf +0 -0
- tfbscript/editor/fonts/ms-sans-serif/license.txt +4 -0
- tfbscript/editor/fonts/ms-sans-serif/readme.txt +26 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/MS Sans Serif Bold.ttf +0 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/license.txt +4 -0
- tfbscript/editor/fonts/ms-sans-serif-bold/readme.txt +26 -0
- tfbscript/editor/fonts.py +98 -0
- tfbscript/editor/search.py +200 -0
- tfbscript/editor/style.py +141 -0
- tfbscript/editor/widgets.py +321 -0
- tfbscript/opcodes/__init__.py +104 -0
- tfbscript/opcodes/base.py +362 -0
- tfbscript/opcodes/block.py +34 -0
- tfbscript/opcodes/enums.py +219 -0
- tfbscript/opcodes/op_behavior_implementation.py +39 -0
- tfbscript/opcodes/op_change_membership.py +62 -0
- tfbscript/opcodes/op_check_fov.py +73 -0
- tfbscript/opcodes/op_check_membership.py +41 -0
- tfbscript/opcodes/op_check_message.py +79 -0
- tfbscript/opcodes/op_check_reference.py +61 -0
- tfbscript/opcodes/op_check_value.py +66 -0
- tfbscript/opcodes/op_comment.py +43 -0
- tfbscript/opcodes/op_control.py +80 -0
- tfbscript/opcodes/op_create_variable.py +42 -0
- tfbscript/opcodes/op_cut_scene.py +44 -0
- tfbscript/opcodes/op_dec_value.py +43 -0
- tfbscript/opcodes/op_displace.py +111 -0
- tfbscript/opcodes/op_find_subset.py +49 -0
- tfbscript/opcodes/op_find_variable.py +28 -0
- tfbscript/opcodes/op_for_each.py +65 -0
- tfbscript/opcodes/op_if_else.py +105 -0
- tfbscript/opcodes/op_inc_value.py +43 -0
- tfbscript/opcodes/op_loop_value.py +28 -0
- tfbscript/opcodes/op_move_from.py +47 -0
- tfbscript/opcodes/op_move_to.py +99 -0
- tfbscript/opcodes/op_play_animation.py +46 -0
- tfbscript/opcodes/op_play_sound.py +42 -0
- tfbscript/opcodes/op_prescript.py +26 -0
- tfbscript/opcodes/op_print.py +53 -0
- tfbscript/opcodes/op_remove.py +43 -0
- tfbscript/opcodes/op_reset.py +42 -0
- tfbscript/opcodes/op_run_as_player.py +26 -0
- tfbscript/opcodes/op_send_message.py +53 -0
- tfbscript/opcodes/op_set_behavior.py +41 -0
- tfbscript/opcodes/op_set_reference.py +52 -0
- tfbscript/opcodes/op_set_value.py +52 -0
- tfbscript/opcodes/op_shutdown.py +26 -0
- tfbscript/opcodes/op_slide_value.py +81 -0
- tfbscript/opcodes/op_spawn_actor.py +96 -0
- tfbscript/opcodes/op_startup.py +26 -0
- tfbscript/opcodes/op_stop_sound.py +26 -0
- tfbscript/opcodes/op_teleport_to.py +47 -0
- tfbscript/opcodes/op_turn_to.py +33 -0
- tfbscript/opcodes/op_use_camera.py +68 -0
- tfbscript/payload.py +40 -0
- tfbscript/reference.py +926 -0
- tfbscript/rhs.py +326 -0
- tfbscript/script.py +160 -0
- tfbscript/string_table.py +101 -0
- tfbscript-0.1.0.dist-info/METADATA +116 -0
- tfbscript-0.1.0.dist-info/RECORD +72 -0
- tfbscript-0.1.0.dist-info/WHEEL +4 -0
- tfbscript-0.1.0.dist-info/entry_points.txt +2 -0
- tfbscript-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
The font file in this archive was created using Fontstruct the free, online
|
|
2
|
+
font-building tool.
|
|
3
|
+
This font was created by “lou”.
|
|
4
|
+
This font has a homepage where this archive and other versions may be found:
|
|
5
|
+
https://fontstruct.com/fontstructions/show/1384862
|
|
6
|
+
|
|
7
|
+
Try Fontstruct at http://fontstruct.com
|
|
8
|
+
It’s easy and it’s fun.
|
|
9
|
+
|
|
10
|
+
NOTE FOR FLASH USERS: Fontstruct fonts (fontstructions) are optimized for Flash.
|
|
11
|
+
If the font in this archive is a pixel font, it is best displayed at a font-size
|
|
12
|
+
of 11.
|
|
13
|
+
|
|
14
|
+
Fontstruct is sponsored by FontShop.
|
|
15
|
+
Visit them at https://fontshop.com
|
|
16
|
+
FontShop is the original independent font retailer. We’ve been around since
|
|
17
|
+
the dawn of digital type. Whether you need the right font or need to create the
|
|
18
|
+
right font from scratch, let our 26 years of experience work for you.
|
|
19
|
+
|
|
20
|
+
Fontstruct is copyright ©2017 Rob Meek
|
|
21
|
+
|
|
22
|
+
LEGAL NOTICE:
|
|
23
|
+
In using this font you must comply with the licensing terms described in the
|
|
24
|
+
file “license.txt” included with this archive.
|
|
25
|
+
If you redistribute the font file in this archive, it must be accompanied by all
|
|
26
|
+
the other files from this archive, including this one.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fontTools.ttLib import TTFont
|
|
7
|
+
|
|
8
|
+
from PySide6.QtGui import QFont, QFontDatabase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_font(
|
|
12
|
+
path: str | Path,
|
|
13
|
+
size: int = 11,
|
|
14
|
+
weight: QFont.Weight = QFont.Weight.Normal,
|
|
15
|
+
italic: bool = False,
|
|
16
|
+
aliased: bool = True,
|
|
17
|
+
) -> QFont:
|
|
18
|
+
"""
|
|
19
|
+
Load a custom font from file, rendered GDI-style (grid-fitted, no AA).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path: Path to the font file (.ttf, .otf, etc.), or a Qt resource path
|
|
23
|
+
size: Size in PIXELS (not points) — 11px == XP's default 8pt Tahoma
|
|
24
|
+
weight: Font weight (e.g. QFont.Weight.Normal, QFont.Weight.Bold)
|
|
25
|
+
italic: Whether the font should be italic
|
|
26
|
+
aliased: True for crisp XP "standard" text, False for soft ClearType-ish text
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The QFont instance.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: if the font file could not be loaded.
|
|
33
|
+
"""
|
|
34
|
+
font_id = QFontDatabase.addApplicationFont(str(path))
|
|
35
|
+
|
|
36
|
+
if font_id == -1:
|
|
37
|
+
raise ValueError(f"Failed to load font from: {path}")
|
|
38
|
+
|
|
39
|
+
families = QFontDatabase.applicationFontFamilies(font_id)
|
|
40
|
+
if not families:
|
|
41
|
+
raise ValueError(f"No font families found in: {path}")
|
|
42
|
+
|
|
43
|
+
font = QFont(families[0])
|
|
44
|
+
font.setPixelSize(size) # pixel size, so DPI can't rescale us into mush
|
|
45
|
+
font.setWeight(weight)
|
|
46
|
+
font.setItalic(italic)
|
|
47
|
+
|
|
48
|
+
if aliased:
|
|
49
|
+
# Hard pixel edges + snap stems to the pixel grid == comctl32 look
|
|
50
|
+
font.setStyleStrategy(
|
|
51
|
+
QFont.StyleStrategy(
|
|
52
|
+
QFont.StyleStrategy.NoAntialias | QFont.StyleStrategy.PreferMatch
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
font.setHintingPreference(QFont.HintingPreference.PreferFullHinting)
|
|
56
|
+
else:
|
|
57
|
+
# Soft, slightly smeared — closer to ClearType on a non-LCD display
|
|
58
|
+
font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
|
|
59
|
+
font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
|
|
60
|
+
|
|
61
|
+
return font
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_bold_variant(regular_family: str, bold_path: str | Path) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Load `bold_path` as the Bold weight of `regular_family`.
|
|
67
|
+
|
|
68
|
+
The bold TTF's own name/OS2 tables declare it as an unrelated Regular-weight
|
|
69
|
+
family (e.g. "MS Sans Serif Bold" @ weight 400), so Qt registers it as a
|
|
70
|
+
separate font family and `font-weight: bold` on the regular family just
|
|
71
|
+
synthesizes a fake bold instead of using the real hinted glyphs. Patch the
|
|
72
|
+
tables in memory so Qt sees it as `regular_family`'s Bold style.
|
|
73
|
+
"""
|
|
74
|
+
font = TTFont(str(bold_path))
|
|
75
|
+
|
|
76
|
+
name = font["name"]
|
|
77
|
+
for name_id, value in (
|
|
78
|
+
(1, regular_family),
|
|
79
|
+
(2, "Bold"),
|
|
80
|
+
(4, f"{regular_family} Bold"),
|
|
81
|
+
(16, regular_family),
|
|
82
|
+
(17, "Bold"),
|
|
83
|
+
):
|
|
84
|
+
name.setName(value, name_id, 3, 1, 0x409)
|
|
85
|
+
name.setName(value, name_id, 1, 0, 0)
|
|
86
|
+
|
|
87
|
+
os2 = font["OS/2"]
|
|
88
|
+
os2.usWeightClass = 700 # pyright: ignore[reportAttributeAccessIssue]
|
|
89
|
+
os2.fsSelection = ( # pyright: ignore[reportAttributeAccessIssue]
|
|
90
|
+
os2.fsSelection & ~0x40 # pyright: ignore[reportAttributeAccessIssue]
|
|
91
|
+
) | 0x20 # clear REGULAR, set BOLD
|
|
92
|
+
font["head"].macStyle |= 0x1 # pyright: ignore[reportAttributeAccessIssue]
|
|
93
|
+
|
|
94
|
+
buf = io.BytesIO()
|
|
95
|
+
font.save(buf)
|
|
96
|
+
|
|
97
|
+
if QFontDatabase.addApplicationFontFromData(buf.getvalue()) == -1:
|
|
98
|
+
raise ValueError(f"Failed to register bold variant from: {bold_path}")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
from PySide6.QtGui import QKeySequence, QShortcut
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QAbstractItemView,
|
|
9
|
+
QHBoxLayout,
|
|
10
|
+
QLabel,
|
|
11
|
+
QLineEdit,
|
|
12
|
+
QPushButton,
|
|
13
|
+
QTreeWidget,
|
|
14
|
+
QTreeWidgetItem,
|
|
15
|
+
QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ROW_OBJECT_NAME = "tfbRow"
|
|
19
|
+
|
|
20
|
+
MATCH_BG = "#FFF2A8"
|
|
21
|
+
CURRENT_MATCH_BG = "#FFC63F"
|
|
22
|
+
|
|
23
|
+
# Space kept around a row's fields so a highlight doesn't hug the text
|
|
24
|
+
ROW_PADDING = 3
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def row_text(layout: QHBoxLayout) -> str:
|
|
28
|
+
"""Flattens a rendered row into one searchable string.
|
|
29
|
+
|
|
30
|
+
Reads the already-built widgets rather than the opcode itself: rendering a
|
|
31
|
+
reference needs the enclosing-op stack, which only exists while the tree is
|
|
32
|
+
being populated.
|
|
33
|
+
"""
|
|
34
|
+
parts: list[str] = []
|
|
35
|
+
for i in range(layout.count()):
|
|
36
|
+
item = layout.itemAt(i)
|
|
37
|
+
if item is None:
|
|
38
|
+
continue
|
|
39
|
+
widget = item.widget()
|
|
40
|
+
if isinstance(widget, (QLabel, QPushButton)):
|
|
41
|
+
parts.append(widget.text())
|
|
42
|
+
return " ".join(parts)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SearchEntry:
|
|
47
|
+
item: QTreeWidgetItem
|
|
48
|
+
content: QWidget
|
|
49
|
+
_text: str = field(default="", repr=False)
|
|
50
|
+
|
|
51
|
+
def text(self) -> str:
|
|
52
|
+
return self._text
|
|
53
|
+
|
|
54
|
+
def set_text(self, text: str) -> None:
|
|
55
|
+
self._text = text.lower()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SearchBar(QWidget):
|
|
59
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
60
|
+
super().__init__(parent)
|
|
61
|
+
|
|
62
|
+
layout = QHBoxLayout(self)
|
|
63
|
+
layout.setContentsMargins(6, 3, 6, 3)
|
|
64
|
+
layout.setSpacing(4)
|
|
65
|
+
|
|
66
|
+
self.edit = QLineEdit()
|
|
67
|
+
self.edit.setPlaceholderText("Find")
|
|
68
|
+
self.edit.setClearButtonEnabled(False)
|
|
69
|
+
|
|
70
|
+
self.status = QLabel("")
|
|
71
|
+
self.status.setMinimumWidth(90)
|
|
72
|
+
self.status.setAlignment(
|
|
73
|
+
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.prev_button = QPushButton("Previous")
|
|
77
|
+
self.next_button = QPushButton("Next")
|
|
78
|
+
self.close_button = QPushButton("Close")
|
|
79
|
+
|
|
80
|
+
layout.addWidget(QLabel("Find:"))
|
|
81
|
+
layout.addWidget(self.edit, 1)
|
|
82
|
+
layout.addWidget(self.status)
|
|
83
|
+
layout.addWidget(self.prev_button)
|
|
84
|
+
layout.addWidget(self.next_button)
|
|
85
|
+
layout.addWidget(self.close_button)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TreeSearch:
|
|
89
|
+
"""Incremental find over the rows of the opcode tree.
|
|
90
|
+
|
|
91
|
+
Rows are custom item widgets, so matching runs against the flattened text of
|
|
92
|
+
each opcode and the hit is shown by tinting the row's container widget.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, tree: QTreeWidget, bar: SearchBar, entries: list[SearchEntry]):
|
|
96
|
+
self.tree = tree
|
|
97
|
+
self.bar = bar
|
|
98
|
+
self.entries = entries
|
|
99
|
+
self.matches: list[int] = []
|
|
100
|
+
self.current = -1
|
|
101
|
+
|
|
102
|
+
for entry in entries:
|
|
103
|
+
entry.content.setObjectName(ROW_OBJECT_NAME)
|
|
104
|
+
entry.content.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
|
105
|
+
|
|
106
|
+
bar.edit.textChanged.connect(self._on_query_changed)
|
|
107
|
+
bar.edit.returnPressed.connect(self.next_match)
|
|
108
|
+
bar.next_button.clicked.connect(self.next_match)
|
|
109
|
+
bar.prev_button.clicked.connect(self.previous_match)
|
|
110
|
+
bar.close_button.clicked.connect(self.close)
|
|
111
|
+
|
|
112
|
+
shift_return = QShortcut(
|
|
113
|
+
QKeySequence(Qt.KeyboardModifier.ShiftModifier | Qt.Key.Key_Return), bar.edit
|
|
114
|
+
)
|
|
115
|
+
shift_return.setContext(Qt.ShortcutContext.WidgetShortcut)
|
|
116
|
+
shift_return.activated.connect(self.previous_match)
|
|
117
|
+
|
|
118
|
+
escape = QShortcut(QKeySequence(Qt.Key.Key_Escape), bar)
|
|
119
|
+
escape.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
|
120
|
+
escape.activated.connect(self.close)
|
|
121
|
+
|
|
122
|
+
bar.hide()
|
|
123
|
+
|
|
124
|
+
def open(self) -> None:
|
|
125
|
+
self.bar.show()
|
|
126
|
+
self.bar.edit.setFocus()
|
|
127
|
+
self.bar.edit.selectAll()
|
|
128
|
+
self._search(self.bar.edit.text())
|
|
129
|
+
|
|
130
|
+
def close(self) -> None:
|
|
131
|
+
self._clear_highlights()
|
|
132
|
+
self.matches = []
|
|
133
|
+
self.current = -1
|
|
134
|
+
self.bar.hide()
|
|
135
|
+
|
|
136
|
+
def next_match(self) -> None:
|
|
137
|
+
self._step(1)
|
|
138
|
+
|
|
139
|
+
def previous_match(self) -> None:
|
|
140
|
+
self._step(-1)
|
|
141
|
+
|
|
142
|
+
def _on_query_changed(self, query: str) -> None:
|
|
143
|
+
self._search(query)
|
|
144
|
+
|
|
145
|
+
def _search(self, query: str) -> None:
|
|
146
|
+
self._clear_highlights()
|
|
147
|
+
|
|
148
|
+
needle = query.strip().lower()
|
|
149
|
+
if not needle:
|
|
150
|
+
self.matches = []
|
|
151
|
+
self.current = -1
|
|
152
|
+
self.bar.status.setText("")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
self.matches = [
|
|
156
|
+
i for i, entry in enumerate(self.entries) if needle in entry.text()
|
|
157
|
+
]
|
|
158
|
+
self.current = 0 if self.matches else -1
|
|
159
|
+
self._apply_highlights()
|
|
160
|
+
self._reveal_current()
|
|
161
|
+
self._update_status()
|
|
162
|
+
|
|
163
|
+
def _step(self, delta: int) -> None:
|
|
164
|
+
if not self.matches:
|
|
165
|
+
return
|
|
166
|
+
self.current = (self.current + delta) % len(self.matches)
|
|
167
|
+
self._apply_highlights()
|
|
168
|
+
self._reveal_current()
|
|
169
|
+
self._update_status()
|
|
170
|
+
|
|
171
|
+
def _clear_highlights(self) -> None:
|
|
172
|
+
for index in self.matches:
|
|
173
|
+
self.entries[index].content.setStyleSheet("")
|
|
174
|
+
|
|
175
|
+
def _apply_highlights(self) -> None:
|
|
176
|
+
for position, index in enumerate(self.matches):
|
|
177
|
+
color = CURRENT_MATCH_BG if position == self.current else MATCH_BG
|
|
178
|
+
self.entries[index].content.setStyleSheet(
|
|
179
|
+
f"#{ROW_OBJECT_NAME} {{ background-color: {color}; }}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _reveal_current(self) -> None:
|
|
183
|
+
if self.current < 0:
|
|
184
|
+
return
|
|
185
|
+
item = self.entries[self.matches[self.current]].item
|
|
186
|
+
|
|
187
|
+
parent = item.parent()
|
|
188
|
+
while parent is not None:
|
|
189
|
+
parent.setExpanded(True)
|
|
190
|
+
parent = parent.parent()
|
|
191
|
+
|
|
192
|
+
self.tree.scrollToItem(
|
|
193
|
+
item, QAbstractItemView.ScrollHint.PositionAtCenter
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _update_status(self) -> None:
|
|
197
|
+
if not self.matches:
|
|
198
|
+
self.bar.status.setText("No results")
|
|
199
|
+
else:
|
|
200
|
+
self.bar.status.setText(f"{self.current + 1} of {len(self.matches)}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import Qt, QRect
|
|
4
|
+
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
|
|
5
|
+
from PySide6.QtWidgets import QProxyStyle, QStyle, QStyleOption, QWidget
|
|
6
|
+
|
|
7
|
+
BG = QColor("#DCDCDC")
|
|
8
|
+
TEXT = QColor("#101010")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def tfb_colored_box(element: QWidget, color: str):
|
|
12
|
+
element.setStyleSheet(
|
|
13
|
+
f"background-color: {color}; color: #FFFFFF; padding: 0px 4px;"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_palette() -> QPalette:
|
|
18
|
+
palette = QPalette()
|
|
19
|
+
palette.setColor(QPalette.ColorRole.Window, BG)
|
|
20
|
+
palette.setColor(QPalette.ColorRole.WindowText, TEXT)
|
|
21
|
+
palette.setColor(QPalette.ColorRole.Base, BG)
|
|
22
|
+
palette.setColor(QPalette.ColorRole.AlternateBase, BG.lighter(105))
|
|
23
|
+
palette.setColor(QPalette.ColorRole.ToolTipBase, BG)
|
|
24
|
+
palette.setColor(QPalette.ColorRole.ToolTipText, TEXT)
|
|
25
|
+
palette.setColor(QPalette.ColorRole.Text, TEXT)
|
|
26
|
+
palette.setColor(QPalette.ColorRole.Button, BG)
|
|
27
|
+
palette.setColor(QPalette.ColorRole.ButtonText, TEXT)
|
|
28
|
+
palette.setColor(QPalette.ColorRole.BrightText, QColor("red"))
|
|
29
|
+
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
|
30
|
+
palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))
|
|
31
|
+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("white"))
|
|
32
|
+
|
|
33
|
+
# Disabled state, so grayed-out buttons/labels don't go invisible
|
|
34
|
+
palette.setColor(
|
|
35
|
+
QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor("#707070")
|
|
36
|
+
)
|
|
37
|
+
palette.setColor(
|
|
38
|
+
QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor("#707070")
|
|
39
|
+
)
|
|
40
|
+
palette.setColor(
|
|
41
|
+
QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor("#707070")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return palette
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Win98TreeStyle(QProxyStyle):
|
|
48
|
+
"""Redraws QTreeView branch connectors as classic Win98/comctl32-style
|
|
49
|
+
dotted lines and a dotted-bordered +/- box, instead of Fusion's arrows."""
|
|
50
|
+
|
|
51
|
+
LINE_COLOR = QColor("#97A1AD")
|
|
52
|
+
BOX_SIZE = 11
|
|
53
|
+
|
|
54
|
+
CONTENT_OFFSET = 0
|
|
55
|
+
|
|
56
|
+
BOX_INSET = 20
|
|
57
|
+
|
|
58
|
+
def drawPrimitive(
|
|
59
|
+
self,
|
|
60
|
+
element: QStyle.PrimitiveElement,
|
|
61
|
+
option: QStyleOption,
|
|
62
|
+
painter: QPainter,
|
|
63
|
+
widget: QWidget | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
if element == QStyle.PrimitiveElement.PE_IndicatorBranch:
|
|
66
|
+
self._draw_branch(option, painter)
|
|
67
|
+
return
|
|
68
|
+
super().drawPrimitive(element, option, painter, widget)
|
|
69
|
+
|
|
70
|
+
def _draw_branch(self, option: QStyleOption, painter: QPainter) -> None:
|
|
71
|
+
rect = option.rect
|
|
72
|
+
mid_x = max(rect.left(), rect.right() - self.BOX_INSET)
|
|
73
|
+
mid_y = rect.center().y()
|
|
74
|
+
state = option.state
|
|
75
|
+
|
|
76
|
+
has_children = bool(state & QStyle.StateFlag.State_Children)
|
|
77
|
+
has_sibling = bool(state & QStyle.StateFlag.State_Sibling)
|
|
78
|
+
is_item = bool(state & QStyle.StateFlag.State_Item)
|
|
79
|
+
is_open = bool(state & QStyle.StateFlag.State_Open)
|
|
80
|
+
|
|
81
|
+
painter.save()
|
|
82
|
+
pen = QPen(self.LINE_COLOR)
|
|
83
|
+
pen.setWidth(1)
|
|
84
|
+
pen.setStyle(Qt.PenStyle.DotLine)
|
|
85
|
+
painter.setPen(pen)
|
|
86
|
+
|
|
87
|
+
if has_sibling:
|
|
88
|
+
painter.drawLine(
|
|
89
|
+
mid_x + self.CONTENT_OFFSET,
|
|
90
|
+
rect.top(),
|
|
91
|
+
mid_x + self.CONTENT_OFFSET,
|
|
92
|
+
rect.bottom(),
|
|
93
|
+
)
|
|
94
|
+
elif is_item:
|
|
95
|
+
painter.drawLine(
|
|
96
|
+
mid_x + self.CONTENT_OFFSET,
|
|
97
|
+
rect.top(),
|
|
98
|
+
mid_x + self.CONTENT_OFFSET,
|
|
99
|
+
mid_y,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Horizontal line
|
|
103
|
+
if is_item:
|
|
104
|
+
painter.drawLine(
|
|
105
|
+
mid_x + self.CONTENT_OFFSET,
|
|
106
|
+
mid_y,
|
|
107
|
+
rect.right() + self.CONTENT_OFFSET,
|
|
108
|
+
mid_y,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
painter.restore()
|
|
112
|
+
|
|
113
|
+
if has_children:
|
|
114
|
+
box_size = max(7, min(self.BOX_SIZE, rect.width() - 2, rect.height() - 2))
|
|
115
|
+
if box_size % 2 == 0:
|
|
116
|
+
box_size -= 1
|
|
117
|
+
half = box_size // 2
|
|
118
|
+
|
|
119
|
+
box_x = mid_x - half
|
|
120
|
+
box_y = mid_y - half
|
|
121
|
+
box = QRect(box_x, box_y, box_size, box_size)
|
|
122
|
+
|
|
123
|
+
painter.save()
|
|
124
|
+
painter.setPen(QPen(QColor("#788897"))) # BOX BORDER COLOR
|
|
125
|
+
painter.setBrush(QColor("#DCDCDC")) # BOX BG COLOR
|
|
126
|
+
painter.drawRect(box)
|
|
127
|
+
|
|
128
|
+
painter.setPen(QPen(QColor("#3F3F3F"))) # SIGN COLOR
|
|
129
|
+
|
|
130
|
+
# + or - sign
|
|
131
|
+
sign_x = mid_x + self.CONTENT_OFFSET
|
|
132
|
+
painter.drawLine(box.left() + 2, box_y + half, box.right() - 2, box_y + half) # -
|
|
133
|
+
if not is_open: # | (| + - = +)
|
|
134
|
+
painter.drawLine(
|
|
135
|
+
sign_x,
|
|
136
|
+
box.top() + 2,
|
|
137
|
+
sign_x,
|
|
138
|
+
box.bottom() - 2,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
painter.restore()
|