CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
display_manager.cpp
Go to the documentation of this file.
1
10
11#include <spdlog/spdlog.h>
12#include <algorithm>
13#include <QScreen>
14
16
18 : QObject(parent)
19 , m_viewport_manager(std::make_unique<ViewportManager>())
20{
21 spdlog::debug("[DisplayManager::DisplayManager]: Created");
22 initialize();
23}
24
25// =============================================================================
26// Initialization
27// =============================================================================
28
30{
31 QScreen* primary_screen { QGuiApplication::primaryScreen() };
32
33 ScreenInfo screen_info;
34 QSize viewport_default(800, 600);
35
36 if (primary_screen)
37 {
38 QSize physical { primary_screen->size() * primary_screen->devicePixelRatio() };
39 screen_info.physical_width = physical.width();
40 screen_info.physical_height = physical.height();
41 screen_info.dpr = static_cast<float>(primary_screen->devicePixelRatio());
42
43 spdlog::info("[DisplayManager::initialize]: Auto-detected screen: {}x{}, DPR: {:.2f}",
44 screen_info.physical_width, screen_info.physical_height, screen_info.dpr);
45 }
46 else
47 {
48 spdlog::warn("[DisplayManager::initialize]: No screen detected, using defaults");
49 screen_info.physical_width = 1920;
50 screen_info.physical_height = 1080;
51 screen_info.dpr = 1.0f;
52 }
53
54 initialize(viewport_default, screen_info);
55}
56
57void DisplayManager::initialize(const QSize& viewport_logical, const ScreenInfo& screen)
58{
59 // =========================================================================
60 // Validate inputs
61 // =========================================================================
62
63 if (!viewport_logical.isValid() || viewport_logical.isEmpty()) {
64 spdlog::error("[DisplayManager::initialize]: Invalid viewport size");
65 return;
66 }
67
68 if (!screen.isValid()) {
69 spdlog::error("[DisplayManager::initialize]: Invalid screen info");
70 return;
71 }
72
73 // =========================================================================
74 // Initialize viewport manager
75 // =========================================================================
76
77 m_viewport_manager->initialize(viewport_logical, screen);
78
79 // =========================================================================
80 // Log configuration
81 // =========================================================================
82
83 spdlog::info("[DisplayManager::initialize]: Initialized successfully");
84 spdlog::info("[DisplayManager::initialize]: Max downsample: {}px", m_viewport_manager->maxDownsample());
85 spdlog::info("[DisplayManager::initialize]: Plafond: {}px", m_viewport_manager->plafond());
86 spdlog::info("[DisplayManager::initialize]: Quality margin: {:.2f}", m_viewport_manager->qualityMargin());
87
88 emit initializedChanged(true);
89}
90
91bool DisplayManager::isInitialized() const noexcept
92{
93 return m_viewport_manager && m_viewport_manager->isInitialized();
94}
95
96
98{
99 if (m_rendering_item == item) {
100 spdlog::trace("[DisplayManager::setRenderingItem]: Item is already set, skipping update");
101 return;
102 }
103
104 spdlog::debug("[DisplayManager::setRenderingItem]: Setting new rendering item");
105 m_rendering_item = item;
106
107 if (m_rendering_item)
108 {
109 spdlog::debug("[DisplayManager::setRenderingItem]: Updating zoom and pan on rendering item");
110 m_rendering_item->setZoom(m_zoom);
111 m_rendering_item->setPan(m_pan);
112 } else {
113 spdlog::warn("[DisplayManager::setRenderingItem]: Rendering item is null");
114 }
115}
116
117void DisplayManager::createDisplayImage(std::unique_ptr<Core::Common::ImageRegion> source_image)
118{
119 // =========================================================================
120 // Validate input
121 // =========================================================================
122
123 if (!source_image || !source_image->isValid()) {
124 spdlog::warn("[DisplayManager::createDisplayImage]: Invalid source image");
125 return;
126 }
127
128 if (!m_rendering_item) {
129 spdlog::warn("[DisplayManager::createDisplayImage]: No rendering item");
130 return;
131 }
132
133 // =========================================================================
134 // Update downsample size from received image
135 // =========================================================================
136
137 m_downsample_size = QSize(source_image->width(), source_image->height());
138
139 // =========================================================================
140 // Calculate display scale (ratio of downsample to source)
141 // =========================================================================
142
143 if (m_source_image_size.isValid() && m_source_image_size.width() > 0) {
144 m_display_scale = static_cast<float>(m_downsample_size.width()) /
145 static_cast<float>(m_source_image_size.width());
146 }
147
148 spdlog::debug("[DisplayManager::createDisplayImage]: Creating display image: {}x{}, scale={:.3f}",
149 m_downsample_size.width(), m_downsample_size.height(),
150 m_display_scale);
151
152 // =========================================================================
153 // Transfer to rendering item
154 // =========================================================================
155
156 m_rendering_item->setImage(std::move(source_image));
157
158 // =========================================================================
159 // Emit signals
160 // =========================================================================
161
162 emit displayImageSizeChanged(m_display_image_size);
163 emit displayScaleChanged(m_display_scale);
164}
165
166void DisplayManager::updateDisplayTile(std::unique_ptr<Core::Common::ImageRegion> source_tile)
167{
168 if (!source_tile || !source_tile->isValid()) {
169 spdlog::warn("[DisplayManager::updateDisplayTile]: Invalid source tile");
170 return;
171 }
172
173 if (!m_rendering_item) {
174 spdlog::warn("[DisplayManager::updateDisplayTile]: No rendering item");
175 return;
176 }
177
178 spdlog::debug("[DisplayManager::updateDisplayTile]: Updating display {}x{}",
179 m_display_image_size.width(), m_display_image_size.height());
180
181 m_rendering_item->updateTile(std::move(source_tile));
182}
183
184void DisplayManager::setSourceImageSize(int width, int height)
185{
186 // =========================================================================
187 // Store source size
188 // =========================================================================
189
190 m_source_image_size = QSize(width, height);
191
192 spdlog::debug("[DisplayManager::setSourceImageSize]: Source image size: {}x{}", width, height);
193
194 // =========================================================================
195 // Calculate display parameters via ViewportManager
196 // =========================================================================
197
198 if (!isInitialized()) {
199 spdlog::warn("[DisplayManager::setSourceImageSize]: Not initialized, using source size as display size");
200 m_display_image_size = m_source_image_size;
201 m_downsample_size = m_source_image_size;
202 m_display_scale = 1.0f;
203 m_fit_zoom = 1.0f;
204 } else {
205 ViewportCalculation calc { m_viewport_manager->calculateDisplay(m_source_image_size) };
206
207 if (calc.isValid())
208 {
209 m_display_image_size = calc.display_size;
210 m_downsample_size = calc.downsample_size;
211 m_fit_zoom = calc.fit_zoom;
212
213 // Calculate display scale
214 if (m_source_image_size.width() > 0) {
215 m_display_scale = static_cast<float>(m_downsample_size.width()) /
216 static_cast<float>(m_source_image_size.width());
217 }
218
219 spdlog::info("[DisplayManager::setSourceImageSize]: Calculated display:");
220 spdlog::info("[DisplayManager::setSourceImageSize]: Source: {}x{}", m_source_image_size.width(), m_source_image_size.height());
221 spdlog::info("[DisplayManager::setSourceImageSize]: Display: {}x{}", m_display_image_size.width(), m_display_image_size.height());
222 spdlog::info("[DisplayManager::setSourceImageSize]: Downsample: {}x{}", m_downsample_size.width(), m_downsample_size.height());
223 spdlog::info("[DisplayManager::setSourceImageSize]: Fit zoom: {:.3f}", m_fit_zoom);
224 spdlog::info("[DisplayManager::setSourceImageSize]: Texture memory: {} MB", calc.texture_memory_mb);
225
226 // Request new downsampled image if needed
227 if (calc.needs_downsample) {
228 spdlog::debug("[DisplayManager::setSourceImageSize]: Requesting downsampled image: {}x{}",
229 m_downsample_size.width(), m_downsample_size.height());
230 emit displayImageRequest(m_downsample_size.width(), m_downsample_size.height());
231 }
232 }
233 }
234
235 // =========================================================================
236 // Emit signals
237 // =========================================================================
238
239 emit sourceImageSizeChanged(m_source_image_size);
240 emit displayImageSizeChanged(m_display_image_size);
241 emit downsampleSizeChanged(m_downsample_size);
242 emit displayScaleChanged(m_display_scale);
243}
244
246{
247 // =========================================================================
248 // Clamp to valid range
249 // =========================================================================
250
251 const float clamped_zoom { std::clamp(zoom, 0.1f, 10.0f) };
252
253 if (std::abs(m_zoom - clamped_zoom) < 0.001f) {
254 return;
255 }
256
257 spdlog::debug("[DisplayManager::setZoom]: Zoom: {:.3f} → {:.3f}", m_zoom, clamped_zoom);
258
259 m_zoom = clamped_zoom;
260 constrainPan();
261
262 // Apply to rendering item
263 if (m_rendering_item) {
264 m_rendering_item->setZoom(m_zoom);
265 }
266
267 emit zoomChanged(m_zoom);
268}
269
270void DisplayManager::setPan(const QPointF& pan)
271{
272 if (m_pan == pan) {
273 spdlog::trace("[DisplayManager::setPan]: Pan is already set to ({:.3f}, {:.3f}), skipping update", pan.x(), pan.y());
274 return;
275 }
276
277 m_pan = pan;
278 constrainPan();
279
280 if (m_rendering_item) {
281 m_rendering_item->setPan(m_pan);
282 }
283
284 emit panChanged(m_pan);
285}
286
287void DisplayManager::zoomAt(const QPointF& point, float zoom_delta)
288{
289 // =========================================================================
290 // Calculate new zoom
291 // =========================================================================
292
293 float old_zoom { m_zoom };
294 float new_zoom { std::clamp(old_zoom * zoom_delta, 0.1f, 10.0f) };
295
296 // =========================================================================
297 // Adjust pan to keep point under cursor
298 // =========================================================================
299
300 const QPointF adjusted_pan { point - (point - m_pan) * (new_zoom / old_zoom) };
301
302 m_zoom = new_zoom;
303 m_pan = adjusted_pan;
304 constrainPan();
305
306 spdlog::debug("[DisplayManager::zoomAt]: Zoom changed from {:.6f} to {:.6f}, pan adjusted to ({:.6f}, {:.6f})",
307 old_zoom, m_zoom, m_pan.x(), m_pan.y());
308
309 if (m_rendering_item) {
310 spdlog::trace("[DisplayManager::zoomAt]: Updating zoom and pan on rendering item");
311 m_rendering_item->setZoom(m_zoom);
312 m_rendering_item->setPan(m_pan);
313 }
314
315 emit zoomChanged(m_zoom);
316 emit panChanged(m_pan);
317}
318
320{
321 spdlog::debug("[DisplayManager::fitToView]: Fitting view to image");
322
323 spdlog::debug("[DisplayManager::fitToView]: m_display_image_size={}x{}, m_source_image_size={}x{}",
324 m_display_image_size.width(), m_display_image_size.height(),
325 m_source_image_size.width(), m_source_image_size.height());
326
327 spdlog::debug("[DisplayManager::fitToView]: viewport={}x{}",
328 m_viewport_manager ? m_viewport_manager->viewportSize().width() : -1,
329 m_viewport_manager ? m_viewport_manager->viewportSize().height() : -1);
330
331 bool image_valid { !m_downsample_size.isEmpty() };
332 bool viewport_manager_valid { m_viewport_manager != nullptr };
333 bool viewport_valid { m_viewport_manager && !m_viewport_manager->viewportSize().isEmpty() };
334
335 spdlog::debug("[DisplayManager::fitToView]: image_valid={}, viewport_manager_valid={}, viewport_valid={}",
336 image_valid, viewport_manager_valid, viewport_valid);
337
338 if (!image_valid || !viewport_manager_valid || !viewport_valid) {
339 spdlog::warn("[DisplayManager::fitToView]: Image size or viewport is empty, using defaults");
340 m_zoom = 1.0f;
341 m_pan = QPointF(0.0, 0.0);
342 } else {
343 // ============================================================
344 // The rendering canvas matches the size of the QML item (m_display_image_size).
345 // QML handles centering this item within the global viewport.
346 // We therefore calculate the zoom level to fill this canvas.
347 // ============================================================
348 const float zoom_x { static_cast<float>(m_display_image_size.width()) / m_downsample_size.width() };
349 const float zoom_y { static_cast<float>(m_display_image_size.height()) / m_downsample_size.height() };
350 m_zoom = std::min(zoom_x, zoom_y);
351
352 // ============================================================
353 // Calculate the pan to center the image WITHIN the canvas (m_display_image_size)
354 // ============================================================
355 const float image_displayed_width { m_downsample_size.width() * m_zoom };
356 const float image_displayed_height { m_downsample_size.height() * m_zoom };
357
358 const float pan_x { (m_display_image_size.width() - image_displayed_width) / 2.0f };
359 const float pan_y { (m_display_image_size.height() - image_displayed_height) / 2.0f };
360 m_pan = QPointF(pan_x, pan_y);
361
362 spdlog::debug("[DisplayManager::fitToView]: zoom={:.3f}, pan=({:.1f}, {:.1f})",
363 m_zoom, m_pan.x(), m_pan.y());
364 }
365
366 if (m_rendering_item)
367 {
368 spdlog::trace("[DisplayManager::fitToView]: Updating zoom and pan on rendering item");
369 m_rendering_item->setZoom(m_zoom);
370 m_rendering_item->setPan(m_pan);
371 }
372
373 emit zoomChanged(m_zoom);
374 emit panChanged(m_pan);
375}
376
378 spdlog::debug("[DisplayManager::resetView]: Resetting view");
379 fitToView();
380}
381
382
384{
385 setZoom(m_zoom * 1.2f);
386}
387
389{
390 setZoom(m_zoom / 1.2f);
391}
392
393
394// =============================================================================
395// Viewport
396// =============================================================================
397
398void DisplayManager::setViewportSize(const QSize& size) {
399 if (!size.isValid() || size.isEmpty()) {
400 spdlog::warn("[DisplayManager::setViewportSize]: Invalid viewport size");
401 return;
402 }
403
404 if (!m_viewport_manager) {
405 spdlog::error("[DisplayManager::setViewportSize]: ViewportManager not created");
406 return;
407 }
408
409
410 const QSize old_viewport { m_viewport_manager->viewportSize() };
411 const int old_max { m_viewport_manager->maxDownsample() };
412
413 m_viewport_manager->setViewportSize(size);
414
415 // Check if max downsample changed
416 const int new_max { m_viewport_manager->maxDownsample() };
417 if (new_max != old_max)
418 {
419 spdlog::debug("[DisplayManager::setViewportSize]: Max downsample changed: {} → {}", old_max, new_max);
420 emit maxDownsampleChanged(new_max);
421
422 // Recalculate display if we have a source image
423 if (m_source_image_size.isValid())
424 {
425 ViewportCalculation calc { m_viewport_manager->calculateDisplay(m_source_image_size) };
426
427 if (calc.isValid() && calc.downsample_size != m_downsample_size)
428 {
429 m_display_image_size = calc.display_size;
430 m_downsample_size = calc.downsample_size;
431 m_fit_zoom = calc.fit_zoom;
432
433 emit displayImageSizeChanged(m_display_image_size);
434 emit downsampleSizeChanged(m_downsample_size);
435 emit displayImageRequest(m_downsample_size.width(), m_downsample_size.height());
436 }
437 }
438 }
439
440 if (old_viewport != size) {
441 emit viewportSizeChanged(size);
442 }
443}
444
446{
447 if (!m_viewport_manager) {
448 spdlog::error("[DisplayManager::setQualityMargin]: ViewportManager not created");
449 return;
450 }
451
452 const int old_max { m_viewport_manager->maxDownsample() };
453 m_viewport_manager->setQualityMargin(margin);
454 const int new_max { m_viewport_manager->maxDownsample() };
455
456 if (new_max != old_max) {
457 emit maxDownsampleChanged(new_max);
458 }
459}
460// =============================================================================
461// Coordinate Mapping
462// =============================================================================
463QPointF DisplayManager::mapBackendToDisplay(int backend_x, int backend_y) const
464{
465 return QPointF(backend_x * m_display_scale, backend_y * m_display_scale);
466}
467
468QPoint DisplayManager::mapDisplayToBackend(float display_x, float display_y) const
469{
470 return QPoint(
471 static_cast<int>(display_x / m_display_scale),
472 static_cast<int>(display_y / m_display_scale)
473 );
474}
475
476
477// =============================================================================
478// Internal Methods
479// =============================================================================
480
481
482void DisplayManager::constrainPan()
483{
484 if (m_display_image_size.isEmpty()) {
485 spdlog::trace("[DisplayManager::constrainPan]: Display image size is empty, skipping constraint");
486 return;
487 }
488
489 const float visible_width { m_display_image_size.width() * m_zoom };
490 const float visible_height { m_display_image_size.height() * m_zoom };
491
492 const float max_pan_x { std::max(0.0f, (visible_width - m_display_image_size.width()) / 2.0f) };
493 const float max_pan_y { std::max(0.0f, (visible_height - m_display_image_size.height()) / 2.0f) };
494
495 const QPointF old_pan { m_pan };
496 m_pan.setX(std::clamp(static_cast<float>(m_pan.x()), -max_pan_x, max_pan_x));
497 m_pan.setY(std::clamp(static_cast<float>(m_pan.y()), -max_pan_y, max_pan_y));
498
499 if (m_pan != old_pan) {
500 spdlog::trace("[DisplayManager::constrainPan]: Pan constrained: ({:.1f}, {:.1f}) → ({:.1f}, {:.1f})",
501 old_pan.x(), old_pan.y(), m_pan.x(), m_pan.y());
502 }
503}
504
505QSize DisplayManager::calculateDisplaySize(const QSize& source_size) const
506{
507 if (!m_viewport_manager || !source_size.isValid()) {
508 return {};
509 }
510
511 ViewportCalculation calc { m_viewport_manager->calculateDisplay(source_size) };
512 return calc.display_size;
513}
514
515// =============================================================================
516// Property Getters
517// =============================================================================
518
520{
521 return m_viewport_manager ? m_viewport_manager->maxDownsample() : 2048;
522}
523
524QSize DisplayManager::viewportSize() const noexcept
525{
526 return m_viewport_manager ? m_viewport_manager->viewportSize() : QSize();
527}
528
529} // namespace CaptureMoment::UI::Display
void viewportSizeChanged(const QSize &size)
Emitted when viewport size changes.
void displayScaleChanged(float scale)
Emitted when display scale changes.
void initialize()
Initializes with auto-detection of screen properties.
void panChanged(const QPointF &pan)
Emitted when pan changes.
void updateDisplayTile(std::unique_ptr< Core::Common::ImageRegion > source_tile)
Updates a specific tile region of the display image.
void displayImageSizeChanged(const QSize &size)
Emitted when display image size changes.
void setRenderingItem(Rendering::IRenderingItemBase *item)
Assigns the rendering item that will receive image updates.
void displayImageRequest(int target_width, int target_height)
Emitted when a new downsampled image is needed.
void createDisplayImage(std::unique_ptr< Core::Common::ImageRegion > source_image)
Creates the display buffer from a downsampled source image.
Q_INVOKABLE void setQualityMargin(float margin)
Sets the quality margin for zoom headroom.
Q_INVOKABLE QPointF mapBackendToDisplay(int backend_x, int backend_y) const
Maps backend (source) coordinates to display coordinates.
Q_INVOKABLE void setSourceImageSize(int width, int height)
Sets the source image size metadata.
Q_INVOKABLE void setZoom(float zoom)
Sets the zoom level.
Q_INVOKABLE QPoint mapDisplayToBackend(float display_x, float display_y) const
Maps display coordinates to backend (source) coordinates.
bool isInitialized() const noexcept
Checks if the manager has been initialized.
void maxDownsampleChanged(int max_dim)
Emitted when max downsample changes.
Q_INVOKABLE void resetView()
Resets the view to default fit state.
void downsampleSizeChanged(const QSize &size)
Emitted when downsample size changes.
Q_INVOKABLE void setViewportSize(const QSize &size)
Sets the viewport size.
void zoomChanged(float zoom)
Emitted when zoom changes.
Q_INVOKABLE void zoomAt(const QPointF &point, float zoom_delta)
Performs a zoom centered on a specific point.
Q_INVOKABLE void zoomOut()
Zooms out by 20% (divides zoom by 1.2).
void sourceImageSizeChanged(const QSize &size)
Emitted when source image size changes.
DisplayManager(QObject *parent=nullptr)
Constructs a DisplayManager.
void initializedChanged(bool initialized)
Emitted when initialization completes.
Q_INVOKABLE void setPan(const QPointF &pan)
Sets the pan offset.
Q_INVOKABLE void zoomIn()
Zooms in by 20%.
Q_INVOKABLE void fitToView()
Fits the image to fill the viewport.
Manages viewport calculations for optimal display across all screen types.
Abstract base class for image display items managing common state (zoom, pan, dimensions).
virtual void setImage(std::unique_ptr< Core::Common::ImageRegion > image)=0
Sets the full image to be displayed.
virtual void updateTile(std::unique_ptr< Core::Common::ImageRegion > tile)=0
Updates a specific tile of the displayed image.
virtual void setPan(const QPointF &pan)=0
Sets the pan offset.
virtual void setZoom(float zoom)=0
Sets the zoom level.
Manages image display with downsampling (with OIIO), zoom, and pan.
Namespace containing Qt-specific UI components for CaptureMoment.
Physical screen characteristics.
float dpr
Device pixel ratio (density)
int physical_width
Physical screen width in pixels.
int physical_height
Physical screen height in pixels.
Result of viewport calculation for an image.
CaptureMoment — footer