NotYAFC 1.0.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,658 @@
1
+ """Color palettes and Qt style sheet for the NotYAFC window."""
2
+
3
+ import logging
4
+ import os
5
+ import tempfile
6
+ from dataclasses import dataclass
7
+ from functools import cache
8
+ from pathlib import Path
9
+ from string import Template
10
+
11
+ from PySide6.QtCore import QPointF, QStandardPaths, Qt
12
+ from PySide6.QtGui import QColor, QImage, QPainter, QPen
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Palette:
19
+ page: str
20
+ surface: str
21
+ field: str
22
+ list_bg: str
23
+ list_alt: str
24
+ button: str
25
+ button_hover: str
26
+ button_pressed: str
27
+ disabled_bg: str
28
+ border: str
29
+ border_strong: str
30
+ border_hover: str
31
+ indicator_border: str
32
+ text: str
33
+ text_title: str
34
+ text_secondary: str
35
+ text_muted: str
36
+ text_disabled: str
37
+ primary: str
38
+ primary_hover: str
39
+ on_primary: str
40
+ primary_disabled: str
41
+ primary_disabled_text: str
42
+ accent_text: str
43
+ selection_bg: str
44
+ selection_text: str
45
+ tree_selection_bg: str
46
+ info_bg: str
47
+ info_border: str
48
+ progress_track: str
49
+ toggle_bg: str
50
+ toggle_checked: str
51
+ toggle_hover: str
52
+ icon_active: str
53
+ icon_inactive: str
54
+ danger: str
55
+ warning: str
56
+
57
+
58
+ LIGHT = Palette(
59
+ page="#f4f6fb",
60
+ surface="#ffffff",
61
+ field="#ffffff",
62
+ list_bg="#fbfcff",
63
+ list_alt="#f0f2f6",
64
+ button="#eef1f8",
65
+ button_hover="#e2e8f5",
66
+ button_pressed="#d7dfef",
67
+ disabled_bg="#f1f3f7",
68
+ border="#e1e6f0",
69
+ border_strong="#c7d1e2",
70
+ border_hover="#aab6cc",
71
+ indicator_border="#7b879b",
72
+ text="#172033",
73
+ text_title="#14213d",
74
+ text_secondary="#45536c",
75
+ text_muted="#58657a",
76
+ text_disabled="#8a96ab",
77
+ primary="#4361ee",
78
+ primary_hover="#3451d1",
79
+ on_primary="#ffffff",
80
+ primary_disabled="#f1f3f7",
81
+ primary_disabled_text="#8a96ab",
82
+ accent_text="#3451d1",
83
+ selection_bg="#e8edff",
84
+ selection_text="#263da1",
85
+ tree_selection_bg="#d5ddff",
86
+ info_bg="#edf1ff",
87
+ info_border="#c9d4ff",
88
+ progress_track="#e8ecf4",
89
+ toggle_bg="#f8fafc",
90
+ toggle_checked="#e2e8f0",
91
+ toggle_hover="#edf1f7",
92
+ icon_active="#101827",
93
+ icon_inactive="#64748b",
94
+ danger="#c62828",
95
+ warning="#8a5a00",
96
+ )
97
+
98
+ DARK = Palette(
99
+ page="#171b24",
100
+ surface="#202633",
101
+ field="#252d3b",
102
+ list_bg="#1c222e",
103
+ list_alt="#222936",
104
+ button="#2b3443",
105
+ button_hover="#354154",
106
+ button_pressed="#222a37",
107
+ disabled_bg="#222833",
108
+ border="#343e50",
109
+ border_strong="#414d61",
110
+ border_hover="#586780",
111
+ indicator_border="#6f7d93",
112
+ text="#e5e9f0",
113
+ text_title="#f1f4fa",
114
+ text_secondary="#c5ccda",
115
+ text_muted="#9ba7bb",
116
+ text_disabled="#778398",
117
+ primary="#5875ff",
118
+ primary_hover="#7189ff",
119
+ on_primary="#ffffff",
120
+ primary_disabled="#222833",
121
+ primary_disabled_text="#778398",
122
+ accent_text="#9aabff",
123
+ selection_bg="#354477",
124
+ selection_text="#f1f4fa",
125
+ tree_selection_bg="#354477",
126
+ info_bg="#29334f",
127
+ info_border="#46598f",
128
+ progress_track="#2b3443",
129
+ toggle_bg="#090e19",
130
+ toggle_checked="#303b5d",
131
+ toggle_hover="#252d3b",
132
+ icon_active="#e5eaf1",
133
+ icon_inactive="#7d8aa1",
134
+ danger="#ff8a8a",
135
+ warning="#f5b84a",
136
+ )
137
+
138
+ _TEMPLATE = Template("""
139
+ QMainWindow, QWidget#page, QScrollArea, QScrollArea > QWidget > QWidget, QDialog {
140
+ background: $page;
141
+ color: $text;
142
+ font-size: 10pt;
143
+ }
144
+ QMessageBox {
145
+ background: $surface;
146
+ }
147
+ QMessageBox QLabel {
148
+ color: $text;
149
+ background: transparent;
150
+ }
151
+ QLabel, QRadioButton, QCheckBox {
152
+ color: $text_secondary;
153
+ }
154
+ QLabel#pageTitle {
155
+ color: $text_title;
156
+ font-size: 20pt;
157
+ font-weight: 700;
158
+ }
159
+ QLabel#pageSubtitle, QLabel#hint {
160
+ color: $text_muted;
161
+ }
162
+ QLabel#dropZoneTitle {
163
+ color: $text;
164
+ font-size: 13pt;
165
+ font-weight: 700;
166
+ }
167
+ QFrame#dropZone {
168
+ border: 2px dashed $border_strong;
169
+ border-radius: 10px;
170
+ }
171
+ QFrame#dropZone[dragOver="true"] {
172
+ border-color: $primary;
173
+ background: $info_bg;
174
+ }
175
+ QLabel#conversionGroupHeading {
176
+ color: $text_secondary;
177
+ font-size: 9pt;
178
+ font-weight: 600;
179
+ }
180
+ QLabel#warningHint {
181
+ color: $warning;
182
+ }
183
+ QLabel#stepLabel {
184
+ color: $accent_text;
185
+ font-weight: 600;
186
+ }
187
+ QLabel#stepHint, QLabel#statusLabel {
188
+ color: $text_secondary;
189
+ }
190
+ QLabel#statusLabel {
191
+ font-weight: 600;
192
+ }
193
+ QLabel#statusLabel[state="warning"] {
194
+ color: $warning;
195
+ }
196
+ QLabel#statusLabel[state="error"] {
197
+ color: $danger;
198
+ }
199
+ QWidget#actionBar {
200
+ background: $surface;
201
+ border-top: 1px solid $border_strong;
202
+ }
203
+ QPushButton#primaryButton[footerPrimary="true"] {
204
+ font-size: 11pt;
205
+ font-weight: 700;
206
+ }
207
+ QLabel#pathLabel {
208
+ color: $text;
209
+ background: $info_bg;
210
+ border: 1px solid $info_border;
211
+ border-radius: 4px;
212
+ padding: 7px 10px;
213
+ }
214
+ QLabel#organizationIntro {
215
+ color: $text_secondary;
216
+ font-size: 10.5pt;
217
+ }
218
+ QLabel#organizationSection {
219
+ color: $text_muted;
220
+ font-size: 8.5pt;
221
+ font-weight: 700;
222
+ }
223
+ QLabel#organizationLevelNumber {
224
+ color: $accent_text;
225
+ background: $info_bg;
226
+ border: 1px solid $info_border;
227
+ border-radius: 11px;
228
+ min-width: 22px;
229
+ max-width: 22px;
230
+ min-height: 22px;
231
+ max-height: 22px;
232
+ qproperty-alignment: AlignCenter;
233
+ font-weight: 700;
234
+ }
235
+ QFrame#organizationLevel {
236
+ background: $surface;
237
+ border: 1px solid $border;
238
+ border-radius: 6px;
239
+ }
240
+ QPushButton#organizationIconButton {
241
+ padding: 2px;
242
+ font-size: 12pt;
243
+ font-weight: 500;
244
+ }
245
+ QPushButton#organizationRemoveButton,
246
+ QPushButton#organizationRemoveCriterionButton {
247
+ color: $text_muted;
248
+ background: transparent;
249
+ padding: 3px 8px;
250
+ }
251
+ QPushButton#organizationRemoveButton:hover,
252
+ QPushButton#organizationRemoveCriterionButton:hover {
253
+ color: $danger;
254
+ background: $button_hover;
255
+ }
256
+ QPushButton#organizationAddButton,
257
+ QPushButton#organizationAddCriterionButton {
258
+ color: $accent_text;
259
+ background: $button;
260
+ border: 1px solid $border_strong;
261
+ padding: 5px 10px;
262
+ }
263
+ QPushButton#organizationAddButton:hover:enabled,
264
+ QPushButton#organizationAddCriterionButton:hover:enabled {
265
+ background: $button_hover;
266
+ border-color: $border_hover;
267
+ }
268
+ QPushButton#organizationAddButton:disabled,
269
+ QPushButton#organizationAddCriterionButton:disabled {
270
+ color: $text_disabled;
271
+ background: $disabled_bg;
272
+ border-color: $border;
273
+ }
274
+ QGroupBox#card {
275
+ background: $surface;
276
+ border: 1px solid $border;
277
+ border-radius: 5px;
278
+ margin-top: 14px;
279
+ padding-top: 9px;
280
+ font-weight: 600;
281
+ color: $text;
282
+ }
283
+ QGroupBox#card::title {
284
+ subcontrol-origin: margin;
285
+ left: 15px;
286
+ padding: 0 6px;
287
+ color: $text_secondary;
288
+ font-size: 11pt;
289
+ font-weight: 700;
290
+ }
291
+ QComboBox, QSpinBox, QDoubleSpinBox, QLineEdit {
292
+ background: $field;
293
+ color: $text;
294
+ border: 1px solid $border_strong;
295
+ border-radius: 4px;
296
+ padding: 6px 10px;
297
+ min-height: 20px;
298
+ selection-background-color: $primary;
299
+ }
300
+ QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus {
301
+ border: 1px solid $primary;
302
+ }
303
+ QComboBox::drop-down {
304
+ subcontrol-origin: padding;
305
+ subcontrol-position: top right;
306
+ border-left: 1px solid $border;
307
+ width: 30px;
308
+ }
309
+ QComboBox::down-arrow {
310
+ image: $sort_down_icon;
311
+ width: 17px;
312
+ height: 17px;
313
+ }
314
+ QComboBox QAbstractItemView {
315
+ background: $field;
316
+ color: $text;
317
+ selection-background-color: $selection_bg;
318
+ selection-color: $selection_text;
319
+ }
320
+ QComboBox QAbstractItemView::item:disabled {
321
+ color: $text_disabled;
322
+ background: $disabled_bg;
323
+ }
324
+ QSpinBox::up-button, QSpinBox::down-button,
325
+ QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {
326
+ width: 0px;
327
+ border: none;
328
+ }
329
+ QLineEdit:disabled {
330
+ background: $disabled_bg;
331
+ color: $text_disabled;
332
+ }
333
+ QPushButton {
334
+ background: $button;
335
+ color: $text;
336
+ border: 1px solid $border_strong;
337
+ border-radius: 4px;
338
+ padding: 6px 13px;
339
+ font-weight: 600;
340
+ }
341
+ QPushButton:hover {
342
+ background: $button_hover;
343
+ border-color: $border_hover;
344
+ }
345
+ QPushButton:pressed {
346
+ background: $button_pressed;
347
+ }
348
+ QPushButton:disabled {
349
+ color: $text_disabled;
350
+ background: $disabled_bg;
351
+ border-color: $border;
352
+ }
353
+ QPushButton#primaryButton {
354
+ background: $primary;
355
+ color: $on_primary;
356
+ border-color: $primary;
357
+ }
358
+ QPushButton#primaryButton:hover {
359
+ background: $primary_hover;
360
+ }
361
+ QPushButton#primaryButton:disabled {
362
+ background: $primary_disabled;
363
+ color: $primary_disabled_text;
364
+ }
365
+ QPushButton#stepperButton {
366
+ min-width: 16px;
367
+ max-width: 16px;
368
+ padding: 6px 6px;
369
+ font-size: 12pt;
370
+ }
371
+ QPushButton#stepperButton:focus {
372
+ border-color: $primary;
373
+ }
374
+ QCheckBox {
375
+ spacing: 8px;
376
+ }
377
+ QCheckBox::indicator, QTableView::indicator, QTreeWidget#instanceTree::indicator {
378
+ width: 17px;
379
+ height: 17px;
380
+ border: 1px solid $indicator_border;
381
+ border-radius: 3px;
382
+ background: $field;
383
+ }
384
+ QCheckBox::indicator:checked, QTableView::indicator:checked,
385
+ QTreeWidget#instanceTree::indicator:checked {
386
+ background: $primary;
387
+ border-color: $primary;
388
+ image: $check_icon;
389
+ }
390
+ QCheckBox::indicator:indeterminate, QTableView::indicator:indeterminate,
391
+ QTreeWidget#instanceTree::indicator:indeterminate {
392
+ background: $selection_bg;
393
+ border-color: $primary;
394
+ image: $dash_icon;
395
+ }
396
+ QTableView::indicator:disabled {
397
+ background: $disabled_bg;
398
+ border-color: $border_strong;
399
+ }
400
+ QCheckBox::indicator:checked:disabled, QTableView::indicator:checked:disabled {
401
+ background: $primary_disabled;
402
+ border-color: $primary_disabled;
403
+ }
404
+ QRadioButton::indicator {
405
+ width: 17px;
406
+ height: 17px;
407
+ border: 1px solid $indicator_border;
408
+ border-radius: 9px;
409
+ background: $field;
410
+ }
411
+ QRadioButton::indicator:checked {
412
+ background: qradialgradient(
413
+ cx: 0.5, cy: 0.5, radius: 0.5, fx: 0.5, fy: 0.5,
414
+ stop: 0 $primary, stop: 0.42 $primary, stop: 0.5 $field
415
+ );
416
+ }
417
+ QListWidget {
418
+ background: $list_bg;
419
+ color: $text;
420
+ border: 1px solid $border_strong;
421
+ border-radius: 4px;
422
+ padding: 6px;
423
+ outline: none;
424
+ }
425
+ QListWidget::item {
426
+ padding: 8px;
427
+ border-radius: 3px;
428
+ }
429
+ QTreeWidget#instanceTree {
430
+ background: $list_bg;
431
+ alternate-background-color: $list_alt;
432
+ color: $text;
433
+ border: 1px solid $border_strong;
434
+ border-radius: 4px;
435
+ selection-background-color: $tree_selection_bg;
436
+ selection-color: $text;
437
+ outline: none;
438
+ }
439
+ QTreeWidget#instanceTree:focus {
440
+ border-color: $primary;
441
+ }
442
+ QTreeWidget#instanceTree::item {
443
+ padding: 3px 2px;
444
+ }
445
+ QTreeWidget#instanceTree::item:selected, QTreeWidget#instanceTree::item:selected:active,
446
+ QTreeWidget#instanceTree::item:selected:!active {
447
+ background: $tree_selection_bg;
448
+ color: $text;
449
+ }
450
+ QTreeWidget#instanceTree::branch:closed:has-children {
451
+ image: $branch_closed_icon;
452
+ width: 16px;
453
+ height: 16px;
454
+ }
455
+ QTreeWidget#instanceTree::branch:open:has-children {
456
+ image: $branch_open_icon;
457
+ width: 16px;
458
+ height: 16px;
459
+ }
460
+ QTableView {
461
+ background: $list_bg;
462
+ alternate-background-color: $list_alt;
463
+ color: $text;
464
+ border: 1px solid $border_strong;
465
+ border-radius: 4px;
466
+ selection-background-color: $selection_bg;
467
+ selection-color: $selection_text;
468
+ outline: none;
469
+ }
470
+ QTableView::item {
471
+ border: none;
472
+ padding: 0px;
473
+ }
474
+ QTableView:focus {
475
+ border-color: $primary;
476
+ }
477
+ QTableView[dragOver="true"], QTreeWidget#instanceTree[dragOver="true"] {
478
+ border-color: $primary;
479
+ }
480
+ QListWidget::item:selected,
481
+ QTableView::item:selected, QTableView::item:selected:active,
482
+ QTableView::item:selected:!active, QTableView::item:selected:disabled {
483
+ background: $selection_bg;
484
+ color: $selection_text;
485
+ }
486
+ QHeaderView {
487
+ background: $button;
488
+ border: none;
489
+ }
490
+ QHeaderView::section, QTableCornerButton::section {
491
+ background: $button;
492
+ color: $text;
493
+ border: 1px solid $border_strong;
494
+ padding: 3px;
495
+ }
496
+ QHeaderView::up-arrow {
497
+ image: $sort_up_icon;
498
+ width: 17px;
499
+ height: 17px;
500
+ }
501
+ QHeaderView::down-arrow {
502
+ image: $sort_down_icon;
503
+ width: 17px;
504
+ height: 17px;
505
+ }
506
+ QScrollBar:vertical {
507
+ background: $list_bg;
508
+ border: none;
509
+ width: 12px;
510
+ margin: 0;
511
+ }
512
+ QScrollBar:horizontal {
513
+ background: $list_bg;
514
+ border: none;
515
+ height: 12px;
516
+ margin: 0;
517
+ }
518
+ QScrollBar::handle:vertical {
519
+ background: $border_strong;
520
+ border-radius: 4px;
521
+ min-height: 28px;
522
+ margin: 2px;
523
+ }
524
+ QScrollBar::handle:horizontal {
525
+ background: $border_strong;
526
+ border-radius: 4px;
527
+ min-width: 28px;
528
+ margin: 2px;
529
+ }
530
+ QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover {
531
+ background: $border_hover;
532
+ }
533
+ QScrollBar::add-line, QScrollBar::sub-line {
534
+ width: 0px;
535
+ height: 0px;
536
+ border: none;
537
+ background: none;
538
+ }
539
+ QScrollBar::add-page, QScrollBar::sub-page {
540
+ background: none;
541
+ }
542
+ QAbstractScrollArea::corner {
543
+ background: $list_bg;
544
+ border: none;
545
+ }
546
+ QProgressBar {
547
+ background: $progress_track;
548
+ border: none;
549
+ border-radius: 3px;
550
+ min-height: 8px;
551
+ max-height: 8px;
552
+ }
553
+ QProgressBar::chunk {
554
+ background: $primary;
555
+ border-radius: 3px;
556
+ }
557
+ QToolTip {
558
+ background: $field;
559
+ color: $text;
560
+ border: 1px solid $border_strong;
561
+ }
562
+ QToolButton#themeToggle {
563
+ background: $toggle_bg;
564
+ border: 2px solid $toggle_bg;
565
+ border-radius: 22px;
566
+ padding: 0;
567
+ }
568
+ QToolButton#themeToggle:hover {
569
+ background: $toggle_hover;
570
+ border-color: $toggle_hover;
571
+ }
572
+ QToolButton#themeToggle:focus {
573
+ border-color: $primary;
574
+ }
575
+ """)
576
+
577
+
578
+ def _icon_directory() -> Path:
579
+ location = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.CacheLocation)
580
+ base = Path(location) if location else Path(tempfile.gettempdir()) / "font_converter"
581
+ return base / "icons"
582
+
583
+
584
+ def _render_mark(name: str, color: str, scale: int, path: Path) -> None:
585
+ size = 17 * scale
586
+ image = QImage(size, size, QImage.Format.Format_ARGB32_Premultiplied)
587
+ image.fill(Qt.GlobalColor.transparent)
588
+ painter = QPainter(image)
589
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
590
+ pen = QPen(QColor(color), 2.0 * scale)
591
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
592
+ pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
593
+ painter.setPen(pen)
594
+ if name == "check":
595
+ painter.drawPolyline(
596
+ [
597
+ QPointF(4.5 * scale, 8.8 * scale),
598
+ QPointF(7.4 * scale, 11.7 * scale),
599
+ QPointF(12.5 * scale, 5.5 * scale),
600
+ ]
601
+ )
602
+ elif name in ("branch_closed", "branch_open"):
603
+ points = (
604
+ [(6.5, 4.5), (10.5, 8.5), (6.5, 12.5)]
605
+ if name == "branch_closed"
606
+ else [(4.5, 6.5), (8.5, 10.5), (12.5, 6.5)]
607
+ )
608
+ painter.drawPolyline([QPointF(x * scale, y * scale) for x, y in points])
609
+ elif name in ("sort_up", "sort_down"):
610
+ top, bottom = (10.5, 6.5) if name == "sort_up" else (6.5, 10.5)
611
+ painter.drawPolyline(
612
+ [
613
+ QPointF(4.5 * scale, top * scale),
614
+ QPointF(8.5 * scale, bottom * scale),
615
+ QPointF(12.5 * scale, top * scale),
616
+ ]
617
+ )
618
+ else:
619
+ painter.drawLine(QPointF(5 * scale, 8.5 * scale), QPointF(12 * scale, 8.5 * scale))
620
+ painter.end()
621
+ # Write to a sibling file first so concurrent instances never read a partial PNG.
622
+ partial = path.with_name(f"{os.getpid()}_{path.name}")
623
+ if not image.save(str(partial)):
624
+ raise OSError(f"Could not write {partial}")
625
+ os.replace(partial, path)
626
+
627
+
628
+ def _indicator_icon(name: str, color: str) -> str:
629
+ """Return a stylesheet image value for a mark, or 'none' if it cannot be written."""
630
+ try:
631
+ directory = _icon_directory()
632
+ directory.mkdir(parents=True, exist_ok=True)
633
+ base = directory / f"{name}_{color[1:]}.png"
634
+ for scale, path in ((1, base), (2, base.with_name(f"{base.stem}@2x.png"))):
635
+ if not path.is_file() or path.stat().st_size == 0:
636
+ _render_mark(name, color, scale, path)
637
+ except OSError as error:
638
+ logger.warning("Could not create the %s indicator icon: %s", name, error)
639
+ return "none"
640
+ return f"url({base.as_posix()})"
641
+
642
+
643
+ def build_stylesheet(palette: Palette) -> str:
644
+ return _TEMPLATE.substitute(
645
+ vars(palette),
646
+ check_icon=_indicator_icon("check", palette.on_primary),
647
+ dash_icon=_indicator_icon("dash", palette.primary),
648
+ branch_closed_icon=_indicator_icon("branch_closed", palette.text),
649
+ branch_open_icon=_indicator_icon("branch_open", palette.text),
650
+ sort_up_icon=_indicator_icon("sort_up", palette.text),
651
+ sort_down_icon=_indicator_icon("sort_down", palette.text),
652
+ )
653
+
654
+
655
+ @cache
656
+ def stylesheet_for(dark: bool) -> str:
657
+ """Return the style sheet for a theme, building it on first use."""
658
+ return build_stylesheet(DARK if dark else LIGHT)