Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@ All notable changes to Q1View are documented here. Releases follow [semantic ver

## [Unreleased]

---

## [2.8.0] — 2026-10-14

### Added

- Windows Viewer and Comparator: choose a shared System, Light, or Dark theme from Viewer's Options > Theme menu; Comparator follows automatically without a duplicate theme menu. Application colors and image-surround brightness change together while honoring Windows high contrast and preserving source/capture pixels, zoom/pan, ROI, comparison results, playback, and thumbnail caches. (issue #118)
- Windows Comparator: compare up to four sources against reference A, with independent A/B, A/C and A/D PSNR, SSIM, LPIPS and ROI results directly under each candidate, separate sequence graphs, and four Timeline lanes. Pair-specific frame ranges and shared reference decoding avoid repeated work. (#139)

- Windows Viewer and Comparator: choose shared System, Light or Dark colors from Viewer's Options > Theme. Comparator follows automatically, including its Timeline scrollbar. UI and image-surround colors change together, respecting Windows high contrast without changing image pixels or media state. (#118)

### Fixed

- Comparator: keep useful image space in small high-DPI windows with wrapped menus and scrollable results/graphs; prevent negative canvas heights.

- Comparator: flag unaligned frames, refresh only affected pairs after seeking/reloading, preserve other results, and reject superseded requests. (#139)

- Viewer and Comparator: eliminate bright flashes in Help/zoom menus, result controls and empty/loading views; preserve keyboard/accessibility behavior and stable menu placement.

- Windows Comparator: the Timeline scrollbar now matches the application theme, with hover and drag feedback, while retaining native keyboard and accessibility support and native rendering in Windows high contrast. Empty, still-image, and fully fitting timelines hide the scrollbar; wheel scrolling no longer applies its movement twice. (issue #118)
Native display checks passed at 100–175%; 200% layout/font calculations passed, but native 200% display testing remains unverified on this PC.

---

Expand Down Expand Up @@ -489,7 +503,8 @@ All notable changes to Q1View are documented here. Releases follow [semantic ver

---

[Unreleased]: https://github.com/chammoru/Q1View/compare/v2.7.28...HEAD
[Unreleased]: https://github.com/chammoru/Q1View/compare/v2.8.0...HEAD
[2.8.0]: https://github.com/chammoru/Q1View/compare/v2.7.28...v2.8.0
[2.7.28]: https://github.com/chammoru/Q1View/compare/v2.7.27...v2.7.28
[2.7.27]: https://github.com/chammoru/Q1View/compare/v2.7.26...v2.7.27
[2.7.26]: https://github.com/chammoru/Q1View/compare/v2.7.25...v2.7.26
Expand Down
33 changes: 33 additions & 0 deletions Comparator/AnalysisLayout.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

#include <algorithm>

struct AnalysisHeights {
int source = 0, footer = 0, graph = 0;
};

// Preserve the preferred layout when it fits. In small/high-DPI windows first
// shrink the scrollable graph, then the scrollable footer, rather than letting
// their ideal heights consume the image canvas. All inputs/results are pixels.
inline AnalysisHeights ResolveAnalysisHeights(int content, int graph, int footer,
int minimumSource, int minimumGraph, int minimumFooter, int dividers)
{
const int available=(std::max)(0,content-(std::max)(0,dividers));
AnalysisHeights result;
result.graph=(std::min)((std::max)(0,graph),available);
result.footer=(std::min)((std::max)(0,footer),available-result.graph);
int deficit=(std::max)(0,(std::min)((std::max)(0,minimumSource),available)
-(available-result.graph-result.footer));
auto shrink=[&](int& height,int floor) {
const int reduction=(std::min)(deficit,(std::max)(0,height-(std::max)(0,floor)));
height-=reduction; deficit-=reduction;
};
shrink(result.graph,minimumGraph);
shrink(result.footer,minimumFooter);
// Extremely small windows cannot fit every minimum. Keep sizes nonnegative
// and prioritize the image; normal windows retain readable panel minima.
shrink(result.graph,0);
shrink(result.footer,0);
result.source=available-result.graph-result.footer;
return result;
}
157 changes: 157 additions & 0 deletions Comparator/AnalysisScrollView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#pragma once

#include "ThemeScrollBar.h"
#include "QViewerCmn.h"
#include "Q1UiFontWin.h"
#include "Q1UiBufferedPaintWin.h"
#include <windowsx.h>

// The same themed, accessible native scrollbar used by Timeline. The child
// remains viewport anchored when content scrolls (CScrollView::ScrollWindow
// would move it, as well as any native pair-selection buttons).
class CAnalysisScrollView : public CScrollView
{
DECLARE_DYNAMIC(CAnalysisScrollView)
protected:
mutable CThemeScrollBar mAnalysisScroll;
int mContentHeight = 0;
CSize mExtentViewport = CSize(-1,-1);
void SetAnalysisExtent(int height)
{
CRect rc; GetClientRect(rc);
if (height==mContentHeight && rc.Size()==mExtentViewport) return;
mExtentViewport=rc.Size();
mContentHeight = max(0,height);
SetScrollSizes(MM_TEXT,CSize(0,mContentHeight),CSize(0,max(1,rc.Height()*9/10)),CSize(0,24));
int width=q1view::WindowsUiPixels(17,q1view::WindowsUiDpi(m_hWnd));
mAnalysisScroll.SetWindowPos(nullptr,max(0,rc.Width()-width),0,width,rc.Height(),SWP_NOZORDER|SWP_NOACTIVATE);
mAnalysisScroll.ShowWindow(height>rc.Height() ? SW_SHOWNA : SW_HIDE);
}
int ContentWidth() const
{
CRect rc; GetClientRect(rc);
return max(0,rc.Width()-(mAnalysisScroll.IsWindowVisible() ? q1view::WindowsUiPixels(17,q1view::WindowsUiDpi(m_hWnd)) : 0));
}
public:
CScrollBar* GetScrollBarCtrl(int bar) const override
{
return bar==SB_VERT && mAnalysisScroll.GetSafeHwnd() ? &mAnalysisScroll : CScrollView::GetScrollBarCtrl(bar);
}
protected:
BOOL OnScrollBy(CSize scroll, BOOL apply=TRUE) override
{
int old=GetScrollPos(SB_VERT);
int pos=int(max(0LL,min((long long)GetScrollLimit(SB_VERT),(long long)old+scroll.cy)));
if (old==pos) return FALSE;
if (apply) { SetScrollPos(SB_VERT,pos); Invalidate(FALSE); }
return TRUE;
}
virtual BOOL AnalysisMouseWheel(UINT flags,short delta,CPoint point)
{
return DoMouseWheel(flags,delta,point);
}
LRESULT WindowProc(UINT message, WPARAM w, LPARAM l) override
{
if (message==WM_CTLCOLORBTN || message==WM_CTLCOLORSTATIC) {
const HWND control=reinterpret_cast<HWND>(l);
const int id=::GetDlgCtrlID(control);
if (::GetParent(control)==m_hWnd &&
((message==WM_CTLCOLORBTN && ((id>=0x7900 && id<0x7903) || id==0x7950)) ||
(message==WM_CTLCOLORSTATIC && id==0x7903))) {
SetBkColor(reinterpret_cast<HDC>(w),Q1UI_COLOR_SURFACE_ALT);
return reinterpret_cast<LRESULT>(q1view::WindowsUiColorBrush(q1view::WindowsUiColor::SurfaceAlt));
}
}
if (message==WM_MOUSEWHEEL) {
CPoint pt(GET_X_LPARAM(l),GET_Y_LPARAM(l));
return AnalysisMouseWheel(LOWORD(w),GET_WHEEL_DELTA_WPARAM(w),pt);
}
if (message==WM_VSCROLL && (LOWORD(w)==SB_THUMBTRACK || LOWORD(w)==SB_THUMBPOSITION)) {
SCROLLINFO si={}; GetScrollInfo(SB_VERT,&si,SIF_TRACKPOS);
int pos=mAnalysisScroll.IsTrackingThumb() ? mAnalysisScroll.TrackPosition() : si.nTrackPos;
return OnScrollBy(CSize(0,pos-GetScrollPos(SB_VERT)));
}
LRESULT result=CScrollView::WindowProc(message,w,l);
if (message==WM_CREATE && result!=-1) {
ModifyStyle(WS_VSCROLL|WS_HSCROLL,WS_CLIPCHILDREN);
if (!mAnalysisScroll.Create(WS_CHILD|WS_TABSTOP|SBS_VERT,CRect(),this,0x7811)) return -1;
mAnalysisScroll.SetWindowText(_T("Analysis scroll bar"));
SetAnalysisExtent(0);
}
if (message==WM_SIZE && mAnalysisScroll.GetSafeHwnd()) SetAnalysisExtent(mContentHeight);
return result;
}
};

// A non-interactive native label for reference A, using the same footer style.
class CReferenceLabel : public CStatic
{
protected:
LRESULT WindowProc(UINT message, WPARAM w, LPARAM l) override
{
if (message==WM_ERASEBKGND && (GetStyle()&SS_TYPEMASK)==SS_OWNERDRAW) return TRUE;
return CStatic::WindowProc(message,w,l);
}
public:
CFont mFont;
void DrawItem(LPDRAWITEMSTRUCT item) override
{
q1view::WindowsUiBufferedPaint paint(item->hDC,item->rcItem,q1view::WindowsUiColor::SurfaceAlt);
CDC dc; dc.Attach(paint.Dc());
CRect rc(item->rcItem);
dc.Draw3dRect(rc,Q1UI_COLOR_BORDER_SOFT,Q1UI_COLOR_BORDER_SOFT);
q1view::EnsureWindowsUiFont(mFont,q1view::WindowsUiFontRole::Body,m_hWnd);
auto* old=dc.SelectObject(&mFont);
dc.SetBkMode(TRANSPARENT); dc.SetTextColor(Q1UI_COLOR_TEXT_MUTED);
CString text; GetWindowText(text); rc.DeflateRect(8,4);
dc.DrawText(text,rc,DT_LEFT|DT_WORDBREAK|DT_NOPREFIX);
dc.SelectObject(old); dc.Detach();
}
};

// A real Win32 button provides focus, keyboard activation, and accessible
// names; owner drawing only changes its themed presentation.
class CComparisonButton : public CButton
{
protected:
LRESULT WindowProc(UINT message, WPARAM w, LPARAM l) override
{
// Keep native focus, keyboard, accessible names and button messages.
// Only owner-drawn controls repaint the entire background themselves.
if (message==WM_ERASEBKGND && (GetStyle()&BS_TYPEMASK)==BS_OWNERDRAW) return TRUE;
return CButton::WindowProc(message,w,l);
}
public:
bool active=false;
CFont mFont;
CToolTipCtrl mInfoTip;
CString mInfoText;
void SetInfoTip(const CString& text)
{
if (mInfoText==text) return;
mInfoText=text;
if (!mInfoTip.GetSafeHwnd()) {
mInfoTip.Create(this); mInfoTip.AddTool(this,text);
mInfoTip.SetMaxTipWidth(q1view::WindowsUiPixels(560,q1view::WindowsUiDpi(m_hWnd)));
} else mInfoTip.UpdateTipText(text,this);
}
BOOL PreTranslateMessage(MSG* message) override
{
if (mInfoTip.GetSafeHwnd()) mInfoTip.RelayEvent(message);
return CButton::PreTranslateMessage(message);
}
void DrawItem(LPDRAWITEMSTRUCT item) override
{
q1view::WindowsUiBufferedPaint paint(item->hDC,item->rcItem,q1view::WindowsUiColor::SurfaceAlt);
CDC dc; dc.Attach(paint.Dc());
CRect rc(item->rcItem);
dc.Draw3dRect(rc,active ? Q1UI_COLOR_ACCENT : Q1UI_COLOR_BORDER_SOFT,active ? Q1UI_COLOR_ACCENT : Q1UI_COLOR_BORDER_SOFT);
q1view::EnsureWindowsUiFont(mFont,q1view::WindowsUiFontRole::Body,m_hWnd);
auto* old=dc.SelectObject(&mFont);
dc.SetBkMode(TRANSPARENT); dc.SetTextColor(Q1UI_COLOR_TEXT);
CString text; GetWindowText(text); rc.DeflateRect(8,4);
dc.DrawText(text,rc,DT_LEFT|DT_WORDBREAK|DT_NOPREFIX);
if (item->itemState&ODS_FOCUS) dc.DrawFocusRect(rc);
dc.SelectObject(old); dc.Detach();
}
};
3 changes: 3 additions & 0 deletions Comparator/Comparator.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ copy /Y "$(ProjectDir)..\assets\fonts\Pretendard-LICENSE.txt" "$(TargetDir)Fonts
<ClInclude Include="CustomSizeDlg.h" />
<ClInclude Include="FileChangeNotiThread.h" />
<ClInclude Include="FileScanThread.h" />
<ClInclude Include="ComparisonResults.h" />
<ClInclude Include="AnalysisLayout.h" />
<ClInclude Include="AnalysisScrollView.h" />
<ClInclude Include="LpipsEngine.h" />
<ClInclude Include="LpipsScanThread.h" />
<ClInclude Include="MlModelProvisioner.h" />
Expand Down
Loading
Loading