ladybird/Servers/WindowServer/WSWindowManager.cpp
Andreas Kling 6e6e0b9de8 WindowServer: Compute some layout rects in WSMenuManager up front
Currently menu applets are laid out relative to the "audio rect" which
is the rect of the little audio muted state icon thingy.

There was an issue where applets would be placed at a negative X coord
if they were added to the WindowServer before the first time drawing
the menubar.
2019-12-05 19:59:54 +01:00

1140 lines
42 KiB
C++

#include "WSWindowManager.h"
#include "WSCompositor.h"
#include "WSEventLoop.h"
#include "WSMenu.h"
#include "WSMenuBar.h"
#include "WSMenuItem.h"
#include "WSScreen.h"
#include "WSWindow.h"
#include <AK/LogStream.h>
#include <AK/QuickSort.h>
#include <AK/StdLibExtras.h>
#include <AK/Vector.h>
#include <LibCore/CDirIterator.h>
#include <LibCore/CTimer.h>
#include <LibDraw/CharacterBitmap.h>
#include <LibDraw/Font.h>
#include <LibDraw/PNGLoader.h>
#include <LibDraw/Painter.h>
#include <LibDraw/StylePainter.h>
#include <WindowServer/WSButton.h>
#include <WindowServer/WSClientConnection.h>
#include <WindowServer/WSCursor.h>
#include <errno.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
//#define DEBUG_COUNTERS
//#define DEBUG_MENUS
//#define RESIZE_DEBUG
//#define DRAG_DEBUG
//#define DOUBLECLICK_DEBUG
static WSWindowManager* s_the;
WSWindowManager& WSWindowManager::the()
{
ASSERT(s_the);
return *s_the;
}
WSWindowManager::WSWindowManager()
{
s_the = this;
reload_config(false);
HashTable<String> seen_app_categories;
CDirIterator dt("/res/apps", CDirIterator::SkipDots);
while (dt.has_next()) {
auto af_name = dt.next_path();
auto af_path = String::format("/res/apps/%s", af_name.characters());
auto af = CConfigFile::open(af_path);
if (!af->has_key("App", "Name") || !af->has_key("App", "Executable"))
continue;
auto app_name = af->read_entry("App", "Name");
auto app_executable = af->read_entry("App", "Executable");
auto app_category = af->read_entry("App", "Category");
auto app_icon_path = af->read_entry("Icons", "16x16");
m_apps.append({ app_executable, app_name, app_icon_path, app_category });
seen_app_categories.set(app_category);
}
Vector<String> sorted_app_categories;
for (auto& category : seen_app_categories)
sorted_app_categories.append(category);
quick_sort(sorted_app_categories.begin(), sorted_app_categories.end(), [](auto& a, auto& b) { return a < b; });
u8 system_menu_name[] = { 0xc3, 0xb8, 0 };
m_system_menu = WSMenu::construct(nullptr, -1, String((const char*)system_menu_name));
// First we construct all the necessary app category submenus.
for (const auto& category : sorted_app_categories) {
if (m_app_category_menus.contains(category))
continue;
auto category_menu = WSMenu::construct(nullptr, 5000 + m_app_category_menus.size(), category);
category_menu->on_item_activation = [this](auto& item) {
if (item.identifier() >= 1 && item.identifier() <= 1u + m_apps.size() - 1) {
if (fork() == 0) {
const auto& bin = m_apps[item.identifier() - 1].executable;
execl(bin.characters(), bin.characters(), nullptr);
ASSERT_NOT_REACHED();
}
}
};
auto item = make<WSMenuItem>(*m_system_menu, -1, category);
item->set_submenu_id(category_menu->menu_id());
m_system_menu->add_item(move(item));
m_app_category_menus.set(category, move(category_menu));
}
// Then we create and insert all the app menu items into the right place.
int app_identifier = 1;
for (const auto& app : m_apps) {
auto parent_menu = m_app_category_menus.get(app.category).value_or(*m_system_menu);
parent_menu->add_item(make<WSMenuItem>(*m_system_menu, app_identifier++, app.name, String(), true, false, false, load_png(app.icon_path)));
}
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, WSMenuItem::Separator));
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, 100, "Reload WM Config File"));
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, WSMenuItem::Separator));
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, 200, "About...", String(), true, false, false, load_png("/res/icons/16x16/ladybug.png")));
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, WSMenuItem::Separator));
m_system_menu->add_item(make<WSMenuItem>(*m_system_menu, 300, "Shutdown..."));
m_system_menu->on_item_activation = [this](WSMenuItem& item) {
if (item.identifier() >= 1 && item.identifier() <= 1u + m_apps.size() - 1) {
if (fork() == 0) {
const auto& bin = m_apps[item.identifier() - 1].executable;
execl(bin.characters(), bin.characters(), nullptr);
ASSERT_NOT_REACHED();
}
}
switch (item.identifier()) {
case 100:
reload_config(true);
break;
case 200:
if (fork() == 0) {
execl("/bin/About", "/bin/About", nullptr);
ASSERT_NOT_REACHED();
}
return;
case 300:
if (fork() == 0) {
execl("/bin/SystemDialog", "/bin/SystemDialog", "--shutdown", nullptr);
ASSERT_NOT_REACHED();
}
return;
}
#ifdef DEBUG_MENUS
dbg() << "WSMenu 1 item activated: " << item.text();
#endif
};
// NOTE: This ensures that the system menu has the correct dimensions.
set_current_menubar(nullptr);
m_menu_manager.setup();
invalidate();
WSCompositor::the().compose();
}
WSWindowManager::~WSWindowManager()
{
}
NonnullRefPtr<WSCursor> WSWindowManager::get_cursor(const String& name, const Point& hotspot)
{
auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
auto gb = GraphicsBitmap::load_from_file(path);
if (gb)
return WSCursor::create(*gb, hotspot);
return WSCursor::create(*GraphicsBitmap::load_from_file("/res/cursors/arrow.png"));
}
NonnullRefPtr<WSCursor> WSWindowManager::get_cursor(const String& name)
{
auto path = m_wm_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
auto gb = GraphicsBitmap::load_from_file(path);
if (gb)
return WSCursor::create(*gb);
return WSCursor::create(*GraphicsBitmap::load_from_file("/res/cursors/arrow.png"));
}
void WSWindowManager::reload_config(bool set_screen)
{
m_wm_config = CConfigFile::get_for_app("WindowManager");
m_double_click_speed = m_wm_config->read_num_entry("Input", "DoubleClickSpeed", 250);
if (set_screen)
set_resolution(m_wm_config->read_num_entry("Screen", "Width", 1920),
m_wm_config->read_num_entry("Screen", "Height", 1080));
m_arrow_cursor = get_cursor("Arrow", { 2, 2 });
m_hand_cursor = get_cursor("Hand", { 8, 4 });
m_resize_horizontally_cursor = get_cursor("ResizeH");
m_resize_vertically_cursor = get_cursor("ResizeV");
m_resize_diagonally_tlbr_cursor = get_cursor("ResizeDTLBR");
m_resize_diagonally_bltr_cursor = get_cursor("ResizeDBLTR");
m_i_beam_cursor = get_cursor("IBeam");
m_disallowed_cursor = get_cursor("Disallowed");
m_move_cursor = get_cursor("Move");
m_background_color = m_wm_config->read_color_entry("Colors", "Background", Color::Red);
m_active_window_border_color = m_wm_config->read_color_entry("Colors", "ActiveWindowBorder", Color::Red);
m_active_window_border_color2 = m_wm_config->read_color_entry("Colors", "ActiveWindowBorder2", Color::Red);
m_active_window_title_color = m_wm_config->read_color_entry("Colors", "ActiveWindowTitle", Color::Red);
m_inactive_window_border_color = m_wm_config->read_color_entry("Colors", "InactiveWindowBorder", Color::Red);
m_inactive_window_border_color2 = m_wm_config->read_color_entry("Colors", "InactiveWindowBorder2", Color::Red);
m_inactive_window_title_color = m_wm_config->read_color_entry("Colors", "InactiveWindowTitle", Color::Red);
m_dragging_window_border_color = m_wm_config->read_color_entry("Colors", "DraggingWindowBorder", Color::Red);
m_dragging_window_border_color2 = m_wm_config->read_color_entry("Colors", "DraggingWindowBorder2", Color::Red);
m_dragging_window_title_color = m_wm_config->read_color_entry("Colors", "DraggingWindowTitle", Color::Red);
m_highlight_window_border_color = m_wm_config->read_color_entry("Colors", "HighlightWindowBorder", Color::Red);
m_highlight_window_border_color2 = m_wm_config->read_color_entry("Colors", "HighlightWindowBorder2", Color::Red);
m_highlight_window_title_color = m_wm_config->read_color_entry("Colors", "HighlightWindowTitle", Color::Red);
m_menu_selection_color = m_wm_config->read_color_entry("Colors", "MenuSelectionColor", Color::Red);
}
const Font& WSWindowManager::font() const
{
return Font::default_font();
}
const Font& WSWindowManager::window_title_font() const
{
return Font::default_bold_font();
}
const Font& WSWindowManager::menu_font() const
{
return Font::default_font();
}
const Font& WSWindowManager::app_menu_font() const
{
return Font::default_bold_font();
}
void WSWindowManager::set_resolution(int width, int height)
{
WSCompositor::the().set_resolution(width, height);
m_menu_manager.set_needs_window_resize();
WSClientConnection::for_each_client([&](WSClientConnection& client) {
client.notify_about_new_screen_rect(WSScreen::the().rect());
});
if (m_wm_config) {
dbg() << "Saving resolution: " << Size(width, height) << " to config file at " << m_wm_config->file_name();
m_wm_config->write_num_entry("Screen", "Width", width);
m_wm_config->write_num_entry("Screen", "Height", height);
m_wm_config->sync();
}
}
void WSWindowManager::set_current_menubar(WSMenuBar* menubar)
{
if (menubar)
m_current_menubar = menubar->make_weak_ptr();
else
m_current_menubar = nullptr;
#ifdef DEBUG_MENUS
dbg() << "[WM] Current menubar is now " << menubar;
#endif
Point next_menu_location { WSMenuManager::menubar_menu_margin() / 2, 0 };
int index = 0;
for_each_active_menubar_menu([&](WSMenu& menu) {
int text_width = index == 1 ? Font::default_bold_font().width(menu.name()) : font().width(menu.name());
menu.set_rect_in_menubar({ next_menu_location.x() - WSMenuManager::menubar_menu_margin() / 2, 0, text_width + WSMenuManager::menubar_menu_margin(), menubar_rect().height() - 1 });
menu.set_text_rect_in_menubar({ next_menu_location, { text_width, menubar_rect().height() } });
next_menu_location.move_by(menu.rect_in_menubar().width(), 0);
++index;
return true;
});
m_menu_manager.refresh();
}
void WSWindowManager::add_window(WSWindow& window)
{
m_windows_in_order.append(&window);
if (window.is_fullscreen()) {
CEventLoop::current().post_event(window, make<WSResizeEvent>(window.rect(), WSScreen::the().rect()));
window.set_rect(WSScreen::the().rect());
}
set_active_window(&window);
if (m_switcher.is_visible() && window.type() != WSWindowType::WindowSwitcher)
m_switcher.refresh();
if (window.listens_to_wm_events()) {
for_each_window([&](WSWindow& other_window) {
if (&window != &other_window) {
tell_wm_listener_about_window(window, other_window);
tell_wm_listener_about_window_icon(window, other_window);
}
return IterationDecision::Continue;
});
}
tell_wm_listeners_window_state_changed(window);
}
void WSWindowManager::move_to_front_and_make_active(WSWindow& window)
{
if (window.is_blocked_by_modal_window())
return;
if (m_windows_in_order.tail() != &window)
invalidate(window);
m_windows_in_order.remove(&window);
m_windows_in_order.append(&window);
set_active_window(&window);
}
void WSWindowManager::remove_window(WSWindow& window)
{
invalidate(window);
m_windows_in_order.remove(&window);
if (window.is_active())
pick_new_active_window();
if (m_switcher.is_visible() && window.type() != WSWindowType::WindowSwitcher)
m_switcher.refresh();
for_each_window_listening_to_wm_events([&window](WSWindow& listener) {
if (!(listener.wm_event_mask() & WSWMEventMask::WindowRemovals))
return IterationDecision::Continue;
if (window.client())
CEventLoop::current().post_event(listener, make<WSWMWindowRemovedEvent>(window.client()->client_id(), window.window_id()));
return IterationDecision::Continue;
});
}
void WSWindowManager::tell_wm_listener_about_window(WSWindow& listener, WSWindow& window)
{
if (!(listener.wm_event_mask() & WSWMEventMask::WindowStateChanges))
return;
if (window.client())
CEventLoop::current().post_event(listener, make<WSWMWindowStateChangedEvent>(window.client()->client_id(), window.window_id(), window.title(), window.rect(), window.is_active(), window.type(), window.is_minimized()));
}
void WSWindowManager::tell_wm_listener_about_window_rect(WSWindow& listener, WSWindow& window)
{
if (!(listener.wm_event_mask() & WSWMEventMask::WindowRectChanges))
return;
if (window.client())
CEventLoop::current().post_event(listener, make<WSWMWindowRectChangedEvent>(window.client()->client_id(), window.window_id(), window.rect()));
}
void WSWindowManager::tell_wm_listener_about_window_icon(WSWindow& listener, WSWindow& window)
{
if (!(listener.wm_event_mask() & WSWMEventMask::WindowIconChanges))
return;
if (window.client() && window.icon().shared_buffer_id() != -1)
CEventLoop::current().post_event(listener, make<WSWMWindowIconBitmapChangedEvent>(window.client()->client_id(), window.window_id(), window.icon().shared_buffer_id(), window.icon().size()));
}
void WSWindowManager::tell_wm_listeners_window_state_changed(WSWindow& window)
{
for_each_window_listening_to_wm_events([&](WSWindow& listener) {
tell_wm_listener_about_window(listener, window);
return IterationDecision::Continue;
});
}
void WSWindowManager::tell_wm_listeners_window_icon_changed(WSWindow& window)
{
for_each_window_listening_to_wm_events([&](WSWindow& listener) {
tell_wm_listener_about_window_icon(listener, window);
return IterationDecision::Continue;
});
}
void WSWindowManager::tell_wm_listeners_window_rect_changed(WSWindow& window)
{
for_each_window_listening_to_wm_events([&](WSWindow& listener) {
tell_wm_listener_about_window_rect(listener, window);
return IterationDecision::Continue;
});
}
void WSWindowManager::notify_title_changed(WSWindow& window)
{
if (window.type() != WSWindowType::Normal)
return;
dbg() << "[WM] WSWindow{" << &window << "} title set to \"" << window.title() << '"';
invalidate(window.frame().rect());
if (m_switcher.is_visible())
m_switcher.refresh();
tell_wm_listeners_window_state_changed(window);
}
void WSWindowManager::notify_rect_changed(WSWindow& window, const Rect& old_rect, const Rect& new_rect)
{
UNUSED_PARAM(old_rect);
UNUSED_PARAM(new_rect);
#ifdef RESIZE_DEBUG
dbg() << "[WM] WSWindow " << &window << " rect changed " << old_rect << " -> " << new_rect;
#endif
if (m_switcher.is_visible() && window.type() != WSWindowType::WindowSwitcher)
m_switcher.refresh();
tell_wm_listeners_window_rect_changed(window);
}
void WSWindowManager::notify_minimization_state_changed(WSWindow& window)
{
tell_wm_listeners_window_state_changed(window);
if (window.is_active() && window.is_minimized())
pick_new_active_window();
}
void WSWindowManager::pick_new_active_window()
{
for_each_visible_window_of_type_from_front_to_back(WSWindowType::Normal, [&](WSWindow& candidate) {
set_active_window(&candidate);
return IterationDecision::Break;
});
}
void WSWindowManager::start_window_drag(WSWindow& window, const WSMouseEvent& event)
{
#ifdef DRAG_DEBUG
dbg() << "[WM] Begin dragging WSWindow{" << &window << "}";
#endif
move_to_front_and_make_active(window);
m_drag_window = window.make_weak_ptr();
m_drag_origin = event.position();
m_drag_window_origin = window.position();
invalidate(window);
}
void WSWindowManager::start_window_resize(WSWindow& window, const Point& position, MouseButton button)
{
move_to_front_and_make_active(window);
constexpr ResizeDirection direction_for_hot_area[3][3] = {
{ ResizeDirection::UpLeft, ResizeDirection::Up, ResizeDirection::UpRight },
{ ResizeDirection::Left, ResizeDirection::None, ResizeDirection::Right },
{ ResizeDirection::DownLeft, ResizeDirection::Down, ResizeDirection::DownRight },
};
Rect outer_rect = window.frame().rect();
ASSERT(outer_rect.contains(position));
int window_relative_x = position.x() - outer_rect.x();
int window_relative_y = position.y() - outer_rect.y();
int hot_area_row = min(2, window_relative_y / (outer_rect.height() / 3));
int hot_area_column = min(2, window_relative_x / (outer_rect.width() / 3));
m_resize_direction = direction_for_hot_area[hot_area_row][hot_area_column];
if (m_resize_direction == ResizeDirection::None) {
ASSERT(!m_resize_window);
return;
}
#ifdef RESIZE_DEBUG
dbg() << "[WM] Begin resizing WSWindow{" << &window << "}";
#endif
m_resizing_mouse_button = button;
m_resize_window = window.make_weak_ptr();
;
m_resize_origin = position;
m_resize_window_original_rect = window.rect();
invalidate(window);
}
void WSWindowManager::start_window_resize(WSWindow& window, const WSMouseEvent& event)
{
start_window_resize(window, event.position(), event.button());
}
bool WSWindowManager::process_ongoing_window_drag(WSMouseEvent& event, WSWindow*& hovered_window)
{
if (!m_drag_window)
return false;
if (event.type() == WSEvent::MouseUp && event.button() == MouseButton::Left) {
#ifdef DRAG_DEBUG
dbg() << "[WM] Finish dragging WSWindow{" << m_drag_window << "}";
#endif
invalidate(*m_drag_window);
if (m_drag_window->rect().contains(event.position()))
hovered_window = m_drag_window;
if (m_drag_window->is_resizable()) {
process_event_for_doubleclick(*m_drag_window, event);
if (event.type() == WSEvent::MouseDoubleClick) {
#if defined(DOUBLECLICK_DEBUG)
dbg() << "[WM] Click up became doubleclick!";
#endif
m_drag_window->set_maximized(!m_drag_window->is_maximized());
}
}
m_drag_window = nullptr;
return true;
}
if (event.type() == WSEvent::MouseMove) {
#ifdef DRAG_DEBUG
dbg() << "[WM] Dragging, origin: " << m_drag_origin << ", now: " << event.position();
if (m_drag_window->is_maximized()) {
dbg() << " [!] The window is still maximized. Not dragging yet.";
}
#endif
if (m_drag_window->is_maximized()) {
auto pixels_moved_from_start = event.position().pixels_moved(m_drag_origin);
// dbg() << "[WM] " << pixels_moved_from_start << " moved since start of drag";
if (pixels_moved_from_start > 5) {
// dbg() << "[WM] de-maximizing window";
m_drag_origin = event.position();
auto width_before_resize = m_drag_window->width();
m_drag_window->set_maximized(false);
m_drag_window->move_to(m_drag_origin.x() - (m_drag_window->width() * ((float)m_drag_origin.x() / width_before_resize)), m_drag_origin.y());
m_drag_window_origin = m_drag_window->position();
}
} else {
Point pos = m_drag_window_origin.translated(event.position() - m_drag_origin);
m_drag_window->set_position_without_repaint(pos);
if (m_drag_window->rect().contains(event.position()))
hovered_window = m_drag_window;
return true;
}
}
return false;
}
bool WSWindowManager::process_ongoing_window_resize(const WSMouseEvent& event, WSWindow*& hovered_window)
{
if (!m_resize_window)
return false;
if (event.type() == WSEvent::MouseUp && event.button() == m_resizing_mouse_button) {
#ifdef RESIZE_DEBUG
dbg() << "[WM] Finish resizing WSWindow{" << m_resize_window << "}";
#endif
CEventLoop::current().post_event(*m_resize_window, make<WSResizeEvent>(m_resize_window->rect(), m_resize_window->rect()));
invalidate(*m_resize_window);
if (m_resize_window->rect().contains(event.position()))
hovered_window = m_resize_window;
m_resize_window = nullptr;
m_resizing_mouse_button = MouseButton::None;
return true;
}
if (event.type() != WSEvent::MouseMove)
return false;
auto old_rect = m_resize_window->rect();
int diff_x = event.x() - m_resize_origin.x();
int diff_y = event.y() - m_resize_origin.y();
int change_w = 0;
int change_h = 0;
switch (m_resize_direction) {
case ResizeDirection::DownRight:
change_w = diff_x;
change_h = diff_y;
break;
case ResizeDirection::Right:
change_w = diff_x;
break;
case ResizeDirection::UpRight:
change_w = diff_x;
change_h = -diff_y;
break;
case ResizeDirection::Up:
change_h = -diff_y;
break;
case ResizeDirection::UpLeft:
change_w = -diff_x;
change_h = -diff_y;
break;
case ResizeDirection::Left:
change_w = -diff_x;
break;
case ResizeDirection::DownLeft:
change_w = -diff_x;
change_h = diff_y;
break;
case ResizeDirection::Down:
change_h = diff_y;
break;
default:
ASSERT_NOT_REACHED();
}
auto new_rect = m_resize_window_original_rect;
// First, size the new rect.
Size minimum_size { 50, 50 };
new_rect.set_width(max(minimum_size.width(), new_rect.width() + change_w));
new_rect.set_height(max(minimum_size.height(), new_rect.height() + change_h));
if (!m_resize_window->size_increment().is_null()) {
int horizontal_incs = (new_rect.width() - m_resize_window->base_size().width()) / m_resize_window->size_increment().width();
new_rect.set_width(m_resize_window->base_size().width() + horizontal_incs * m_resize_window->size_increment().width());
int vertical_incs = (new_rect.height() - m_resize_window->base_size().height()) / m_resize_window->size_increment().height();
new_rect.set_height(m_resize_window->base_size().height() + vertical_incs * m_resize_window->size_increment().height());
}
// Second, set its position so that the sides of the window
// that end up moving are the same ones as the user is dragging,
// no matter which part of the logic above caused us to decide
// to resize by this much.
switch (m_resize_direction) {
case ResizeDirection::DownRight:
case ResizeDirection::Right:
case ResizeDirection::Down:
break;
case ResizeDirection::Left:
case ResizeDirection::Up:
case ResizeDirection::UpLeft:
new_rect.set_right_without_resize(m_resize_window_original_rect.right());
new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom());
break;
case ResizeDirection::UpRight:
new_rect.set_bottom_without_resize(m_resize_window_original_rect.bottom());
break;
case ResizeDirection::DownLeft:
new_rect.set_right_without_resize(m_resize_window_original_rect.right());
break;
default:
ASSERT_NOT_REACHED();
}
if (new_rect.contains(event.position()))
hovered_window = m_resize_window;
if (m_resize_window->rect() == new_rect)
return true;
#ifdef RESIZE_DEBUG
dbg() << "[WM] Resizing, original: " << m_resize_window_original_rect << ", now: " << new_rect;
#endif
m_resize_window->set_rect(new_rect);
CEventLoop::current().post_event(*m_resize_window, make<WSResizeEvent>(old_rect, new_rect));
return true;
}
void WSWindowManager::set_cursor_tracking_button(WSButton* button)
{
m_cursor_tracking_button = button ? button->make_weak_ptr() : nullptr;
}
auto WSWindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) -> ClickMetadata&
{
switch (button) {
case MouseButton::Left:
return m_left;
case MouseButton::Right:
return m_right;
case MouseButton::Middle:
return m_middle;
default:
ASSERT_NOT_REACHED();
}
}
// #define DOUBLECLICK_DEBUG
void WSWindowManager::process_event_for_doubleclick(WSWindow& window, WSMouseEvent& event)
{
// We only care about button presses (because otherwise it's not a doubleclick, duh!)
ASSERT(event.type() == WSEvent::MouseUp);
if (&window != m_double_click_info.m_clicked_window) {
// we either haven't clicked anywhere, or we haven't clicked on this
// window. set the current click window, and reset the timers.
#if defined(DOUBLECLICK_DEBUG)
dbg() << "Initial mouseup on window " << &window << " (previous was " << m_double_click_info.m_clicked_window << ')';
#endif
m_double_click_info.m_clicked_window = window.make_weak_ptr();
m_double_click_info.reset();
}
auto& metadata = m_double_click_info.metadata_for_button(event.button());
// if the clock is invalid, we haven't clicked with this button on this
// window yet, so there's nothing to do.
if (!metadata.clock.is_valid()) {
metadata.clock.start();
} else {
int elapsed_since_last_click = metadata.clock.elapsed();
metadata.clock.start();
if (elapsed_since_last_click < m_double_click_speed) {
auto diff = event.position() - metadata.last_position;
auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
if (distance_travelled_squared > (m_max_distance_for_double_click * m_max_distance_for_double_click)) {
// too far; try again
metadata.clock.start();
} else {
#if defined(DOUBLECLICK_DEBUG)
dbg() << "Transforming MouseUp to MouseDoubleClick (" << elapsed_since_last_click << " < " << m_double_click_speed << ")!";
#endif
event = WSMouseEvent(WSEvent::MouseDoubleClick, event.position(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta());
// invalidate this now we've delivered a doubleclick, otherwise
// tripleclick will deliver two doubleclick events (incorrectly).
metadata.clock = {};
}
} else {
// too slow; try again
metadata.clock.start();
}
}
metadata.last_position = event.position();
}
void WSWindowManager::deliver_mouse_event(WSWindow& window, WSMouseEvent& event)
{
window.dispatch_event(event);
if (event.type() == WSEvent::MouseUp) {
process_event_for_doubleclick(window, event);
if (event.type() == WSEvent::MouseDoubleClick)
window.dispatch_event(event);
}
}
void WSWindowManager::process_mouse_event(WSMouseEvent& event, WSWindow*& hovered_window)
{
hovered_window = nullptr;
if (process_ongoing_window_drag(event, hovered_window))
return;
if (process_ongoing_window_resize(event, hovered_window))
return;
if (m_cursor_tracking_button)
return m_cursor_tracking_button->on_mouse_event(event.translated(-m_cursor_tracking_button->screen_rect().location()));
// This is quite hackish, but it's how the WSButton hover effect is implemented.
if (m_hovered_button && event.type() == WSEvent::MouseMove)
m_hovered_button->on_mouse_event(event.translated(-m_hovered_button->screen_rect().location()));
HashTable<WSWindow*> windows_who_received_mouse_event_due_to_cursor_tracking;
for (auto* window = m_windows_in_order.tail(); window; window = window->prev()) {
if (!window->global_cursor_tracking())
continue;
ASSERT(window->is_visible()); // Maybe this should be supported? Idk. Let's catch it and think about it later.
ASSERT(!window->is_minimized()); // Maybe this should also be supported? Idk.
windows_who_received_mouse_event_due_to_cursor_tracking.set(window);
auto translated_event = event.translated(-window->position());
deliver_mouse_event(*window, translated_event);
}
// FIXME: Now that the menubar has a dedicated window, is this special-casing really necessary?
if (!active_window_is_modal() && menubar_rect().contains(event.position())) {
m_menu_manager.dispatch_event(event);
return;
}
if (!menu_manager().open_menu_stack().is_empty()) {
auto* topmost_menu = menu_manager().open_menu_stack().last().ptr();
ASSERT(topmost_menu);
auto* window = topmost_menu->menu_window();
ASSERT(window);
bool event_is_inside_current_menu = window->rect().contains(event.position());
if (!event_is_inside_current_menu) {
if (topmost_menu->hovered_item())
topmost_menu->clear_hovered_item();
if (event.type() == WSEvent::MouseDown || event.type() == WSEvent::MouseUp)
m_menu_manager.close_bar();
if (event.type() == WSEvent::MouseMove) {
for (auto& menu : m_menu_manager.open_menu_stack()) {
if (!menu)
continue;
if (!menu->menu_window()->rect().contains(event.position()))
continue;
hovered_window = menu->menu_window();
auto translated_event = event.translated(-menu->menu_window()->position());
deliver_mouse_event(*menu->menu_window(), translated_event);
break;
}
}
} else {
hovered_window = window;
auto translated_event = event.translated(-window->position());
deliver_mouse_event(*window, translated_event);
}
return;
}
WSWindow* event_window_with_frame = nullptr;
if (m_active_input_window) {
// At this point, we have delivered the start of an input sequence to a
// client application. We must keep delivering to that client
// application until the input sequence is done.
//
// This prevents e.g. dragging on one window out of the bounds starting
// a drag in that other unrelated window, and other silly shennanigans.
if (!windows_who_received_mouse_event_due_to_cursor_tracking.contains(m_active_input_window)) {
auto translated_event = event.translated(-m_active_input_window->position());
deliver_mouse_event(*m_active_input_window, translated_event);
windows_who_received_mouse_event_due_to_cursor_tracking.set(m_active_input_window.ptr());
}
if (event.type() == WSEvent::MouseUp && event.buttons() == 0) {
m_active_input_window = nullptr;
}
for_each_visible_window_from_front_to_back([&](auto& window) {
if (window.frame().rect().contains(event.position())) {
hovered_window = &window;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
} else {
for_each_visible_window_from_front_to_back([&](WSWindow& window) {
auto window_frame_rect = window.frame().rect();
if (!window_frame_rect.contains(event.position()))
return IterationDecision::Continue;
if (&window != m_resize_candidate.ptr())
clear_resize_candidate();
// First check if we should initiate a drag or resize (Logo+LMB or Logo+RMB).
// In those cases, the event is swallowed by the window manager.
if (window.is_movable()) {
if (!window.is_fullscreen() && m_keyboard_modifiers == Mod_Logo && event.type() == WSEvent::MouseDown && event.button() == MouseButton::Left) {
hovered_window = &window;
start_window_drag(window, event);
return IterationDecision::Break;
}
if (window.is_resizable() && m_keyboard_modifiers == Mod_Logo && event.type() == WSEvent::MouseDown && event.button() == MouseButton::Right && !window.is_blocked_by_modal_window()) {
hovered_window = &window;
start_window_resize(window, event);
return IterationDecision::Break;
}
}
if (m_keyboard_modifiers == Mod_Logo && event.type() == WSEvent::MouseWheel) {
float opacity_change = -event.wheel_delta() * 0.05f;
float new_opacity = window.opacity() + opacity_change;
if (new_opacity < 0.05f)
new_opacity = 0.05f;
if (new_opacity > 1.0f)
new_opacity = 1.0f;
window.set_opacity(new_opacity);
window.invalidate();
return IterationDecision::Break;
}
// Well okay, let's see if we're hitting the frame or the window inside the frame.
if (window.rect().contains(event.position())) {
if (window.type() == WSWindowType::Normal && event.type() == WSEvent::MouseDown)
move_to_front_and_make_active(window);
hovered_window = &window;
if (!window.global_cursor_tracking() && !windows_who_received_mouse_event_due_to_cursor_tracking.contains(&window)) {
auto translated_event = event.translated(-window.position());
deliver_mouse_event(window, translated_event);
if (event.type() == WSEvent::MouseDown) {
m_active_input_window = window.make_weak_ptr();
}
}
return IterationDecision::Break;
}
// We are hitting the frame, pass the event along to WSWindowFrame.
window.frame().on_mouse_event(event.translated(-window_frame_rect.location()));
event_window_with_frame = &window;
return IterationDecision::Break;
});
}
if (event_window_with_frame != m_resize_candidate.ptr())
clear_resize_candidate();
}
void WSWindowManager::clear_resize_candidate()
{
if (m_resize_candidate)
WSCompositor::the().invalidate_cursor();
m_resize_candidate = nullptr;
}
bool WSWindowManager::any_opaque_window_contains_rect(const Rect& rect)
{
bool found_containing_window = false;
for_each_window([&](WSWindow& window) {
if (!window.is_visible())
return IterationDecision::Continue;
if (window.is_minimized())
return IterationDecision::Continue;
if (window.opacity() < 1.0f)
return IterationDecision::Continue;
if (window.has_alpha_channel()) {
// FIXME: Just because the window has an alpha channel doesn't mean it's not opaque.
// Maybe there's some way we could know this?
return IterationDecision::Continue;
}
if (window.frame().rect().contains(rect)) {
found_containing_window = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return found_containing_window;
};
bool WSWindowManager::any_opaque_window_above_this_one_contains_rect(const WSWindow& a_window, const Rect& rect)
{
bool found_containing_window = false;
bool checking = false;
for_each_visible_window_from_back_to_front([&](WSWindow& window) {
if (&window == &a_window) {
checking = true;
return IterationDecision::Continue;
}
if (!checking)
return IterationDecision::Continue;
if (!window.is_visible())
return IterationDecision::Continue;
if (window.is_minimized())
return IterationDecision::Continue;
if (window.opacity() < 1.0f)
return IterationDecision::Continue;
if (window.has_alpha_channel())
return IterationDecision::Continue;
if (window.frame().rect().contains(rect)) {
found_containing_window = true;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return found_containing_window;
};
Rect WSWindowManager::menubar_rect() const
{
if (active_fullscreen_window())
return {};
return m_menu_manager.menubar_rect();
}
void WSWindowManager::draw_window_switcher()
{
if (m_switcher.is_visible())
m_switcher.draw();
}
void WSWindowManager::event(CEvent& event)
{
if (static_cast<WSEvent&>(event).is_mouse_event()) {
WSWindow* hovered_window = nullptr;
process_mouse_event(static_cast<WSMouseEvent&>(event), hovered_window);
set_hovered_window(hovered_window);
return;
}
if (static_cast<WSEvent&>(event).is_key_event()) {
auto& key_event = static_cast<const WSKeyEvent&>(event);
m_keyboard_modifiers = key_event.modifiers();
if (key_event.type() == WSEvent::KeyDown && key_event.modifiers() == Mod_Logo && key_event.key() == Key_Tab)
m_switcher.show();
if (m_switcher.is_visible()) {
m_switcher.on_key_event(key_event);
return;
}
if (m_active_window)
return m_active_window->dispatch_event(event);
return;
}
CObject::event(event);
}
void WSWindowManager::set_highlight_window(WSWindow* window)
{
if (window == m_highlight_window)
return;
if (auto* previous_highlight_window = m_highlight_window.ptr())
invalidate(*previous_highlight_window);
m_highlight_window = window ? window->make_weak_ptr() : nullptr;
if (m_highlight_window)
invalidate(*m_highlight_window);
}
void WSWindowManager::set_active_window(WSWindow* window)
{
if (window && window->is_blocked_by_modal_window())
return;
if (window->type() != WSWindowType::Normal)
return;
if (window == m_active_window)
return;
auto* previously_active_window = m_active_window.ptr();
if (previously_active_window) {
CEventLoop::current().post_event(*previously_active_window, make<WSEvent>(WSEvent::WindowDeactivated));
invalidate(*previously_active_window);
}
m_active_window = window->make_weak_ptr();
if (m_active_window) {
CEventLoop::current().post_event(*m_active_window, make<WSEvent>(WSEvent::WindowActivated));
invalidate(*m_active_window);
auto* client = window->client();
ASSERT(client);
set_current_menubar(client->app_menubar());
if (previously_active_window)
tell_wm_listeners_window_state_changed(*previously_active_window);
tell_wm_listeners_window_state_changed(*m_active_window);
}
}
void WSWindowManager::set_hovered_window(WSWindow* window)
{
if (m_hovered_window == window)
return;
if (m_hovered_window)
CEventLoop::current().post_event(*m_hovered_window, make<WSEvent>(WSEvent::WindowLeft));
m_hovered_window = window ? window->make_weak_ptr() : nullptr;
if (m_hovered_window)
CEventLoop::current().post_event(*m_hovered_window, make<WSEvent>(WSEvent::WindowEntered));
}
void WSWindowManager::invalidate()
{
WSCompositor::the().invalidate();
}
void WSWindowManager::invalidate(const Rect& rect)
{
WSCompositor::the().invalidate(rect);
}
void WSWindowManager::invalidate(const WSWindow& window)
{
invalidate(window.frame().rect());
}
void WSWindowManager::invalidate(const WSWindow& window, const Rect& rect)
{
if (rect.is_empty()) {
invalidate(window);
return;
}
auto outer_rect = window.frame().rect();
auto inner_rect = rect;
inner_rect.move_by(window.position());
// FIXME: This seems slightly wrong; the inner rect shouldn't intersect the border part of the outer rect.
inner_rect.intersect(outer_rect);
invalidate(inner_rect);
}
void WSWindowManager::close_menubar(WSMenuBar& menubar)
{
if (current_menubar() == &menubar)
set_current_menubar(nullptr);
}
const WSClientConnection* WSWindowManager::active_client() const
{
if (m_active_window)
return m_active_window->client();
return nullptr;
}
void WSWindowManager::notify_client_changed_app_menubar(WSClientConnection& client)
{
if (active_client() == &client)
set_current_menubar(client.app_menubar());
m_menu_manager.refresh();
}
const WSCursor& WSWindowManager::active_cursor() const
{
if (m_drag_window)
return *m_move_cursor;
if (m_resize_window || m_resize_candidate) {
switch (m_resize_direction) {
case ResizeDirection::Up:
case ResizeDirection::Down:
return *m_resize_vertically_cursor;
case ResizeDirection::Left:
case ResizeDirection::Right:
return *m_resize_horizontally_cursor;
case ResizeDirection::UpLeft:
case ResizeDirection::DownRight:
return *m_resize_diagonally_tlbr_cursor;
case ResizeDirection::UpRight:
case ResizeDirection::DownLeft:
return *m_resize_diagonally_bltr_cursor;
case ResizeDirection::None:
break;
}
}
if (m_hovered_window && m_hovered_window->override_cursor())
return *m_hovered_window->override_cursor();
return *m_arrow_cursor;
}
void WSWindowManager::set_hovered_button(WSButton* button)
{
m_hovered_button = button ? button->make_weak_ptr() : nullptr;
}
void WSWindowManager::set_resize_candidate(WSWindow& window, ResizeDirection direction)
{
m_resize_candidate = window.make_weak_ptr();
m_resize_direction = direction;
}
ResizeDirection WSWindowManager::resize_direction_of_window(const WSWindow& window)
{
if (&window != m_resize_window)
return ResizeDirection::None;
return m_resize_direction;
}
Rect WSWindowManager::maximized_window_rect(const WSWindow& window) const
{
Rect rect = WSScreen::the().rect();
// Subtract window title bar (leaving the border)
rect.set_y(rect.y() + window.frame().title_bar_rect().height());
rect.set_height(rect.height() - window.frame().title_bar_rect().height());
// Subtract menu bar
rect.set_y(rect.y() + menubar_rect().height());
rect.set_height(rect.height() - menubar_rect().height());
// Subtract taskbar window height if present
const_cast<WSWindowManager*>(this)->for_each_visible_window_of_type_from_back_to_front(WSWindowType::Taskbar, [&rect](WSWindow& taskbar_window) {
rect.set_height(rect.height() - taskbar_window.height());
return IterationDecision::Break;
});
return rect;
}
WSMenu* WSWindowManager::find_internal_menu_by_id(int menu_id)
{
for (auto& it : m_app_category_menus) {
if (menu_id == it.value->menu_id())
return it.value;
}
return nullptr;
}