Skip to content

Commit 5396e5a

Browse files
committed
feat(pureref): integrate PureRef badges UI and folder open
1 parent c32fbf2 commit 5396e5a

6 files changed

Lines changed: 376 additions & 26 deletions

File tree

config_manager.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class ConfigManager:
2525
"show_osd": "True",
2626
"pureref_path": "C:/Program Files/PureRef/PureRef.exe",
2727
"pureref_filename": "reference.pur",
28+
"show_pureref_badges": "True",
29+
"show_pureref_badges_when_missing": "False",
2830
},
2931
"Paths": {
3032
"paths": "",
@@ -405,6 +407,20 @@ def get_window_state(self) -> dict:
405407
)
406408
except (ValueError, TypeError):
407409
pass
410+
if config.has_option("General", "show_pureref_badges"):
411+
try:
412+
state["show_pureref_badges"] = config.getboolean(
413+
"General", "show_pureref_badges"
414+
)
415+
except (ValueError, TypeError):
416+
pass
417+
if config.has_option("General", "show_pureref_badges_when_missing"):
418+
try:
419+
state["show_pureref_badges_when_missing"] = config.getboolean(
420+
"General", "show_pureref_badges_when_missing"
421+
)
422+
except (ValueError, TypeError):
423+
pass
408424

409425
return state
410426

@@ -487,6 +503,30 @@ def set_show_tree_lines(self, value: bool):
487503
config["Window"]["show_tree_lines"] = str(value)
488504
self._write_config(config)
489505

506+
def get_show_pureref_badges(self) -> bool:
507+
config = self._read_config()
508+
return config.getboolean("General", "show_pureref_badges", fallback=True)
509+
510+
def set_show_pureref_badges(self, value: bool):
511+
config = self._read_config()
512+
if "General" not in config:
513+
config["General"] = {}
514+
config["General"]["show_pureref_badges"] = str(value)
515+
self._write_config(config)
516+
517+
def get_show_pureref_badges_when_missing(self) -> bool:
518+
config = self._read_config()
519+
return config.getboolean(
520+
"General", "show_pureref_badges_when_missing", fallback=False
521+
)
522+
523+
def set_show_pureref_badges_when_missing(self, value: bool):
524+
config = self._read_config()
525+
if "General" not in config:
526+
config["General"] = {}
527+
config["General"]["show_pureref_badges_when_missing"] = str(value)
528+
self._write_config(config)
529+
490530
def get_raw_config(self) -> configparser.ConfigParser:
491531
"""Get the raw ConfigParser for binary path resolution or other direct access."""
492532
return self._read_config()

library.py

Lines changed: 210 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self, config, parent=None):
5151
QColor("#2980b9"), # Strong blue
5252
]
5353
self._init_style_widgets()
54+
self.pureref_manager = None
5455

5556
def _init_style_widgets(self):
5657
"""Create widgets to retrieve styles from QSS."""
@@ -167,6 +168,32 @@ def get_play_button_rect(self, rect, index=None):
167168
logging.error(f"Error in get_play_button_rect: {e}")
168169
return QRect()
169170

171+
def get_pureref_badge_rect(self, icon_rect: QRectF) -> QRectF:
172+
"""Return PureRef badge rect at top-right corner of folder icon."""
173+
badge_size = 28 # Same as play button
174+
margin = 3
175+
return QRectF(
176+
icon_rect.right() - badge_size - margin,
177+
icon_rect.top() + margin,
178+
badge_size,
179+
badge_size,
180+
)
181+
182+
def _get_folder_icon_rect(self, rect, index) -> QRectF:
183+
"""Calculate folder icon rectangle."""
184+
chain = self._get_nesting_chain(index)
185+
# Call _draw_nesting_lines with painter=None to get offset without drawing
186+
nesting_offset = self._draw_nesting_lines(None, rect, chain, index)
187+
icon_height = self.config.get("folder_row_height", 70) - 14
188+
icon_width = int(icon_height * 16 / 9)
189+
center_y = rect.center().y()
190+
return QRectF(
191+
rect.left() + nesting_offset + 5,
192+
center_y - icon_height / 2 - 2,
193+
icon_width,
194+
icon_height,
195+
)
196+
170197
def sizeHint(self, option, index):
171198
try:
172199
# import sys; sys.stderr.write(f"[SIZEHINT] row={index.row()}\n"); sys.stderr.flush()
@@ -246,6 +273,10 @@ def _draw_nesting_lines(self, painter, rect, chain, index=None):
246273

247274
spacing = max(2, indent - line_width)
248275

