ladybird/Servers/WindowServer/WSMenuManager.cpp
Andreas Kling 9009390f9c WindowServer: Add an audio icon to the menu bar
Clicking on this icon toggles the AudioServer muted state.

It currently does not react to muted state changes caused by other
programs, since it has no way of learning about those from AudioServer,
other than performing a synchronous IPC call (GetMuted), which we don't
want to be doing in the WindowServer :^)
2019-11-22 22:15:39 +01:00

279 lines
8.5 KiB
C++

#include <LibAudio/AClientConnection.h>
#include <LibCore/CTimer.h>
#include <LibDraw/Font.h>
#include <LibDraw/Painter.h>
#include <WindowServer/WSMenuManager.h>
#include <WindowServer/WSWindowManager.h>
#include <time.h>
#include <unistd.h>
WSMenuManager::WSMenuManager()
{
m_audio_client = make<AClientConnection>();
m_unmuted_bitmap = GraphicsBitmap::load_from_file("/res/icons/audio-unmuted.png");
m_muted_bitmap = GraphicsBitmap::load_from_file("/res/icons/audio-muted.png");
m_username = getlogin();
m_needs_window_resize = false;
m_timer = CTimer::construct(300, [this] {
static time_t last_update_time;
time_t now = time(nullptr);
if (now != last_update_time || m_cpu_monitor.is_dirty()) {
tick_clock();
last_update_time = now;
m_cpu_monitor.set_dirty(false);
}
});
}
WSMenuManager::~WSMenuManager()
{
}
void WSMenuManager::setup()
{
m_window = WSWindow::construct(*this, WSWindowType::Menubar);
m_window->set_rect(WSWindowManager::the().menubar_rect());
}
bool WSMenuManager::is_open(const WSMenu& menu) const
{
for (int i = 0; i < m_open_menu_stack.size(); ++i) {
if (&menu == m_open_menu_stack[i].ptr())
return true;
}
return false;
}
void WSMenuManager::draw()
{
auto& wm = WSWindowManager::the();
auto menubar_rect = wm.menubar_rect();
if (m_needs_window_resize) {
m_window->set_rect(menubar_rect);
m_needs_window_resize = false;
}
Painter painter(*window().backing_store());
painter.fill_rect(menubar_rect, Color::WarmGray);
painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, Color::MidGray);
int index = 0;
wm.for_each_active_menubar_menu([&](WSMenu& menu) {
Color text_color = Color::Black;
if (is_open(menu)) {
painter.fill_rect(menu.rect_in_menubar(), Color::from_rgb(0xad714f));
painter.draw_rect(menu.rect_in_menubar(), Color::from_rgb(0x793016));
text_color = Color::White;
}
painter.draw_text(
menu.text_rect_in_menubar(),
menu.name(),
index == 1 ? wm.app_menu_font() : wm.menu_font(),
TextAlignment::CenterLeft,
text_color);
++index;
return true;
});
int username_width = Font::default_bold_font().width(m_username);
// FIXME: This rect should only be computed once.
Rect username_rect {
menubar_rect.right() - wm.menubar_menu_margin() / 2 - Font::default_bold_font().width(m_username),
menubar_rect.y(),
username_width,
menubar_rect.height()
};
painter.draw_text(username_rect, m_username, Font::default_bold_font(), TextAlignment::CenterRight, Color::Black);
time_t now = time(nullptr);
auto* tm = localtime(&now);
auto time_text = String::format("%4u-%02u-%02u %02u:%02u:%02u",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec);
int time_width = wm.font().width(time_text);
// FIXME: This rect should only be computed once.
Rect time_rect {
username_rect.left() - wm.menubar_menu_margin() / 2 - time_width,
menubar_rect.y(),
time_width,
menubar_rect.height()
};
painter.draw_text(time_rect, time_text, wm.font(), TextAlignment::CenterRight, Color::Black);
// FIXME: This rect should only be computed once.
Rect cpu_rect { time_rect.right() - wm.font().width(time_text) - m_cpu_monitor.capacity() - 10, time_rect.y() + 1, m_cpu_monitor.capacity(), time_rect.height() - 2 };
m_cpu_monitor.paint(painter, cpu_rect);
// FIXME: This rect should only be computed once.
m_audio_rect = { cpu_rect.left() - 20, cpu_rect.y(), 12, 16 };
auto& audio_bitmap = m_audio_muted ? *m_muted_bitmap : *m_unmuted_bitmap;
painter.blit(m_audio_rect.location(), audio_bitmap, audio_bitmap.rect());
}
void WSMenuManager::tick_clock()
{
refresh();
}
void WSMenuManager::refresh()
{
if (!m_window)
return;
draw();
window().invalidate();
}
void WSMenuManager::event(CEvent& event)
{
if (WSWindowManager::the().active_window_is_modal())
return CObject::event(event);
if (event.type() == WSEvent::MouseMove || event.type() == WSEvent::MouseUp || event.type() == WSEvent::MouseDown || event.type() == WSEvent::MouseWheel) {
auto& mouse_event = static_cast<WSMouseEvent&>(event);
WSWindowManager::the().for_each_active_menubar_menu([&](WSMenu& menu) {
if (menu.rect_in_menubar().contains(mouse_event.position())) {
handle_menu_mouse_event(menu, mouse_event);
return false;
}
return true;
});
if (mouse_event.type() == WSEvent::MouseDown
&& mouse_event.button() == MouseButton::Left
&& m_audio_rect.contains(mouse_event.position())) {
// FIXME: This should listen for notifications from the AudioServer, once those actually exist.
// Right now, we won't notice if another program changes the AudioServer muted state.
m_audio_muted = !m_audio_muted;
m_audio_client->set_muted(m_audio_muted);
draw();
m_window->invalidate();
}
}
return CObject::event(event);
}
void WSMenuManager::handle_menu_mouse_event(WSMenu& menu, const WSMouseEvent& event)
{
auto& wm = WSWindowManager::the();
bool is_hover_with_any_menu_open = event.type() == WSMouseEvent::MouseMove
&& !m_open_menu_stack.is_empty()
&& (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == wm.system_menu());
bool is_mousedown_with_left_button = event.type() == WSMouseEvent::MouseDown && event.button() == MouseButton::Left;
bool should_open_menu = &menu != m_current_menu && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
if (is_mousedown_with_left_button)
m_bar_open = !m_bar_open;
if (should_open_menu && m_bar_open) {
if (m_current_menu == &menu)
return;
close_everyone();
if (!menu.is_empty()) {
auto& menu_window = menu.ensure_menu_window();
menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 });
menu_window.set_visible(true);
}
set_current_menu(&menu);
refresh();
return;
}
if (!m_bar_open)
close_everyone();
}
void WSMenuManager::set_needs_window_resize()
{
m_needs_window_resize = true;
}
void WSMenuManager::close_everyone()
{
for (auto& menu : m_open_menu_stack) {
if (menu && menu->menu_window())
menu->menu_window()->set_visible(false);
}
m_open_menu_stack.clear();
m_current_menu = nullptr;
refresh();
}
void WSMenuManager::close_everyone_not_in_lineage(WSMenu& menu)
{
Vector<WSMenu*> menus_to_close;
for (auto& open_menu : m_open_menu_stack) {
if (!open_menu)
continue;
if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
continue;
menus_to_close.append(open_menu);
}
close_menus(menus_to_close);
}
void WSMenuManager::close_menus(const Vector<WSMenu*>& menus)
{
for (auto& menu : menus) {
if (menu == m_current_menu)
m_current_menu = nullptr;
if (menu->menu_window())
menu->menu_window()->set_visible(false);
m_open_menu_stack.remove_first_matching([&](auto& entry) {
return entry == menu;
});
}
refresh();
}
static void collect_menu_subtree(WSMenu& menu, Vector<WSMenu*>& menus)
{
menus.append(&menu);
for (int i = 0; i < menu.item_count(); ++i) {
auto& item = menu.item(i);
if (!item.is_submenu())
continue;
collect_menu_subtree(*const_cast<WSMenuItem&>(item).submenu(), menus);
}
}
void WSMenuManager::close_menu_and_descendants(WSMenu& menu)
{
Vector<WSMenu*> menus_to_close;
collect_menu_subtree(menu, menus_to_close);
close_menus(menus_to_close);
}
void WSMenuManager::set_current_menu(WSMenu* menu, bool is_submenu)
{
if (!is_submenu && m_current_menu)
m_current_menu->close();
if (menu)
m_current_menu = menu->make_weak_ptr();
if (!is_submenu) {
close_everyone();
if (menu)
m_open_menu_stack.append(menu->make_weak_ptr());
} else {
m_open_menu_stack.append(menu->make_weak_ptr());
}
}
void WSMenuManager::close_bar()
{
close_everyone();
m_bar_open = false;
}