Skip to content

Commit 6e9a4cc

Browse files
committed
Coverage nav UI: prev/next shortcuts, settings dropdown, layout polish
1 parent 9749248 commit 6e9a4cc

File tree

6 files changed

+549
-48
lines changed

6 files changed

+549
-48
lines changed

gcovr-templates/html/base.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@
104104
<svg class="icon-moon" viewBox="0 0 16 16" fill="currentColor"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/></svg>
105105
<svg class="icon-sun" viewBox="0 0 16 16" fill="currentColor"><path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/></svg>
106106
</button>
107+
<button class="settings-btn" id="settings-btn" title="Settings">
108+
<svg viewBox="0 0 16 16" width="18" height="18" fill="currentColor"><path d="M8 4.754a3.246 3.246 0 100 6.492 3.246 3.246 0 000-6.492zM5.754 8a2.246 2.246 0 114.492 0 2.246 2.246 0 01-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 01-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 01-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 01.52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 011.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 011.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 01.52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 01-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 01-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 002.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 001.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 00-1.115 2.693l.16.291c.415.764-.421 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 00-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 00-2.692-1.115l-.292.16c-.764.415-1.6-.421-1.184-1.185l.159-.291A1.873 1.873 0 001.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 003.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 002.692-1.115l.094-.319z"/></svg>
109+
</button>
110+
<div class="settings-dropdown" id="settings-dropdown"></div>
107111
</header>
108112

109113
<div class="content-wrapper">

gcovr-templates/html/gcovr.js

Lines changed: 269 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
initSearch();
1616
initSorting();
1717
initToggleButtons();
18+
initCoverageNav();
1819
initTreeControls();
1920
initViewToggle();
21+
initSettingsDropdown();
2022
initTlaNavigation();
2123
initLineHighlight();
2224
initColumnToggles();
2325
initPopupResize();
26+
initFileNavTooltips();
27+
initFileNavKeys();
2428

2529
// Reveal page now that all init is done
2630
document.documentElement.classList.remove('no-transitions');
@@ -994,6 +998,12 @@
994998
const buttons = document.querySelectorAll('.button_toggle_coveredLine, .button_toggle_uncoveredLine, .button_toggle_partialCoveredLine, .button_toggle_excludedLine');
995999

9961000
buttons.forEach(function(button) {
1001+
var lineClass = button.value;
1002+
if (!document.querySelector('.' + lineClass)) {
1003+
button.disabled = true;
1004+
button.classList.remove('show_' + lineClass);
1005+
return;
1006+
}
9971007
button.addEventListener('click', function() {
9981008
const lineClass = this.value;
9991009
const showClass = 'show_' + lineClass;
@@ -1006,6 +1016,7 @@
10061016
lines.forEach(function(line) {
10071017
line.classList.toggle(showClass);
10081018
});
1019+
document.dispatchEvent(new CustomEvent('coverage-toggled'));
10091020
});
10101021
});
10111022