276+
# If painter is None, we're only calculating offset, not drawing
277+
if painter is None:
278+
return line_width + spacing
279+
249280
# Determine if this item is the last child of its immediate parent
250281
is_last_child = False
251282
if index is not None and index.isValid() and index.parent().isValid():
@@ -934,6 +965,25 @@ def get_marker_at_pos(self, rect, pos, index):
934965
def editorEvent(self, event, model, option, index):
935966
try:
936967
if event.type() == QEvent.Type.MouseButtonRelease:
968+
item_type = index.data(Qt.ItemDataRole.UserRole + 1)
969+
970+
# Handle PureRef badge click for folders
971+
if item_type == "folder" and getattr(self, "pureref_manager", None):
972+
folder_path = index.data(Qt.ItemDataRole.UserRole)
973+
root_path = index.data(Qt.ItemDataRole.UserRole + 3)
974+
if folder_path and root_path:
975+
full_folder = Path(root_path) / folder_path
976+
icon_rect = self._get_folder_icon_rect(option.rect, index)
977+
badge_rect = self.get_pureref_badge_rect(icon_rect)
978+
if badge_rect.contains(QPointF(event.pos())):
979+
tree = self.parent()
980+
if tree:
981+
window = tree.window()
982+
if hasattr(window, "open_pureref_for_folder"):
983+
window.open_pureref_for_folder(full_folder)
984+
return True
985+
986+
# Original marker logic
937987
marker = self.get_marker_at_pos(option.rect, event.pos(), index)
938988
if marker:
939989
file_path = index.data(Qt.ItemDataRole.UserRole)
@@ -1024,9 +1074,9 @@ def paint_folder(self, painter, option, index):
10241074
painter.setClipPath(clip_path)
10251075

10261076
painter.drawPixmap(target_rect, scaled_pixmap, QRectF(scaled_pixmap.rect()))
1077+
painter.restore()
10271078

10281079
# Draw border
1029-
painter.restore()
10301080
painter.setClipping(False)
10311081
painter.setPen(
10321082
self.thumbnail_border.palette().color(QPalette.ColorRole.Mid)
@@ -1098,6 +1148,8 @@ def paint_folder(self, painter, option, index):
10981148
)
10991149
painter.drawRoundedRect(icon_rect, 3, 3)
11001150

1151+
painter.restore()
1152+
11011153
# Draw Folder Progress Bar
11021154
progress = index.data(Qt.ItemDataRole.UserRole + 6)
11031155
if progress is not None:
@@ -1258,6 +1310,72 @@ def paint_folder(self, painter, option, index):
12581310

12591311
painter.drawLine(line_start_x, y_pos, option.rect.right(), y_pos)
12601312

1313+
# Draw PureRef button in top-right corner
1314+
if getattr(self, "pureref_manager", None):
1315+
folder_path = index.data(Qt.ItemDataRole.UserRole)
1316+
root_path = index.data(Qt.ItemDataRole.UserRole + 3)
1317+
if folder_path and root_path:
1318+
full_folder = Path(root_path) / folder_path
1319+
has_file = self.pureref_manager.has_pur_file(full_folder)
1320+
1321+
# Check settings: show badges and show when missing
1322+
show_badges = self.config.get("show_pureref_badges", True)
1323+
show_when_missing = self.config.get(
1324+
"show_pureref_badges_when_missing", False
1325+
)
1326+
1327+
# Logic: if show_badges is False, don't show at all
1328+
# If show_badges is True, show only if file exists OR show_when_missing is True
1329+
should_show = show_badges and (has_file or show_when_missing)
1330+
1331+
if should_show:
1332+
badge_rect = self.get_pureref_badge_rect(icon_rect)
1333+
is_open = self.pureref_manager.is_running(full_folder)
1334+
1335+
is_over_badge = False
1336+
if self.mouse_pos and badge_rect.contains(QPointF(self.mouse_pos)):
1337+
is_over_badge = True
1338+
1339+
painter.save()
1340+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
1341+
1342+
# Draw button background (Play button style, but always fully visible)
1343+
bg_color = self.row_play_button_bg.palette().color(
1344+
QPalette.ColorRole.Window
1345+
)
1346+
if is_over_badge:
1347+
painter.setBrush(bg_color.lighter(110))
1348+
else:
1349+
painter.setBrush(bg_color)
1350+
1351+
painter.setPen(Qt.PenStyle.NoPen)
1352+
painter.drawRoundedRect(badge_rect, 3, 3)
1353+
1354+
# Draw "P" letter
1355+
painter.setPen(QColor("#ffffff"))
1356+
font = painter.font()
1357+
font.setPixelSize(14)
1358+
font.setBold(True)
1359+
painter.setFont(font)
1360+
painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, "P")
1361+
1362+
# Draw status dot
1363+
if not has_file:
1364+
status_color = QColor("#e74c3c") # Red (Missing)
1365+
elif is_open:
1366+
status_color = QColor("#2ecc71") # Green (Running)
1367+
else:
1368+
status_color = QColor("#f9ca24") # Yellow (Exists)
1369+
1370+
painter.setBrush(status_color)
1371+
painter.setPen(Qt.PenStyle.NoPen)
1372+
painter.drawEllipse(
1373+
QRectF(badge_rect.right() - 8, badge_rect.top() + 2, 6, 6)
1374+
)
1375+
painter.restore()
1376+
1377+
painter.restore()
1378+
12611379
painter.restore()
12621380

12631381

@@ -1328,6 +1446,38 @@ def mouseMoveEvent(self, event):
13281446
index, self.thumbnail_frame, mouse_pos=event.pos()
13291447
)
13301448

