diff --git a/src/window.cpp b/src/window.cpp index 2ce1124ed..7d5f582ce 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1547,160 +1547,129 @@ void Window::FindWindowPlacementAndResize(int def_width, int def_height) this->SetDirty(); } -/** - * Decide whether a given rectangle is a good place to open a completely visible new window. - * The new window should be within screen borders, and not overlap with another already - * existing window (except for the main window in the background). - * @param left Left edge of the rectangle - * @param top Top edge of the rectangle - * @param width Width of the rectangle - * @param height Height of the rectangle - * @param toolbar_y Height of main toolbar - * @param pos If rectangle is good, use this parameter to return the top-left corner of the new window - * @return Boolean indication that the rectangle is a good place for the new window - */ -static bool IsGoodAutoPlace1(int left, int top, int width, int height, int toolbar_y, Point &pos) -{ +/** The penalty for placing a window partially off-screen. */ +static const uint WINDOW_PLACEMENT_PENALTY_OFFSCREEN = 5; +/** The penalty for placing a window such that it overlaps with existing windows, per window. */ +static const uint WINDOW_PLACEMENT_PENALTY_OVERLAP = 2; +/** The sentinel value for placement at a spot where nothing should ever be placed, e.g. entirely off-screen. */ +static const uint WINDOW_PLACEMENT_PENALTY_FORBIDDEN = UINT_MAX; + +/** + * Decide whether a given rectangle is a good place to open a new window. + * The new window should ideally be within screen borders, and not overlap with another already + * existing window (except for the main window in the background), though positions that do not + * fit these conditions only get a higher penalty instead of being rejected. + * @param left Left edge of the rectangle + * @param top Top edge of the rectangle + * @param width Width of the rectangle + * @param height Height of the rectangle + * @param priority The priority of the new window, used for checking if it would be obscured by a window with higher priority + * @param pos If rectangle is good (better than previous best), use this parameter to return the top-left corner of the new window + * @param penalty_best The score for the best previous placement. If rectangle is good, overwrite this parameter with the new best score. + * @return Score for for this position, as a sum of penalties. + */ +static uint IsGoodAutoPlace(int left, int top, int width, int height, uint priority, Point &pos, uint &penalty_best) +{ + uint penalty = 0; int right = width + left; int bottom = height + top; - if (left < 0 || top < toolbar_y || right > _screen.width || bottom > _screen.height) return false; - - /* Make sure it is not obscured by any window. */ - const Window *w; - FOR_ALL_WINDOWS_FROM_BACK(w) { - if (w->window_class == WC_MAIN_WINDOW) continue; - - if (right > w->left && - w->left + w->width > left && - bottom > w->top && - w->top + w->height > top) { - return false; - } - } - - pos.x = left; - pos.y = top; - return true; -} + if (left < 0 || top < 0 || right > _screen.width || bottom > _screen.height) { + penalty += WINDOW_PLACEMENT_PENALTY_OFFSCREEN; -/** - * Decide whether a given rectangle is a good place to open a mostly visible new window. - * The new window should be mostly within screen borders, and not overlap with another already - * existing window (except for the main window in the background). - * @param left Left edge of the rectangle - * @param top Top edge of the rectangle - * @param width Width of the rectangle - * @param height Height of the rectangle - * @param toolbar_y Height of main toolbar - * @param pos If rectangle is good, use this parameter to return the top-left corner of the new window - * @return Boolean indication that the rectangle is a good place for the new window - */ -static bool IsGoodAutoPlace2(int left, int top, int width, int height, int toolbar_y, Point &pos) -{ - bool rtl = _current_text_dir == TD_RTL; + bool rtl = _current_text_dir == TD_RTL; - /* Left part of the rectangle may be at most 1/4 off-screen, - * right part of the rectangle may be at most 1/2 off-screen - */ - if (rtl) { - if (left < -(width >> 1) || left > _screen.width - (width >> 2)) return false; - } else { - if (left < -(width >> 2) || left > _screen.width - (width >> 1)) return false; + /* Left part (for LTR, otherwise right) of the rectangle may be at most 1/4 off-screen, + * right part (for LTR, otherwise left) of the rectangle may be at most 1/2 off-screen + */ + if (left < -(width >> (rtl ? 1 : 2)) || left > _screen.width - (width >> (rtl ? 2 : 1))) return WINDOW_PLACEMENT_PENALTY_FORBIDDEN; + /* Bottom part of the rectangle may be at most 1/4 off-screen */ + if (top < 0 || top > _screen.height - (height >> 2)) return WINDOW_PLACEMENT_PENALTY_FORBIDDEN; } - /* Bottom part of the rectangle may be at most 1/4 off-screen */ - if (top < toolbar_y || top > _screen.height - (height >> 2)) return false; - /* Make sure it is not obscured by any window. */ const Window *w; FOR_ALL_WINDOWS_FROM_BACK(w) { - if (w->window_class == WC_MAIN_WINDOW) continue; + if (w->window_class == WC_MAIN_WINDOW || w->window_class == WC_TOOLTIPS) continue; - if (left + width > w->left && + if (right > w->left && w->left + w->width > left && - top + height > w->top && + bottom > w->top && w->top + w->height > top) { - return false; + if (GetWindowZPriority(w->window_class) > priority) return WINDOW_PLACEMENT_PENALTY_FORBIDDEN; + + penalty += WINDOW_PLACEMENT_PENALTY_OVERLAP; } } - pos.x = left; - pos.y = top; - return true; + if (penalty_best == WINDOW_PLACEMENT_PENALTY_FORBIDDEN || penalty_best > penalty) { + pos.x = left; + pos.y = top; + penalty_best = penalty; + } + return penalty; } /** * Find a good place for opening a new window of a given width and height. * @param width Width of the new window * @param height Height of the new window + * @param priority The priority of the new window, used for checking if it would be obscured by a window with higher priority * @return Top-left coordinate of the new window */ -static Point GetAutoPlacePosition(int width, int height) +static Point GetAutoPlacePosition(int width, int height, uint priority) { Point pt; + uint penalty_best = WINDOW_PLACEMENT_PENALTY_FORBIDDEN; bool rtl = _current_text_dir == TD_RTL; /* First attempt, try top-left of the screen */ const Window *main_toolbar = FindWindowByClass(WC_MAIN_TOOLBAR); const int toolbar_y = main_toolbar != NULL ? main_toolbar->height : 0; - if (IsGoodAutoPlace1(rtl ? _screen.width - width : 0, toolbar_y, width, height, toolbar_y, pt)) return pt; + /* Does it fit next to the toolbar? If that is ideal, return right away. + * If it collides with the toolbar, this spot is rated forbidden and discarded by IsGoodAutoPlace. */ + if (IsGoodAutoPlace(rtl ? _screen.width - width : 0, 0, width, height, priority, pt, penalty_best) == 0) { + return pt; + } + /* If the position next to the toolbar is forbidden (most likely due to a collision with the toolbar), + * perhaps the position right below the toolbar is better. */ + if (penalty_best == WINDOW_PLACEMENT_PENALTY_FORBIDDEN && + IsGoodAutoPlace(rtl ? _screen.width - width : 0, toolbar_y, width, height, priority, pt, penalty_best) == 0) { + return pt; + } /* Second attempt, try around all existing windows. - * The new window must be entirely on-screen, and not overlap with an existing window. + * The new window should ideally be entirely on-screen, and not overlap with an existing window. * Eight starting points are tried, two at each corner. */ const Window *w; FOR_ALL_WINDOWS_FROM_BACK(w) { - if (w->window_class == WC_MAIN_WINDOW) continue; - - if (IsGoodAutoPlace1(w->left + w->width, w->top, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left - width, w->top, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left, w->top + w->height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left, w->top - height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left + w->width, w->top + w->height - height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left - width, w->top + w->height - height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left + w->width - width, w->top + w->height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace1(w->left + w->width - width, w->top - height, width, height, toolbar_y, pt)) return pt; - } - - /* Third attempt, try around all existing windows. - * The new window may be partly off-screen, and must not overlap with an existing window. - * Only four starting points are tried. - */ - FOR_ALL_WINDOWS_FROM_BACK(w) { - if (w->window_class == WC_MAIN_WINDOW) continue; - - if (IsGoodAutoPlace2(w->left + w->width, w->top, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace2(w->left - width, w->top, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace2(w->left, w->top + w->height, width, height, toolbar_y, pt)) return pt; - if (IsGoodAutoPlace2(w->left, w->top - height, width, height, toolbar_y, pt)) return pt; - } - - /* Fourth and final attempt, put window at diagonal starting from (0, toolbar_y), try multiples - * of the closebox + if (w->window_class == WC_MAIN_WINDOW || w->window_class == WC_TOOLTIPS) continue; + + if (IsGoodAutoPlace(w->left + w->width, w->top, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left - width, w->top, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left, w->top + w->height, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left, w->top - height, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left + w->width, w->top + w->height - height, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left - width, w->top + w->height - height, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left + w->width - width, w->top + w->height, width, height, priority, pt, penalty_best) == 0) return pt; + if (IsGoodAutoPlace(w->left + w->width - width, w->top - height, width, height, priority, pt, penalty_best) == 0) return pt; + } + + if (penalty_best != WINDOW_PLACEMENT_PENALTY_FORBIDDEN) return pt; + /* Third and final attempt, if all previous spots were forbidden (which is very unlikely unless using an unsupported tiny screen size), + * just place the window such that the closebox and caption are inside the screen and not covered by immovable objects like the toolbar. + * Other windows of higher priority may need to be moved or closed to access the new window. If this is necessary, also print out a warning. */ - int left = rtl ? _screen.width - width : 0, top = toolbar_y; - int offset_x = rtl ? -NWidgetLeaf::closebox_dimension.width : NWidgetLeaf::closebox_dimension.width; - int offset_y = max(NWidgetLeaf::closebox_dimension.height, FONT_HEIGHT_NORMAL + WD_CAPTIONTEXT_TOP + WD_CAPTIONTEXT_BOTTOM); - -restart: - FOR_ALL_WINDOWS_FROM_BACK(w) { - if (w->left == left && w->top == top) { - left += offset_x; - top += offset_y; - goto restart; - } - } - - pt.x = left; - pt.y = top; + DEBUG(misc, 0, "Could not find a suitable spot for internal window, falling back to top left"); + pt.x = rtl ? _screen.width - width : 0; + pt.y = toolbar_y; return pt; } /** - * Computer the position of the top-left corner of a window to be opened right + * Compute the position of the top-left corner of a window to be opened right * under the toolbar. * @param window_width the width of the window to get the position for * @return Coordinate of the top-left corner of the new window. @@ -1770,7 +1739,7 @@ static Point LocalGetWindowPlacement(const WindowDesc *desc, int16 sm_width, int return GetToolbarAlignedWindowPosition(default_width); case WDP_AUTO: // Find a good automatic position for the window - return GetAutoPlacePosition(default_width, default_height); + return GetAutoPlacePosition(default_width, default_height, GetWindowZPriority(desc->cls)); case WDP_CENTER: // Centre the window horizontally pt.x = (_screen.width - default_width) / 2;