@@ -1023,8 +1034,102 @@
10231034
lines.forEach(function(line) {
10241035
line.classList.toggle(showClass);
10251036
});
1037+
document.dispatchEvent(new CustomEvent('coverage-toggled'));
1038+
});
1039+
});
1040+
}
1041+
1042+
// ===========================================
1043+
// Coverage Navigation (prev/next uncovered)
1044+
// ===========================================
1045+
1046+
function initCoverageNav() {
1047+
var prevBtn = document.getElementById('nav-prev');
1048+
var nextBtn = document.getElementById('nav-next');
1049+
var counter = document.getElementById('nav-counter');
1050+
1051+
if (!prevBtn || !nextBtn || !counter) return;
1052+
1053+
var gapLines = [];
1054+
var currentIndex = -1;
1055+
1056+
function collectGapLines() {
1057+
var uncovered = document.querySelectorAll('tr.uncoveredLine.show_uncoveredLine');
1058+
var partial = document.querySelectorAll('tr.partialCoveredLine.show_partialCoveredLine');
1059+
var merged = [];
1060+
var i;
1061+
for (i = 0; i < uncovered.length; i++) merged.push(uncovered[i]);
1062+
for (i = 0; i < partial.length; i++) merged.push(partial[i]);
1063+
// Sort by DOM order
1064+
merged.sort(function(a, b) {
1065+
var pos = a.compareDocumentPosition(b);
1066+
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
1067+
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
1068+
return 0;
10261069
});
1070+
gapLines = merged;
1071+
currentIndex = -1;
1072+
updateCounter();
1073+
}
1074+
1075+
function updateCounter() {
1076+
if (gapLines.length === 0) {
1077+
counter.textContent = 'All lines covered';
1078+
prevBtn.disabled = true;
1079+
nextBtn.disabled = true;
1080+
} else {
1081+
var display = currentIndex >= 0 ? (currentIndex + 1) : 0;
1082+
counter.textContent = display + ' / ' + gapLines.length;
1083+
prevBtn.disabled = false;
1084+
nextBtn.disabled = false;
1085+
}
1086+
}
1087+
1088+
function navigateTo(index) {
1089+
if (gapLines.length === 0) return;
1090+
// Remove previous highlight
1091+
var prev = document.querySelector('tr.source-line.nav-highlight');
1092+
if (prev) prev.classList.remove('nav-highlight');
1093+
1094+
currentIndex = index;
1095+
var row = gapLines[currentIndex];
1096+
row.scrollIntoView({ block: 'center', behavior: 'instant' });
1097+
row.classList.add('nav-highlight');
1098+
setTimeout(function() {
1099+
row.classList.remove('nav-highlight');
1100+
}, 1500);
1101+
updateCounter();
1102+
}
1103+
1104+
function nextGap() {
1105+
if (gapLines.length === 0) return;
1106+
var next = currentIndex + 1;
1107+
if (next >= gapLines.length) next = 0;
1108+
navigateTo(next);
1109+
}
1110+
1111+
function prevGap() {
1112+
if (gapLines.length === 0) return;
1113+
var prev = currentIndex - 1;
1114+
if (prev < 0) prev = gapLines.length - 1;
1115+
navigateTo(prev);
1116+
}
1117+
1118+
prevBtn.addEventListener('click', prevGap);
1119+
nextBtn.addEventListener('click', nextGap);
1120+
1121+
document.addEventListener('keydown', function(e) {
1122+
var tag = (e.target.tagName || '').toLowerCase();
1123+
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
1124+
if (e.key === 'n') nextGap();
1125+
if (e.key === 'p') prevGap();
1126+
});
1127+
1128+
document.addEventListener('coverage-toggled', function() {
1129+
collectGapLines();
10271130
});
1131+
1132+
collectGapLines();
10281133
}
10291134

10301135
// ===========================================
@@ -1274,6 +1379,79 @@
12741379
}
12751380
}
12761381

1382+
// ===========================================
1383+
// Settings Dropdown (mobile gear icon)
1384+
// ===========================================
1385+
1386+
function initSettingsDropdown() {
1387+
var btn = document.getElementById('settings-btn');
1388+
var dropdown = document.getElementById('settings-dropdown');
1389+
var header = document.querySelector('.main-header');
1390+
if (!btn || !dropdown || !header) return;
1391+
1392+
var viewToggle = document.getElementById('view-toggle');
1393+
var themeToggle = document.getElementById('theme-toggle');
1394+
var isMobile = false;
1395+
1396+
// Reference node: settings-btn, so we can insert before it when moving back
1397+
function moveToDropdown() {
1398+
if (viewToggle && viewToggle.parentNode !== dropdown) {
1399+
dropdown.appendChild(viewToggle);
1400+
}
1401+
if (themeToggle && themeToggle.parentNode !== dropdown) {
1402+
dropdown.appendChild(themeToggle);
1403+
}
1404+
}
1405+
1406+
function moveToHeader() {
1407+
// Insert before settings-btn so they appear in original order
1408+
if (viewToggle && viewToggle.parentNode !== header) {
1409+
header.insertBefore(viewToggle, btn);
1410+
}
1411+
if (themeToggle && themeToggle.parentNode !== header) {
1412+
header.insertBefore(themeToggle, btn);
1413+
}
1414+
}
1415+
1416+
function checkBreakpoint() {
1417+
var nowMobile = window.innerWidth <= 1024;
1418+
if (nowMobile === isMobile) return;
1419+
isMobile = nowMobile;
1420+
if (isMobile) {
1421+
moveToDropdown();
1422+
} else {
1423+
dropdown.classList.remove('open');
1424+
moveToHeader();
1425+
}
1426+
}
1427+
1428+
// Toggle dropdown on button click
1429+
btn.addEventListener('click', function(e) {
1430+
e.stopPropagation();
1431+
dropdown.classList.toggle('open');
1432+
});
1433+
1434+
// Close on outside click
1435+
document.addEventListener('click', function(e) {
1436+
if (!dropdown.contains(e.target) && e.target !== btn) {
1437+
dropdown.classList.remove('open');
1438+
}
1439+
});
1440+
1441+
// Close on Escape
1442+
document.addEventListener('keydown', function(e) {
1443+
if (e.key === 'Escape') {
1444+
dropdown.classList.remove('open');
1445+
}
1446+
});
1447+
1448+
// Respond to resize
1449+
window.addEventListener('resize', checkBreakpoint);
1450+
1451+
// Initial check
1452+
checkBreakpoint();
1453+
}
1454+
12771455
// ===========================================
12781456
// Popup Resize (only when overflowing)
12791457
// ===========================================
@@ -1331,11 +1509,42 @@
13311509
var idx = fileLinks.indexOf(currentPage);
13321510
if (idx === -1) return;
13331511

