From 264b04508bb51bda8af4fa654278c837d5cf9e4c Mon Sep 17 00:00:00 2001 From: Raphael Dumusc Date: Fri, 26 Oct 2018 17:49:06 +0200 Subject: [PATCH] Setup min/max sizes for whiteboard and webbrowser, adapt resizing --- .gitsubprojects | 2 +- apps/Webbrowser/qml/Webengine.qml | 15 +- apps/Whiteboard/qml/whiteboard.qml | 13 +- doc/Changelog.md | 2 + tests/cpp/core/ContentControllerTests.cpp | 27 +- tests/cpp/core/PixelStreamControllerTests.cpp | 26 ++ .../core/PixelStreamWindowManagerTests.cpp | 19 +- tests/cpp/core/WindowControllerTests.cpp | 220 ++++++++---- tide/core/scene/Content.cpp | 89 +++-- tide/core/scene/Content.h | 11 +- tide/core/scene/VectorialContent.cpp | 2 +- tide/core/scene/VectorialContent.h | 8 +- tide/core/scene/ZoomHelper.cpp | 63 ++-- tide/core/scene/ZoomHelper.h | 9 +- tide/core/utils/geometry.cpp | 42 ++- tide/core/utils/geometry.h | 16 +- tide/master/CMakeLists.txt | 2 + tide/master/control/ContentController.cpp | 4 + tide/master/control/PixelStreamController.cpp | 11 +- .../control/PixelStreamWindowManager.cpp | 19 +- .../master/control/PixelStreamWindowManager.h | 3 +- tide/master/control/WindowController.cpp | 321 ++++++++---------- tide/master/control/WindowController.h | 70 ++-- .../control/WindowResizeHandlesController.cpp | 128 +++++++ .../control/WindowResizeHandlesController.h | 78 +++++ tide/master/control/ZoomController.cpp | 6 +- tide/master/layout/LineLayout.cpp | 2 +- .../master/qml/MasterDisplayGroupRenderer.cpp | 5 +- tide/master/resources/MasterWindow.qml | 8 +- 29 files changed, 827 insertions(+), 394 deletions(-) create mode 100644 tide/master/control/WindowResizeHandlesController.cpp create mode 100644 tide/master/control/WindowResizeHandlesController.h diff --git a/.gitsubprojects b/.gitsubprojects index 771e8eaa..fc27becd 100644 --- a/.gitsubprojects +++ b/.gitsubprojects @@ -1,4 +1,4 @@ # -*- mode: cmake -*- -git_subproject(Deflect https://github.com/BlueBrain/Deflect.git 4af58e7) +git_subproject(Deflect https://github.com/BlueBrain/Deflect.git 75c562f) git_subproject(Rockets https://github.com/BlueBrain/Rockets.git 7877344) git_subproject(VirtualKeyboard https://github.com/BlueBrain/QtFreeVirtualKeyboard.git d026536) diff --git a/apps/Webbrowser/qml/Webengine.qml b/apps/Webbrowser/qml/Webengine.qml index 9a901dfb..be16bbf3 100644 --- a/apps/Webbrowser/qml/Webengine.qml +++ b/apps/Webbrowser/qml/Webengine.qml @@ -1,6 +1,7 @@ -// Copyright (c) 2016, EPFL/Blue Brain Project -// Raphael Dumusc -import QtQuick 2.0 +// Copyright (c) 2016-2018, EPFL/Blue Brain Project +// Raphael Dumusc +import QtQuick 2.4 +import QtQuick.Window 2.2 import QtWebEngine 1.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.2 @@ -14,6 +15,14 @@ Item { width: 640 height: 480 + onWindowChanged: { + // size constraints for the stream window + window.minimumWidth = 640 + window.minimumHeight = 480 + window.maximumWidth = 3840 + window.maximumHeight = 2160 + } + signal addressBarTextEntered(string url) Item { diff --git a/apps/Whiteboard/qml/whiteboard.qml b/apps/Whiteboard/qml/whiteboard.qml index 006e149b..bbe4dd21 100755 --- a/apps/Whiteboard/qml/whiteboard.qml +++ b/apps/Whiteboard/qml/whiteboard.qml @@ -1,5 +1,6 @@ -// Copyright (c) 2016, EPFL/Blue Brain Project -// Pawel Podhajski +// Copyright (c) 2016-2018, EPFL/Blue Brain Project +// Pawel Podhajski +// Raphael Dumusc import QtQuick 2.4 import QtQuick.Window 2.2 @@ -14,6 +15,14 @@ Item { width: 1920 height: 1080 + onWindowChanged: { + // size constraints for the stream window + window.minimumWidth = 640 + window.minimumHeight = 480 + window.maximumWidth = 3840 + window.maximumHeight = 2160 + } + property int headerHeight: 100 property int oldWidth: width property int oldHeight: height diff --git a/doc/Changelog.md b/doc/Changelog.md index 51596815..7f6e4baa 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -3,6 +3,8 @@ Changelog {#changelog} # Release 1.5 (git master) +* [282](https://github.com/BlueBrain/Tide/pull/282): + Improve resizing of webbrowsers and whiteboards. * [279](https://github.com/BlueBrain/Tide/pull/279): Screens can be powered on/off from the web interface by clicking the screen status icon. diff --git a/tests/cpp/core/ContentControllerTests.cpp b/tests/cpp/core/ContentControllerTests.cpp index 25dda1ac..bc3a6c71 100644 --- a/tests/cpp/core/ContentControllerTests.cpp +++ b/tests/cpp/core/ContentControllerTests.cpp @@ -67,14 +67,20 @@ BOOST_AUTO_TEST_CASE(factory_method) auto& dummyContent = dynamic_cast(window.getContent()); + // Check DummyContent (used for unit testing other components) auto controller = ContentController::create(window); - BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(dynamic_cast(controller.get())); + + dummyContent.zoomable = false; + controller = ContentController::create(window); + BOOST_CHECK(!dynamic_cast(controller.get())); dummyContent.type = ContentType::pixel_stream; BOOST_CHECK_THROW(ContentController::create(window), std::bad_cast); Window streamWin(ContentFactory::getPixelStreamContent("xyz", QSize())); BOOST_CHECK_NO_THROW(controller = ContentController::create(streamWin)); BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(!dynamic_cast(controller.get())); #if TIDE_ENABLE_WEBBROWSER_SUPPORT dummyContent.type = ContentType::webbrowser; @@ -84,18 +90,23 @@ BOOST_AUTO_TEST_CASE(factory_method) StreamType::WEBBROWSER)); BOOST_CHECK_NO_THROW(controller = ContentController::create(webWindow)); BOOST_CHECK(dynamic_cast(controller.get())); -#endif - -#if TIDE_ENABLE_PDF_SUPPORT - dummyContent.type = ContentType::pdf; - controller = ContentController::create(window); - BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(!dynamic_cast(controller.get())); #endif #if TIDE_ENABLE_MOVIE_SUPPORT dummyContent.type = ContentType::movie; controller = ContentController::create(window); BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(!dynamic_cast(controller.get())); +#endif + + dummyContent.zoomable = true; + +#if TIDE_ENABLE_PDF_SUPPORT + dummyContent.type = ContentType::pdf; + controller = ContentController::create(window); + BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(dynamic_cast(controller.get())); #endif #if TIDE_USE_TIFF @@ -106,7 +117,7 @@ BOOST_AUTO_TEST_CASE(factory_method) dummyContent.type = ContentType::svg; controller = ContentController::create(window); - BOOST_CHECK(dynamic_cast(controller.get())); + BOOST_CHECK(dynamic_cast(controller.get())); dummyContent.type = ContentType::image; controller = ContentController::create(window); diff --git a/tests/cpp/core/PixelStreamControllerTests.cpp b/tests/cpp/core/PixelStreamControllerTests.cpp index f9b62461..76316afe 100644 --- a/tests/cpp/core/PixelStreamControllerTests.cpp +++ b/tests/cpp/core/PixelStreamControllerTests.cpp @@ -127,3 +127,29 @@ BOOST_FIXTURE_TEST_CASE(pan_event, PixelStreamFixture) BOOST_CHECK_EQUAL(eventPosition(), QPointF(0.5, 0.25)); BOOST_CHECK_EQUAL(eventDelta(), _normalize(delta)); } + +BOOST_FIXTURE_TEST_CASE(resize_event, PixelStreamFixture) +{ + window.setCoordinates(QRectF{QPointF(), QSizeF{100, 150}}); + BOOST_CHECK_EQUAL(event.type, deflect::Event::EVT_VIEW_SIZE_CHANGED); + BOOST_CHECK_EQUAL(eventDelta(), QPointF(100, 150)); + + window.setCoordinates(QRectF{QPointF(), QSizeF{2000, 1500}}); + BOOST_CHECK_EQUAL(eventDelta(), QPointF(2000, 1500)); + + deflect::SizeHints hints; + hints.minWidth = 200; + hints.minHeight = 200; + hints.maxWidth = 1000; + hints.maxHeight = 1000; + window.getContent().setSizeHints(hints); + + window.setCoordinates(QRectF{QPointF(), QSizeF{500, 500}}); + BOOST_CHECK_EQUAL(eventDelta(), QPointF(500, 500)); + + window.setCoordinates(QRectF{QPointF(), QSizeF{100, 150}}); + BOOST_CHECK_EQUAL(eventDelta(), QPointF(200, 300)); + + window.setCoordinates(QRectF{QPointF(), QSizeF{2000, 1500}}); + BOOST_CHECK_EQUAL(eventDelta(), QPointF(1000, 750)); +} diff --git a/tests/cpp/core/PixelStreamWindowManagerTests.cpp b/tests/cpp/core/PixelStreamWindowManagerTests.cpp index 04095da9..3291c3a8 100644 --- a/tests/cpp/core/PixelStreamWindowManagerTests.cpp +++ b/tests/cpp/core/PixelStreamWindowManagerTests.cpp @@ -240,7 +240,6 @@ BOOST_AUTO_TEST_CASE(testImplicitWindowCreation) const auto& uri = CONTENT_URI; // window will be positioned centered const auto pos = QPointF(wallSize.width() * 0.5, wallSize.height() * 0.5); - const auto size = QSize(defaultPixelStreamWindowSize); windowManager.handleStreamStart(uri); BOOST_REQUIRE(windowManager.getWindows(uri).size() == 1); @@ -254,7 +253,7 @@ BOOST_AUTO_TEST_CASE(testImplicitWindowCreation) const auto& coords = window->getCoordinates(); BOOST_CHECK_EQUAL(coords.center(), pos); - BOOST_CHECK_EQUAL(coords.size(), size); + BOOST_CHECK_EQUAL(coords.size(), defaultPixelStreamWindowSize); // Check that the window is resized to the first frame dimensions windowManager.updateStreamWindows(createTestFrame(testFrameSize)); @@ -461,6 +460,22 @@ BOOST_FIXTURE_TEST_CASE(local_streams_open_without_validation_signal, BOOST_CHECK(!windowManager.getWindows(whiteboardUri).at(0)->isHidden()); } +BOOST_FIXTURE_TEST_CASE(large_stream_is_sized_to_fit_displaygroup_when_opening, + SingleSurface) +{ + windowManager.handleStreamStart(CONTENT_URI); + const auto frame = createTestFrame(2 * wallSize); + windowManager.updateStreamWindows(frame); + + BOOST_REQUIRE(externalStreamOpened); + + const auto window = windowManager.getWindows(CONTENT_URI)[0]; + + BOOST_CHECK_EQUAL(window->getContent().getDimensions(), 2 * wallSize); + BOOST_CHECK( + scene->getGroup(0).getCoordinates().contains(window->getCoordinates())); +} + BOOST_FIXTURE_TEST_CASE( multi_channel_stream_shows_first_channel_on_single_surface, SingleSurface) { diff --git a/tests/cpp/core/WindowControllerTests.cpp b/tests/cpp/core/WindowControllerTests.cpp index 5b588897..b8170133 100644 --- a/tests/cpp/core/WindowControllerTests.cpp +++ b/tests/cpp/core/WindowControllerTests.cpp @@ -43,6 +43,7 @@ #include #include "control/WindowController.h" +#include "control/WindowResizeHandlesController.h" #include "scene/DisplayGroup.h" #include "scene/Window.h" @@ -53,8 +54,9 @@ BOOST_GLOBAL_FIXTURE(MinimalGlobalQtApp); namespace { -const QSizeF wallSize(1000, 1000); -const QSize CONTENT_SIZE(800, 600); +constexpr QSizeF wallSize(1000, 1000); +constexpr QSize CONTENT_SIZE(800, 600); +constexpr qreal maxDelta = 0.00001; const QSize BIG_CONTENT_SIZE(CONTENT_SIZE*(Content::getMaxScale() + 1)); const QSize SMALL_CONTENT_SIZE(CONTENT_SIZE / 4); const qreal CONTENT_AR = @@ -96,10 +98,10 @@ BOOST_AUTO_TEST_CASE(testResizeAndMove) controller.resize(centeredSize, WindowPoint::CENTER); - BOOST_CHECK_CLOSE(coords.center().x(), fixedCenter.x(), 0.00001); - BOOST_CHECK_CLOSE(coords.center().y(), fixedCenter.y(), 0.00001); - BOOST_CHECK_CLOSE(coords.width(), centeredSize.width(), 0.00001); - BOOST_CHECK_CLOSE(coords.height(), centeredSize.height(), 0.00001); + BOOST_CHECK_CLOSE(coords.center().x(), fixedCenter.x(), maxDelta); + BOOST_CHECK_CLOSE(coords.center().y(), fixedCenter.y(), maxDelta); + BOOST_CHECK_CLOSE(coords.width(), centeredSize.width(), maxDelta); + BOOST_CHECK_CLOSE(coords.height(), centeredSize.height(), maxDelta); } BOOST_AUTO_TEST_CASE(testScaleByPixelDelta) @@ -113,17 +115,18 @@ BOOST_AUTO_TEST_CASE(testScaleByPixelDelta) const auto pixelDelta = 40.0; controller.scale(QPointF(), pixelDelta); - BOOST_CHECK_EQUAL(coords.height(), CONTENT_SIZE.height() + pixelDelta); - BOOST_CHECK_EQUAL(coords.width(), - CONTENT_SIZE.width() + pixelDelta * CONTENT_AR); + BOOST_CHECK_CLOSE(coords.height(), CONTENT_SIZE.height() + pixelDelta, + maxDelta); + BOOST_CHECK_CLOSE(coords.width(), + CONTENT_SIZE.width() + pixelDelta * CONTENT_AR, maxDelta); BOOST_CHECK_EQUAL(coords.x(), 0); BOOST_CHECK_EQUAL(coords.y(), 0); controller.scale(coords.bottomRight(), -pixelDelta); - BOOST_CHECK_EQUAL(coords.size().width(), CONTENT_SIZE.width()); - BOOST_CHECK_EQUAL(coords.size().height(), CONTENT_SIZE.height()); - BOOST_CHECK_EQUAL(coords.y(), pixelDelta); - BOOST_CHECK_CLOSE(coords.x(), pixelDelta * CONTENT_AR, 0.00001); + BOOST_CHECK_CLOSE(coords.size().width(), CONTENT_SIZE.width(), maxDelta); + BOOST_CHECK_CLOSE(coords.size().height(), CONTENT_SIZE.height(), maxDelta); + BOOST_CHECK_CLOSE(coords.y(), pixelDelta, maxDelta); + BOOST_CHECK_CLOSE(coords.x(), pixelDelta * CONTENT_AR, maxDelta); } WindowPtr makeDummyWindow() @@ -153,6 +156,7 @@ BOOST_FIXTURE_TEST_CASE(testOneToOneSize, TestFixture) const auto& coords = window->getCoordinates(); BOOST_CHECK_EQUAL(coords.size(), CONTENT_SIZE); BOOST_CHECK_EQUAL(coords.center(), QPointF(625, 240)); + BOOST_CHECK(!window->getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testOneToOneSizeUsesPreferedSize, TestFixture) @@ -160,7 +164,11 @@ BOOST_FIXTURE_TEST_CASE(testOneToOneSizeUsesPreferedSize, TestFixture) deflect::SizeHints hints; hints.preferredWidth = CONTENT_SIZE.width() * 0.8; hints.preferredHeight = CONTENT_SIZE.height() * 1.1; + hints.maxWidth = CONTENT_SIZE.width() * 2; + hints.maxHeight = CONTENT_SIZE.height() * 2; window->getContent().setSizeHints(hints); + static_cast(window->getContent()).fixedAspectRatio = false; + static_cast(window->getContent()).zoomable = false; controller.adjustSize(SIZE_1TO1); @@ -169,6 +177,7 @@ BOOST_FIXTURE_TEST_CASE(testOneToOneSizeUsesPreferedSize, TestFixture) BOOST_CHECK_EQUAL(coords.size(), QSizeF(hints.preferredWidth, hints.preferredHeight)); BOOST_CHECK_EQUAL(coords.center(), QPointF(625, 240)); + BOOST_CHECK(!window->getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testOneToOneFittingSize, TestFixture) @@ -179,12 +188,25 @@ BOOST_FIXTURE_TEST_CASE(testOneToOneFittingSize, TestFixture) const auto& coords = window->getCoordinates(); BOOST_CHECK_EQUAL(coords.size(), CONTENT_SIZE); BOOST_CHECK_EQUAL(coords.center(), QPointF(625, 240)); + BOOST_CHECK(!window->getContent().isZoomed()); // Big content constrained to 0.9 * wallSize window->getContent().setDimensions(2 * wallSize.toSize()); controller.adjustSize(SIZE_1TO1_FITTING); BOOST_CHECK_EQUAL(coords.size(), 0.9 * wallSize); BOOST_CHECK_EQUAL(coords.center(), QPointF(625, 240)); + BOOST_CHECK(!window->getContent().isZoomed()); + + // And without zooming the content if the aspect ratio is different + const auto bigSize = + QSize{4 * wallSize.toSize().width(), 2 * wallSize.toSize().height()}; + const auto expectedSize = + QSizeF{0.9 * wallSize.width(), 0.45 * wallSize.height()}; + window->getContent().setDimensions(bigSize); + controller.adjustSize(SIZE_1TO1_FITTING); + BOOST_CHECK_EQUAL(coords.size(), expectedSize); + BOOST_CHECK_EQUAL(coords.center(), QPointF(625, 240)); + BOOST_CHECK(!window->getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testSizeLimitsBigContent, TestFixture) @@ -193,7 +215,8 @@ BOOST_FIXTURE_TEST_CASE(testSizeLimitsBigContent, TestFixture) auto& content = window->getContent(); content.setDimensions(BIG_CONTENT_SIZE); BOOST_REQUIRE_EQUAL(Content::getMaxScale(), 3.0); - BOOST_REQUIRE_EQUAL(content.getMaxDimensions(), + BOOST_REQUIRE_EQUAL(content.getMaxDimensions(), BIG_CONTENT_SIZE); + BOOST_REQUIRE_EQUAL(content.getMaxUpscaledDimensions(), BIG_CONTENT_SIZE * Content::getMaxScale()); // Test controller and zoom limits @@ -203,7 +226,7 @@ BOOST_FIXTURE_TEST_CASE(testSizeLimitsBigContent, TestFixture) BOOST_CHECK_EQUAL(controller.getMinSizeAspectRatioCorrect(), QSize(400, 300)); - const QSizeF normalMaxSize = controller.getMaxSize(); + const auto normalMaxSize = controller.getMaxSize(); window->getContent().setZoomRect( QRectF(QPointF(0.3, 0.1), QSizeF(0.25, 0.25))); BOOST_CHECK_EQUAL(controller.getMaxSize(), 0.25 * normalMaxSize); @@ -214,7 +237,8 @@ BOOST_FIXTURE_TEST_CASE(testSizeLimitsSmallContent, TestFixture) // Make a small content and validate it auto& content = window->getContent(); content.setDimensions(SMALL_CONTENT_SIZE); - BOOST_REQUIRE_EQUAL(content.getMaxDimensions(), + BOOST_REQUIRE_EQUAL(content.getMaxDimensions(), SMALL_CONTENT_SIZE); + BOOST_REQUIRE_EQUAL(content.getMaxUpscaledDimensions(), SMALL_CONTENT_SIZE * Content::getMaxScale()); BOOST_REQUIRE_EQUAL(Content::getMaxScale(), 3.0); @@ -239,10 +263,22 @@ BOOST_FIXTURE_TEST_CASE(smallContentMadeFullscreenRespectsMaxContentSize, BOOST_REQUIRE(content.getMaxDimensions() < wallSize); + window->setMode(Window::WindowMode::FULLSCREEN); controller.adjustSize(SizeState::SIZE_FULLSCREEN); + BOOST_REQUIRE(window->isFullscreen()); - BOOST_CHECK_EQUAL(window->getCoordinates().size(), - content.getMaxDimensions()); + BOOST_CHECK_EQUAL(window->getDisplayCoordinates().size(), + content.getMaxUpscaledDimensions()); + BOOST_CHECK(!window->getContent().isZoomed()); + + // Can't enlarge or reduce the window beyond its maximum size + controller.scale(window->getDisplayCoordinates().center(), 5.0); + BOOST_CHECK_EQUAL(window->getDisplayCoordinates().size(), + content.getMaxUpscaledDimensions()); + + controller.scale(window->getDisplayCoordinates().center(), -5.0); + BOOST_CHECK_EQUAL(window->getDisplayCoordinates().size(), + content.getMaxUpscaledDimensions()); } BOOST_FIXTURE_TEST_CASE(smallContentWithBigMaxSizeHintsCanBeMadeFullscreen, @@ -255,6 +291,7 @@ BOOST_FIXTURE_TEST_CASE(smallContentWithBigMaxSizeHintsCanBeMadeFullscreen, hints.maxWidth = std::numeric_limits::max(); hints.maxHeight = std::numeric_limits::max(); content.setSizeHints(hints); + static_cast(content).zoomable = false; BOOST_REQUIRE_EQUAL(content.getMaxDimensions(), QSize(std::numeric_limits::max(), @@ -264,14 +301,23 @@ BOOST_FIXTURE_TEST_CASE(smallContentWithBigMaxSizeHintsCanBeMadeFullscreen, controller.adjustSize(SizeState::SIZE_FULLSCREEN); BOOST_CHECK_EQUAL(window->getCoordinates(), QRectF(QPointF{0, 125}, QSizeF{1000, 750})); + BOOST_CHECK(!window->getContent().isZoomed()); - // Use preferred size aspect ratio + // Always enforce content aspect ratio over preferred size if it is fixed hints.preferredWidth = wallSize.width() / 2; hints.preferredHeight = wallSize.height() / 2; content.setSizeHints(hints); controller.adjustSize(SizeState::SIZE_FULLSCREEN); + BOOST_CHECK_EQUAL(window->getCoordinates(), + QRectF(QPointF{0, 125}, QSizeF{1000, 750})); + BOOST_CHECK(!window->getContent().isZoomed()); + + // But use preferred size aspect ratio if it can change freely + static_cast(window->getContent()).fixedAspectRatio = false; + controller.adjustSize(SizeState::SIZE_FULLSCREEN); BOOST_CHECK_EQUAL(window->getCoordinates(), QRectF(QPointF{0, 0}, wallSize)); + BOOST_CHECK(!window->getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testAspectRatioMinSize, TestFixture) @@ -357,20 +403,21 @@ BOOST_FIXTURE_TEST_CASE(testSizeHints, TestFixture) hints.preferredHeight = CONTENT_SIZE.height(); auto& content = window->getContent(); content.setSizeHints(hints); - content.setDimensions(CONTENT_SIZE); + static_cast(content).zoomable = false; + const auto& coords = window->getCoordinates(); - // too big, constrains to maxSize - controller.resize(maxSize * 2, CENTER); - BOOST_CHECK_EQUAL(coords.size(), maxSize); + // too big => constrains to maxSize * upscaling factor + controller.resize(maxSize * Content::getMaxScale() * 1.5, CENTER); + BOOST_CHECK_EQUAL(coords.size(), maxSize * Content::getMaxScale()); // go back to preferred size controller.adjustSize(SIZE_1TO1); BOOST_CHECK_EQUAL(coords.size(), CONTENT_SIZE); - // perfect max size - controller.resize(maxSize, CENTER); - BOOST_CHECK_EQUAL(coords.size(), maxSize); + // perfect max size * upscaling factor + controller.resize(maxSize * Content::getMaxScale(), CENTER); + BOOST_CHECK_EQUAL(coords.size(), maxSize * Content::getMaxScale()); // too small, clamped to minSize controller.resize(minSize / 2, CENTER); @@ -381,24 +428,32 @@ BOOST_FIXTURE_TEST_CASE(testSizeHints, TestFixture) window->setWidth(CONTENT_SIZE.height() * 2); controller.adjustSize(SizeState::SIZE_FULLSCREEN); _checkFullscreen(coords); + BOOST_CHECK(!window->getContent().isZoomed()); - // even if prefered size is different than content size + // but content aspect ratio is presered if preferred size is inconsitent... hints.preferredWidth = CONTENT_SIZE.width(); hints.preferredHeight = CONTENT_SIZE.width() * 2; content.setSizeHints(hints); window->setWidth(CONTENT_SIZE.width() / 4); window->setWidth(CONTENT_SIZE.height() / 2); controller.adjustSize(SizeState::SIZE_FULLSCREEN); + _checkFullscreen(coords); + BOOST_CHECK(!window->getContent().isZoomed()); + + // ...unless content aspect ratio can vary freely + static_cast(window->getContent()).fixedAspectRatio = false; + controller.adjustSize(SizeState::SIZE_FULLSCREEN); BOOST_CHECK_EQUAL(coords.x(), (wallSize.height() - coords.width()) / 2); BOOST_CHECK_EQUAL(coords.y(), 0.0); BOOST_CHECK_EQUAL(coords.width(), wallSize.height() / 2); BOOST_CHECK_EQUAL(coords.height(), wallSize.height()); + BOOST_CHECK(!window->getContent().isZoomed()); } void _checkFullscreenMax(const QRectF& coords) { // full screen maximized, centered on wall - BOOST_CHECK_CLOSE(coords.x(), -166.66666, 0.00001); + BOOST_CHECK_CLOSE(coords.x(), -166.66666, maxDelta); BOOST_CHECK_EQUAL(coords.y(), 0.0); BOOST_CHECK_EQUAL(coords.width(), wallSize.height() * CONTENT_AR); BOOST_CHECK_EQUAL(coords.height(), wallSize.height()); @@ -424,7 +479,7 @@ BOOST_FIXTURE_TEST_CASE(testFullScreenSize, ZoomedContentFixture) controller.adjustSize(SIZE_FULLSCREEN); _checkFullscreen(window.getCoordinates()); // zoom reset - BOOST_CHECK_EQUAL(window.getContent().getZoomRect(), UNIT_RECTF); + BOOST_CHECK(!window.getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testFullScreenMaxSize, ZoomedContentFixture) @@ -432,7 +487,7 @@ BOOST_FIXTURE_TEST_CASE(testFullScreenMaxSize, ZoomedContentFixture) controller.adjustSize(SIZE_FULLSCREEN_MAX); _checkFullscreenMax(window.getCoordinates()); // zoom reset - BOOST_CHECK_EQUAL(window.getContent().getZoomRect(), UNIT_RECTF); + BOOST_CHECK(!window.getContent().isZoomed()); } BOOST_FIXTURE_TEST_CASE(testToggleFullScreenMaxSize, ZoomedContentFixture) @@ -481,61 +536,84 @@ BOOST_FIXTURE_TEST_CASE(testResizeAndMoveInFullScreenMode, ZoomedContentFixture) _checkFullscreen(window.getDisplayCoordinates()); } -BOOST_AUTO_TEST_CASE(testResizeRelativeToBorder) +struct ResizeHandlesFixture { - Window window(make_dummy_content()); - auto displayGroup = DisplayGroup::create(wallSize); - WindowController controller(window, *displayGroup); + Window window{make_dummy_content()}; + DisplayGroupPtr displayGroup{DisplayGroup::create(wallSize)}; + WindowResizeHandlesController controller{window, *displayGroup}; + const QRectF originalCoords{window.getCoordinates()}; - const auto originalCoords = window.getCoordinates(); + void enableFreeResizeMode() + { + auto& content = static_cast(window.getContent()); + content.fixedAspectRatio = false; + content.zoomable = false; + BOOST_REQUIRE(window.setResizePolicy(Window::ADJUST_CONTENT)); + } +}; - controller.resizeRelative(QPointF(5, 5)); - BOOST_CHECK(window.getCoordinates() == originalCoords); +BOOST_FIXTURE_TEST_CASE(testResizeHandlesWithFixedAspectRatio, + ResizeHandlesFixture) +{ + const auto& coord = window.getCoordinates(); + + BOOST_REQUIRE(window.setResizePolicy(Window::KEEP_ASPECT_RATIO)); + + controller.resizeRelative(QPointF(5.0, 5.0)); + BOOST_CHECK(coord == originalCoords); - // These border resize conserve the window aspect ratio window.setActiveHandle(Window::TOP); - controller.resizeRelative(QPointF(5, 5)); - BOOST_CHECK_EQUAL(window.getCoordinates().top() - 5, originalCoords.top()); - BOOST_CHECK_EQUAL(window.getCoordinates().width() + 5.0 * CONTENT_AR, - originalCoords.width()); + controller.resizeRelative(QPointF(5.0, 5.0)); + BOOST_CHECK_CLOSE(coord.top(), originalCoords.top() + 5.0, maxDelta); + BOOST_CHECK_CLOSE(coord.width(), originalCoords.width() - 5.0 * CONTENT_AR, + maxDelta); window.setActiveHandle(Window::BOTTOM); - controller.resizeRelative(QPointF(2, 2)); - BOOST_CHECK_EQUAL(window.getCoordinates().bottom() - 2, - originalCoords.bottom()); - BOOST_CHECK_EQUAL(window.getCoordinates().width() + 3.0 * CONTENT_AR, - originalCoords.width()); + controller.resizeRelative(QPointF(2.0, 2.0)); + BOOST_CHECK_CLOSE(coord.bottom(), originalCoords.bottom() + 2.0, maxDelta); + BOOST_CHECK_CLOSE(coord.width(), originalCoords.width() - 3.0 * CONTENT_AR, + maxDelta); } -BOOST_AUTO_TEST_CASE(testResizeRelativeToCorner) +BOOST_FIXTURE_TEST_CASE(testResizeHandlesWithFreeAspectRatio, + ResizeHandlesFixture) { - Window window(make_dummy_content()); - auto displayGroup = DisplayGroup::create(wallSize); - WindowController controller(window, *displayGroup); - - const auto originalCoords = window.getCoordinates(); + const auto& coord = window.getCoordinates(); - // These corner resize alters the window aspect ratio - static_cast(window.getContent()).fixedAspectRatio = false; - BOOST_REQUIRE(window.setResizePolicy(Window::ADJUST_CONTENT)); + // reduce window size towards bottom-left (modifies aspect ratio) + enableFreeResizeMode(); window.setActiveHandle(Window::TOP_RIGHT); - controller.resizeRelative(QPointF(2, 10)); - BOOST_CHECK_EQUAL(window.getCoordinates().top() - 10, originalCoords.top()); - BOOST_CHECK_EQUAL(window.getCoordinates().right() - 2, - originalCoords.right()); - BOOST_CHECK_EQUAL(window.getCoordinates().height() + 10, - originalCoords.height()); - BOOST_CHECK_EQUAL(window.getCoordinates().width() - 2, - originalCoords.width()); + controller.resizeRelative(QPointF(-2.0, 10.0)); + + BOOST_CHECK_EQUAL(coord.top(), originalCoords.top() + 10.0); + BOOST_CHECK_EQUAL(coord.right(), originalCoords.right() - 2.0); + BOOST_CHECK_EQUAL(coord.height(), originalCoords.height() - 10.0); + BOOST_CHECK_EQUAL(coord.width(), originalCoords.width() - 2.0); const auto prevCoords = window.getCoordinates(); window.setActiveHandle(Window::BOTTOM_LEFT); - controller.resizeRelative(QPointF(1, 2)); - BOOST_CHECK_EQUAL(window.getCoordinates().bottom() - 2, - prevCoords.bottom()); - BOOST_CHECK_EQUAL(window.getCoordinates().left() - 1, prevCoords.left()); - BOOST_CHECK_EQUAL(window.getCoordinates().height() - 2, - prevCoords.height()); - BOOST_CHECK_EQUAL(window.getCoordinates().width() + 1, prevCoords.width()); + controller.resizeRelative(QPointF(1.0, 2.0)); + BOOST_CHECK_EQUAL(coord.bottom(), prevCoords.bottom() + 2.0); + BOOST_CHECK_EQUAL(coord.left(), prevCoords.left() + 1.0); + BOOST_CHECK_EQUAL(coord.height(), prevCoords.height() + 2.0); + BOOST_CHECK_EQUAL(coord.width(), prevCoords.width() - 1.0); +} + +BOOST_FIXTURE_TEST_CASE(testResizeHandlesKeepsAspectRatioWhenExceedingMaxSize, + ResizeHandlesFixture) +{ + const auto& coord = window.getCoordinates(); + + // enlarge towards top-right (keeps aspect ratio as it exceeds content size) + enableFreeResizeMode(); + window.setActiveHandle(Window::TOP_RIGHT); + controller.resizeRelative(QPointF(2.0, -10.0)); + + BOOST_CHECK_EQUAL(coord.top(), originalCoords.top() - 10.0); + BOOST_CHECK_CLOSE(coord.right(), originalCoords.right() + 10.0 * CONTENT_AR, + maxDelta); + BOOST_CHECK_CLOSE(coord.height(), originalCoords.height() + 10.0, maxDelta); + BOOST_CHECK_CLOSE(coord.width(), originalCoords.width() + 10.0 * CONTENT_AR, + maxDelta); } diff --git a/tide/core/scene/Content.cpp b/tide/core/scene/Content.cpp index 9ec94894..8329616a 100644 --- a/tide/core/scene/Content.cpp +++ b/tide/core/scene/Content.cpp @@ -47,9 +47,44 @@ IMPLEMENT_SERIALIZE_FOR_XML(Content) namespace { -int _clampToInt(const unsigned int n) +int _clampToInt(const uint n) { - return std::min((unsigned int)std::numeric_limits::max(), n); + return static_cast( + std::min(static_cast(std::numeric_limits::max()), n)); +} + +bool _isMinSizeSpecified(const deflect::SizeHints& hints) +{ + return hints.minWidth != deflect::SizeHints::UNSPECIFIED_SIZE && + hints.minHeight != deflect::SizeHints::UNSPECIFIED_SIZE; +} + +bool _isPreferedSizeSpecified(const deflect::SizeHints& hints) +{ + return hints.preferredWidth != deflect::SizeHints::UNSPECIFIED_SIZE && + hints.preferredHeight != deflect::SizeHints::UNSPECIFIED_SIZE; +} + +bool _isMaxSizeSpecified(const deflect::SizeHints& hints) +{ + return hints.maxWidth != deflect::SizeHints::UNSPECIFIED_SIZE && + hints.maxHeight != deflect::SizeHints::UNSPECIFIED_SIZE; +} + +QSize _getMinSize(const deflect::SizeHints& hints) +{ + return QSize{_clampToInt(hints.minWidth), _clampToInt(hints.minHeight)}; +} + +QSize _getPreferedSize(const deflect::SizeHints& hints) +{ + return QSize{_clampToInt(hints.preferredWidth), + _clampToInt(hints.preferredHeight)}; +} + +QSize _getMaxSize(const deflect::SizeHints& hints) +{ + return QSize{_clampToInt(hints.maxWidth), _clampToInt(hints.maxHeight)}; } } @@ -114,34 +149,42 @@ int Content::height() const QSize Content::getMinDimensions() const { - if (_sizeHints.minWidth == deflect::SizeHints::UNSPECIFIED_SIZE || - _sizeHints.minHeight == deflect::SizeHints::UNSPECIFIED_SIZE) - { - return UNDEFINED_SIZE; - } - return QSize(_sizeHints.minWidth, _sizeHints.minHeight); + return getSizeHintsMin(); } QSize Content::getPreferredDimensions() const { - if (_sizeHints.preferredWidth == deflect::SizeHints::UNSPECIFIED_SIZE || - _sizeHints.preferredHeight == deflect::SizeHints::UNSPECIFIED_SIZE) - { - return getDimensions(); - } - return QSize(_sizeHints.preferredWidth, _sizeHints.preferredHeight); + if (_isPreferedSizeSpecified(_sizeHints)) + return _getPreferedSize(_sizeHints); + + return getDimensions(); } QSize Content::getMaxDimensions() const { - if (_sizeHints.maxWidth == deflect::SizeHints::UNSPECIFIED_SIZE || - _sizeHints.maxHeight == deflect::SizeHints::UNSPECIFIED_SIZE) - { - return getDimensions().isValid() ? getDimensions() * getMaxScale() - : UNDEFINED_SIZE; - } - return QSize{_clampToInt(_sizeHints.maxWidth), - _clampToInt(_sizeHints.maxHeight)}; + if (_isMaxSizeSpecified(_sizeHints)) + return _getMaxSize(_sizeHints); + + const auto dimensions = getDimensions(); + return dimensions.isValid() ? dimensions : UNDEFINED_SIZE; +} + +QSizeF Content::getMaxUpscaledDimensions() const +{ + const auto dimensions = getMaxDimensions(); + return dimensions.isValid() ? dimensions * getMaxScale() : UNDEFINED_SIZE; +} + +QSize Content::getSizeHintsMin() const +{ + return _isMinSizeSpecified(_sizeHints) ? _getMinSize(_sizeHints) + : UNDEFINED_SIZE; +} + +QSize Content::getSizeHintsMax() const +{ + return _isMaxSizeSpecified(_sizeHints) ? _getMaxSize(_sizeHints) + : UNDEFINED_SIZE; } void Content::setSizeHints(const deflect::SizeHints& sizeHints) @@ -155,7 +198,7 @@ void Content::setSizeHints(const deflect::SizeHints& sizeHints) void Content::setMaxScale(const qreal value) { - if (value > 0.0) + if (value >= 1.0) _maxScale = value; } diff --git a/tide/core/scene/Content.h b/tide/core/scene/Content.h index 29e66219..93b3b4fa 100644 --- a/tide/core/scene/Content.h +++ b/tide/core/scene/Content.h @@ -124,9 +124,18 @@ class Content : public QObject /** @return the preferred dimensions, used to for 1:1 size. */ QSize getPreferredDimensions() const; - /** @return the max dimensions, used to constrain resize/scale. */ + /** @return the max content dimensions, used to constrain resize/scale. */ virtual QSize getMaxDimensions() const; + /** @return the max upscaled dimensions, used to constrain resize/scale. */ + QSizeF getMaxUpscaledDimensions() const; + + /** @return the minimum size defined by the size hints. */ + QSize getSizeHintsMin() const; + + /** @return the maximum size defined by the size hints. */ + QSize getSizeHintsMax() const; + /** Set the dimensions. */ void setDimensions(const QSize& dimensions); diff --git a/tide/core/scene/VectorialContent.cpp b/tide/core/scene/VectorialContent.cpp index 42fc194d..34e89580 100644 --- a/tide/core/scene/VectorialContent.cpp +++ b/tide/core/scene/VectorialContent.cpp @@ -63,6 +63,6 @@ qreal VectorialContent::getMaxScale() void VectorialContent::setMaxScale(const qreal value) { - if (value > 0.0) + if (value >= 1.0) _maxScale = value; } diff --git a/tide/core/scene/VectorialContent.h b/tide/core/scene/VectorialContent.h index 99f0caf4..feb9ba8d 100644 --- a/tide/core/scene/VectorialContent.h +++ b/tide/core/scene/VectorialContent.h @@ -50,19 +50,19 @@ class VectorialContent : public Content Q_OBJECT public: - /** Constructor **/ + /** Constructor. **/ VectorialContent(const QString& uri); /** @return the max dimensions, used to constrain resize/scale. */ QSize getMaxDimensions() const override; - /** @return true */ + /** @return true. */ bool canBeZoomed() const final; - /** Set the maximum factor for zoom and resize; value times base size */ + /** Set the maximum factor for zoom and resize; value times base size. */ static void setMaxScale(qreal value); - /** @return the maxium scale factor for zoom and resize */ + /** @return the maxium scale factor for zoom and resize. */ static qreal getMaxScale(); protected: diff --git a/tide/core/scene/ZoomHelper.cpp b/tide/core/scene/ZoomHelper.cpp index aae633f7..7c0a5933 100644 --- a/tide/core/scene/ZoomHelper.cpp +++ b/tide/core/scene/ZoomHelper.cpp @@ -1,6 +1,6 @@ /*********************************************************************/ -/* Copyright (c) 2015, EPFL/Blue Brain Project */ -/* Raphael Dumusc */ +/* Copyright (c) 2015-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ /* All rights reserved. */ /* */ /* Redistribution and use in source and binary forms, with or */ @@ -42,7 +42,7 @@ #include "scene/Window.h" ZoomHelper::ZoomHelper(const Window& window) - : _window(window) + : _window{window} { } @@ -53,44 +53,61 @@ QRectF ZoomHelper::getContentRect() const QRectF ZoomHelper::toContentRect(const QRectF& zoomRect) const { - const QRectF& window = _window.getDisplayCoordinates(); + const auto& window = _window.getDisplayCoordinates(); - const qreal w = window.width() / zoomRect.width(); - const qreal h = window.height() / zoomRect.height(); + const auto w = window.width() / zoomRect.width(); + const auto h = window.height() / zoomRect.height(); - const qreal posX = -zoomRect.x() * w; - const qreal posY = -zoomRect.y() * h; + const auto posX = -zoomRect.x() * w; + const auto posY = -zoomRect.y() * h; - return QRectF(posX, posY, w, h); + return QRectF{posX, posY, w, h}; } QRectF ZoomHelper::toZoomRect(const QRectF& contentRect) const { - const QRectF& window = _window.getDisplayCoordinates(); + const auto& window = _window.getDisplayCoordinates(); - const qreal w = window.width() / contentRect.width(); - const qreal h = window.height() / contentRect.height(); + const auto w = window.width() / contentRect.width(); + const auto h = window.height() / contentRect.height(); - const qreal posX = -contentRect.x() / contentRect.width(); - const qreal posY = -contentRect.y() / contentRect.height(); + const auto posX = -contentRect.x() / contentRect.width(); + const auto posY = -contentRect.y() / contentRect.height(); - return QRectF(posX, posY, w, h); + return QRectF{posX, posY, w, h}; } QRectF ZoomHelper::toTilesArea(const QRectF& windowArea, const QSize& tilesSurface) const { - const QRectF contentRect = getContentRect(); + const auto contentRect = getContentRect(); // Map window visibleArea to content space for tiles origin at (0,0) - const QRectF visibleContentArea = + const auto visibleContentArea = windowArea.translated(-contentRect.x(), -contentRect.y()); // Scale content area to tiles area size - const qreal xScale = tilesSurface.width() / contentRect.width(); - const qreal yScale = tilesSurface.height() / contentRect.height(); - const QRectF visibleTilesArea(visibleContentArea.x() * xScale, - visibleContentArea.y() * yScale, - visibleContentArea.width() * xScale, - visibleContentArea.height() * yScale); + const auto xScale = tilesSurface.width() / contentRect.width(); + const auto yScale = tilesSurface.height() / contentRect.height(); + const auto visibleTilesArea = + QRectF{visibleContentArea.x() * xScale, visibleContentArea.y() * yScale, + visibleContentArea.width() * xScale, + visibleContentArea.height() * yScale}; return visibleTilesArea; } + +QSizeF ZoomHelper::getMaxWindowSizeUpscaled() const +{ + return _applyZoom(_window.getContent().getMaxUpscaledDimensions()); +} + +QSizeF ZoomHelper::getMaxWindowSizeAtNativeResolution() const +{ + return _applyZoom(_window.getContent().getMaxDimensions()); +} + +QSizeF ZoomHelper::_applyZoom(const QSizeF& size) const +{ + const auto& zoomRect = _window.getContent().getZoomRect(); + return QSizeF{size.width() * zoomRect.width(), + size.height() * zoomRect.height()}; +} diff --git a/tide/core/scene/ZoomHelper.h b/tide/core/scene/ZoomHelper.h index 5fd711a9..7c7684e7 100644 --- a/tide/core/scene/ZoomHelper.h +++ b/tide/core/scene/ZoomHelper.h @@ -1,6 +1,6 @@ /*********************************************************************/ -/* Copyright (c) 2015, EPFL/Blue Brain Project */ -/* Raphael Dumusc */ +/* Copyright (c) 2015-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ /* All rights reserved. */ /* */ /* Redistribution and use in source and binary forms, with or */ @@ -58,7 +58,12 @@ class ZoomHelper QRectF toTilesArea(const QRectF& windowArea, const QSize& tilesSurface) const; + QSizeF getMaxWindowSizeUpscaled() const; + QSizeF getMaxWindowSizeAtNativeResolution() const; + private: + QSizeF _applyZoom(const QSizeF& size) const; + const Window& _window; }; diff --git a/tide/core/utils/geometry.cpp b/tide/core/utils/geometry.cpp index c8eeb94c..5549db34 100644 --- a/tide/core/utils/geometry.cpp +++ b/tide/core/utils/geometry.cpp @@ -1,6 +1,6 @@ /*********************************************************************/ -/* Copyright (c) 2016, EPFL/Blue Brain Project */ -/* Raphael Dumusc */ +/* Copyright (c) 2016-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ /* All rights reserved. */ /* */ /* Redistribution and use in source and binary forms, with or */ @@ -43,9 +43,10 @@ #include -QRectF geometry::resizeAroundPosition(const QRectF& rect, - const QPointF& position, - const QSizeF& size) +namespace geometry +{ +QRectF resizeAroundPosition(const QRectF& rect, const QPointF& position, + const QSizeF& size) { QTransform transform; transform.translate(position.x(), position.y()); @@ -55,7 +56,7 @@ QRectF geometry::resizeAroundPosition(const QRectF& rect, return transform.mapRect(rect); } -QRectF geometry::scaleAroundCenter(const QRectF& rect, const qreal factor) +QRectF scaleAroundCenter(const QRectF& rect, const qreal factor) { QTransform transform; transform.translate(rect.center().x(), rect.center().y()); @@ -65,14 +66,31 @@ QRectF geometry::scaleAroundCenter(const QRectF& rect, const qreal factor) return transform.mapRect(rect); } -QSizeF geometry::constrain(const QSizeF& size, const QSizeF& min, - const QSizeF& max) +QSizeF adjustAspectRatio(const QSizeF& size, const QSizeF& referenceSize) { - if (max.isValid() && (min > max || size > max)) - return size.scaled(max, Qt::KeepAspectRatio); + const auto mode = size < referenceSize ? Qt::KeepAspectRatio + : Qt::KeepAspectRatioByExpanding; + return referenceSize.scaled(size, mode); +} - if (min.isValid() && size < min) - return size.scaled(min, Qt::KeepAspectRatioByExpanding); +QSizeF constrain(const QSizeF& size, const QSizeF& min, const QSizeF& max, + const bool keepAspectRatio) +{ + if (keepAspectRatio) + { + if (size < min) + return size.scaled(min, Qt::KeepAspectRatioByExpanding); + if (max.isValid() && size > max) + return size.scaled(max, Qt::KeepAspectRatio); + } + else if (size < min || (max.isValid() && size > max)) + { + return QSizeF{std::max(min.width(), + std::min(size.width(), max.width())), + std::max(min.height(), + std::min(size.height(), max.height()))}; + } return size; } +} diff --git a/tide/core/utils/geometry.h b/tide/core/utils/geometry.h index 129cbd20..61a1b897 100644 --- a/tide/core/utils/geometry.h +++ b/tide/core/utils/geometry.h @@ -1,6 +1,6 @@ /*********************************************************************/ -/* Copyright (c) 2016, EPFL/Blue Brain Project */ -/* Raphael Dumusc */ +/* Copyright (c) 2016-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ /* All rights reserved. */ /* */ /* Redistribution and use in source and binary forms, with or */ @@ -107,14 +107,24 @@ QRectF resizeAroundPosition(const QRectF& rect, const QPointF& position, */ QRectF scaleAroundCenter(const QRectF& rect, const qreal factor); +/** + * Adjust the aspect ratio to another reference size. + * @param size to adjust + * @param referenceSize for the reference aspect ratio + * @return size with adjusted aspect ratio + */ +QSizeF adjustAspectRatio(const QSizeF& size, const QSizeF& referenceSize); + /** * Constrain a size between min and max values. * @param size the size to constrain * @param min the minimum size (ignored if not valid) * @param max the maximum size (ignored if not valid) + * @param keepAspectRatio if the aspect ratio of the size must be preserved * @return the size comprised between min and max */ -QSizeF constrain(const QSizeF& size, const QSizeF& min, const QSizeF& max); +QSizeF constrain(const QSizeF& size, const QSizeF& min, const QSizeF& max, + bool keepAspectRatio = true); } #endif diff --git a/tide/master/CMakeLists.txt b/tide/master/CMakeLists.txt index d34018da..496020cc 100644 --- a/tide/master/CMakeLists.txt +++ b/tide/master/CMakeLists.txt @@ -27,6 +27,7 @@ list(APPEND TIDEMASTER_PUBLIC_HEADERS control/SessionController.h control/SideController.h control/WindowController.h + control/WindowResizeHandlesController.h control/WindowTouchController.h control/ZoomController.h gui/BackgroundWidget.h @@ -78,6 +79,7 @@ list(APPEND TIDEMASTER_SOURCES control/SessionController.cpp control/SideController.cpp control/WindowController.cpp + control/WindowResizeHandlesController.cpp control/WindowTouchController.cpp control/ZoomController.cpp gui/BackgroundWidget.cpp diff --git a/tide/master/control/ContentController.cpp b/tide/master/control/ContentController.cpp index 61d6c0b4..1899526c 100644 --- a/tide/master/control/ContentController.cpp +++ b/tide/master/control/ContentController.cpp @@ -76,6 +76,10 @@ std::unique_ptr ContentController::create(Window& window) return std::make_unique(window); #endif case ContentType::invalid: + // For unit-testing purposes + return window.getContent().canBeZoomed() + ? std::make_unique(window) + : std::make_unique(window); default: return std::make_unique(window); } diff --git a/tide/master/control/PixelStreamController.cpp b/tide/master/control/PixelStreamController.cpp index 075ec5e4..3f4458b9 100644 --- a/tide/master/control/PixelStreamController.cpp +++ b/tide/master/control/PixelStreamController.cpp @@ -41,9 +41,10 @@ #include "scene/PixelStreamContent.h" #include "scene/Window.h" +#include "utils/geometry.h" PixelStreamController::PixelStreamController(Window& window) - : ContentController(window) + : ContentController{window} , _keyboardController{*_getPixelStreamContent().getKeyboardState()} { connect(&window, &Window::coordinatesChanged, this, @@ -239,12 +240,14 @@ void PixelStreamController::_keyRelease(const int key, const int modifiers, void PixelStreamController::_sendSizeChangedEvent() { - const auto win = getWindowSize(); + const auto min = getContent().getSizeHintsMin(); + const auto max = getContent().getSizeHintsMax(); + const auto size = geometry::constrain(getWindowSize(), min, max); deflect::Event deflectEvent; deflectEvent.type = deflect::Event::EVT_VIEW_SIZE_CHANGED; - deflectEvent.dx = win.width(); - deflectEvent.dy = win.height(); + deflectEvent.dx = size.width(); + deflectEvent.dy = size.height(); emit notify(deflectEvent); } diff --git a/tide/master/control/PixelStreamWindowManager.cpp b/tide/master/control/PixelStreamWindowManager.cpp index 88c02228..8a7a5d75 100644 --- a/tide/master/control/PixelStreamWindowManager.cpp +++ b/tide/master/control/PixelStreamWindowManager.cpp @@ -67,7 +67,10 @@ WindowPtr _makeStreamWindow(const QString& uri, const QSize& size, const auto windowType = (type == StreamType::LAUNCHER) ? Window::PANEL : Window::DEFAULT; static_cast(*content).setChannel(channel); - return std::make_shared(std::move(content), windowType); + auto window = std::make_shared(std::move(content), windowType); + if (size.isEmpty()) + window->setCoordinates(QRectF{QPointF(), EMPTY_STREAM_SIZE}); + return window; } std::set _findAllChannels(const deflect::server::Frame& frame) @@ -148,8 +151,9 @@ void PixelStreamWindowManager::openWindow(const uint surfaceIndex, auto& group = _scene.getGroup(surfaceIndex); WindowController controller{*window, group}; - controller.resize(size.isValid() ? size : EMPTY_STREAM_SIZE); controller.moveCenterTo(!pos.isNull() ? pos : group.center()); + if (!size.isEmpty()) + controller.adjustSize(SizeState::SIZE_1TO1_FITTING); window->setState(Window::HIDDEN); group.add(window); // triggers _onWindowAdded() @@ -404,7 +408,7 @@ void PixelStreamWindowManager::_updateWindowSize(Window& window, content.setDimensions(size); if (externalStreamIsOpening) - _resizeInPlace(window, group, size); + _resizeInPlace(window, group); // Must come after window resize; window size is used by focus algorithm if (window.isFocused()) @@ -416,10 +420,9 @@ void PixelStreamWindowManager::_updateWindowSize(Window& window, } void PixelStreamWindowManager::_resizeInPlace(Window& window, - const DisplayGroup& group, - const QSize& size) + const DisplayGroup& group) { - const auto target = WindowController::Coordinates::STANDARD; - WindowController controller{window, group, target}; - controller.resize(size, CENTER); + WindowController controller{window, group, + WindowController::Coordinates::STANDARD}; + controller.adjustSize(SizeState::SIZE_1TO1_FITTING); } diff --git a/tide/master/control/PixelStreamWindowManager.h b/tide/master/control/PixelStreamWindowManager.h index 9b5779eb..8138dc9a 100644 --- a/tide/master/control/PixelStreamWindowManager.h +++ b/tide/master/control/PixelStreamWindowManager.h @@ -224,8 +224,7 @@ class PixelStreamWindowManager : public QObject const std::set& channels); void _updateWindowSize(Window& window, DisplayGroup& group, const QSize& size); - void _resizeInPlace(Window& window, const DisplayGroup& group, - const QSize& size); + void _resizeInPlace(Window& window, const DisplayGroup& group); }; #endif diff --git a/tide/master/control/WindowController.cpp b/tide/master/control/WindowController.cpp index 64271f0c..f1039e39 100644 --- a/tide/master/control/WindowController.cpp +++ b/tide/master/control/WindowController.cpp @@ -42,7 +42,7 @@ #include "ZoomController.h" #include "scene/DisplayGroup.h" -#include "scene/Window.h" +#include "scene/ZoomHelper.h" #include "ui.h" #include "utils/geometry.h" @@ -50,128 +50,63 @@ namespace { -const qreal FITTING_SIZE_SCALE = 0.9; -const qreal ONE_PERCENT = 0.01; -} +constexpr qreal FITTING_SIZE_SCALE = 0.9; +constexpr qreal ONE_PERCENT = 0.01; +constexpr auto INVALID_CONTROLLER_MSG = + "ContentController MUST be of class type ZoomController when " + "content.canBeZoomed() is true"; -WindowController::WindowController(Window& window, const DisplayGroup& group, - const Coordinates target) - : _window(window) - , _displayGroup(group) - , _target(target) +Window::ResizePolicy _getResizePolicyForAjustingSize(const Content& content) { + return content.hasFixedAspectRatio() || content.canBeZoomed() + ? Window::ResizePolicy::KEEP_ASPECT_RATIO + : Window::ResizePolicy::ADJUST_CONTENT; } - -void WindowController::startResizing(const Window::ResizeHandle handle) -{ - _window.setActiveHandle(handle); - _window.setState(Window::WindowState::RESIZING); } -void WindowController::toggleResizeMode() +WindowController::WindowController(Window& window, const DisplayGroup& group, + const Coordinates target) + : _window{window} + , _group{group} + , _target{target} { - if (_window.getContent().hasFixedAspectRatio()) - _window.setResizePolicy(Window::ResizePolicy::ADJUST_CONTENT); - else - _window.setResizePolicy(Window::ResizePolicy::KEEP_ASPECT_RATIO); } -void WindowController::stopResizing() +void WindowController::resize(const QSizeF& size, const WindowPoint fixedPoint, + const Window::ResizePolicy policy) { - _window.setState(Window::WindowState::NONE); - _window.setActiveHandle(Window::ResizeHandle::NOHANDLE); - - if (_window.getContent().hasFixedAspectRatio()) - _window.setResizePolicy(Window::ResizePolicy::KEEP_ASPECT_RATIO); - else - _window.setResizePolicy(Window::ResizePolicy::ADJUST_CONTENT); -} - -void WindowController::resize(const QSizeF& size, const WindowPoint fixedPoint) -{ - auto newSize = QSizeF{_window.getContent().getPreferredDimensions()}; - if (newSize.isEmpty()) - newSize = size; - else - newSize.scale(size, Qt::KeepAspectRatio); - switch (fixedPoint) { case CENTER: - _resize(_getCoordinates().center(), newSize); + resize(_getCoordinates().center(), size, policy); break; case TOP_LEFT: - _resize(_getCoordinates().topLeft(), newSize); + resize(_getCoordinates().topLeft(), size, policy); } } -void WindowController::resizeRelative(const QPointF& delta) +void WindowController::resize(const QPointF& center, QSizeF newSize, + const Window::ResizePolicy policy) { - const auto& coord = _getCoordinates(); + constrainSize(newSize, policy); - auto fixedPoint = QPointF(); - auto newSize = coord.size(); - - switch (_window.getActiveHandle()) - { - case Window::TOP: - fixedPoint = - QPointF(coord.left() + coord.width() * 0.5, coord.bottom()); - newSize += QSizeF(0, -delta.y()); - break; - case Window::RIGHT: - fixedPoint = QPointF(coord.left(), coord.top() + coord.height() * 0.5); - newSize += QSizeF(delta.x(), 0); - break; - case Window::BOTTOM: - fixedPoint = QPointF(coord.left() + coord.width() * 0.5, coord.top()); - newSize += QSizeF(0, delta.y()); - break; - case Window::LEFT: - fixedPoint = QPointF(coord.right(), coord.top() + coord.height() * 0.5); - newSize += QSizeF(-delta.x(), 0); - break; - case Window::TOP_LEFT: - fixedPoint = coord.bottomRight(); - newSize += QSizeF(-delta.x(), -delta.y()); - break; - case Window::BOTTOM_LEFT: - fixedPoint = coord.topRight(); - newSize += QSizeF(-delta.x(), delta.y()); - break; - case Window::TOP_RIGHT: - fixedPoint = coord.bottomLeft(); - newSize += QSizeF(delta.x(), -delta.y()); - break; - case Window::BOTTOM_RIGHT: - fixedPoint = coord.topLeft(); - newSize += QSizeF(delta.x(), delta.y()); - break; - case Window::NOHANDLE: - return; - } + auto coordinates = _getCoordinates(); + coordinates = geometry::resizeAroundPosition(coordinates, center, newSize); + _constrainPosition(coordinates); - // Resizing from one of the corners modifies the aspect ratio. - // Resizing from one of the sides borders tend to let the window snap back - // to its content's aspect ratio. - if (_window.getResizePolicy() == Window::KEEP_ASPECT_RATIO) - { - if (_window.getContent().getZoomRect() == UNIT_RECTF) - _constrainAspectRatio(newSize); - if (_isCloseToContentAspectRatio(newSize)) - _snapToContentAspectRatio(newSize); - } + _apply(coordinates); - _resize(fixedPoint, newSize); + if (_contentZoomCanBeAdjusted()) + _adjustZoom(); } void WindowController::scale(const QPointF& center, const double pixelDelta) { - auto newSize = _getCoordinates().size(); + auto newSize = _getSize(); newSize.scale(newSize.width() + pixelDelta, newSize.height() + pixelDelta, pixelDelta < 0 ? Qt::KeepAspectRatio : Qt::KeepAspectRatioByExpanding); - _resize(center, newSize); + resize(center, newSize); } void WindowController::scale(const QPointF& center, const QPointF& pixelDelta) @@ -184,50 +119,51 @@ void WindowController::scale(const QPointF& center, const QPointF& pixelDelta) void WindowController::adjustSize(const SizeState state) { + const auto policy = _getResizePolicyForAjustingSize(_window.getContent()); + switch (state) { case SIZE_1TO1: - resize(_window.getContent().getPreferredDimensions(), CENTER); + resize(_getPreferredDimensions(), CENTER, policy); break; case SIZE_1TO1_FITTING: { - const auto oneToOneSize = _window.getContent().getPreferredDimensions(); - const auto maxSize = _displayGroup.size() * FITTING_SIZE_SCALE; - resize(std::min(oneToOneSize, maxSize), CENTER); + const auto oneToOneSize = _getPreferredDimensions(); + const auto maxSize = _group.size() * FITTING_SIZE_SCALE; + const auto size = geometry::constrain(oneToOneSize, QSizeF(), maxSize); + resize(size, CENTER, policy); } break; case SIZE_FULLSCREEN: { - auto& content = _window.getContent(); - content.resetZoom(); + _window.getContent().resetZoom(); - auto size = geometry::getAdjustedSize(content.getPreferredDimensions(), - _displayGroup); - constrainSize(size); + auto size = + geometry::getAdjustedSize(_getPreferredDimensions(), _group); + constrainSize(size, policy); + size = geometry::constrain(size, QSizeF(), _group.size()); _apply(_getCenteredCoordinates(size)); } break; case SIZE_FULLSCREEN_MAX: { - auto& content = _window.getContent(); - content.resetZoom(); + _window.getContent().resetZoom(); - auto size = geometry::getExpandedSize(content, _displayGroup); - constrainSize(size); + auto size = geometry::getExpandedSize(_window.getContent(), _group); + constrainSize(size, policy); _apply(_getCenteredCoordinates(size)); } break; case SIZE_FULLSCREEN_1TO1: { - auto& content = _window.getContent(); - content.resetZoom(); + _window.getContent().resetZoom(); - auto size = QSizeF{content.getPreferredDimensions()}; - constrainSize(size); + auto size = _getPreferredDimensions(); + constrainSize(size, policy); _apply(_getCenteredCoordinates(size)); } break; @@ -239,8 +175,7 @@ void WindowController::toogleFullscreenMaxSize() if (!_targetIsFullscreen()) return; - const auto windowSize = _getCoordinates().size(); - if (windowSize > _displayGroup.size()) + if (_getSize() > _group.size()) adjustSize(SizeState::SIZE_FULLSCREEN); else adjustSize(SizeState::SIZE_FULLSCREEN_MAX); @@ -277,8 +212,9 @@ QSizeF WindowController::getMinSize() const { if (_targetIsFullscreen()) { - const auto size = QSizeF{_window.getContent().getPreferredDimensions()}; - return size.scaled(_displayGroup.size(), Qt::KeepAspectRatio); + const auto size = _getPreferredDimensions(); + const auto targetSize = size.scaled(_group.size(), Qt::KeepAspectRatio); + return std::min(targetSize, getMaxSize()); } const auto minContentSize = QSizeF{_window.getContent().getMinDimensions()}; @@ -288,73 +224,53 @@ QSizeF WindowController::getMinSize() const QSizeF WindowController::getMaxSize() const { - const auto& zoomRect = _window.getContent().getZoomRect(); - auto maxSize = QSizeF{_window.getContent().getMaxDimensions()}; - maxSize.rwidth() *= zoomRect.size().width(); - maxSize.rheight() *= zoomRect.size().height(); - return maxSize; -} - -void WindowController::constrainSize(QSizeF& windowSize) const -{ - windowSize = geometry::constrain(windowSize, getMinSize(), getMaxSize()); + return ZoomHelper{_window}.getMaxWindowSizeUpscaled(); } QSizeF WindowController::getMinSizeAspectRatioCorrect() const { - const auto contentAspectRatio = _window.getContent().getAspectRatio(); const auto min = getMinSize(); const auto max = getMaxSize(); - const auto aspectRatioCorrectSize = QSizeF(contentAspectRatio, 1.0); if (min > max) - return aspectRatioCorrectSize.scaled(max, Qt::KeepAspectRatio); - return aspectRatioCorrectSize.scaled(min, Qt::KeepAspectRatioByExpanding); + return _getAspectRatioSize().scaled(max, Qt::KeepAspectRatio); + return _getAspectRatioSize().scaled(min, Qt::KeepAspectRatioByExpanding); } -void WindowController::_resize(const QPointF& center, QSizeF size) +void WindowController::constrainSize(QSizeF& windowSize, + const Window::ResizePolicy policy) const { - constrainSize(size); + const auto snapToAspectRatio = + policy == Window::KEEP_ASPECT_RATIO && _window.getContent().isZoomed(); - auto coordinates = _getCoordinates(); - coordinates = geometry::resizeAroundPosition(coordinates, center, size); - _constrainPosition(coordinates); + const auto tryKeepAspectRatio = + policy == Window::KEEP_ASPECT_RATIO && !_window.getContent().isZoomed(); - _apply(coordinates); + const auto keepAspectRatio = + tryKeepAspectRatio || _mustKeepAspectRatio(windowSize); - auto controller = ContentController::create(_window); - auto zoomController = dynamic_cast(controller.get()); - if (zoomController) - zoomController->adjustZoomToContentAspectRatio(); -} + if (keepAspectRatio) + _constrainAspectRatio(windowSize); -void WindowController::_constrainAspectRatio(QSizeF& windowSize) const -{ - const auto currentSize = _getCoordinates().size(); - const auto mode = windowSize < currentSize ? Qt::KeepAspectRatio - : Qt::KeepAspectRatioByExpanding; - windowSize = currentSize.scaled(windowSize, mode); -} + windowSize = geometry::constrain(windowSize, getMinSize(), getMaxSize(), + keepAspectRatio); -bool WindowController::_isCloseToContentAspectRatio( - const QSizeF& windowSize) const -{ - const auto windowAR = windowSize.width() / windowSize.height(); - const auto contentAR = _window.getContent().getAspectRatio(); - - return std::fabs(windowAR - contentAR) < ONE_PERCENT; + if (snapToAspectRatio && _isCloseToContentAspectRatio(windowSize)) + windowSize = geometry::adjustAspectRatio(windowSize, _getContentSize()); } -void WindowController::_snapToContentAspectRatio(QSizeF& windowSize) const +void WindowController::_constrainAspectRatio(QSizeF& newSize) const { - const auto contentSize = QSizeF{_window.getContent().getDimensions()}; - const auto mode = windowSize < contentSize ? Qt::KeepAspectRatio - : Qt::KeepAspectRatioByExpanding; - windowSize = contentSize.scaled(windowSize, mode); + // Warning: using the current size to decide on scaling mode is needed for + // interactive resize() with handles but does not make sense on adjust(). + const auto currentSize = _window.getDisplayCoordinates().size(); + const auto mode = newSize < currentSize ? Qt::KeepAspectRatio + : Qt::KeepAspectRatioByExpanding; + newSize = _getAspectRatioSize().scaled(newSize, mode); } void WindowController::_constrainPosition(QRectF& window) const { - const auto& group = _displayGroup.getCoordinates(); + const auto& group = _group.getCoordinates(); if (_targetIsFullscreen()) { @@ -382,14 +298,37 @@ void WindowController::_constrainPosition(QRectF& window) const std::max(minY, std::min(window.y(), maxY))}); } -QRectF WindowController::_getCenteredCoordinates(const QSizeF& size) const +void WindowController::_adjustZoom() const { - const auto& group = _displayGroup.getCoordinates(); + auto controller = ContentController::create(_window); + auto zoomController = dynamic_cast(controller.get()); + if (!zoomController) + throw std::logic_error(INVALID_CONTROLLER_MSG); + zoomController->adjustZoomToContentAspectRatio(); +} - // centered coordinates on the display group - auto coord = QRectF{QPointF(), size}; - coord.moveCenter(group.center()); - return coord; +bool WindowController::_mustKeepAspectRatio(const QSizeF& newSize) const +{ + return !_contentZoomCanBeAdjusted() && !_contentSourceCanBeResized(newSize); +} + +bool WindowController::_contentZoomCanBeAdjusted() const +{ + return _window.getContent().canBeZoomed(); +} + +bool WindowController::_contentSourceCanBeResized(const QSizeF& newSize) const +{ + return !_window.getContent().hasFixedAspectRatio() && + !(newSize >= + ZoomHelper{_window}.getMaxWindowSizeAtNativeResolution()); +} + +bool WindowController::_isCloseToContentAspectRatio( + const QSizeF& windowSize) const +{ + const auto windowAR = windowSize.width() / windowSize.height(); + return std::fabs(windowAR - _getContentAspectRatio()) < ONE_PERCENT; } bool WindowController::_targetIsFullscreen() const @@ -398,36 +337,68 @@ bool WindowController::_targetIsFullscreen() const (_target == Coordinates::AUTO && _window.isFullscreen()); } +QSizeF WindowController::_getAspectRatioSize() const +{ + return QSizeF{_getContentAspectRatio(), 1.0}; +} + +qreal WindowController::_getContentAspectRatio() const +{ + return _window.getContent().getAspectRatio(); +} + +QSizeF WindowController::_getPreferredDimensions() const +{ + return _window.getContent().getPreferredDimensions(); +} + +QRectF WindowController::_getCenteredCoordinates(const QSizeF& size) const +{ + auto coord = QRectF{QPointF(), size}; + coord.moveCenter(_group.getCoordinates().center()); + return coord; +} + const QRectF& WindowController::_getCoordinates() const { switch (_target) { - case WindowController::Coordinates::STANDARD: + case Coordinates::STANDARD: return _window.getCoordinates(); - case WindowController::Coordinates::FOCUSED: + case Coordinates::FOCUSED: return _window.getFocusedCoordinates(); - case WindowController::Coordinates::FULLSCREEN: + case Coordinates::FULLSCREEN: return _window.getFullscreenCoordinates(); - case WindowController::Coordinates::AUTO: + case Coordinates::AUTO: return _window.getDisplayCoordinates(); } throw std::logic_error("invalid _target"); } +QSizeF WindowController::_getSize() const +{ + return _getCoordinates().size(); +} + +QSizeF WindowController::_getContentSize() const +{ + return _window.getContent().getDimensions(); +} + void WindowController::_apply(const QRectF& coordinates) { switch (_target) { - case WindowController::Coordinates::STANDARD: + case Coordinates::STANDARD: _window.setCoordinates(coordinates); break; - case WindowController::Coordinates::FOCUSED: + case Coordinates::FOCUSED: _window.setFocusedCoordinates(coordinates); break; - case WindowController::Coordinates::FULLSCREEN: + case Coordinates::FULLSCREEN: _window.setFullscreenCoordinates(coordinates); break; - case WindowController::Coordinates::AUTO: + case Coordinates::AUTO: _window.setDisplayCoordinates(coordinates); break; } diff --git a/tide/master/control/WindowController.h b/tide/master/control/WindowController.h index fb55304f..f908fdb5 100644 --- a/tide/master/control/WindowController.h +++ b/tide/master/control/WindowController.h @@ -44,8 +44,6 @@ #include "scene/Window.h" -#include - /** Common window size states. */ enum SizeState { @@ -66,11 +64,8 @@ enum WindowPoint /** * Controller for moving and resizing windows. */ -class WindowController : public QObject +class WindowController { - Q_OBJECT - Q_DISABLE_COPY(WindowController) - public: /** * The set of window coordinates that the controller acts upon. @@ -92,36 +87,29 @@ class WindowController : public QObject WindowController(Window& window, const DisplayGroup& group, Coordinates target = Coordinates::AUTO); - /** @name UI resize handle events. */ - //@{ - Q_INVOKABLE void startResizing(const Window::ResizeHandle handle); - Q_INVOKABLE void toggleResizeMode(); - Q_INVOKABLE void stopResizing(); - //@} - /** Resize the window. */ - Q_INVOKABLE void resize(const QSizeF& size, - WindowPoint fixedPoint = TOP_LEFT); - - /** Resize the window relative to the current active window border */ - Q_INVOKABLE void resizeRelative(const QPointF& delta); + void resize( + const QSizeF& size, WindowPoint fixedPoint = TOP_LEFT, + Window::ResizePolicy policy = Window::ResizePolicy::KEEP_ASPECT_RATIO); + void resize( + const QPointF& center, QSizeF size, + Window::ResizePolicy policy = Window::ResizePolicy::KEEP_ASPECT_RATIO); /** Scale the window by the given pixel delta (around the given center). */ - Q_INVOKABLE void scale(const QPointF& center, double pixelDelta); - Q_INVOKABLE void scale(const QPointF& center, const QPointF& pixelDelta); + void scale(const QPointF& center, double pixelDelta); + void scale(const QPointF& center, const QPointF& pixelDelta); /** Adjust the window coordinates to match the desired state. */ void adjustSize(const SizeState state); /** Toggle between SIZE_FULLSCREEN and SIZE_FULLSCREEN_MAX. */ - Q_INVOKABLE void toogleFullscreenMaxSize(); + void toogleFullscreenMaxSize(); /** Toggle the selected state of the window. */ - Q_INVOKABLE void toggleSelected(); + void toggleSelected(); /** Move the window to the desired position. */ - Q_INVOKABLE void moveTo(const QPointF& position, - WindowPoint handle = TOP_LEFT); + void moveTo(const QPointF& position, WindowPoint handle = TOP_LEFT); /** Move the center of the window to the desired position. */ inline void moveCenterTo(const QPointF& position) @@ -130,41 +118,45 @@ class WindowController : public QObject } /** Move the window by the given delta. */ - Q_INVOKABLE void moveBy(const QPointF& delta); + void moveBy(const QPointF& delta); - /** @return the minimum size of the window, 5% of wall size or 300px. */ + /** @return the minimum size of the window. */ QSizeF getMinSize() const; /** @return the maximum size of the window, considering max size of content, * wall size and current content zoom. */ QSizeF getMaxSize() const; - /** Constrain the given size between getMinSize() and getMaxSize(). */ - void constrainSize(QSizeF& windowSize) const; - /** @return the minimum size of the window respecting its aspect ratio. */ QSizeF getMinSizeAspectRatioCorrect() const; + /** Constrain the given size between getMinSize() and getMaxSize(). */ + void constrainSize(QSizeF& windowSize, Window::ResizePolicy policy) const; + private: - /** - * Resize the window around a given center point. - * @param center the center of scaling - * @param size the new desired size - */ - void _resize(const QPointF& center, QSizeF size); + void _constrainAspectRatio(QSizeF& newSize) const; + void _constrainPosition(QRectF& window) const; + void _adjustZoom() const; - void _constrainAspectRatio(QSizeF& windowSize) const; + bool _mustKeepAspectRatio(const QSizeF& newSize) const; + bool _contentZoomCanBeAdjusted() const; + bool _contentSourceCanBeResized(const QSizeF& newSize) const; bool _isCloseToContentAspectRatio(const QSizeF& windowSize) const; void _snapToContentAspectRatio(QSizeF& windowSize) const; - void _constrainPosition(QRectF& window) const; - QRectF _getCenteredCoordinates(const QSizeF& size) const; bool _targetIsFullscreen() const; + QSizeF _getAspectRatioSize() const; + qreal _getContentAspectRatio() const; + QSizeF _getPreferredDimensions() const; + QRectF _getCenteredCoordinates(const QSizeF& size) const; const QRectF& _getCoordinates() const; + QSizeF _getSize() const; + QSizeF _getContentSize() const; + void _apply(const QRectF& coordinates); Window& _window; - const DisplayGroup& _displayGroup; + const DisplayGroup& _group; Coordinates _target; }; diff --git a/tide/master/control/WindowResizeHandlesController.cpp b/tide/master/control/WindowResizeHandlesController.cpp new file mode 100644 index 00000000..aa4fbb2a --- /dev/null +++ b/tide/master/control/WindowResizeHandlesController.cpp @@ -0,0 +1,128 @@ +/*********************************************************************/ +/* Copyright (c) 2014-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* Daniel.Nachbaur@epfl.ch */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#include "WindowResizeHandlesController.h" + +#include "WindowController.h" +#include "scene/DisplayGroup.h" +#include "scene/Window.h" + +WindowResizeHandlesController::WindowResizeHandlesController( + Window& window, const DisplayGroup& group) + : _window{window} + , _displayGroup{group} +{ +} + +void WindowResizeHandlesController::startResizing( + const Window::ResizeHandle handle) +{ + _window.setActiveHandle(handle); + _window.setState(Window::WindowState::RESIZING); +} + +void WindowResizeHandlesController::toggleResizeMode() +{ + if (_window.getContent().hasFixedAspectRatio()) + _window.setResizePolicy(Window::ResizePolicy::ADJUST_CONTENT); + else + _window.setResizePolicy(Window::ResizePolicy::KEEP_ASPECT_RATIO); +} + +void WindowResizeHandlesController::stopResizing() +{ + _window.setState(Window::WindowState::NONE); + _window.setActiveHandle(Window::ResizeHandle::NOHANDLE); + + if (_window.getContent().hasFixedAspectRatio()) + _window.setResizePolicy(Window::ResizePolicy::KEEP_ASPECT_RATIO); + else + _window.setResizePolicy(Window::ResizePolicy::ADJUST_CONTENT); +} + +void WindowResizeHandlesController::resizeRelative(const QPointF& delta) +{ + const auto& coord = _window.getDisplayCoordinates(); + + auto fixedPoint = QPointF(); + auto newSize = coord.size(); + + switch (_window.getActiveHandle()) + { + case Window::TOP: + fixedPoint = + QPointF(coord.left() + coord.width() * 0.5, coord.bottom()); + newSize += QSizeF(0, -delta.y()); + break; + case Window::RIGHT: + fixedPoint = QPointF(coord.left(), coord.top() + coord.height() * 0.5); + newSize += QSizeF(delta.x(), 0); + break; + case Window::BOTTOM: + fixedPoint = QPointF(coord.left() + coord.width() * 0.5, coord.top()); + newSize += QSizeF(0, delta.y()); + break; + case Window::LEFT: + fixedPoint = QPointF(coord.right(), coord.top() + coord.height() * 0.5); + newSize += QSizeF(-delta.x(), 0); + break; + case Window::TOP_LEFT: + fixedPoint = coord.bottomRight(); + newSize += QSizeF(-delta.x(), -delta.y()); + break; + case Window::BOTTOM_LEFT: + fixedPoint = coord.topRight(); + newSize += QSizeF(-delta.x(), delta.y()); + break; + case Window::TOP_RIGHT: + fixedPoint = coord.bottomLeft(); + newSize += QSizeF(delta.x(), -delta.y()); + break; + case Window::BOTTOM_RIGHT: + fixedPoint = coord.topLeft(); + newSize += QSizeF(delta.x(), delta.y()); + break; + case Window::NOHANDLE: + return; + } + + auto controller = WindowController{_window, _displayGroup}; + controller.resize(fixedPoint, newSize, _window.getResizePolicy()); +} diff --git a/tide/master/control/WindowResizeHandlesController.h b/tide/master/control/WindowResizeHandlesController.h new file mode 100644 index 00000000..60d9fdfe --- /dev/null +++ b/tide/master/control/WindowResizeHandlesController.h @@ -0,0 +1,78 @@ +/*********************************************************************/ +/* Copyright (c) 2014-2018, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#ifndef WINDOW_RESIZE_HANDLES_CONTROLLER_H +#define WINDOW_RESIZE_HANDLES_CONTROLLER_H + +#include "types.h" + +#include "scene/Window.h" + +#include + +/** + * Controller for resizing windows using handles on the sides and corners. + */ +class WindowResizeHandlesController : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(WindowResizeHandlesController) + +public: + /** + * Create a controller for the resize handles of a window. + * @param window the target window. + * @param group the group to which the window belongs. + */ + WindowResizeHandlesController(Window& window, const DisplayGroup& group); + + /** @name UI resize handle events. */ + //@{ + Q_INVOKABLE void startResizing(Window::ResizeHandle handle); + Q_INVOKABLE void toggleResizeMode(); + Q_INVOKABLE void resizeRelative(const QPointF& delta); + Q_INVOKABLE void stopResizing(); + //@} + +private: + Window& _window; + const DisplayGroup& _displayGroup; +}; + +#endif diff --git a/tide/master/control/ZoomController.cpp b/tide/master/control/ZoomController.cpp index 7e985dad..848b9e40 100644 --- a/tide/master/control/ZoomController.cpp +++ b/tide/master/control/ZoomController.cpp @@ -164,9 +164,9 @@ void ZoomController::_constraintPosition(QRectF& zoomRect) const QSizeF ZoomController::_getMaxZoom() const { const auto window = getWindowSize(); - const auto content = QSizeF(getContent().getMaxDimensions()); - return QSizeF(window.width() / content.width(), - window.height() / content.height()); + const auto content = getContent().getMaxUpscaledDimensions(); + return QSizeF{window.width() / content.width(), + window.height() / content.height()}; } QSizeF ZoomController::_getMinZoom() const diff --git a/tide/master/layout/LineLayout.cpp b/tide/master/layout/LineLayout.cpp index 19d69e3d..f9146c92 100644 --- a/tide/master/layout/LineLayout.cpp +++ b/tide/master/layout/LineLayout.cpp @@ -144,7 +144,7 @@ WindowCoordinates LineLayout::_getNominalCoord(const Window& window) const WindowController(const_cast(window), _group, WindowController::Coordinates::STANDARD) - .constrainSize(size); + .constrainSize(size, Window::ResizePolicy::KEEP_ASPECT_RATIO); auto coord = QRectF(QPointF(), size); coord.moveCenter(QPointF(window.center().x(), surface.center().y())); diff --git a/tide/master/qml/MasterDisplayGroupRenderer.cpp b/tide/master/qml/MasterDisplayGroupRenderer.cpp index de0c9bf3..a681af74 100644 --- a/tide/master/qml/MasterDisplayGroupRenderer.cpp +++ b/tide/master/qml/MasterDisplayGroupRenderer.cpp @@ -41,6 +41,7 @@ #include "control/ContentController.h" #include "control/DisplayGroupController.h" +#include "control/WindowResizeHandlesController.h" #include "control/WindowTouchController.h" #include "scene/DisplayGroup.h" #include "scene/Window.h" @@ -109,9 +110,9 @@ void MasterDisplayGroupRenderer::_add(WindowPtr window) auto windowContext = new QQmlContext(_qmlContext.get()); windowContext->setContextProperty("window", window.get()); - auto controller = new WindowController(*window, *_displayGroup); + auto controller = new WindowResizeHandlesController(*window, *_displayGroup); controller->setParent(windowContext); - windowContext->setContextProperty("controller", controller); + windowContext->setContextProperty("resizehandlescontroller", controller); auto touchController = new WindowTouchController(*window, *_displayGroup); touchController->setParent(windowContext); diff --git a/tide/master/resources/MasterWindow.qml b/tide/master/resources/MasterWindow.qml index 0dd3e921..0d6d2b82 100644 --- a/tide/master/resources/MasterWindow.qml +++ b/tide/master/resources/MasterWindow.qml @@ -130,10 +130,10 @@ BaseWindow { referenceItem: windowRect.parent panThreshold: 10 - onTouchStarted: controller.startResizing(parent.handle) - onTapAndHold: controller.toggleResizeMode() - onPan: controller.resizeRelative(delta) - onTouchEnded: controller.stopResizing() + onTouchStarted: resizehandlescontroller.startResizing(parent.handle) + onTapAndHold: resizehandlescontroller.toggleResizeMode() + onPan: resizehandlescontroller.resizeRelative(delta) + onTouchEnded: resizehandlescontroller.stopResizing() } }