1449+
self._update_item_rect(index)
1450+
return
1451+
elif item_type == "folder":
1452+
delegate = self.itemDelegate()
1453+
if isinstance(delegate, VideoItemDelegate):
1454+
if index != self.current_hover_index:
1455+
prev_index = self.current_hover_index
1456+
self.current_hover_index = index
1457+
self.hover_timer.stop()
1458+
delegate.set_hovered_index(index, 0, mouse_pos=event.pos())
1459+
if prev_index is not None and prev_index.isValid():
1460+
self._update_item_rect(prev_index)
1461+
else:
1462+
delegate.set_hovered_index(index, 0, mouse_pos=event.pos())
1463+
1464+
visual_rect = self.visualRect(index)
1465+
if not visual_rect.isNull():
1466+
icon_rect = delegate._get_folder_icon_rect(
1467+
visual_rect, index
1468+
)
1469+
badge_rect = delegate.get_pureref_badge_rect(icon_rect)
1470+
1471+
# Check if mouse is over icon or badge
1472+
if icon_rect.contains(
1473+
QPointF(event.pos())
1474+
) or badge_rect.contains(QPointF(event.pos())):
1475+
self.viewport().setCursor(
1476+
Qt.CursorShape.PointingHandCursor
1477+
)
1478+
else:
1479+
self.viewport().setCursor(Qt.CursorShape.ArrowCursor)
1480+
13311481
self._update_item_rect(index)
13321482
return
13331483
else:
@@ -1346,24 +1496,65 @@ def mousePressEvent(self, event):
13461496
if index.isValid():
13471497
delegate = self.itemDelegate()
13481498
if isinstance(delegate, VideoItemDelegate):
1349-
# Check hover over play button
1350-
play_rect = delegate.get_play_button_rect(self.visualRect(index), index)
1351-
1352-
if play_rect.contains(event.pos()):
1353-
item = self.itemFromIndex(index)
1354-
if item:
1355-
file_path = item.data(0, Qt.ItemDataRole.UserRole)
1356-
# If already playing video - toggle pause
1357-
main_window = self.window()
1358-
delegate = self.itemDelegate()
1359-
if delegate.playing_path == file_path:
1360-
if hasattr(main_window, "video_player"):
1361-
main_window.video_player.play_pause()
1362-
else:
1363-
# Otherwise start new
1364-
if hasattr(main_window, "play_video_in_player"):
1365-
main_window.play_video_in_player(item, resume=True)
1366-
return # Stop processing to avoid standard row selection
1499+
item_type = index.data(Qt.ItemDataRole.UserRole + 1)
1500+
1501+
# Handle video play button clicks
1502+
if item_type == "video":
1503+
play_rect = delegate.get_play_button_rect(
1504+
self.visualRect(index), index
1505+
)
1506+
if play_rect.contains(event.pos()):
1507+
item = self.itemFromIndex(index)
1508+
if item:
1509+
file_path = item.data(0, Qt.ItemDataRole.UserRole)
1510+
# If already playing video - toggle pause
1511+
main_window = self.window()
1512+
delegate = self.itemDelegate()
1513+
if delegate.playing_path == file_path:
1514+
if hasattr(main_window, "video_player"):
1515+
main_window.video_player.play_pause()
1516+
else:
1517+
# Otherwise start new
1518+
if hasattr(main_window, "play_video_in_player"):
1519+
main_window.play_video_in_player(item, resume=True)
1520+
return # Stop processing to avoid standard row selection
1521+
1522+
# Handle folder icon/badge clicks
1523+
elif item_type == "folder":
1524+
visual_rect = self.visualRect(index)
1525+
if not visual_rect.isNull():
1526+
icon_rect = delegate._get_folder_icon_rect(visual_rect, index)
1527+
1528+
# Check if PureRef badges should be shown
1529+
show_badges = delegate.config.get("show_pureref_badges", True)
1530+
show_when_missing = delegate.config.get(
1531+
"show_pureref_badges_when_missing", False
1532+
)
1533+
1534+
if show_badges and getattr(delegate, "pureref_manager", None):
1535+
folder_path = index.data(Qt.ItemDataRole.UserRole)
1536+
root_path = index.data(Qt.ItemDataRole.UserRole + 3)
1537+
if folder_path and root_path:
1538+
full_folder = Path(root_path) / folder_path
1539+
has_file = delegate.pureref_manager.has_pur_file(
1540+
full_folder
1541+
)
1542+
should_show_badge = has_file or show_when_missing
1543+
1544+
if should_show_badge:
1545+
badge_rect = delegate.get_pureref_badge_rect(
1546+
icon_rect
1547+
)
1548+
# If click is on icon or badge, open PureRef
1549+
if icon_rect.contains(
1550+
QPointF(event.pos())
1551+
) or badge_rect.contains(QPointF(event.pos())):
1552+
item = self.itemFromIndex(index)
1553+
if item:
1554+
main_window = self.window()
1555+
if hasattr(main_window, "open_pureref"):
1556+
main_window.open_pureref(item)
1557+
return # Stop processing to avoid standard row selection
13671558

13681559
super().mousePressEvent(event)
13691560

0 commit comments

Comments
 (0)