1334-
var prev = idx > 0 ? fileLinks[idx - 1] : 'index.html';
1335-
var next = idx < fileLinks.length - 1 ? fileLinks[idx + 1] : 'index.html';
1512+
var prev = idx > 0 ? fileLinks[idx - 1] : null;
1513+
var next = idx < fileLinks.length - 1 ? fileLinks[idx + 1] : null;
13361514

1337-
navPrevs.forEach(function(el) { el.setAttribute('href', prev); });
1338-
navNexts.forEach(function(el) { el.setAttribute('href', next); });
1515+
function updateNavLinks(els, href) {
1516+
for (var i = 0; i < els.length; i++) {
1517+
var el = els[i];
1518+
if (href) {
1519+
// Enable: ensure it's an <a> with the correct href
1520+
if (el.tagName === 'A') {
1521+
el.setAttribute('href', href);
1522+
} else {
1523+
// Replace disabled <span> with an <a>
1524+
var a = document.createElement('a');
1525+
a.className = el.className.replace(/\bdisabled\b/, '').trim();
1526+
a.href = href;
1527+
a.title = el.title;
1528+
a.innerHTML = el.innerHTML;
1529+
el.parentNode.replaceChild(a, el);
1530+
}
1531+
} else {
1532+
// Disable: ensure it's a <span> with disabled class
1533+
if (el.tagName === 'A') {
1534+
var span = document.createElement('span');
1535+
span.className = el.className + ' disabled';
1536+
span.title = el.title;
1537+
span.innerHTML = el.innerHTML;
1538+
el.parentNode.replaceChild(span, el);
1539+
} else {
1540+
el.classList.add('disabled');
1541+
}
1542+
}
1543+
}
1544+
}
1545+
1546+
updateNavLinks(navPrevs, prev);
1547+
updateNavLinks(navNexts, next);
13391548
}
13401549

13411550
// ===========================================
@@ -1550,6 +1759,62 @@
15501759
});
15511760
}
15521761

1762+
// ===========================================
1763+
// File nav keyboard shortcuts ([ and ])
1764+
// ===========================================
1765+
1766+
function initFileNavKeys() {
1767+
var prevLink = document.querySelector('.source-nav-links .nav-prev') || document.querySelector('.nav-links .nav-prev');
1768+
var nextLink = document.querySelector('.source-nav-links .nav-next') || document.querySelector('.nav-links .nav-next');
1769+
if (!prevLink && !nextLink) return;
1770+
1771+
document.addEventListener('keydown', function(e) {
1772+
var tag = (e.target.tagName || '').toLowerCase();
1773+
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
1774+
// Re-query to pick up any DOM replacements by initNavOverride
1775+
var prev = document.querySelector('.source-nav-links a.nav-prev') || document.querySelector('.nav-links a.nav-prev');
1776+
var next = document.querySelector('.source-nav-links a.nav-next') || document.querySelector('.nav-links a.nav-next');
1777+
if (e.key === '[' && prev) {
1778+
window.location.href = prev.href;
1779+
}
1780+
if (e.key === ']' && next) {
1781+
window.location.href = next.href;
1782+
}
1783+
});
1784+
}
1785+
1786+
// ===========================================
1787+
// Enrich file nav tooltips with actual filenames
1788+
// ===========================================
1789+
1790+
function initFileNavTooltips() {
1791+
if (!window.GCOVR_TREE_DATA) return;
1792+
var links = document.querySelectorAll('.source-nav-links .nav-prev, .source-nav-links .nav-next, .nav-links .nav-prev, .nav-links .nav-next');
1793+
for (var i = 0; i < links.length; i++) {
1794+
var anchor = links[i];
1795+
var href = anchor.getAttribute('href');
1796+
if (!href || href === '#') continue;
1797+
var filename = href.replace(/^.*\//, '').replace(/#.*$/, '');
1798+
var node = findNodeInTree(window.GCOVR_TREE_DATA, filename);
1799+
if (node && node.name) {
1800+
var direction = anchor.classList.contains('nav-prev') ? 'Previous' : 'Next';
1801+
anchor.title = direction + ': ' + node.name;
1802+
}
1803+
}
1804+
}
1805+
1806+
function findNodeInTree(nodes, targetLink) {
1807+
for (var i = 0; i < nodes.length; i++) {
1808+
var node = nodes[i];
1809+
if (node.link === targetLink) return node;
1810+
if (node.children) {
1811+
var found = findNodeInTree(node.children, targetLink);
1812+
if (found) return found;
1813+
}
1814+
}
1815+
return null;
1816+
}
1817+
15531818
// ===========================================
15541819
// Prefetch pages on hover for instant nav
15551820
// ===========================================

0 commit comments

Comments
 (0)