You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
13 KiB
336 lines
13 KiB
#include "Layout.h" |
|
|
|
#include <algorithm> |
|
#include <cstdlib> |
|
#include <numeric> |
|
#include <optional> |
|
|
|
#include "control/Control.h" |
|
#include "gui/scroll/ScrollHandling.h" |
|
#include "widgets/XournalWidget.h" |
|
|
|
#include "XournalView.h" |
|
/** |
|
* Padding outside the pages, including shadow |
|
*/ |
|
constexpr size_t const XOURNAL_PADDING = 10; |
|
|
|
/** |
|
* Allowance for shadow between page pairs in paired page mode |
|
*/ |
|
constexpr size_t const XOURNAL_ROOM_FOR_SHADOW = 3; |
|
|
|
/** |
|
* Padding between the pages |
|
*/ |
|
constexpr size_t const XOURNAL_PADDING_BETWEEN = 15; |
|
|
|
|
|
Layout::Layout(XournalView* view, ScrollHandling* scrollHandling): view(view), scrollHandling(scrollHandling) { |
|
this->hadjHandler = g_signal_connect(scrollHandling->getHorizontal(), "value-changed", |
|
G_CALLBACK(horizontalScrollChanged), this); |
|
|
|
this->vadjHandler = |
|
g_signal_connect(scrollHandling->getVertical(), "value-changed", G_CALLBACK(verticalScrollChanged), this); |
|
|
|
|
|
lastScrollHorizontal = gtk_adjustment_get_value(scrollHandling->getHorizontal()); |
|
lastScrollVertical = gtk_adjustment_get_value(scrollHandling->getVertical()); |
|
} |
|
|
|
void Layout::horizontalScrollChanged(GtkAdjustment* adjustment, Layout* layout) { |
|
g_signal_handler_block(layout->scrollHandling->getHorizontal(), layout->hadjHandler); |
|
Layout::checkScroll(adjustment, layout->lastScrollHorizontal); |
|
layout->updateVisibility(); |
|
layout->scrollHandling->scrollChanged(); |
|
g_signal_handler_unblock(layout->scrollHandling->getHorizontal(), layout->hadjHandler); |
|
} |
|
|
|
void Layout::verticalScrollChanged(GtkAdjustment* adjustment, Layout* layout) { |
|
g_signal_handler_block(layout->scrollHandling->getVertical(), layout->vadjHandler); |
|
Layout::checkScroll(adjustment, layout->lastScrollVertical); |
|
layout->updateVisibility(); |
|
layout->scrollHandling->scrollChanged(); |
|
g_signal_handler_unblock(layout->scrollHandling->getVertical(), layout->vadjHandler); |
|
} |
|
|
|
Layout::~Layout() = default; |
|
|
|
void Layout::checkScroll(GtkAdjustment* adjustment, double& lastScroll) { |
|
lastScroll = gtk_adjustment_get_value(adjustment); |
|
} |
|
|
|
void Layout::updateVisibility() { |
|
Rectangle visRect = getVisibleRect(); |
|
|
|
// step through every possible page position and update using p->setIsVisible() |
|
// Using initial grid aprox speeds things up by a factor of 5. See previous git check-in for specifics. |
|
int x1 = 0; |
|
int y1 = 0; |
|
|
|
// Data to select page based on visibility |
|
std::optional<size_t> mostPageNr; |
|
double mostPagePercent = 0; |
|
|
|
for (size_t row = 0; row < this->heightRows.size(); ++row) { |
|
int y2 = this->heightRows[row]; |
|
for (size_t col = 0; col < this->widthCols.size(); ++col) { |
|
int x2 = this->widthCols[col]; |
|
auto optionalPage = this->mapper.at({col, row}); |
|
if (optionalPage) // a page exists at this grid location |
|
{ |
|
XojPageView* pageView = this->view->viewPages[*optionalPage]; |
|
|
|
|
|
// check if grid location is visible as an aprox for page visiblity: |
|
if (!(visRect.x > x2 || visRect.x + visRect.width < x1) // visrect not outside current row/col |
|
&& !(visRect.y > y2 || visRect.y + visRect.height < y1)) { |
|
// now use exact check of page itself: |
|
// visrect not outside current page dimensions: |
|
auto const& pageRect = pageView->getRect(); |
|
if (auto intersection = pageRect.intersects(visRect); intersection) { |
|
pageView->setIsVisible(true); |
|
// Set the selected page |
|
double percent = intersection->area() / pageRect.area(); |
|
|
|
if (percent > mostPagePercent) { |
|
mostPageNr = *optionalPage; |
|
mostPagePercent = percent; |
|
} |
|
} |
|
} else { |
|
pageView->setIsVisible(false); |
|
} |
|
} |
|
x1 = x2; |
|
} |
|
y1 = y2; |
|
x1 = 0; |
|
} |
|
|
|
if (mostPageNr) { |
|
this->view->getControl()->firePageSelected(*mostPageNr); |
|
} |
|
} |
|
|
|
auto Layout::getVisibleRect() -> Rectangle<double> { |
|
return Rectangle(gtk_adjustment_get_value(scrollHandling->getHorizontal()), |
|
gtk_adjustment_get_value(scrollHandling->getVertical()), |
|
gtk_adjustment_get_page_size(scrollHandling->getHorizontal()), |
|
gtk_adjustment_get_page_size(scrollHandling->getVertical())); |
|
} |
|
|
|
|
|
/** |
|
* adds the addend to base if the predicate is true |
|
*/ |
|
inline auto sumIf(size_t base, size_t addend, bool predicate) -> size_t { |
|
if (predicate) { |
|
return base + addend; |
|
} |
|
return base; |
|
} |
|
|
|
|
|
void Layout::recalculate() { |
|
size_t padding = 0; |
|
size_t paddingBetween = 0; |
|
size_t roomForShadow = 0; |
|
|
|
if (!view->getControl()->getZoomControl()->isZoomPresentationMode()) { |
|
padding = XOURNAL_PADDING; |
|
paddingBetween = XOURNAL_PADDING_BETWEEN; |
|
roomForShadow = XOURNAL_ROOM_FOR_SHADOW; |
|
} |
|
|
|
|
|
auto* settings = view->getControl()->getSettings(); |
|
size_t len = view->viewPages.size(); |
|
mapper.configureFromSettings(len, settings); |
|
size_t colCount = mapper.getColumns(); |
|
size_t rowCount = mapper.getRows(); |
|
|
|
widthCols.assign(colCount, 0); |
|
heightRows.assign(rowCount, 0); |
|
|
|
for (size_t pageIdx{}; pageIdx < len; ++pageIdx) { |
|
auto const& raster_p = mapper.at(pageIdx); // auto [c, r] raster = mapper.at(); |
|
auto const& c = raster_p.first; |
|
auto const& r = raster_p.second; |
|
XojPageView* v = view->viewPages[pageIdx]; |
|
widthCols[c] = std::max<unsigned>(widthCols[c], v->getDisplayWidth()); |
|
heightRows[r] = std::max<unsigned>(heightRows[r], v->getDisplayHeight()); |
|
} |
|
|
|
// add space around the entire page area to accomodate older Wacom tablets with limited sense area. |
|
size_t const vPadding = sumIf(padding, settings->getAddVerticalSpaceAmount(), settings->getAddVerticalSpace()); |
|
size_t const hPadding = sumIf(padding, settings->getAddHorizontalSpaceAmount(), settings->getAddHorizontalSpace()); |
|
|
|
minWidth = 2 * hPadding + (widthCols.size() - 1) * paddingBetween; |
|
minHeight = 2 * vPadding + (heightRows.size() - 1) * paddingBetween; |
|
|
|
minWidth = std::accumulate(begin(widthCols), end(widthCols), minWidth); |
|
minHeight = std::accumulate(begin(heightRows), end(heightRows), minHeight); |
|
|
|
setLayoutSize(minWidth, minHeight); |
|
valid = true; |
|
} |
|
|
|
void Layout::layoutPages(int width, int height) { |
|
if (!valid) { |
|
recalculate(); |
|
} |
|
valid = false; |
|
|
|
size_t padding = 0; |
|
size_t paddingBetween = 0; |
|
size_t roomForShadow = 0; |
|
|
|
if (!view->getControl()->getZoomControl()->isZoomPresentationMode()) { |
|
padding = XOURNAL_PADDING; |
|
paddingBetween = XOURNAL_PADDING_BETWEEN; |
|
roomForShadow = XOURNAL_ROOM_FOR_SHADOW; |
|
} |
|
|
|
size_t const len = this->view->viewPages.size(); |
|
Settings* settings = this->view->getControl()->getSettings(); |
|
|
|
// get from mapper (some may have changed to accomodate paired setting etc.) |
|
bool const isPairedPages = this->mapper.isPairedPages(); |
|
|
|
auto const rows = this->heightRows.size(); |
|
auto const columns = this->widthCols.size(); |
|
|
|
|
|
// add space around the entire page area to accomodate older Wacom tablets with limited sense area. |
|
int64_t const v_padding = sumIf(padding, settings->getAddVerticalSpaceAmount(), settings->getAddVerticalSpace()); |
|
int64_t const h_padding = |
|
sumIf(padding, settings->getAddHorizontalSpaceAmount(), settings->getAddHorizontalSpace()); |
|
|
|
int64_t const centeringXBorder = static_cast<int64_t>(width - minWidth) / 2; |
|
int64_t const centeringYBorder = static_cast<int64_t>(height - minHeight) / 2; |
|
|
|
int64_t const borderX = std::max<int64_t>(h_padding, centeringXBorder); |
|
int64_t const borderY = std::max<int64_t>(v_padding, centeringYBorder); |
|
|
|
// initialize here and x again in loop below. |
|
int64_t x = borderX; |
|
int64_t y = borderY; |
|
|
|
|
|
// Iterate over ALL possible rows and columns. |
|
// We don't know which page, if any, is to be displayed in each row, column - ask the mapper object! |
|
// Then assign that page coordinates with center, left or right justify within row,column grid cell as required. |
|
for (size_t r = 0; r < rows; r++) { |
|
for (size_t c = 0; c < columns; c++) { |
|
auto optionalPage = this->mapper.at({c, r}); |
|
|
|
if (optionalPage) { |
|
|
|
XojPageView* v = this->view->viewPages[*optionalPage]; |
|
v->setMappedRowCol(r, c); // store row and column for e.g. proper arrow key navigation |
|
int64_t vDisplayWidth = v->getDisplayWidth(); |
|
{ |
|
int64_t paddingLeft = 0; |
|
int64_t paddingRight = 0; |
|
auto columnPadding = static_cast<int64_t>(this->widthCols[c] - vDisplayWidth); |
|
|
|
if (isPairedPages && len > 1) { |
|
// pair pages mode |
|
if (c % 2 == 0) { |
|
// align right |
|
paddingLeft = paddingBetween - roomForShadow + columnPadding; |
|
paddingRight = roomForShadow; |
|
} else { // align left |
|
paddingLeft = roomForShadow; |
|
paddingRight = paddingBetween - roomForShadow + columnPadding; |
|
} |
|
} else { // not paired page mode - center |
|
paddingLeft = paddingBetween / 2 + columnPadding / 2; // center justify |
|
paddingRight = paddingBetween - paddingLeft + columnPadding / 2; |
|
} |
|
|
|
x += paddingLeft; |
|
|
|
v->setX(x); // set the page position |
|
v->setY(y); |
|
|
|
x += vDisplayWidth + paddingRight; |
|
} |
|
} else { |
|
x += this->widthCols[c] + paddingBetween; |
|
} |
|
} |
|
x = borderX; |
|
y += this->heightRows[r] + paddingBetween; |
|
} |
|
|
|
int64_t totalWidth = borderX; |
|
for (auto&& widthCol: this->widthCols) { |
|
// accumulated - absolute pixel location for use by getViewAt() and updateVisibility() |
|
totalWidth += widthCol + paddingBetween; |
|
widthCol = totalWidth; |
|
} |
|
|
|
int64_t totalHeight = borderY; |
|
for (auto&& heightRow: this->heightRows) { |
|
totalHeight += heightRow + paddingBetween; |
|
heightRow = totalHeight; |
|
} |
|
} |
|
|
|
void Layout::setLayoutSize(int width, int height) { this->scrollHandling->setLayoutSize(width, height); } |
|
|
|
void Layout::scrollRelative(double x, double y) { |
|
if (this->view->getControl()->getSettings()->isPresentationMode()) { |
|
return; |
|
} |
|
|
|
gtk_adjustment_set_value(scrollHandling->getHorizontal(), |
|
gtk_adjustment_get_value(scrollHandling->getHorizontal()) + x); |
|
gtk_adjustment_set_value(scrollHandling->getVertical(), |
|
gtk_adjustment_get_value(scrollHandling->getVertical()) + y); |
|
} |
|
|
|
void Layout::scrollAbs(double x, double y) { |
|
if (this->view->getControl()->getSettings()->isPresentationMode()) { |
|
return; |
|
} |
|
|
|
gtk_adjustment_set_value(scrollHandling->getHorizontal(), x); |
|
gtk_adjustment_set_value(scrollHandling->getVertical(), y); |
|
} |
|
|
|
|
|
void Layout::ensureRectIsVisible(int x, int y, int width, int height) { |
|
int offset = (this->view->getControl()->getSettings()->isPresentationMode()) ? 0 : XOURNAL_PADDING / 2; |
|
gtk_adjustment_clamp_page(scrollHandling->getHorizontal(), x - offset, x + width + 2 * offset); |
|
gtk_adjustment_clamp_page(scrollHandling->getVertical(), y - offset, y + height + 2 * offset); |
|
} |
|
|
|
|
|
auto Layout::getViewAt(int x, int y) -> XojPageView* { |
|
// Binary Search: |
|
auto rit = std::lower_bound(this->heightRows.begin(), this->heightRows.end(), y); |
|
int const foundRow = std::distance(this->heightRows.begin(), rit); |
|
auto cit = std::lower_bound(this->widthCols.begin(), this->widthCols.end(), x); |
|
int const foundCol = std::distance(this->widthCols.begin(), cit); |
|
|
|
auto optionalPage = this->mapper.at({foundCol, foundRow}); |
|
|
|
if (optionalPage && this->view->viewPages[*optionalPage]->containsPoint(x, y, false)) { |
|
return this->view->viewPages[*optionalPage]; |
|
} |
|
|
|
return nullptr; |
|
} |
|
|
|
// Todo replace with boost::optional<size_t> Layout::getIndexAtGridMap(size_t row, size_t col) |
|
// or std::optional<size_t> Layout::getIndexAtGridMap(size_t row, size_t col) |
|
auto Layout::getIndexAtGridMap(size_t row, size_t col) -> std::optional<size_t> { |
|
return this->mapper.at({col, row}); // watch out.. x,y --> c,r |
|
} |
|
|
|
auto Layout::getMinimalHeight() const -> int { return this->minHeight; } |
|
|
|
auto Layout::getMinimalWidth() const -> int { return this->minWidth; }
|
|
|