Skip to content
Open
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
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ set(QUICKVIEW_SOURCES
QuickView/GeekIconRenderer.cpp
QuickView/GeekIconData.cpp
QuickView/exif.cpp
QuickView/RatingMetadata.cpp
QuickView/RatingStore.cpp
QuickView/RatingWriter.cpp
QuickView/RenderEngine.cpp
QuickView/ImageLoader.cpp
QuickView/MiniTiff.cpp
Expand Down Expand Up @@ -392,6 +395,7 @@ add_executable(QuickViewTests
tests/SvgNeedsFallbackTests.cpp
tests/MetafileCodecTests.cpp
tests/GeekWidgetsTests.cpp
tests/RatingMetadataTests.cpp
QuickView/GeekWidgets.cpp
QuickView/GeekIconLibrary.cpp
QuickView/GeekIconData.cpp
Expand All @@ -406,6 +410,7 @@ add_executable(QuickViewTests
QuickView/FileNavigator.cpp
QuickView/ArchiveVFS.cpp
QuickView/exif.cpp
QuickView/RatingMetadata.cpp
QuickView/QuickViewETW.cpp
QuickView/pch.cpp
)
Expand Down
60 changes: 44 additions & 16 deletions QuickView/AppStrings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1273,8 +1273,8 @@ static const LanguageTable Table_EN = {
L"Flip Horizontal\tH", // Context_FlipH
L"Flip Vertical\tV", // Context_FlipV
L"Transform", // Context_Transform
L"Actual Size (100%)\t1 / Z", // Context_ActualSize
L"Fit to Screen\t0 / F", // Context_FitToScreen
L"Actual Size (100%)\tZ", // Context_ActualSize
L"Fit to Screen\tF", // Context_FitToScreen
L"Fit Window", // Context_FitWindow
L"Fill Window", // Context_FillWindow
L"Zoom In\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -1849,8 +1849,8 @@ static const LanguageTable Table_CN = {
L"水平翻转\tH", // Context_FlipH
L"垂直翻转\tV", // Context_FlipV
L"变换", // Context_Transform
L"实际大小 (100%)\t1 / Z", // Context_ActualSize
L"适应屏幕\t0 / F", // Context_FitToScreen
L"实际大小 (100%)\tZ", // Context_ActualSize
L"适应屏幕\tF", // Context_FitToScreen
L"适应窗口", // Context_FitWindow
L"填充窗口", // Context_FillWindow
L"放大\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -2425,8 +2425,8 @@ static const LanguageTable Table_TW = {
L"水平翻轉\tH", // Context_FlipH
L"垂直翻轉\tV", // Context_FlipV
L"變換", // Context_Transform
L"實際大小 (100%)\t1 / Z", // Context_ActualSize
L"適應螢幕\t0 / F", // Context_FitToScreen
L"實際大小 (100%)\tZ", // Context_ActualSize
L"適應螢幕\tF", // Context_FitToScreen
L"適應視窗", // Context_FitWindow
L"填滿視窗", // Context_FillWindow
L"放大\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -3001,8 +3001,8 @@ static const LanguageTable Table_JA = {
L"左右反転\tH", // Context_FlipH
L"上下反転\tV", // Context_FlipV
L"変形", // Context_Transform
L"原寸大 (100%)\t1 / Z", // Context_ActualSize
L"画面に合わせる\t0 / F", // Context_FitToScreen
L"原寸大 (100%)\tZ", // Context_ActualSize
L"画面に合わせる\tF", // Context_FitToScreen
L"ウィンドウに合わせる", // Context_FitWindow
L"ウィンドウを埋める", // Context_FillWindow
L"拡大\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -3577,8 +3577,8 @@ static const LanguageTable Table_RU = {
L"Отразить по горизонтали\tH", // Context_FlipH
L"Отразить по вертикали\tV", // Context_FlipV
L"Преобразовать", // Context_Transform
L"Настоящий размер (100%)\t1 / Z", // Context_ActualSize
L"По размеру экрана\t0 / F", // Context_FitToScreen
L"Настоящий размер (100%)\tZ", // Context_ActualSize
L"По размеру экрана\tF", // Context_FitToScreen
L"По размеру окна", // Context_FitWindow
L"Заполнить окно", // Context_FillWindow
L"Увеличить\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -4153,8 +4153,8 @@ static const LanguageTable Table_DE = {
L"Horizontal spiegeln\tH", // Context_FlipH
L"Vertikal spiegeln\tV", // Context_FlipV
L"Transformieren", // Context_Transform
L"Originalgröße (100%)\t1 / Z", // Context_ActualSize
L"An Bildschirm anpassen\t0 / F", // Context_FitToScreen
L"Originalgröße (100%)\tZ", // Context_ActualSize
L"An Bildschirm anpassen\tF", // Context_FitToScreen
L"An Fenster anpassen", // Context_FitWindow
L"Fenster ausfüllen", // Context_FillWindow
L"Vergrößern\t+ / Strg +", // Context_ZoomIn
Expand Down Expand Up @@ -4729,8 +4729,8 @@ static const LanguageTable Table_ES = {
L"Voltear horizontal\tH", // Context_FlipH
L"Voltear vertical\tV", // Context_FlipV
L"Transformar", // Context_Transform
L"Tamaño real (100%)\t1 / Z", // Context_ActualSize
L"Ajustar a pantalla\t0 / F", // Context_FitToScreen
L"Tamaño real (100%)\tZ", // Context_ActualSize
L"Ajustar a pantalla\tF", // Context_FitToScreen
L"Ajustar a ventana", // Context_FitWindow
L"Rellenar ventana", // Context_FillWindow
L"Acercar\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -5305,8 +5305,8 @@ static const LanguageTable Table_FR = {
L"Flip Horizontal\tH", // Context_FlipH
L"Flip Vertical\tV", // Context_FlipV
L"Transform", // Context_Transform
L"Actual Size (100%)\t1 / Z", // Context_ActualSize
L"Fit to Screen\t0 / F", // Context_FitToScreen
L"Actual Size (100%)\tZ", // Context_ActualSize
L"Fit to Screen\tF", // Context_FitToScreen
L"Fit Window", // Context_FitWindow
L"Fill Window", // Context_FillWindow
L"Zoom In\t+ / Ctrl +", // Context_ZoomIn
Expand Down Expand Up @@ -6717,6 +6717,34 @@ std::wstring GetHotkeyActionName(HotkeyAction action) {
case HotkeyAction::RenderRaw:
raw = AppStrings::Context_RenderRAW;
break;
case HotkeyAction::Rate0:
case HotkeyAction::Rate1:
case HotkeyAction::Rate2:
case HotkeyAction::Rate3:
case HotkeyAction::Rate4:
case HotkeyAction::Rate5: {
needsCleaning = false;
const int stars = (int)action - (int)HotkeyAction::Rate0;
// Kept as one string per language rather than a table entry: these
// names appear only in the shortcut list.
static thread_local std::wstring s_rateName;
const wchar_t* pattern;
switch (GetActiveLanguage()) {
case AppStrings::Language::ChineseSimplified: pattern = stars ? L"评分 %d 星" : L"清除评分"; break;
case AppStrings::Language::ChineseTraditional: pattern = stars ? L"評分 %d 星" : L"清除評分"; break;
case AppStrings::Language::Japanese: pattern = stars ? L"評価 %d つ星" : L"評価を消去"; break;
case AppStrings::Language::Russian: pattern = stars ? L"Оценка: %d" : L"Снять оценку"; break;
case AppStrings::Language::German: pattern = stars ? L"%d Sterne" : L"Bewertung löschen"; break;
case AppStrings::Language::Spanish: pattern = stars ? L"%d estrellas" : L"Quitar valoración"; break;
case AppStrings::Language::French: pattern = stars ? L"%d étoiles" : L"Effacer la note"; break;
default: pattern = stars ? L"Rate %d Stars" : L"Clear Rating"; break;
}
wchar_t buf[64];
swprintf_s(buf, pattern, stars);
s_rateName = buf;
raw = s_rateName.c_str();
break;
}
case HotkeyAction::ComparePair: {
needsCleaning = false;
switch (GetActiveLanguage()) {
Expand Down
22 changes: 20 additions & 2 deletions QuickView/EditState.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ enum class HotkeyAction : uint8_t {
ToggleSpan, // Toggle Span Displays
ToggleSlideshow, // Toggle Slideshow Mode
RenderRaw, // Toggle RAW decode / switch to the paired RAW
Rate0, // Clear the star rating
Rate1, // Rate the photo 1..5 stars
Rate2,
Rate3,
Rate4,
Rate5,
OpenFile, // Open File Dialog
EditFile, // Edit with External Editor
RenameFile, // Rename File Dialog
Expand Down Expand Up @@ -262,6 +268,12 @@ inline std::wstring_view HotkeyActionToString(HotkeyAction action) noexcept {
case HotkeyAction::ShowInExplorer: return L"ShowInExplorer";
case HotkeyAction::ToggleCompare: return L"ToggleCompare";
case HotkeyAction::ComparePair: return L"ComparePair";
case HotkeyAction::Rate0: return L"Rate0";
case HotkeyAction::Rate1: return L"Rate1";
case HotkeyAction::Rate2: return L"Rate2";
case HotkeyAction::Rate3: return L"Rate3";
case HotkeyAction::Rate4: return L"Rate4";
case HotkeyAction::Rate5: return L"Rate5";
case HotkeyAction::AlwaysOnTop: return L"AlwaysOnTop";
case HotkeyAction::ToggleDebugHud: return L"ToggleDebugHud";
case HotkeyAction::Print: return L"Print";
Expand Down Expand Up @@ -323,6 +335,12 @@ inline HotkeyAction StringToHotkeyAction(std::wstring_view sv) noexcept {
if (sv == L"ShowInExplorer") return HotkeyAction::ShowInExplorer;
if (sv == L"ToggleCompare") return HotkeyAction::ToggleCompare;
if (sv == L"ComparePair") return HotkeyAction::ComparePair;
if (sv == L"Rate0") return HotkeyAction::Rate0;
if (sv == L"Rate1") return HotkeyAction::Rate1;
if (sv == L"Rate2") return HotkeyAction::Rate2;
if (sv == L"Rate3") return HotkeyAction::Rate3;
if (sv == L"Rate4") return HotkeyAction::Rate4;
if (sv == L"Rate5") return HotkeyAction::Rate5;
if (sv == L"AlwaysOnTop") return HotkeyAction::AlwaysOnTop;
if (sv == L"ToggleDebugHud") return HotkeyAction::ToggleDebugHud;
if (sv == L"Print") return HotkeyAction::Print;
Expand Down Expand Up @@ -728,8 +746,8 @@ struct AppConfig {
// --- Customizable Info Panel Lite ---
std::wstring InfoPanelLiteItemsNormal = L"Zoom,Progress,File,Size,Disk,Format";
std::wstring InfoPanelLiteItemsCompare = L"File,Size,Disk,Sharp,Ent,BPP,Date";
std::wstring InfoPanelFullItemsNormal = L"Histogram,File,Position,RAW,Size,Disk,Date,Camera,Exp,Lens,Focal,Profile,HDR,Flash,W.Bal,Meter,Prog,Program,Format,GPS";
std::wstring InfoPanelFullItemsCompare = L"Histogram,File,RAW,Size,Disk,Date,Camera,Exp,Lens,Focal,Profile,HDR,Flash,W.Bal,Meter,Prog,Program,Format,Sharp,Ent,BPP,GPS";
std::wstring InfoPanelFullItemsNormal = L"Histogram,File,Position,RAW,Rating,Size,Disk,Date,Camera,Exp,Lens,Focal,Profile,HDR,Flash,W.Bal,Meter,Prog,Program,Format,GPS";
std::wstring InfoPanelFullItemsCompare = L"Histogram,File,RAW,Rating,Size,Disk,Date,Camera,Exp,Lens,Focal,Profile,HDR,Flash,W.Bal,Meter,Prog,Program,Format,Sharp,Ent,BPP,GPS";
int InfoPanelScale = 0; // 0=Global, 1=100%, 2=125%, 3=150%, 4=175%, 5=200%
std::wstring InfoPanelLiteSeparator = L" \u00b7 ";

Expand Down
10 changes: 10 additions & 0 deletions QuickView/FileNavigator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,16 @@ void FileNavigator::WatcherThreadProc() {
}
if (cancelled) break;

// [Ratings] Writing a sidecar creates a file in this very folder, and
// rescanning because of our own write would be pure waste: an .xmp is
// not in the playlist, so neither the list nor the pairing can change.
// The notification carries no file name, so the only usable signal is
// that we wrote something a moment ago.
if (s_selfWriteProbe && s_selfWriteProbe()) {
if (!FindNextChangeNotification(hNotify)) break;
continue;
}

// Materialized playlists rescan; Explorer cursor just refreshes ItemCount.
if (m_playlistReady.load() || m_needInitialScan.load()) {
publishScan();
Expand Down
9 changes: 9 additions & 0 deletions QuickView/FileNavigator.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ class FileNavigator {
// only) cannot parse: RAW via LibRaw, HEIF etc. via WIC. Injected at
// startup because the unit-test binary links FileNavigator without
// LibRaw/WIC. Returns 0 when unavailable.
// [Ratings] Injected predicate: true when QuickView itself wrote a file
// in the watched folder a moment ago, so a directory-change notification
// is its own echo and rescanning would be waste. Injected rather than
// called directly to keep the navigator (and the test binary) free of the
// rating subsystem.
using SelfWriteProbe = bool (*)();
static void SetSelfWriteProbe(SelfWriteProbe probe) { s_selfWriteProbe = probe; }

using CaptureTimeFallbackReader = int64_t (*)(const wchar_t* path);
static void SetCaptureTimeFallbackReader(CaptureTimeFallbackReader reader) { s_captureTimeFallback = reader; }

Expand Down Expand Up @@ -247,6 +255,7 @@ class FileNavigator {
std::wstring m_verifyDir;
std::atomic<uint32_t> m_verifyGeneration{ 0 }; // cancels superseded runs
inline static CaptureTimeFallbackReader s_captureTimeFallback = nullptr;
inline static SelfWriteProbe s_selfWriteProbe = nullptr;
int m_currentIndex = -1;
bool m_hitEnd = false;
std::wstring m_crossFolderMessage;
Expand Down
38 changes: 38 additions & 0 deletions QuickView/GalleryOverlay.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "pch.h"
#include "AppStrings.h"
#include "GalleryOverlay.h"
#include "RatingStore.h"
#include "Toolbar.h"
#include "ThumbnailManager.h"
#include "ImageTypes.h"
Expand All @@ -16,6 +17,7 @@ extern AppConfig g_config;
extern HWND g_mainHwnd;
extern bool IsLightThemeActive();
extern float g_uiScale;
extern RatingStore g_ratingStore;
extern Toolbar g_toolbar;
extern RuntimeConfig g_runtime;
extern std::wstring& g_imagePath;
Expand Down Expand Up @@ -798,6 +800,42 @@ void GalleryOverlay::Render(ID2D1DeviceContext *pDC, const D2D1_SIZE_F &size,
m_pThumbMgr->QueueRequest(imgId, path.c_str(), prio);
}
}
// [Ratings] Filled stars in the opposite corner from the RAW badge.
// The read is queued lazily, riding the same visibility pass as the
// thumbnails; an unrated photo gets no chip, so a folder nobody has
// rated stays exactly as quiet as before.
{
const auto rating = g_ratingStore.TryGet(imgId);
if (!rating && !m_isZooming) {
const FileNavigator::PairedRaw* ratingRaw = m_pNav->GetPairedRaw(imgId);
g_ratingStore.QueueRead(imgId, path, ratingRaw ? ratingRaw->path : std::wstring());
}
if (rating && rating->stars > 0) {
std::wstring stars;
for (int sIdx = 0; sIdx < rating->stars; ++sIdx) stars += L"\u2605";

const float bw = (8.0f + 7.5f * (float)stars.length()) * g_uiScale;
const float bh = 15.0f * g_uiScale;
const float bm = 5.0f * g_uiScale;
const float br = 3.5f * g_uiScale;
D2D1_RECT_F badge = D2D1::RectF(cellRect.left + bm, cellRect.bottom - bm - bh,
cellRect.left + bm + bw, cellRect.bottom - bm);
m_brushBg->SetColor(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.55f));
m_brushBg->SetOpacity(m_transitionProgress);
pDC->FillRoundedRectangle(D2D1::RoundedRect(badge, br, br), m_brushBg.Get());

D2D1_COLOR_F prevStarTxt = m_brushText->GetColor();
// Same white-on-dark chip as the RAW badge: the star shape
// already carries the meaning, so a coloured one would only
// shout across a wall of thumbnails.
m_brushText->SetColor(D2D1::ColorF(D2D1::ColorF::White));
m_brushText->SetOpacity(m_transitionProgress * 0.95f);
pDC->DrawText(stars.c_str(), (UINT32)stars.length(), m_textFormatBadge.Get(), badge, m_brushText.Get());
m_brushText->SetColor(prevStarTxt);
m_brushText->SetOpacity(1.0f);
}
}

// [RAW+JPEG Pairing] "+CR3"-style badge: this item carries a hidden
// RAW. Theme-independent dark chip so it reads on any photo content.
if (const FileNavigator::PairedRaw* pairedRaw = m_pNav->GetPairedRaw(imgId)) {
Expand Down
5 changes: 3 additions & 2 deletions QuickView/HelpOverlay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,13 @@ void HelpOverlay::RebuildList() {
m_items.push_back({ false, L"F12", L"Debug HUD (Enable in Settings)" });

m_items.push_back({ false, L"T / Ctrl+T", m_strHudTopCombined.c_str() });
m_items.push_back({ false, L"1 / Z", AppStrings::OSD_Zoom100 });
m_items.push_back({ false, L"0 / F", AppStrings::OSD_ZoomFit });
m_items.push_back({ false, L"Z", AppStrings::OSD_Zoom100 });
m_items.push_back({ false, L"F", AppStrings::OSD_ZoomFit });
m_items.push_back({ false, L"+ (\x2191) / - (\x2193)", L"Zoom (+/- 10%)" });
m_items.push_back({ false, L"Ctrl + (+/-)", L"Zoom (+/- 1%)" });

m_items.push_back({ false, L"I / Tab", L"Info Panel (Full / Lite)" });
m_items.push_back({ false, L"Numpad 0-5", L"Star Rating (0 clears)" });
m_items.push_back({ false, L"C", AppStrings::Help_Item_Compare });
m_items.push_back({ false, L"Ctrl + F11", AppStrings::Settings_Label_SpanDisplays });

Expand Down
Loading