1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 05:57:45 +00:00

Libraries: Move to Userland/Libraries/

This commit is contained in:
Andreas Kling 2021-01-12 12:17:30 +01:00
parent dc28c07fa5
commit 13d7c09125
1857 changed files with 266 additions and 274 deletions

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <LibCore/ConfigFile.h>
#include <LibGUI/AboutDialog.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Label.h>
#include <LibGUI/Widget.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
namespace GUI {
AboutDialog::AboutDialog(const StringView& name, const Gfx::Bitmap* icon, Window* parent_window)
: Dialog(parent_window)
, m_name(name)
, m_icon(icon)
{
resize(413, 205);
set_title(String::formatted("About {}", m_name));
set_resizable(false);
if (parent_window)
set_icon(parent_window->icon());
auto& widget = set_main_widget<Widget>();
widget.set_fill_with_background_color(true);
widget.set_layout<VerticalBoxLayout>();
widget.layout()->set_spacing(0);
auto& banner_image = widget.add<GUI::ImageWidget>();
banner_image.load_from_file("/res/graphics/brand-banner.png");
auto& content_container = widget.add<Widget>();
content_container.set_layout<HorizontalBoxLayout>();
auto& left_container = content_container.add<Widget>();
left_container.set_fixed_width(60);
left_container.set_layout<VerticalBoxLayout>();
left_container.layout()->set_margins({ 0, 12, 0, 0 });
if (icon) {
auto& icon_wrapper = left_container.add<Widget>();
icon_wrapper.set_fixed_size(32, 48);
icon_wrapper.set_layout<VerticalBoxLayout>();
auto& icon_image = icon_wrapper.add<ImageWidget>();
icon_image.set_bitmap(m_icon);
}
auto& right_container = content_container.add<Widget>();
right_container.set_layout<VerticalBoxLayout>();
right_container.layout()->set_margins({ 0, 12, 12, 8 });
auto make_label = [&](const StringView& text, bool bold = false) {
auto& label = right_container.add<Label>(text);
label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
label.set_fixed_height(14);
if (bold)
label.set_font(Gfx::FontDatabase::default_bold_font());
};
make_label(m_name, true);
// If we are displaying a dialog for an application, insert 'SerenityOS' below the application name
if (m_name != "SerenityOS")
make_label("SerenityOS");
make_label(version_string());
make_label("Copyright \xC2\xA9 the SerenityOS developers, 2018-2021");
right_container.layout()->add_spacer();
auto& button_container = right_container.add<Widget>();
button_container.set_fixed_height(23);
button_container.set_layout<HorizontalBoxLayout>();
button_container.layout()->add_spacer();
auto& ok_button = button_container.add<Button>("OK");
ok_button.set_fixed_size(80, 23);
ok_button.on_click = [this](auto) {
done(Dialog::ExecOK);
};
}
AboutDialog::~AboutDialog()
{
}
String AboutDialog::version_string() const
{
auto version_config = Core::ConfigFile::open("/res/version.ini");
auto major_version = version_config->read_entry("Version", "Major", "0");
auto minor_version = version_config->read_entry("Version", "Minor", "0");
auto git_version = version_config->read_entry("Version", "Git", "");
StringBuilder builder;
builder.append("Version ");
builder.append(major_version);
builder.append('.');
builder.append(minor_version);
if (git_version != "") {
builder.append(".g");
builder.append(git_version);
}
return builder.to_string();
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Dialog.h>
namespace GUI {
class AboutDialog final : public Dialog {
C_OBJECT(AboutDialog)
public:
virtual ~AboutDialog() override;
static void show(const StringView& name, const Gfx::Bitmap* icon = nullptr, Window* parent_window = nullptr, const Gfx::Bitmap* window_icon = nullptr)
{
auto dialog = AboutDialog::construct(name, icon, parent_window);
if (window_icon)
dialog->set_icon(window_icon);
dialog->exec();
}
private:
AboutDialog(const StringView& name, const Gfx::Bitmap* icon = nullptr, Window* parent_window = nullptr);
String version_string() const;
String m_name;
RefPtr<Gfx::Bitmap> m_icon;
};
}

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/JsonObject.h>
#include <LibCore/Timer.h>
#include <LibGUI/AbstractButton.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
namespace GUI {
AbstractButton::AbstractButton(String text)
{
set_text(move(text));
set_focus_policy(GUI::FocusPolicy::StrongFocus);
set_background_role(Gfx::ColorRole::Button);
set_foreground_role(Gfx::ColorRole::ButtonText);
m_auto_repeat_timer = add<Core::Timer>();
m_auto_repeat_timer->on_timeout = [this] {
click();
};
REGISTER_STRING_PROPERTY("text", text, set_text);
REGISTER_BOOL_PROPERTY("checked", is_checked, set_checked);
REGISTER_BOOL_PROPERTY("checkable", is_checkable, set_checkable);
REGISTER_BOOL_PROPERTY("exclusive", is_exclusive, set_exclusive);
}
AbstractButton::~AbstractButton()
{
}
void AbstractButton::set_text(String text)
{
if (m_text == text)
return;
m_text = move(text);
update();
}
void AbstractButton::set_checked(bool checked)
{
if (m_checked == checked)
return;
m_checked = checked;
if (is_exclusive() && checked && parent_widget()) {
bool sibling_had_focus = false;
parent_widget()->for_each_child_of_type<AbstractButton>([&](auto& sibling) {
if (!sibling.is_exclusive())
return IterationDecision::Continue;
if (window() && window()->focused_widget() == &sibling)
sibling_had_focus = true;
if (!sibling.is_checked())
return IterationDecision::Continue;
sibling.m_checked = false;
sibling.update();
if (sibling.on_checked)
sibling.on_checked(false);
return IterationDecision::Continue;
});
m_checked = true;
if (sibling_had_focus)
set_focus(true);
}
update();
if (on_checked)
on_checked(checked);
}
void AbstractButton::set_checkable(bool checkable)
{
if (m_checkable == checkable)
return;
m_checkable = checkable;
update();
}
void AbstractButton::mousemove_event(MouseEvent& event)
{
bool is_over = rect().contains(event.position());
m_hovered = is_over;
if (event.buttons() & MouseButton::Left) {
bool being_pressed = is_over;
if (being_pressed != m_being_pressed) {
m_being_pressed = being_pressed;
if (m_auto_repeat_interval) {
if (!m_being_pressed)
m_auto_repeat_timer->stop();
else
m_auto_repeat_timer->start(m_auto_repeat_interval);
}
update();
}
}
Widget::mousemove_event(event);
}
void AbstractButton::mousedown_event(MouseEvent& event)
{
if (event.button() == MouseButton::Left) {
m_being_pressed = true;
update();
if (m_auto_repeat_interval) {
click();
m_auto_repeat_timer->start(m_auto_repeat_interval);
}
}
Widget::mousedown_event(event);
}
void AbstractButton::mouseup_event(MouseEvent& event)
{
if (event.button() == MouseButton::Left) {
bool was_auto_repeating = m_auto_repeat_timer->is_active();
m_auto_repeat_timer->stop();
bool was_being_pressed = m_being_pressed;
m_being_pressed = false;
update();
if (was_being_pressed && !was_auto_repeating)
click(event.modifiers());
}
Widget::mouseup_event(event);
}
void AbstractButton::enter_event(Core::Event&)
{
m_hovered = true;
update();
}
void AbstractButton::leave_event(Core::Event&)
{
m_hovered = false;
update();
}
void AbstractButton::keydown_event(KeyEvent& event)
{
if (event.key() == KeyCode::Key_Return || event.key() == KeyCode::Key_Space) {
click(event.modifiers());
event.accept();
return;
}
Widget::keydown_event(event);
}
void AbstractButton::paint_text(Painter& painter, const Gfx::IntRect& rect, const Gfx::Font& font, Gfx::TextAlignment text_alignment)
{
auto clipped_rect = rect.intersected(this->rect());
if (!is_enabled()) {
painter.draw_text(clipped_rect.translated(1, 1), text(), font, text_alignment, Color::White, Gfx::TextElision::Right);
painter.draw_text(clipped_rect, text(), font, text_alignment, Color::from_rgb(0x808080), Gfx::TextElision::Right);
return;
}
if (text().is_empty())
return;
painter.draw_text(clipped_rect, text(), font, text_alignment, palette().color(foreground_role()), Gfx::TextElision::Right);
}
void AbstractButton::change_event(Event& event)
{
if (event.type() == Event::Type::EnabledChange) {
if (!is_enabled()) {
bool was_being_pressed = m_being_pressed;
m_being_pressed = false;
if (was_being_pressed)
update();
}
}
Widget::change_event(event);
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace GUI {
class AbstractButton : public Widget {
C_OBJECT_ABSTRACT(AbstractButton);
public:
virtual ~AbstractButton() override;
Function<void(bool)> on_checked;
void set_text(String);
const String& text() const { return m_text; }
bool is_exclusive() const { return m_exclusive; }
void set_exclusive(bool b) { m_exclusive = b; }
bool is_checked() const { return m_checked; }
void set_checked(bool);
bool is_checkable() const { return m_checkable; }
void set_checkable(bool);
bool is_hovered() const { return m_hovered; }
bool is_being_pressed() const { return m_being_pressed; }
virtual void click(unsigned modifiers = 0) = 0;
virtual bool is_uncheckable() const { return true; }
int auto_repeat_interval() const { return m_auto_repeat_interval; }
void set_auto_repeat_interval(int interval) { m_auto_repeat_interval = interval; }
protected:
explicit AbstractButton(String = {});
virtual void mousedown_event(MouseEvent&) override;
virtual void mousemove_event(MouseEvent&) override;
virtual void mouseup_event(MouseEvent&) override;
virtual void keydown_event(KeyEvent&) override;
virtual void enter_event(Core::Event&) override;
virtual void leave_event(Core::Event&) override;
virtual void change_event(Event&) override;
void paint_text(Painter&, const Gfx::IntRect&, const Gfx::Font&, Gfx::TextAlignment);
private:
String m_text;
bool m_checked { false };
bool m_checkable { false };
bool m_hovered { false };
bool m_being_pressed { false };
bool m_exclusive { false };
int m_auto_repeat_interval { 0 };
RefPtr<Core::Timer> m_auto_repeat_timer;
};
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Assertions.h>
#include <AK/StdLibExtras.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Slider.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
namespace GUI {
AbstractSlider::AbstractSlider(Orientation orientation)
: m_orientation(orientation)
{
REGISTER_INT_PROPERTY("value", value, set_value);
REGISTER_INT_PROPERTY("min", min, set_min);
REGISTER_INT_PROPERTY("max", max, set_max);
REGISTER_INT_PROPERTY("step", step, set_step);
REGISTER_INT_PROPERTY("page_step", page_step, set_page_step);
REGISTER_ENUM_PROPERTY("orientation", this->orientation, set_orientation, Orientation,
{ Orientation::Horizontal, "Horizontal" },
{ Orientation::Vertical, "Vertical" });
}
AbstractSlider::~AbstractSlider()
{
}
void AbstractSlider::set_orientation(Orientation value)
{
if (m_orientation == value)
return;
m_orientation = value;
update();
}
void AbstractSlider::set_page_step(int page_step)
{
m_page_step = AK::max(0, page_step);
}
void AbstractSlider::set_range(int min, int max)
{
ASSERT(min <= max);
if (m_min == min && m_max == max)
return;
m_min = min;
m_max = max;
m_value = clamp(m_value, m_min, m_max);
update();
}
void AbstractSlider::set_value(int value)
{
value = clamp(value, m_min, m_max);
if (m_value == value)
return;
m_value = value;
update();
if (on_change)
on_change(m_value);
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace GUI {
class AbstractSlider : public Widget {
C_OBJECT_ABSTRACT(AbstractSlider);
public:
virtual ~AbstractSlider() override;
void set_orientation(Orientation value);
Orientation orientation() const { return m_orientation; }
int value() const { return m_value; }
int min() const { return m_min; }
int max() const { return m_max; }
int step() const { return m_step; }
int page_step() const { return m_page_step; }
void set_range(int min, int max);
void set_value(int);
void set_min(int min) { set_range(min, max()); }
void set_max(int max) { set_range(min(), max); }
void set_step(int step) { m_step = step; }
void set_page_step(int page_step);
Function<void(int)> on_change;
protected:
explicit AbstractSlider(Orientation = Orientation::Vertical);
private:
void set_knob_hovered(bool);
int m_value { 0 };
int m_min { 0 };
int m_max { 0 };
int m_step { 1 };
int m_page_step { 10 };
Orientation m_orientation { Orientation::Horizontal };
};
}

View file

@ -0,0 +1,403 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <AK/Vector.h>
#include <LibGUI/AbstractTableView.h>
#include <LibGUI/Action.h>
#include <LibGUI/Button.h>
#include <LibGUI/HeaderView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
namespace GUI {
AbstractTableView::AbstractTableView()
{
set_selection_behavior(SelectionBehavior::SelectRows);
m_corner_button = add<Button>();
m_corner_button->move_to_back();
m_corner_button->set_background_role(Gfx::ColorRole::ThreedShadow1);
m_corner_button->set_fill_with_background_color(true);
m_column_header = add<HeaderView>(*this, Gfx::Orientation::Horizontal);
m_column_header->move_to_back();
m_row_header = add<HeaderView>(*this, Gfx::Orientation::Vertical);
m_row_header->move_to_back();
m_row_header->set_visible(false);
set_should_hide_unnecessary_scrollbars(true);
}
AbstractTableView::~AbstractTableView()
{
}
void AbstractTableView::select_all()
{
selection().clear();
for (int item_index = 0; item_index < item_count(); ++item_index) {
auto index = model()->index(item_index);
selection().add(index);
}
}
void AbstractTableView::update_column_sizes()
{
if (!model())
return;
auto& model = *this->model();
int column_count = model.column_count();
int row_count = model.row_count();
for (int column = 0; column < column_count; ++column) {
if (!column_header().is_section_visible(column))
continue;
int header_width = m_column_header->font().width(model.column_name(column));
if (column == m_key_column && model.is_column_sortable(column))
header_width += font().width(" \xE2\xAC\x86"); // UPWARDS BLACK ARROW
int column_width = header_width;
for (int row = 0; row < row_count; ++row) {
auto cell_data = model.index(row, column).data();
int cell_width = 0;
if (cell_data.is_icon()) {
cell_width = row_height();
} else if (cell_data.is_bitmap()) {
cell_width = cell_data.as_bitmap().width();
} else if (cell_data.is_valid()) {
cell_width = font().width(cell_data.to_string());
}
column_width = max(column_width, cell_width);
}
column_header().set_section_size(column, max(m_column_header->section_size(column), column_width));
}
}
void AbstractTableView::update_row_sizes()
{
if (!model())
return;
auto& model = *this->model();
int row_count = model.row_count();
for (int row = 0; row < row_count; ++row) {
if (!column_header().is_section_visible(row))
continue;
row_header().set_section_size(row, row_height());
}
}
void AbstractTableView::update_content_size()
{
if (!model())
return set_content_size({});
int content_width = 0;
int column_count = model()->column_count();
for (int i = 0; i < column_count; ++i) {
if (column_header().is_section_visible(i))
content_width += column_width(i) + horizontal_padding() * 2;
}
int content_height = item_count() * row_height();
set_content_size({ content_width, content_height });
set_size_occupied_by_fixed_elements({ row_header().width(), column_header().height() });
layout_headers();
}
TableCellPaintingDelegate* AbstractTableView::column_painting_delegate(int column) const
{
// FIXME: This should return a const pointer I think..
return const_cast<TableCellPaintingDelegate*>(m_column_painting_delegate.get(column).value_or(nullptr));
}
void AbstractTableView::set_column_painting_delegate(int column, OwnPtr<TableCellPaintingDelegate> delegate)
{
if (!delegate)
m_column_painting_delegate.remove(column);
else
m_column_painting_delegate.set(column, move(delegate));
}
int AbstractTableView::column_width(int column_index) const
{
if (!model())
return 0;
return m_column_header->section_size(column_index);
}
void AbstractTableView::set_column_width(int column, int width)
{
column_header().set_section_size(column, width);
}
Gfx::TextAlignment AbstractTableView::column_header_alignment(int column_index) const
{
if (!model())
return Gfx::TextAlignment::CenterLeft;
return m_column_header->section_alignment(column_index);
}
void AbstractTableView::set_column_header_alignment(int column, Gfx::TextAlignment alignment)
{
column_header().set_section_alignment(column, alignment);
}
void AbstractTableView::mousedown_event(MouseEvent& event)
{
if (!model())
return AbstractView::mousedown_event(event);
if (event.button() != MouseButton::Left)
return AbstractView::mousedown_event(event);
bool is_toggle;
auto index = index_at_event_position(event.position(), is_toggle);
if (index.is_valid() && is_toggle && model()->row_count(index)) {
toggle_index(index);
return;
}
AbstractView::mousedown_event(event);
}
ModelIndex AbstractTableView::index_at_event_position(const Gfx::IntPoint& position, bool& is_toggle) const
{
is_toggle = false;
if (!model())
return {};
auto adjusted_position = this->adjusted_position(position);
for (int row = 0, row_count = model()->row_count(); row < row_count; ++row) {
if (!row_rect(row).contains(adjusted_position))
continue;
for (int column = 0, column_count = model()->column_count(); column < column_count; ++column) {
if (!content_rect(row, column).contains(adjusted_position))
continue;
return model()->index(row, column);
}
return model()->index(row, 0);
}
return {};
}
ModelIndex AbstractTableView::index_at_event_position(const Gfx::IntPoint& position) const
{
bool is_toggle;
auto index = index_at_event_position(position, is_toggle);
return is_toggle ? ModelIndex() : index;
}
int AbstractTableView::item_count() const
{
if (!model())
return 0;
return model()->row_count();
}
void AbstractTableView::move_cursor_relative(int vertical_steps, int horizontal_steps, SelectionUpdate selection_update)
{
if (!model())
return;
auto& model = *this->model();
ModelIndex new_index;
if (cursor_index().is_valid()) {
new_index = model.index(cursor_index().row() + vertical_steps, cursor_index().column() + horizontal_steps);
} else {
new_index = model.index(0, 0);
}
set_cursor(new_index, selection_update);
}
void AbstractTableView::scroll_into_view(const ModelIndex& index, bool scroll_horizontally, bool scroll_vertically)
{
Gfx::IntRect rect;
switch (selection_behavior()) {
case SelectionBehavior::SelectItems:
rect = content_rect(index);
break;
case SelectionBehavior::SelectRows:
rect = row_rect(index.row());
break;
}
ScrollableWidget::scroll_into_view(rect, scroll_horizontally, scroll_vertically);
}
void AbstractTableView::context_menu_event(ContextMenuEvent& event)
{
if (!model())
return;
bool is_toggle;
auto index = index_at_event_position(event.position(), is_toggle);
if (index.is_valid()) {
if (!selection().contains(index))
selection().set(index);
} else {
selection().clear();
}
if (on_context_menu_request)
on_context_menu_request(index, event);
}
Gfx::IntRect AbstractTableView::content_rect(int row, int column) const
{
auto row_rect = this->row_rect(row);
int x = 0;
for (int i = 0; i < column; ++i)
x += column_width(i) + horizontal_padding() * 2;
return { row_rect.x() + x, row_rect.y(), column_width(column) + horizontal_padding() * 2, row_height() };
}
Gfx::IntRect AbstractTableView::content_rect(const ModelIndex& index) const
{
return content_rect(index.row(), index.column());
}
Gfx::IntRect AbstractTableView::row_rect(int item_index) const
{
return { row_header().is_visible() ? row_header().width() : 0,
(column_header().is_visible() ? column_header().height() : 0) + (item_index * row_height()),
max(content_size().width(), width()),
row_height() };
}
Gfx::IntPoint AbstractTableView::adjusted_position(const Gfx::IntPoint& position) const
{
return position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
}
void AbstractTableView::model_did_update(unsigned flags)
{
AbstractView::model_did_update(flags);
update_row_sizes();
update_column_sizes();
update_content_size();
update();
}
void AbstractTableView::resize_event(ResizeEvent& event)
{
AbstractView::resize_event(event);
layout_headers();
}
void AbstractTableView::header_did_change_section_size(Badge<HeaderView>, Gfx::Orientation, int, int)
{
update_content_size();
update();
}
void AbstractTableView::header_did_change_section_visibility(Badge<HeaderView>, Gfx::Orientation, int, bool)
{
update_content_size();
update();
}
void AbstractTableView::set_column_hidden(int column, bool hidden)
{
column_header().set_section_visible(column, !hidden);
}
void AbstractTableView::set_column_headers_visible(bool visible)
{
column_header().set_visible(visible);
}
void AbstractTableView::did_scroll()
{
AbstractView::did_scroll();
layout_headers();
}
void AbstractTableView::layout_headers()
{
if (column_header().is_visible()) {
int row_header_width = row_header().is_visible() ? row_header().width() : 0;
int vertical_scrollbar_width = vertical_scrollbar().is_visible() ? vertical_scrollbar().width() : 0;
int x = frame_thickness() + row_header_width - horizontal_scrollbar().value();
int y = frame_thickness();
int width = AK::max(content_width(), rect().width() - frame_thickness() * 2 - row_header_width - vertical_scrollbar_width);
column_header().set_relative_rect(x, y, width, column_header().min_size().height());
}
if (row_header().is_visible()) {
int column_header_height = column_header().is_visible() ? column_header().height() : 0;
int horizontal_scrollbar_height = horizontal_scrollbar().is_visible() ? horizontal_scrollbar().height() : 0;
int x = frame_thickness();
int y = frame_thickness() + column_header_height - vertical_scrollbar().value();
int height = AK::max(content_height(), rect().height() - frame_thickness() * 2 - column_header_height - horizontal_scrollbar_height);
row_header().set_relative_rect(x, y, row_header().min_size().width(), height);
}
if (row_header().is_visible() && column_header().is_visible()) {
m_corner_button->set_relative_rect(frame_thickness(), frame_thickness(), row_header().width(), column_header().height());
m_corner_button->set_visible(true);
} else {
m_corner_button->set_visible(false);
}
}
void AbstractTableView::keydown_event(KeyEvent& event)
{
if (is_tab_key_navigation_enabled()) {
if (event.modifiers() == KeyModifier::Mod_Shift && event.key() == KeyCode::Key_Tab) {
move_cursor(CursorMovement::Left, SelectionUpdate::Set);
event.accept();
return;
}
if (!event.modifiers() && event.key() == KeyCode::Key_Tab) {
move_cursor(CursorMovement::Right, SelectionUpdate::Set);
event.accept();
return;
}
}
AbstractView::keydown_event(event);
}
int AbstractTableView::horizontal_padding() const
{
return font().glyph_height() / 2;
}
int AbstractTableView::row_height() const
{
return font().glyph_height() + 6;
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/AbstractView.h>
namespace GUI {
class TableCellPaintingDelegate {
public:
virtual ~TableCellPaintingDelegate() { }
virtual void paint(Painter&, const Gfx::IntRect&, const Gfx::Palette&, const ModelIndex&) = 0;
};
class AbstractTableView : public AbstractView {
public:
int row_height() const;
bool alternating_row_colors() const { return m_alternating_row_colors; }
void set_alternating_row_colors(bool b) { m_alternating_row_colors = b; }
bool highlight_selected_rows() const { return m_highlight_selected_rows; }
void set_highlight_selected_rows(bool b) { m_highlight_selected_rows = b; }
bool column_headers_visible() const;
void set_column_headers_visible(bool);
void set_column_hidden(int, bool);
int column_width(int column) const;
void set_column_width(int column, int width);
Gfx::TextAlignment column_header_alignment(int column) const;
void set_column_header_alignment(int column, Gfx::TextAlignment);
void set_column_painting_delegate(int column, OwnPtr<TableCellPaintingDelegate>);
int horizontal_padding() const;
Gfx::IntPoint adjusted_position(const Gfx::IntPoint&) const;
virtual Gfx::IntRect content_rect(const ModelIndex&) const override;
Gfx::IntRect content_rect(int row, int column) const;
Gfx::IntRect row_rect(int item_index) const;
virtual void scroll_into_view(const ModelIndex&, bool scroll_horizontally = true, bool scroll_vertically = true) override;
void scroll_into_view(const ModelIndex& index, Orientation orientation)
{
scroll_into_view(index, orientation == Gfx::Orientation::Horizontal, orientation == Gfx::Orientation::Vertical);
}
virtual ModelIndex index_at_event_position(const Gfx::IntPoint&, bool& is_toggle) const;
virtual ModelIndex index_at_event_position(const Gfx::IntPoint&) const override;
virtual void select_all() override;
void header_did_change_section_visibility(Badge<HeaderView>, Gfx::Orientation, int section, bool visible);
void header_did_change_section_size(Badge<HeaderView>, Gfx::Orientation, int section, int size);
virtual void did_scroll() override;
HeaderView& column_header() { return *m_column_header; }
const HeaderView& column_header() const { return *m_column_header; }
HeaderView& row_header() { return *m_row_header; }
const HeaderView& row_header() const { return *m_row_header; }
virtual void model_did_update(unsigned flags) override;
protected:
virtual ~AbstractTableView() override;
AbstractTableView();
virtual void mousedown_event(MouseEvent&) override;
virtual void context_menu_event(ContextMenuEvent&) override;
virtual void keydown_event(KeyEvent&) override;
virtual void resize_event(ResizeEvent&) override;
virtual void toggle_index(const ModelIndex&) { }
void update_content_size();
virtual void update_column_sizes();
virtual void update_row_sizes();
virtual int item_count() const;
TableCellPaintingDelegate* column_painting_delegate(int column) const;
void move_cursor_relative(int vertical_steps, int horizontal_steps, SelectionUpdate);
private:
void layout_headers();
RefPtr<HeaderView> m_column_header;
RefPtr<HeaderView> m_row_header;
RefPtr<Button> m_corner_button;
HashMap<int, OwnPtr<TableCellPaintingDelegate>> m_column_painting_delegate;
bool m_alternating_row_colors { true };
bool m_highlight_selected_rows { true };
};
}

View file

@ -0,0 +1,765 @@
/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
#include <AK/Vector.h>
#include <LibCore/Timer.h>
#include <LibGUI/AbstractView.h>
#include <LibGUI/DragOperation.h>
#include <LibGUI/Model.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/Palette.h>
namespace GUI {
AbstractView::AbstractView()
: m_sort_order(SortOrder::Ascending)
, m_selection(*this)
{
set_focus_policy(GUI::FocusPolicy::StrongFocus);
}
AbstractView::~AbstractView()
{
if (m_searching_timer)
m_searching_timer->stop();
if (m_model)
m_model->unregister_view({}, *this);
}
void AbstractView::set_model(RefPtr<Model> model)
{
if (model == m_model)
return;
if (m_model)
m_model->unregister_view({}, *this);
m_model = move(model);
if (m_model)
m_model->register_view({}, *this);
model_did_update(GUI::Model::InvalidateAllIndexes);
scroll_to_top();
}
void AbstractView::model_did_update(unsigned int flags)
{
if (!model() || (flags & GUI::Model::InvalidateAllIndexes)) {
stop_editing();
m_edit_index = {};
m_hovered_index = {};
m_cursor_index = {};
m_drop_candidate_index = {};
clear_selection();
} else {
// FIXME: These may no longer point to whatever they did before,
// but let's be optimistic until we can be sure about it.
if (!model()->is_valid(m_edit_index)) {
stop_editing();
m_edit_index = {};
}
if (!model()->is_valid(m_hovered_index))
m_hovered_index = {};
if (!model()->is_valid(m_cursor_index))
m_cursor_index = {};
if (!model()->is_valid(m_drop_candidate_index))
m_drop_candidate_index = {};
selection().remove_matching([this](auto& index) { return !model()->is_valid(index); });
}
}
void AbstractView::clear_selection()
{
m_selection.clear();
}
void AbstractView::set_selection(const ModelIndex& new_index)
{
m_selection.set(new_index);
}
void AbstractView::add_selection(const ModelIndex& new_index)
{
m_selection.add(new_index);
}
void AbstractView::remove_selection(const ModelIndex& new_index)
{
m_selection.remove(new_index);
}
void AbstractView::toggle_selection(const ModelIndex& new_index)
{
m_selection.toggle(new_index);
}
void AbstractView::did_update_selection()
{
if (!model() || selection().first() != m_edit_index)
stop_editing();
if (model() && on_selection && selection().first().is_valid())
on_selection(selection().first());
}
void AbstractView::did_scroll()
{
update_edit_widget_position();
}
void AbstractView::update_edit_widget_position()
{
if (!m_edit_widget)
return;
m_edit_widget->set_relative_rect(m_edit_widget_content_rect.translated(-horizontal_scrollbar().value(), -vertical_scrollbar().value()));
}
void AbstractView::begin_editing(const ModelIndex& index)
{
ASSERT(is_editable());
ASSERT(model());
if (m_edit_index == index)
return;
if (!model()->is_editable(index))
return;
if (m_edit_widget) {
remove_child(*m_edit_widget);
m_edit_widget = nullptr;
}
m_edit_index = index;
ASSERT(aid_create_editing_delegate);
m_editing_delegate = aid_create_editing_delegate(index);
m_editing_delegate->bind(*model(), index);
m_editing_delegate->set_value(index.data());
m_edit_widget = m_editing_delegate->widget();
add_child(*m_edit_widget);
m_edit_widget->move_to_back();
m_edit_widget_content_rect = content_rect(index).translated(frame_thickness(), frame_thickness());
update_edit_widget_position();
m_edit_widget->set_focus(true);
m_editing_delegate->will_begin_editing();
m_editing_delegate->on_commit = [this] {
ASSERT(model());
model()->set_data(m_edit_index, m_editing_delegate->value());
stop_editing();
};
m_editing_delegate->on_rollback = [this] {
ASSERT(model());
stop_editing();
};
}
void AbstractView::stop_editing()
{
bool take_back_focus = false;
m_edit_index = {};
if (m_edit_widget) {
take_back_focus = m_edit_widget->is_focused();
remove_child(*m_edit_widget);
m_edit_widget = nullptr;
}
if (take_back_focus)
set_focus(true);
}
void AbstractView::activate(const ModelIndex& index)
{
if (on_activation)
on_activation(index);
}
void AbstractView::activate_selected()
{
if (!on_activation)
return;
selection().for_each_index([this](auto& index) {
on_activation(index);
});
}
void AbstractView::notify_selection_changed(Badge<ModelSelection>)
{
did_update_selection();
if (on_selection_change)
on_selection_change();
update();
}
NonnullRefPtr<Gfx::Font> AbstractView::font_for_index(const ModelIndex& index) const
{
if (!model())
return font();
auto font_data = index.data(ModelRole::Font);
if (font_data.is_font())
return font_data.as_font();
return font();
}
void AbstractView::mousedown_event(MouseEvent& event)
{
ScrollableWidget::mousedown_event(event);
if (!model())
return;
if (event.button() == MouseButton::Left)
m_left_mousedown_position = event.position();
auto index = index_at_event_position(event.position());
m_might_drag = false;
if (!index.is_valid()) {
clear_selection();
} else if (event.modifiers() & Mod_Ctrl) {
set_cursor(index, SelectionUpdate::Ctrl);
} else if (event.modifiers() & Mod_Shift) {
set_cursor(index, SelectionUpdate::Shift);
} else if (event.button() == MouseButton::Left && m_selection.contains(index) && !m_model->drag_data_type().is_null()) {
// We might be starting a drag, so don't throw away other selected items yet.
m_might_drag = true;
} else if (event.button() == MouseButton::Right) {
set_cursor(index, SelectionUpdate::ClearIfNotSelected);
} else {
set_cursor(index, SelectionUpdate::Set);
m_might_drag = true;
}
update();
}
void AbstractView::set_hovered_index(const ModelIndex& index)
{
if (m_hovered_index == index)
return;
auto old_index = m_hovered_index;
m_hovered_index = index;
did_change_hovered_index(old_index, index);
update();
}
void AbstractView::leave_event(Core::Event& event)
{
ScrollableWidget::leave_event(event);
set_hovered_index({});
}
void AbstractView::mousemove_event(MouseEvent& event)
{
if (!model())
return ScrollableWidget::mousemove_event(event);
auto hovered_index = index_at_event_position(event.position());
set_hovered_index(hovered_index);
auto data_type = m_model->drag_data_type();
if (data_type.is_null())
return ScrollableWidget::mousemove_event(event);
if (!m_might_drag)
return ScrollableWidget::mousemove_event(event);
if (!(event.buttons() & MouseButton::Left) || m_selection.is_empty()) {
m_might_drag = false;
return ScrollableWidget::mousemove_event(event);
}
auto diff = event.position() - m_left_mousedown_position;
auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
constexpr int drag_distance_threshold = 5;
if (distance_travelled_squared <= drag_distance_threshold)
return ScrollableWidget::mousemove_event(event);
ASSERT(!data_type.is_null());
if (m_is_dragging)
return;
// An event might sneak in between us constructing the drag operation and the
// event loop exec at the end of `drag_operation->exec()' if the user is fast enough.
// Prevent this by just ignoring later drag initiations (until the current drag operation ends).
TemporaryChange dragging { m_is_dragging, true };
dbgln("Initiate drag!");
auto drag_operation = DragOperation::construct();
drag_operation->set_mime_data(m_model->mime_data(m_selection));
auto outcome = drag_operation->exec();
switch (outcome) {
case DragOperation::Outcome::Accepted:
dbgln("Drag was accepted!");
break;
case DragOperation::Outcome::Cancelled:
dbgln("Drag was cancelled!");
break;
default:
ASSERT_NOT_REACHED();
break;
}
}
void AbstractView::mouseup_event(MouseEvent& event)
{
ScrollableWidget::mouseup_event(event);
if (!model())
return;
if (m_might_drag) {
// We were unsure about unselecting items other than the current one
// in mousedown_event(), because we could be seeing a start of a drag.
// Since we're here, it was not that; so fix up the selection now.
auto index = index_at_event_position(event.position());
if (index.is_valid())
set_selection(index);
else
clear_selection();
m_might_drag = false;
update();
}
if (activates_on_selection())
activate_selected();
}
void AbstractView::doubleclick_event(MouseEvent& event)
{
if (!model())
return;
if (event.button() != MouseButton::Left)
return;
m_might_drag = false;
auto index = index_at_event_position(event.position());
if (!index.is_valid()) {
clear_selection();
return;
}
if (!m_selection.contains(index))
set_selection(index);
if (is_editable() && edit_triggers() & EditTrigger::DoubleClicked)
begin_editing(cursor_index());
else
activate(cursor_index());
}
void AbstractView::context_menu_event(ContextMenuEvent& event)
{
if (!model())
return;
auto index = index_at_event_position(event.position());
if (index.is_valid())
add_selection(index);
else
clear_selection();
if (on_context_menu_request)
on_context_menu_request(index, event);
}
void AbstractView::drop_event(DropEvent& event)
{
event.accept();
if (!model())
return;
auto index = index_at_event_position(event.position());
if (on_drop)
on_drop(index, event);
}
void AbstractView::set_selection_mode(SelectionMode selection_mode)
{
if (m_selection_mode == selection_mode)
return;
m_selection_mode = selection_mode;
if (m_selection_mode == SelectionMode::NoSelection)
m_selection.clear();
else if (m_selection_mode != SelectionMode::SingleSelection && m_selection.size() > 1) {
auto first_selected = m_selection.first();
m_selection.clear();
m_selection.set(first_selected);
}
update();
}
void AbstractView::set_key_column_and_sort_order(int column, SortOrder sort_order)
{
m_key_column = column;
m_sort_order = sort_order;
if (model())
model()->sort(column, sort_order);
update();
}
void AbstractView::set_cursor(ModelIndex index, SelectionUpdate selection_update, bool scroll_cursor_into_view)
{
if (!model() || !index.is_valid() || selection_mode() == SelectionMode::NoSelection) {
m_cursor_index = {};
cancel_searching();
return;
}
if (!m_cursor_index.is_valid() || model()->parent_index(m_cursor_index) != model()->parent_index(index))
cancel_searching();
if (selection_mode() == SelectionMode::SingleSelection && (selection_update == SelectionUpdate::Ctrl || selection_update == SelectionUpdate::Shift))
selection_update = SelectionUpdate::Set;
if (model()->is_valid(index)) {
if (selection_update == SelectionUpdate::Set)
set_selection(index);
else if (selection_update == SelectionUpdate::Ctrl)
toggle_selection(index);
else if (selection_update == SelectionUpdate::ClearIfNotSelected) {
if (!m_selection.contains(index))
clear_selection();
} else if (selection_update == SelectionUpdate::Shift) {
// Toggle all from cursor to new index.
auto min_row = min(cursor_index().row(), index.row());
auto max_row = max(cursor_index().row(), index.row());
auto min_column = min(cursor_index().column(), index.column());
auto max_column = max(cursor_index().column(), index.column());
for (auto row = min_row; row <= max_row; ++row) {
for (auto column = min_column; column <= max_column; ++column) {
auto new_index = model()->index(row, column);
if (new_index.is_valid())
toggle_selection(new_index);
}
}
// Finally toggle the cursor index again to make it go back to its current state.
toggle_selection(cursor_index());
}
// FIXME: Support the other SelectionUpdate types
auto old_cursor_index = m_cursor_index;
m_cursor_index = index;
did_change_cursor_index(old_cursor_index, m_cursor_index);
if (scroll_cursor_into_view)
scroll_into_view(index, true, true);
update();
}
}
void AbstractView::set_edit_triggers(unsigned triggers)
{
m_edit_triggers = triggers;
}
void AbstractView::hide_event(HideEvent& event)
{
stop_editing();
ScrollableWidget::hide_event(event);
}
void AbstractView::keydown_event(KeyEvent& event)
{
if (event.key() == KeyCode::Key_F2) {
if (is_editable() && edit_triggers() & EditTrigger::EditKeyPressed) {
begin_editing(cursor_index());
event.accept();
return;
}
}
if (event.key() == KeyCode::Key_Return) {
activate_selected();
event.accept();
return;
}
SelectionUpdate selection_update = SelectionUpdate::Set;
if (event.modifiers() == KeyModifier::Mod_Shift) {
selection_update = SelectionUpdate::Shift;
}
if (event.key() == KeyCode::Key_Left) {
move_cursor(CursorMovement::Left, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_Right) {
move_cursor(CursorMovement::Right, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_Up) {
move_cursor(CursorMovement::Up, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_Down) {
move_cursor(CursorMovement::Down, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_Home) {
move_cursor(CursorMovement::Home, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_End) {
move_cursor(CursorMovement::End, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_PageUp) {
move_cursor(CursorMovement::PageUp, selection_update);
event.accept();
return;
}
if (event.key() == KeyCode::Key_PageDown) {
move_cursor(CursorMovement::PageDown, selection_update);
event.accept();
return;
}
if (is_searchable()) {
if (event.key() == KeyCode::Key_Backspace) {
if (is_searching()) {
//if (event.modifiers() == Mod_Ctrl) {
// TODO: delete last word
//}
Utf8View view(m_searching);
size_t n_code_points = view.length();
if (n_code_points > 1) {
n_code_points--;
StringBuilder sb;
for (auto it = view.begin(); it != view.end(); ++it) {
if (n_code_points == 0)
break;
n_code_points--;
sb.append_code_point(*it);
}
do_search(sb.to_string());
start_searching_timer();
} else {
cancel_searching();
}
event.accept();
return;
}
} else if (event.key() == KeyCode::Key_Escape) {
if (is_searching()) {
cancel_searching();
event.accept();
return;
}
} else if (event.key() != KeyCode::Key_Tab && !event.ctrl() && !event.alt() && event.code_point() != 0) {
StringBuilder sb;
sb.append(m_searching);
sb.append_code_point(event.code_point());
do_search(sb.to_string());
start_searching_timer();
event.accept();
return;
}
}
ScrollableWidget::keydown_event(event);
}
void AbstractView::cancel_searching()
{
m_searching = nullptr;
if (m_searching_timer)
m_searching_timer->stop();
if (m_highlighted_search_index.is_valid()) {
m_highlighted_search_index = {};
update();
}
}
void AbstractView::start_searching_timer()
{
if (!m_searching_timer) {
m_searching_timer = add<Core::Timer>();
m_searching_timer->set_single_shot(true);
m_searching_timer->on_timeout = [this] {
cancel_searching();
};
}
m_searching_timer->set_interval(5 * 1000);
m_searching_timer->restart();
}
void AbstractView::do_search(String&& searching)
{
if (searching.is_empty() || !model()) {
cancel_searching();
return;
}
auto found_indexes = model()->matches(searching, Model::MatchesFlag::FirstMatchOnly | Model::MatchesFlag::MatchAtStart | Model::MatchesFlag::CaseInsensitive, model()->parent_index(cursor_index()));
if (!found_indexes.is_empty() && found_indexes[0].is_valid()) {
auto& index = found_indexes[0];
m_highlighted_search_index = index;
m_searching = move(searching);
set_selection(index);
scroll_into_view(index);
update();
}
}
bool AbstractView::is_searchable() const
{
if (!m_searchable || !model())
return false;
return model()->is_searchable();
}
void AbstractView::set_searchable(bool searchable)
{
if (m_searchable == searchable)
return;
m_searchable = searchable;
if (!m_searchable)
cancel_searching();
}
bool AbstractView::is_highlighting_searching(const ModelIndex& index) const
{
return index == m_highlighted_search_index;
}
void AbstractView::draw_item_text(Gfx::Painter& painter, const ModelIndex& index, bool is_selected, const Gfx::IntRect& text_rect, const StringView& item_text, const Gfx::Font& font, Gfx::TextAlignment alignment, Gfx::TextElision elision)
{
Color text_color;
if (is_selected)
text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text();
else
text_color = index.data(ModelRole::ForegroundColor).to_color(palette().color(foreground_role()));
if (is_highlighting_searching(index)) {
Utf8View searching_text(searching());
auto searching_length = searching_text.length();
// Highlight the text background first
painter.draw_text([&](const Gfx::IntRect& rect, u32) {
if (searching_length > 0) {
searching_length--;
painter.fill_rect(rect.inflated(0, 2), palette().highlight_searching());
}
},
text_rect, item_text, font, alignment, elision);
// Then draw the text
auto highlight_text_color = palette().highlight_searching_text();
searching_length = searching_text.length();
painter.draw_text([&](const Gfx::IntRect& rect, u32 code_point) {
if (searching_length > 0) {
searching_length--;
painter.draw_glyph_or_emoji(rect.location(), code_point, font, highlight_text_color);
} else {
painter.draw_glyph_or_emoji(rect.location(), code_point, font, text_color);
}
},
text_rect, item_text, font, alignment, elision);
} else {
if (m_draw_item_text_with_shadow) {
painter.draw_text(text_rect.translated(1, 1), item_text, font, alignment, Color::Black, elision);
painter.draw_text(text_rect, item_text, font, alignment, Color::White, elision);
} else {
painter.draw_text(text_rect, item_text, font, alignment, text_color, elision);
}
}
}
void AbstractView::focusin_event(FocusEvent& event)
{
ScrollableWidget::focusin_event(event);
if (model() && !cursor_index().is_valid()) {
move_cursor(CursorMovement::Home, SelectionUpdate::None);
clear_selection();
}
}
void AbstractView::drag_enter_event(DragEvent& event)
{
if (!model())
return;
// NOTE: Right now, AbstractView always accepts drags since we won't get "drag move" events
// unless we accept the "drag enter" event.
// We might be able to reduce event traffic by communicating the set of drag-accepting
// rects in this widget to the windowing system somehow.
event.accept();
dbgln("accepting drag of {}", event.mime_types().first());
}
void AbstractView::drag_move_event(DragEvent& event)
{
if (!model())
return;
auto index = index_at_event_position(event.position());
ModelIndex new_drop_candidate_index;
if (index.is_valid()) {
bool acceptable = model()->accepts_drag(index, event.mime_types());
if (acceptable)
new_drop_candidate_index = index;
}
if (m_drop_candidate_index != new_drop_candidate_index) {
m_drop_candidate_index = new_drop_candidate_index;
update();
}
if (m_drop_candidate_index.is_valid())
event.accept();
}
void AbstractView::drag_leave_event(Event&)
{
if (m_drop_candidate_index.is_valid()) {
m_drop_candidate_index = {};
update();
}
}
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Function.h>
#include <LibGUI/Model.h>
#include <LibGUI/ModelSelection.h>
#include <LibGUI/ScrollableWidget.h>
#include <LibGfx/TextElision.h>
namespace GUI {
class AbstractView
: public ScrollableWidget
, public ModelClient {
C_OBJECT_ABSTRACT(AbstractView);
public:
enum class CursorMovement {
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
};
enum class SelectionUpdate {
None,
Set,
Shift,
Ctrl,
ClearIfNotSelected
};
enum class SelectionBehavior {
SelectItems,
SelectRows,
};
enum class SelectionMode {
SingleSelection,
MultiSelection,
NoSelection,
};
virtual void move_cursor(CursorMovement, SelectionUpdate) { }
void set_model(RefPtr<Model>);
Model* model() { return m_model.ptr(); }
const Model* model() const { return m_model.ptr(); }
ModelSelection& selection() { return m_selection; }
const ModelSelection& selection() const { return m_selection; }
virtual void select_all() { }
bool is_editable() const { return m_editable; }
void set_editable(bool editable) { m_editable = editable; }
bool is_searching() const { return !m_searching.is_null(); }
bool is_searchable() const;
void set_searchable(bool);
enum EditTrigger {
None = 0,
DoubleClicked = 1 << 0,
EditKeyPressed = 1 << 1,
AnyKeyPressed = 1 << 2,
};
unsigned edit_triggers() const { return m_edit_triggers; }
void set_edit_triggers(unsigned);
SelectionBehavior selection_behavior() const { return m_selection_behavior; }
void set_selection_behavior(SelectionBehavior behavior) { m_selection_behavior = behavior; }
SelectionMode selection_mode() const { return m_selection_mode; }
void set_selection_mode(SelectionMode);
virtual void model_did_update(unsigned flags) override;
virtual void did_update_selection();
virtual Gfx::IntRect content_rect(const ModelIndex&) const { return {}; }
virtual ModelIndex index_at_event_position(const Gfx::IntPoint&) const { return {}; }
void begin_editing(const ModelIndex&);
void stop_editing();
void set_activates_on_selection(bool b) { m_activates_on_selection = b; }
bool activates_on_selection() const { return m_activates_on_selection; }
Function<void()> on_selection_change;
Function<void(const ModelIndex&)> on_activation;
Function<void(const ModelIndex&)> on_selection;
Function<void(const ModelIndex&, const ContextMenuEvent&)> on_context_menu_request;
Function<void(const ModelIndex&, const DropEvent&)> on_drop;
Function<OwnPtr<ModelEditingDelegate>(const ModelIndex&)> aid_create_editing_delegate;
void notify_selection_changed(Badge<ModelSelection>);
NonnullRefPtr<Gfx::Font> font_for_index(const ModelIndex&) const;
void set_key_column_and_sort_order(int column, SortOrder);
int key_column() const { return m_key_column; }
SortOrder sort_order() const { return m_sort_order; }
virtual void scroll_into_view(const ModelIndex&, [[maybe_unused]] bool scroll_horizontally = true, [[maybe_unused]] bool scroll_vertically = true) { }
const ModelIndex& cursor_index() const { return m_cursor_index; }
void set_cursor(ModelIndex, SelectionUpdate, bool scroll_cursor_into_view = true);
bool is_tab_key_navigation_enabled() const { return m_tab_key_navigation_enabled; }
void set_tab_key_navigation_enabled(bool enabled) { m_tab_key_navigation_enabled = enabled; }
void set_draw_item_text_with_shadow(bool b) { m_draw_item_text_with_shadow = b; }
protected:
AbstractView();
virtual ~AbstractView() override;
virtual void keydown_event(KeyEvent&) override;
virtual void mousedown_event(MouseEvent&) override;
virtual void mousemove_event(MouseEvent&) override;
virtual void mouseup_event(MouseEvent&) override;
virtual void doubleclick_event(MouseEvent&) override;
virtual void context_menu_event(ContextMenuEvent&) override;
virtual void drag_enter_event(DragEvent&) override;
virtual void drag_move_event(DragEvent&) override;
virtual void drag_leave_event(Event&) override;
virtual void drop_event(DropEvent&) override;
virtual void leave_event(Core::Event&) override;
virtual void hide_event(HideEvent&) override;
virtual void focusin_event(FocusEvent&) override;
virtual void clear_selection();
virtual void set_selection(const ModelIndex&);
virtual void add_selection(const ModelIndex&);
virtual void remove_selection(const ModelIndex&);
virtual void toggle_selection(const ModelIndex&);
virtual void did_change_hovered_index([[maybe_unused]] const ModelIndex& old_index, [[maybe_unused]] const ModelIndex& new_index) { }
virtual void did_change_cursor_index([[maybe_unused]] const ModelIndex& old_index, [[maybe_unused]] const ModelIndex& new_index) { }
void draw_item_text(Gfx::Painter&, const ModelIndex&, bool, const Gfx::IntRect&, const StringView&, const Gfx::Font&, Gfx::TextAlignment, Gfx::TextElision);
virtual void did_scroll() override;
void set_hovered_index(const ModelIndex&);
void activate(const ModelIndex&);
void activate_selected();
void update_edit_widget_position();
StringView searching() const { return m_searching; }
void cancel_searching();
void start_searching_timer();
void do_search(String&&);
bool is_highlighting_searching(const ModelIndex&) const;
ModelIndex drop_candidate_index() const { return m_drop_candidate_index; }
bool m_editable { false };
bool m_searchable { true };
ModelIndex m_edit_index;
RefPtr<Widget> m_edit_widget;
Gfx::IntRect m_edit_widget_content_rect;
OwnPtr<ModelEditingDelegate> m_editing_delegate;
Gfx::IntPoint m_left_mousedown_position;
bool m_might_drag { false };
ModelIndex m_hovered_index;
ModelIndex m_highlighted_search_index;
int m_key_column { -1 };
SortOrder m_sort_order;
private:
RefPtr<Model> m_model;
ModelSelection m_selection;
String m_searching;
RefPtr<Core::Timer> m_searching_timer;
ModelIndex m_cursor_index;
ModelIndex m_drop_candidate_index;
SelectionBehavior m_selection_behavior { SelectionBehavior::SelectItems };
SelectionMode m_selection_mode { SelectionMode::SingleSelection };
unsigned m_edit_triggers { EditTrigger::DoubleClicked | EditTrigger::EditKeyPressed };
bool m_activates_on_selection { false };
bool m_tab_key_navigation_enabled { false };
bool m_is_dragging { false };
bool m_draw_item_text_with_shadow { false };
};
}

View file

@ -0,0 +1,300 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/WeakPtr.h>
#include <LibGUI/AboutDialog.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/Button.h>
#include <LibGUI/Icon.h>
#include <LibGUI/MenuItem.h>
#include <LibGUI/Window.h>
namespace GUI {
namespace CommonActions {
NonnullRefPtr<Action> make_about_action(const String& app_name, const Icon& app_icon, Window* parent)
{
auto weak_parent = AK::try_make_weak_ptr<Window>(parent);
return Action::create(String::formatted("About {}", app_name), app_icon.bitmap_for_size(16), [=](auto&) {
AboutDialog::show(app_name, app_icon.bitmap_for_size(32), weak_parent.ptr());
});
}
NonnullRefPtr<Action> make_open_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Open...", { Mod_Ctrl, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_save_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_save_as_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Save As...", { Mod_Ctrl | Mod_Shift, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_move_to_front_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Move to front", { Mod_Ctrl | Mod_Shift, Key_Up }, Gfx::Bitmap::load_from_file("/res/icons/16x16/move-to-front.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_move_to_back_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Move to back", { Mod_Ctrl | Mod_Shift, Key_Down }, Gfx::Bitmap::load_from_file("/res/icons/16x16/move-to-back.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_undo_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Undo", { Mod_Ctrl, Key_Z }, Gfx::Bitmap::load_from_file("/res/icons/16x16/undo.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_redo_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Redo", { Mod_Ctrl, Key_Y }, Gfx::Bitmap::load_from_file("/res/icons/16x16/redo.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_delete_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Delete", { Mod_None, Key_Delete }, Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_cut_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Cut", { Mod_Ctrl, Key_X }, Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-cut.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_copy_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Copy", { Mod_Ctrl, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_paste_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Paste", { Mod_Ctrl, Key_V }, Gfx::Bitmap::load_from_file("/res/icons/16x16/paste.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_fullscreen_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Fullscreen", { Mod_None, Key_F11 }, move(callback), parent);
}
NonnullRefPtr<Action> make_quit_action(Function<void(Action&)> callback)
{
return Action::create("Quit", { Mod_Alt, Key_F4 }, move(callback));
}
NonnullRefPtr<Action> make_help_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Contents", { Mod_None, Key_F1 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-help.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_go_back_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Go back", { Mod_Alt, Key_Left }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_go_forward_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Go forward", { Mod_Alt, Key_Right }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_go_home_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Go home", { Mod_Alt, Key_Home }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-home.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_reload_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Reload", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"), move(callback), parent);
}
NonnullRefPtr<Action> make_select_all_action(Function<void(Action&)> callback, Core::Object* parent)
{
return Action::create("Select all", { Mod_Ctrl, Key_A }, Gfx::Bitmap::load_from_file("/res/icons/16x16/select-all.png"), move(callback), parent);
}
}
Action::Action(const StringView& text, Function<void(Action&)> on_activation_callback, Core::Object* parent, bool checkable)
: Core::Object(parent)
, on_activation(move(on_activation_callback))
, m_text(text)
, m_checkable(checkable)
{
}
Action::Action(const StringView& text, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> on_activation_callback, Core::Object* parent, bool checkable)
: Core::Object(parent)
, on_activation(move(on_activation_callback))
, m_text(text)
, m_icon(move(icon))
, m_checkable(checkable)
{
}
Action::Action(const StringView& text, const Shortcut& shortcut, Function<void(Action&)> on_activation_callback, Core::Object* parent, bool checkable)
: Action(text, shortcut, nullptr, move(on_activation_callback), parent, checkable)
{
}
Action::Action(const StringView& text, const Shortcut& shortcut, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> on_activation_callback, Core::Object* parent, bool checkable)
: Core::Object(parent)
, on_activation(move(on_activation_callback))
, m_text(text)
, m_icon(move(icon))
, m_shortcut(shortcut)
, m_checkable(checkable)
{
if (parent && is<Widget>(*parent)) {
m_scope = ShortcutScope::WidgetLocal;
} else if (parent && is<Window>(*parent)) {
m_scope = ShortcutScope::WindowLocal;
} else {
m_scope = ShortcutScope::ApplicationGlobal;
if (auto* app = Application::the()) {
app->register_global_shortcut_action({}, *this);
}
}
}
Action::~Action()
{
if (m_shortcut.is_valid() && m_scope == ShortcutScope::ApplicationGlobal) {
if (auto* app = Application::the())
app->unregister_global_shortcut_action({}, *this);
}
}
void Action::activate(Core::Object* activator)
{
if (!on_activation)
return;
if (activator)
m_activator = activator->make_weak_ptr();
if (is_checkable()) {
if (m_action_group) {
if (m_action_group->is_unchecking_allowed())
set_checked(!is_checked());
else
set_checked(true);
} else {
set_checked(!is_checked());
}
}
on_activation(*this);
m_activator = nullptr;
}
void Action::register_button(Badge<Button>, Button& button)
{
m_buttons.set(&button);
}
void Action::unregister_button(Badge<Button>, Button& button)
{
m_buttons.remove(&button);
}
void Action::register_menu_item(Badge<MenuItem>, MenuItem& menu_item)
{
m_menu_items.set(&menu_item);
}
void Action::unregister_menu_item(Badge<MenuItem>, MenuItem& menu_item)
{
m_menu_items.remove(&menu_item);
}
template<typename Callback>
void Action::for_each_toolbar_button(Callback callback)
{
for (auto& it : m_buttons)
callback(*it);
}
template<typename Callback>
void Action::for_each_menu_item(Callback callback)
{
for (auto& it : m_menu_items)
callback(*it);
}
void Action::set_enabled(bool enabled)
{
if (m_enabled == enabled)
return;
m_enabled = enabled;
for_each_toolbar_button([enabled](auto& button) {
button.set_enabled(enabled);
});
for_each_menu_item([enabled](auto& item) {
item.set_enabled(enabled);
});
}
void Action::set_checked(bool checked)
{
if (m_checked == checked)
return;
m_checked = checked;
if (m_checked && m_action_group) {
m_action_group->for_each_action([this](auto& other_action) {
if (this == &other_action)
return IterationDecision::Continue;
if (other_action.is_checkable())
other_action.set_checked(false);
return IterationDecision::Continue;
});
}
for_each_toolbar_button([checked](auto& button) {
button.set_checked(checked);
});
for_each_menu_item([checked](MenuItem& item) {
item.set_checked(checked);
});
}
void Action::set_group(Badge<ActionGroup>, ActionGroup* group)
{
m_action_group = AK::try_make_weak_ptr(group);
}
void Action::set_icon(const Gfx::Bitmap* icon)
{
m_icon = icon;
}
}

View file

@ -0,0 +1,174 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Badge.h>
#include <AK/Function.h>
#include <AK/HashTable.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <AK/String.h>
#include <AK/WeakPtr.h>
#include <AK/Weakable.h>
#include <LibCore/Object.h>
#include <LibGUI/Forward.h>
#include <LibGUI/Shortcut.h>
#include <LibGfx/Forward.h>
namespace GUI {
namespace CommonActions {
NonnullRefPtr<Action> make_about_action(const String& app_name, const Icon& app_icon, Window* parent = nullptr);
NonnullRefPtr<Action> make_open_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_save_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_save_as_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_undo_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_redo_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_cut_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_copy_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_paste_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_delete_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_move_to_front_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_move_to_back_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_fullscreen_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_quit_action(Function<void(Action&)>);
NonnullRefPtr<Action> make_help_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_go_back_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_go_forward_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_go_home_action(Function<void(Action&)> callback, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_reload_action(Function<void(Action&)>, Core::Object* parent = nullptr);
NonnullRefPtr<Action> make_select_all_action(Function<void(Action&)>, Core::Object* parent = nullptr);
};
class Action final : public Core::Object {
C_OBJECT(Action)
public:
enum class ShortcutScope {
None,
WidgetLocal,
WindowLocal,
ApplicationGlobal,
};
static NonnullRefPtr<Action> create(const StringView& text, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, move(callback), parent));
}
static NonnullRefPtr<Action> create(const StringView& text, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, move(icon), move(callback), parent));
}
static NonnullRefPtr<Action> create(const StringView& text, const Shortcut& shortcut, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, shortcut, move(callback), parent));
}
static NonnullRefPtr<Action> create(const StringView& text, const Shortcut& shortcut, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, shortcut, move(icon), move(callback), parent));
}
static NonnullRefPtr<Action> create_checkable(const StringView& text, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, move(callback), parent, true));
}
static NonnullRefPtr<Action> create_checkable(const StringView& text, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, move(icon), move(callback), parent, true));
}
static NonnullRefPtr<Action> create_checkable(const StringView& text, const Shortcut& shortcut, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, shortcut, move(callback), parent, true));
}
static NonnullRefPtr<Action> create_checkable(const StringView& text, const Shortcut& shortcut, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> callback, Core::Object* parent = nullptr)
{
return adopt(*new Action(text, shortcut, move(icon), move(callback), parent, true));
}
virtual ~Action() override;
String text() const { return m_text; }
void set_text(String text) { m_text = move(text); }
Shortcut shortcut() const { return m_shortcut; }
const Gfx::Bitmap* icon() const { return m_icon.ptr(); }
void set_icon(const Gfx::Bitmap*);
const Core::Object* activator() const { return m_activator.ptr(); }
Core::Object* activator() { return m_activator.ptr(); }
Function<void(Action&)> on_activation;
void activate(Core::Object* activator = nullptr);
bool is_enabled() const { return m_enabled; }
void set_enabled(bool);
bool is_checkable() const { return m_checkable; }
void set_checkable(bool checkable) { m_checkable = checkable; }
bool is_checked() const
{
ASSERT(is_checkable());
return m_checked;
}
void set_checked(bool);
bool swallow_key_event_when_disabled() const { return m_swallow_key_event_when_disabled; }
void set_swallow_key_event_when_disabled(bool swallow) { m_swallow_key_event_when_disabled = swallow; }
void register_button(Badge<Button>, Button&);
void unregister_button(Badge<Button>, Button&);
void register_menu_item(Badge<MenuItem>, MenuItem&);
void unregister_menu_item(Badge<MenuItem>, MenuItem&);
const ActionGroup* group() const { return m_action_group.ptr(); }
void set_group(Badge<ActionGroup>, ActionGroup*);
private:
Action(const StringView& text, Function<void(Action&)> = nullptr, Core::Object* = nullptr, bool checkable = false);
Action(const StringView& text, const Shortcut&, Function<void(Action&)> = nullptr, Core::Object* = nullptr, bool checkable = false);
Action(const StringView& text, const Shortcut&, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> = nullptr, Core::Object* = nullptr, bool checkable = false);
Action(const StringView& text, RefPtr<Gfx::Bitmap>&& icon, Function<void(Action&)> = nullptr, Core::Object* = nullptr, bool checkable = false);
template<typename Callback>
void for_each_toolbar_button(Callback);
template<typename Callback>
void for_each_menu_item(Callback);
String m_text;
RefPtr<Gfx::Bitmap> m_icon;
Shortcut m_shortcut;
bool m_enabled { true };
bool m_checkable { false };
bool m_checked { false };
bool m_swallow_key_event_when_disabled { false };
ShortcutScope m_scope { ShortcutScope::None };
HashTable<Button*> m_buttons;
HashTable<MenuItem*> m_menu_items;
WeakPtr<ActionGroup> m_action_group;
WeakPtr<Core::Object> m_activator;
};
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
namespace GUI {
void ActionGroup::add_action(Action& action)
{
action.set_group({}, this);
m_actions.set(&action);
}
void ActionGroup::remove_action(Action& action)
{
action.set_group({}, nullptr);
m_actions.remove(&action);
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/HashTable.h>
#include <AK/Weakable.h>
#include <LibGUI/Forward.h>
namespace GUI {
class ActionGroup : public Weakable<ActionGroup> {
public:
ActionGroup() { }
~ActionGroup() { }
void add_action(Action&);
void remove_action(Action&);
bool is_exclusive() const { return m_exclusive; }
void set_exclusive(bool exclusive) { m_exclusive = exclusive; }
bool is_unchecking_allowed() const { return m_unchecking_allowed; }
void set_unchecking_allowed(bool unchecking_allowed) { m_unchecking_allowed = unchecking_allowed; }
template<typename C>
void for_each_action(C callback)
{
for (auto& it : m_actions) {
if (callback(*it) == IterationDecision::Break)
break;
}
}
private:
HashTable<Action*> m_actions;
bool m_exclusive { false };
bool m_unchecking_allowed { false };
};
}

View file

@ -0,0 +1,291 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/NeverDestroyed.h>
#include <LibCore/EventLoop.h>
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/Label.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGUI/WindowServerConnection.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
namespace GUI {
class Application::TooltipWindow final : public Window {
C_OBJECT(TooltipWindow);
public:
void set_tooltip(String tooltip)
{
// FIXME: Add some kind of GUI::Label auto-sizing feature.
int text_width = m_label->font().width(tooltip);
set_rect(rect().x(), rect().y(), text_width + 10, m_label->font().glyph_height() + 8);
m_label->set_text(move(tooltip));
}
private:
TooltipWindow()
{
set_window_type(WindowType::Tooltip);
m_label = set_main_widget<Label>();
m_label->set_background_role(Gfx::ColorRole::Tooltip);
m_label->set_foreground_role(Gfx::ColorRole::TooltipText);
m_label->set_fill_with_background_color(true);
m_label->set_frame_thickness(1);
m_label->set_frame_shape(Gfx::FrameShape::Container);
m_label->set_frame_shadow(Gfx::FrameShadow::Plain);
}
RefPtr<Label> m_label;
};
static NeverDestroyed<WeakPtr<Application>> s_the;
Application* Application::the()
{
// NOTE: If we don't explicitly call revoke_weak_ptrs() in the
// ~Application destructor, we would have to change this to
// return s_the->strong_ref().ptr();
// This is because this is using the unsafe operator*/operator->
// that do not have the ability to check the ref count!
return *s_the;
}
Application::Application(int argc, char** argv)
{
ASSERT(!*s_the);
*s_the = *this;
m_event_loop = make<Core::EventLoop>();
WindowServerConnection::the();
Clipboard::initialize({});
if (argc > 0)
m_invoked_as = argv[0];
if (getenv("GUI_FOCUS_DEBUG"))
m_focus_debugging_enabled = true;
if (getenv("GUI_DND_DEBUG"))
m_dnd_debugging_enabled = true;
for (int i = 1; i < argc; i++) {
String arg(argv[i]);
m_args.append(move(arg));
}
m_tooltip_show_timer = Core::Timer::create_single_shot(700, [this] {
tooltip_show_timer_did_fire();
});
m_tooltip_hide_timer = Core::Timer::create_single_shot(50, [this] {
tooltip_hide_timer_did_fire();
});
}
Application::~Application()
{
revoke_weak_ptrs();
}
int Application::exec()
{
return m_event_loop->exec();
}
void Application::quit(int exit_code)
{
m_event_loop->quit(exit_code);
}
void Application::set_menubar(RefPtr<MenuBar> menubar)
{
if (m_menubar)
m_menubar->notify_removed_from_application({});
m_menubar = move(menubar);
if (m_menubar)
m_menubar->notify_added_to_application({});
}
void Application::register_global_shortcut_action(Badge<Action>, Action& action)
{
m_global_shortcut_actions.set(action.shortcut(), &action);
}
void Application::unregister_global_shortcut_action(Badge<Action>, Action& action)
{
m_global_shortcut_actions.remove(action.shortcut());
}
Action* Application::action_for_key_event(const KeyEvent& event)
{
auto it = m_global_shortcut_actions.find(Shortcut(event.modifiers(), (KeyCode)event.key()));
if (it == m_global_shortcut_actions.end())
return nullptr;
return (*it).value;
}
void Application::show_tooltip(String tooltip, const Widget* tooltip_source_widget)
{
m_tooltip_source_widget = tooltip_source_widget;
if (!m_tooltip_window) {
m_tooltip_window = TooltipWindow::construct();
m_tooltip_window->set_double_buffering_enabled(false);
}
m_tooltip_window->set_tooltip(move(tooltip));
if (m_tooltip_window->is_visible()) {
tooltip_show_timer_did_fire();
m_tooltip_show_timer->stop();
m_tooltip_hide_timer->stop();
} else {
m_tooltip_show_timer->restart();
m_tooltip_hide_timer->stop();
}
}
void Application::hide_tooltip()
{
m_tooltip_show_timer->stop();
m_tooltip_hide_timer->start();
}
void Application::did_create_window(Badge<Window>)
{
if (m_event_loop->was_exit_requested())
m_event_loop->unquit();
}
void Application::did_delete_last_window(Badge<Window>)
{
if (m_quit_when_last_window_deleted)
m_event_loop->quit(0);
}
void Application::set_system_palette(SharedBuffer& buffer)
{
if (!m_system_palette)
m_system_palette = Gfx::PaletteImpl::create_with_shared_buffer(buffer);
else
m_system_palette->replace_internal_buffer({}, buffer);
if (!m_palette)
m_palette = m_system_palette;
}
void Application::set_palette(const Palette& palette)
{
m_palette = palette.impl();
}
Gfx::Palette Application::palette() const
{
return Palette(*m_palette);
}
void Application::tooltip_show_timer_did_fire()
{
ASSERT(m_tooltip_window);
Gfx::IntRect desktop_rect = Desktop::the().rect();
const int margin = 30;
Gfx::IntPoint adjusted_pos = WindowServerConnection::the().send_sync<Messages::WindowServer::GetGlobalCursorPosition>()->position();
adjusted_pos.move_by(0, 18);
if (adjusted_pos.x() + m_tooltip_window->width() >= desktop_rect.width() - margin) {
adjusted_pos = adjusted_pos.translated(-m_tooltip_window->width(), 0);
}
if (adjusted_pos.y() + m_tooltip_window->height() >= desktop_rect.height() - margin) {
adjusted_pos = adjusted_pos.translated(0, -(m_tooltip_window->height() * 2));
}
m_tooltip_window->move_to(adjusted_pos);
m_tooltip_window->show();
}
void Application::tooltip_hide_timer_did_fire()
{
m_tooltip_source_widget = nullptr;
if (m_tooltip_window)
m_tooltip_window->hide();
}
void Application::window_did_become_active(Badge<Window>, Window& window)
{
m_active_window = window.make_weak_ptr<Window>();
}
void Application::window_did_become_inactive(Badge<Window>, Window& window)
{
if (m_active_window.ptr() != &window)
return;
m_active_window = nullptr;
}
void Application::set_pending_drop_widget(Widget* widget)
{
if (m_pending_drop_widget == widget)
return;
if (m_pending_drop_widget)
m_pending_drop_widget->update();
m_pending_drop_widget = widget;
if (m_pending_drop_widget)
m_pending_drop_widget->update();
}
void Application::set_drag_hovered_widget_impl(Widget* widget, const Gfx::IntPoint& position, Vector<String> mime_types)
{
if (widget == m_drag_hovered_widget)
return;
if (m_drag_hovered_widget) {
Event leave_event(Event::DragLeave);
m_drag_hovered_widget->dispatch_event(leave_event, m_drag_hovered_widget->window());
}
set_pending_drop_widget(nullptr);
m_drag_hovered_widget = widget;
if (m_drag_hovered_widget) {
DragEvent enter_event(Event::DragEnter, position, move(mime_types));
enter_event.ignore();
m_drag_hovered_widget->dispatch_event(enter_event, m_drag_hovered_widget->window());
if (enter_event.is_accepted())
set_pending_drop_widget(m_drag_hovered_widget);
}
}
void Application::notify_drag_cancelled(Badge<WindowServerConnection>)
{
set_drag_hovered_widget_impl(nullptr);
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
#include <AK/String.h>
#include <AK/WeakPtr.h>
#include <LibCore/Object.h>
#include <LibGUI/Forward.h>
#include <LibGUI/Shortcut.h>
#include <LibGUI/Widget.h>
#include <LibGfx/Point.h>
namespace GUI {
class Application : public Core::Object {
C_OBJECT(Application);
public:
static Application* the();
~Application();
int exec();
void quit(int = 0);
void set_menubar(RefPtr<MenuBar>);
Action* action_for_key_event(const KeyEvent&);
void register_global_shortcut_action(Badge<Action>, Action&);
void unregister_global_shortcut_action(Badge<Action>, Action&);
void show_tooltip(String, const Widget* tooltip_source_widget);
void hide_tooltip();
Widget* tooltip_source_widget() { return m_tooltip_source_widget; };
bool quit_when_last_window_deleted() const { return m_quit_when_last_window_deleted; }
void set_quit_when_last_window_deleted(bool b) { m_quit_when_last_window_deleted = b; }
void did_create_window(Badge<Window>);
void did_delete_last_window(Badge<Window>);
const String& invoked_as() const { return m_invoked_as; }
const Vector<String>& args() const { return m_args; }
Gfx::Palette palette() const;
void set_palette(const Gfx::Palette&);
void set_system_palette(SharedBuffer&);
bool focus_debugging_enabled() const { return m_focus_debugging_enabled; }
bool dnd_debugging_enabled() const { return m_dnd_debugging_enabled; }
Core::EventLoop& event_loop() { return *m_event_loop; }
Window* active_window() { return m_active_window; }
const Window* active_window() const { return m_active_window; }
void window_did_become_active(Badge<Window>, Window&);
void window_did_become_inactive(Badge<Window>, Window&);
Widget* drag_hovered_widget() { return m_drag_hovered_widget.ptr(); }
const Widget* drag_hovered_widget() const { return m_drag_hovered_widget.ptr(); }
Widget* pending_drop_widget() { return m_pending_drop_widget.ptr(); }
const Widget* pending_drop_widget() const { return m_pending_drop_widget.ptr(); }
void set_drag_hovered_widget(Badge<Window>, Widget* widget, const Gfx::IntPoint& position = {}, Vector<String> mime_types = {})
{
set_drag_hovered_widget_impl(widget, position, move(mime_types));
}
void notify_drag_cancelled(Badge<WindowServerConnection>);
private:
Application(int argc, char** argv);
void tooltip_show_timer_did_fire();
void tooltip_hide_timer_did_fire();
void set_drag_hovered_widget_impl(Widget*, const Gfx::IntPoint& = {}, Vector<String> = {});
void set_pending_drop_widget(Widget*);
OwnPtr<Core::EventLoop> m_event_loop;
RefPtr<MenuBar> m_menubar;
RefPtr<Gfx::PaletteImpl> m_palette;
RefPtr<Gfx::PaletteImpl> m_system_palette;
HashMap<Shortcut, Action*> m_global_shortcut_actions;
class TooltipWindow;
RefPtr<Core::Timer> m_tooltip_show_timer;
RefPtr<Core::Timer> m_tooltip_hide_timer;
RefPtr<TooltipWindow> m_tooltip_window;
RefPtr<Widget> m_tooltip_source_widget;
WeakPtr<Window> m_active_window;
bool m_quit_when_last_window_deleted { true };
bool m_focus_debugging_enabled { false };
bool m_dnd_debugging_enabled { false };
String m_invoked_as;
Vector<String> m_args;
WeakPtr<Widget> m_drag_hovered_widget;
WeakPtr<Widget> m_pending_drop_widget;
};
}

View file

@ -0,0 +1,203 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/AutocompleteProvider.h>
#include <LibGUI/Model.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
static RefPtr<Gfx::Bitmap> s_cpp_identifier_icon;
static RefPtr<Gfx::Bitmap> s_unspecified_identifier_icon;
namespace GUI {
class AutocompleteSuggestionModel final : public GUI::Model {
public:
explicit AutocompleteSuggestionModel(Vector<AutocompleteProvider::Entry>&& suggestions)
: m_suggestions(move(suggestions))
{
}
enum Column {
Icon,
Name,
__Column_Count,
};
enum InternalRole {
__ModelRoleCustom = (int)GUI::ModelRole::Custom,
PartialInputLength,
Kind,
};
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
auto& suggestion = m_suggestions.at(index.row());
if (role == GUI::ModelRole::Display) {
if (index.column() == Column::Name) {
return suggestion.completion;
}
if (index.column() == Column::Icon) {
// TODO
if (suggestion.language == GUI::AutocompleteProvider::Language::Cpp) {
if (!s_cpp_identifier_icon) {
s_cpp_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/cpp-identifier.png");
}
return *s_cpp_identifier_icon;
}
if (suggestion.language == GUI::AutocompleteProvider::Language::Unspecified) {
if (!s_unspecified_identifier_icon) {
s_unspecified_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/unspecified-identifier.png");
}
return *s_unspecified_identifier_icon;
}
return {};
}
}
if ((int)role == InternalRole::Kind)
return (u32)suggestion.kind;
if ((int)role == InternalRole::PartialInputLength)
return (i64)suggestion.partial_input_length;
return {};
}
virtual void update() override {};
void set_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions) { m_suggestions = move(suggestions); }
private:
Vector<AutocompleteProvider::Entry> m_suggestions;
};
AutocompleteBox::~AutocompleteBox() { }
AutocompleteBox::AutocompleteBox(TextEditor& editor)
: m_editor(editor)
{
m_popup_window = GUI::Window::construct(m_editor->window());
m_popup_window->set_window_type(GUI::WindowType::Tooltip);
m_popup_window->set_rect(0, 0, 200, 100);
m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>();
m_suggestion_view->set_column_headers_visible(false);
}
void AutocompleteBox::update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions)
{
bool has_suggestions = !suggestions.is_empty();
if (m_suggestion_view->model()) {
auto& model = *static_cast<AutocompleteSuggestionModel*>(m_suggestion_view->model());
model.set_suggestions(move(suggestions));
} else {
m_suggestion_view->set_model(adopt(*new AutocompleteSuggestionModel(move(suggestions))));
m_suggestion_view->update();
if (has_suggestions)
m_suggestion_view->set_cursor(m_suggestion_view->model()->index(0), GUI::AbstractView::SelectionUpdate::Set);
}
m_suggestion_view->model()->update();
m_suggestion_view->update();
if (!has_suggestions)
close();
}
bool AutocompleteBox::is_visible() const
{
return m_popup_window->is_visible();
}
void AutocompleteBox::show(Gfx::IntPoint suggstion_box_location)
{
if (!m_suggestion_view->model() || m_suggestion_view->model()->row_count() == 0)
return;
m_popup_window->move_to(suggstion_box_location);
if (!is_visible())
m_suggestion_view->move_cursor(GUI::AbstractView::CursorMovement::Home, GUI::AbstractTableView::SelectionUpdate::Set);
m_popup_window->show();
}
void AutocompleteBox::close()
{
m_popup_window->hide();
}
void AutocompleteBox::next_suggestion()
{
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() + 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_valid(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
}
void AutocompleteBox::previous_suggestion()
{
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() - 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_valid(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
}
void AutocompleteBox::apply_suggestion()
{
if (m_editor.is_null())
return;
if (!m_editor->is_editable())
return;
auto selected_index = m_suggestion_view->selection().first();
if (!selected_index.is_valid())
return;
auto suggestion_index = m_suggestion_view->model()->index(selected_index.row(), AutocompleteSuggestionModel::Column::Name);
auto suggestion = suggestion_index.data().to_string();
size_t partial_length = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::PartialInputLength).to_i64();
ASSERT(suggestion.length() >= partial_length);
auto completion = suggestion.substring_view(partial_length, suggestion.length() - partial_length);
m_editor->insert_at_cursor_or_replace_selection(completion);
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Forward.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
namespace GUI {
class AutocompleteProvider {
AK_MAKE_NONCOPYABLE(AutocompleteProvider);
AK_MAKE_NONMOVABLE(AutocompleteProvider);
public:
virtual ~AutocompleteProvider() { }
enum class CompletionKind {
Identifier,
};
enum class Language {
Unspecified,
Cpp,
};
struct Entry {
String completion;
size_t partial_input_length { 0 };
CompletionKind kind { CompletionKind::Identifier };
Language language { Language::Unspecified };
};
virtual void provide_completions(Function<void(Vector<Entry>)>) = 0;
void attach(TextEditor& editor)
{
ASSERT(!m_editor);
m_editor = editor;
}
void detach() { m_editor.clear(); }
protected:
AutocompleteProvider() { }
WeakPtr<TextEditor> m_editor;
};
class AutocompleteBox final {
public:
explicit AutocompleteBox(TextEditor&);
~AutocompleteBox();
void update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions);
bool is_visible() const;
void show(Gfx::IntPoint suggstion_box_location);
void close();
void next_suggestion();
void previous_suggestion();
void apply_suggestion();
private:
WeakPtr<TextEditor> m_editor;
RefPtr<GUI::Window> m_popup_window;
RefPtr<GUI::TableView> m_suggestion_view;
};
}

View file

@ -0,0 +1,239 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/JsonObject.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Widget.h>
#include <LibGfx/Orientation.h>
#include <stdio.h>
//#define GBOXLAYOUT_DEBUG
REGISTER_WIDGET(GUI, HorizontalBoxLayout)
REGISTER_WIDGET(GUI, VerticalBoxLayout)
namespace GUI {
BoxLayout::BoxLayout(Orientation orientation)
: m_orientation(orientation)
{
register_property(
"orientation", [this] { return m_orientation == Gfx::Orientation::Vertical ? "Vertical" : "Horizontal"; }, nullptr);
}
Gfx::IntSize BoxLayout::preferred_size() const
{
Gfx::IntSize size;
size.set_primary_size_for_orientation(orientation(), preferred_primary_size());
size.set_secondary_size_for_orientation(orientation(), preferred_secondary_size());
return size;
}
int BoxLayout::preferred_primary_size() const
{
int size = 0;
for (auto& entry : m_entries) {
if (!entry.widget || !entry.widget->is_visible())
continue;
int min_size = entry.widget->min_size().primary_size_for_orientation(orientation());
int max_size = entry.widget->max_size().primary_size_for_orientation(orientation());
int preferred_primary_size = -1;
if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
preferred_primary_size = entry.widget->layout()->preferred_size().primary_size_for_orientation(orientation());
}
int item_size = max(0, preferred_primary_size);
item_size = max(min_size, item_size);
item_size = min(max_size, item_size);
size += item_size + spacing();
}
if (size > 0)
size -= spacing();
if (orientation() == Gfx::Orientation::Horizontal)
size += margins().left() + margins().right();
else
size += margins().top() + margins().bottom();
if (!size)
return -1;
return size;
}
int BoxLayout::preferred_secondary_size() const
{
int size = 0;
for (auto& entry : m_entries) {
if (!entry.widget || !entry.widget->is_visible())
continue;
int min_size = entry.widget->min_size().secondary_size_for_orientation(orientation());
int preferred_secondary_size = -1;
if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
preferred_secondary_size = entry.widget->layout()->preferred_size().secondary_size_for_orientation(orientation());
size = max(size, preferred_secondary_size);
}
size = max(min_size, size);
}
if (orientation() == Gfx::Orientation::Horizontal)
size += margins().top() + margins().bottom();
else
size += margins().left() + margins().right();
if (!size)
return -1;
return size;
}
void BoxLayout::run(Widget& widget)
{
if (m_entries.is_empty())
return;
struct Item {
Widget* widget { nullptr };
int min_size { -1 };
int max_size { -1 };
int size { 0 };
bool final { false };
};
Vector<Item, 32> items;
for (size_t i = 0; i < m_entries.size(); ++i) {
auto& entry = m_entries[i];
if (entry.type == Entry::Type::Spacer) {
items.append(Item { nullptr, -1, -1 });
continue;
}
if (!entry.widget)
continue;
if (!entry.widget->is_visible())
continue;
auto min_size = entry.widget->min_size();
auto max_size = entry.widget->max_size();
if (entry.widget->is_shrink_to_fit() && entry.widget->layout()) {
auto preferred_size = entry.widget->layout()->preferred_size();
min_size = max_size = preferred_size;
}
items.append(Item { entry.widget.ptr(), min_size.primary_size_for_orientation(orientation()), max_size.primary_size_for_orientation(orientation()) });
}
if (items.is_empty())
return;
int available_size = widget.size().primary_size_for_orientation(orientation()) - spacing() * (items.size() - 1);
int unfinished_items = items.size();
if (orientation() == Gfx::Orientation::Horizontal)
available_size -= margins().left() + margins().right();
else
available_size -= margins().top() + margins().bottom();
// Pass 1: Set all items to their minimum size.
for (auto& item : items) {
item.size = 0;
if (item.min_size >= 0)
item.size = item.min_size;
available_size -= item.size;
if (item.min_size >= 0 && item.max_size >= 0 && item.min_size == item.max_size) {
// Fixed-size items finish immediately in the first pass.
item.final = true;
--unfinished_items;
}
}
// Pass 2: Distribute remaining available size evenly, respecting each item's maximum size.
while (unfinished_items && available_size > 0) {
int slice = available_size / unfinished_items;
available_size = 0;
for (auto& item : items) {
if (item.final)
continue;
int item_size_with_full_slice = item.size + slice;
item.size = item_size_with_full_slice;
if (item.max_size >= 0)
item.size = min(item.max_size, item_size_with_full_slice);
// If the slice was more than we needed, return remained to available_size.
int remainder_to_give_back = item_size_with_full_slice - item.size;
available_size += remainder_to_give_back;
if (item.max_size >= 0 && item.size == item.max_size) {
// We've hit the item's max size. Don't give it any more space.
item.final = true;
--unfinished_items;
}
}
}
// Pass 3: Place the widgets.
int current_x = margins().left();
int current_y = margins().top();
for (auto& item : items) {
Gfx::IntRect rect { current_x, current_y, 0, 0 };
rect.set_primary_size_for_orientation(orientation(), item.size);
if (item.widget) {
int secondary = widget.size().secondary_size_for_orientation(orientation());
if (orientation() == Gfx::Orientation::Horizontal)
secondary -= margins().top() + margins().bottom();
else
secondary -= margins().left() + margins().right();
int min_secondary = item.widget->min_size().secondary_size_for_orientation(orientation());
int max_secondary = item.widget->max_size().secondary_size_for_orientation(orientation());
if (min_secondary >= 0)
secondary = max(secondary, min_secondary);
if (max_secondary >= 0)
secondary = min(secondary, max_secondary);
rect.set_secondary_size_for_orientation(orientation(), secondary);
if (orientation() == Gfx::Orientation::Horizontal)
rect.center_vertically_within(widget.rect());
else
rect.center_horizontally_within(widget.rect());
item.widget->set_relative_rect(rect);
}
if (orientation() == Gfx::Orientation::Horizontal)
current_x += rect.width() + spacing();
else
current_y += rect.height() + spacing();
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Forward.h>
#include <LibGUI/Layout.h>
#include <LibGfx/Orientation.h>
namespace GUI {
class BoxLayout : public Layout {
C_OBJECT(BoxLayout);
public:
virtual ~BoxLayout() override { }
Gfx::Orientation orientation() const { return m_orientation; }
virtual void run(Widget&) override;
virtual Gfx::IntSize preferred_size() const override;
protected:
explicit BoxLayout(Gfx::Orientation);
private:
int preferred_primary_size() const;
int preferred_secondary_size() const;
Gfx::Orientation m_orientation;
};
class VerticalBoxLayout final : public BoxLayout {
C_OBJECT(VerticalBoxLayout);
public:
explicit VerticalBoxLayout()
: BoxLayout(Gfx::Orientation::Vertical)
{
}
virtual ~VerticalBoxLayout() override { }
};
class HorizontalBoxLayout final : public BoxLayout {
C_OBJECT(HorizontalBoxLayout);
public:
explicit HorizontalBoxLayout()
: BoxLayout(Gfx::Orientation::Horizontal)
{
}
virtual ~HorizontalBoxLayout() override { }
};
}

View file

@ -0,0 +1,148 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/BoxLayout.h>
#include <LibGUI/BreadcrumbBar.h>
#include <LibGUI/Button.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
REGISTER_WIDGET(GUI, BreadcrumbBar)
namespace GUI {
class BreadcrumbButton : public Button {
C_OBJECT(BreadcrumbButton);
public:
virtual ~BreadcrumbButton() override { }
virtual bool is_uncheckable() const override { return false; }
virtual void drop_event(DropEvent& event) override
{
if (on_drop)
on_drop(event);
}
virtual void drag_enter_event(DragEvent& event) override
{
update();
if (on_drag_enter)
on_drag_enter(event);
}
virtual void drag_leave_event(Event&) override
{
update();
}
virtual void paint_event(PaintEvent& event) override
{
Button::paint_event(event);
if (has_pending_drop()) {
Painter painter(*this);
painter.draw_rect(rect(), palette().selection(), true);
}
}
Function<void(DropEvent&)> on_drop;
Function<void(DragEvent&)> on_drag_enter;
private:
BreadcrumbButton() { }
};
BreadcrumbBar::BreadcrumbBar()
{
auto& layout = set_layout<HorizontalBoxLayout>();
layout.set_spacing(0);
}
BreadcrumbBar::~BreadcrumbBar()
{
}
void BreadcrumbBar::clear_segments()
{
m_segments.clear();
remove_all_children();
}
void BreadcrumbBar::append_segment(const String& text, const Gfx::Bitmap* icon, const String& data)
{
auto& button = add<BreadcrumbButton>();
button.set_button_style(Gfx::ButtonStyle::CoolBar);
button.set_text(text);
button.set_icon(icon);
button.set_focus_policy(FocusPolicy::TabFocus);
button.set_checkable(true);
button.set_exclusive(true);
button.on_click = [this, index = m_segments.size()](auto) {
if (on_segment_click)
on_segment_click(index);
};
button.on_drop = [this, index = m_segments.size()](auto& drop_event) {
if (on_segment_drop)
on_segment_drop(index, drop_event);
};
button.on_drag_enter = [this, index = m_segments.size()](auto& event) {
if (on_segment_drag_enter)
on_segment_drag_enter(index, event);
};
auto button_text_width = button.font().width(text);
auto icon_width = icon ? icon->width() : 0;
auto icon_padding = icon ? 4 : 0;
button.set_fixed_size(button_text_width + icon_width + icon_padding + 16, 16 + 8);
Segment segment { icon, text, data, button.make_weak_ptr<GUI::Button>() };
m_segments.append(move(segment));
}
void BreadcrumbBar::set_selected_segment(Optional<size_t> index)
{
if (!index.has_value()) {
for_each_child_of_type<GUI::AbstractButton>([&](auto& button) {
button.set_checked(false);
return IterationDecision::Continue;
});
return;
}
auto& segment = m_segments[index.value()];
ASSERT(segment.button);
segment.button->set_checked(true);
}
void BreadcrumbBar::doubleclick_event(MouseEvent& event)
{
if (on_doubleclick)
on_doubleclick(event);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace GUI {
class BreadcrumbBar : public GUI::Widget {
C_OBJECT(BreadcrumbBar);
public:
virtual ~BreadcrumbBar() override;
void clear_segments();
void append_segment(const String& text, const Gfx::Bitmap* icon = nullptr, const String& data = {});
size_t segment_count() const { return m_segments.size(); }
String segment_data(size_t index) const { return m_segments[index].data; }
void set_selected_segment(Optional<size_t> index);
Optional<size_t> selected_segment() const { return m_selected_segment; }
Function<void(size_t index)> on_segment_click;
Function<void(size_t index, DropEvent&)> on_segment_drop;
Function<void(size_t index, DragEvent&)> on_segment_drag_enter;
Function<void(MouseEvent& event)> on_doubleclick;
private:
BreadcrumbBar();
struct Segment {
RefPtr<const Gfx::Bitmap> icon;
String text;
String data;
WeakPtr<GUI::Button> button;
};
Vector<Segment> m_segments;
Optional<size_t> m_selected_segment;
virtual void doubleclick_event(GUI::MouseEvent&) override;
};
}

View file

@ -0,0 +1,170 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Button.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
REGISTER_WIDGET(GUI, Button)
namespace GUI {
Button::Button(String text)
: AbstractButton(move(text))
{
set_min_width(32);
set_fixed_height(22);
set_focus_policy(GUI::FocusPolicy::StrongFocus);
REGISTER_ENUM_PROPERTY(
"button_style", button_style, set_button_style, Gfx::ButtonStyle,
{ Gfx::ButtonStyle::Normal, "Normal" },
{ Gfx::ButtonStyle::CoolBar, "CoolBar" });
}
Button::~Button()
{
if (m_action)
m_action->unregister_button({}, *this);
}
void Button::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
Gfx::StylePainter::paint_button(painter, rect(), palette(), m_button_style, is_being_pressed(), is_hovered(), is_checked(), is_enabled(), is_focused());
if (text().is_empty() && !m_icon)
return;
auto content_rect = rect().shrunken(8, 2);
auto icon_location = m_icon ? content_rect.center().translated(-(m_icon->width() / 2), -(m_icon->height() / 2)) : Gfx::IntPoint();
if (m_icon && !text().is_empty())
icon_location.set_x(content_rect.x());
if (is_being_pressed() || is_checked())
painter.translate(1, 1);
else if (m_icon && is_enabled() && is_hovered() && button_style() == Gfx::ButtonStyle::CoolBar) {
auto shadow_color = palette().button().darkened(0.7f);
painter.blit_filtered(icon_location.translated(1, 1), *m_icon, m_icon->rect(), [&shadow_color](auto) {
return shadow_color;
});
icon_location.move_by(-1, -1);
}
if (m_icon) {
if (is_enabled()) {
if (is_hovered())
painter.blit_brightened(icon_location, *m_icon, m_icon->rect());
else
painter.blit(icon_location, *m_icon, m_icon->rect());
} else {
painter.blit_disabled(icon_location, *m_icon, m_icon->rect(), palette());
}
}
auto& font = is_checked() ? Gfx::FontDatabase::default_bold_font() : this->font();
if (m_icon && !text().is_empty()) {
content_rect.move_by(m_icon->width() + 4, 0);
content_rect.set_width(content_rect.width() - m_icon->width() - 4);
}
Gfx::IntRect text_rect { 0, 0, font.width(text()), font.glyph_height() };
if (text_rect.width() > content_rect.width())
text_rect.set_width(content_rect.width());
text_rect.align_within(content_rect, text_alignment());
paint_text(painter, text_rect, font, text_alignment());
if (is_focused()) {
Gfx::IntRect focus_rect;
if (m_icon && !text().is_empty())
focus_rect = text_rect.inflated(6, 6);
else
focus_rect = rect().shrunken(8, 8);
painter.draw_focus_rect(focus_rect, palette().focus_outline());
}
}
void Button::click(unsigned modifiers)
{
if (!is_enabled())
return;
NonnullRefPtr protector = *this;
if (is_checkable()) {
if (is_checked() && !is_uncheckable())
return;
set_checked(!is_checked());
}
if (on_click)
on_click(modifiers);
if (m_action)
m_action->activate(this);
}
void Button::context_menu_event(ContextMenuEvent& context_menu_event)
{
if (!is_enabled())
return;
if (on_context_menu_request)
on_context_menu_request(context_menu_event);
}
void Button::set_action(Action& action)
{
m_action = action;
action.register_button({}, *this);
set_enabled(action.is_enabled());
set_checkable(action.is_checkable());
if (action.is_checkable())
set_checked(action.is_checked());
}
void Button::set_icon(RefPtr<Gfx::Bitmap>&& icon)
{
if (m_icon == icon)
return;
m_icon = move(icon);
update();
}
bool Button::is_uncheckable() const
{
if (!m_action)
return true;
if (!m_action->group())
return true;
return m_action->group()->is_unchecking_allowed();
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Function.h>
#include <LibGUI/AbstractButton.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/StylePainter.h>
#include <LibGfx/TextAlignment.h>
namespace GUI {
class Button : public AbstractButton {
C_OBJECT(Button);
public:
virtual ~Button() override;
void set_icon(RefPtr<Gfx::Bitmap>&&);
const Gfx::Bitmap* icon() const { return m_icon.ptr(); }
Gfx::Bitmap* icon() { return m_icon.ptr(); }
void set_text_alignment(Gfx::TextAlignment text_alignment) { m_text_alignment = text_alignment; }
Gfx::TextAlignment text_alignment() const { return m_text_alignment; }
Function<void(unsigned modifiers)> on_click;
Function<void(const ContextMenuEvent&)> on_context_menu_request;
void set_button_style(Gfx::ButtonStyle style) { m_button_style = style; }
Gfx::ButtonStyle button_style() const { return m_button_style; }
virtual void click(unsigned modifiers = 0) override;
virtual void context_menu_event(ContextMenuEvent&) override;
void set_action(Action&);
virtual bool is_uncheckable() const override;
protected:
explicit Button(String text = {});
virtual void paint_event(PaintEvent&) override;
private:
RefPtr<Gfx::Bitmap> m_icon;
Gfx::ButtonStyle m_button_style { Gfx::ButtonStyle::Normal };
Gfx::TextAlignment m_text_alignment { Gfx::TextAlignment::Center };
WeakPtr<Action> m_action;
};
}

View file

@ -0,0 +1,114 @@
compile_gml(FontPickerDialog.gml FontPickerDialogGML.h font_picker_dialog_gml)
set(SOURCES
AboutDialog.cpp
AbstractButton.cpp
AbstractSlider.cpp
AbstractTableView.cpp
AbstractView.cpp
Action.cpp
ActionGroup.cpp
Application.cpp
AutocompleteProvider.cpp
BoxLayout.cpp
BreadcrumbBar.cpp
Button.cpp
Calendar.cpp
CheckBox.cpp
Clipboard.cpp
ColorInput.cpp
ColorPicker.cpp
ColumnsView.cpp
ComboBox.cpp
Command.cpp
ControlBoxButton.cpp
CppSyntaxHighlighter.cpp
Desktop.cpp
Dialog.cpp
DisplayLink.cpp
DragOperation.cpp
EditingEngine.cpp
EmojiInputDialog.cpp
Event.cpp
FileIconProvider.cpp
FilePicker.cpp
FileSystemModel.cpp
FilteringProxyModel.cpp
FontPicker.cpp
FontPickerDialogGML.h
Frame.cpp
GMLFormatter.cpp
GMLLexer.cpp
GMLParser.cpp
GMLSyntaxHighlighter.cpp
GroupBox.cpp
HeaderView.cpp
INILexer.cpp
INISyntaxHighlighter.cpp
Icon.cpp
IconView.cpp
ImageWidget.cpp
InputBox.cpp
JSSyntaxHighlighter.cpp
JsonArrayModel.cpp
Label.cpp
Layout.cpp
LazyWidget.cpp
LinkLabel.cpp
ListView.cpp
Menu.cpp
MenuBar.cpp
MenuItem.cpp
MessageBox.cpp
Model.cpp
ModelIndex.cpp
ModelSelection.cpp
MultiView.cpp
Notification.cpp
OpacitySlider.cpp
Painter.cpp
ProcessChooser.cpp
ProgressBar.cpp
RadioButton.cpp
RegularEditingEngine.cpp
ResizeCorner.cpp
RunningProcessesModel.cpp
ScrollBar.cpp
ScrollableWidget.cpp
SeparatorWidget.cpp
ShellSyntaxHighlighter.cpp
Shortcut.cpp
Slider.cpp
SortingProxyModel.cpp
SpinBox.cpp
Splitter.cpp
StackWidget.cpp
StatusBar.cpp
SyntaxHighlighter.cpp
TabWidget.cpp
TableView.cpp
TextBox.cpp
TextDocument.cpp
TextEditor.cpp
ToolBar.cpp
ToolBarContainer.cpp
TreeView.cpp
UndoStack.cpp
Variant.cpp
VimEditingEngine.cpp
Widget.cpp
Window.cpp
WindowServerConnection.cpp
)
set(GENERATED_SOURCES
../../Services/WindowServer/WindowClientEndpoint.h
../../Services/WindowServer/WindowServerEndpoint.h
../../Services/NotificationServer/NotificationClientEndpoint.h
../../Services/NotificationServer/NotificationServerEndpoint.h
../../Services/Clipboard/ClipboardClientEndpoint.h
../../Services/Clipboard/ClipboardServerEndpoint.h
)
serenity_lib(LibGUI gui)
target_link_libraries(LibGUI LibCore LibGfx LibIPC LibThread LibCpp LibShell LibRegex LibJS)

View file

@ -0,0 +1,371 @@
/*
* Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibCore/DateTime.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/Calendar.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
namespace GUI {
static const char* long_day_names[] = {
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"
};
static const char* short_day_names[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char* mini_day_names[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
static const char* micro_day_names[] = { "S", "M", "T", "W", "T", "F", "S" };
static const char* long_month_names[] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
static const char* short_month_names[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
Calendar::Calendar(Core::DateTime date_time)
: m_selected_date(date_time)
, m_selected_year(date_time.year())
, m_selected_month(date_time.month())
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>();
layout()->set_spacing(0);
m_day_name_container = add<GUI::Widget>();
m_day_name_container->set_layout<GUI::HorizontalBoxLayout>();
m_day_name_container->set_fixed_height(16);
m_day_name_container->layout()->set_spacing(0);
m_day_name_container->set_fill_with_background_color(true);
m_day_name_container->set_background_role(Gfx::ColorRole::HoverHighlight);
for (auto& day : m_day_names) {
day = m_day_name_container->add<GUI::Label>();
day->set_font(Gfx::FontDatabase::default_bold_font());
}
m_calendar_tile_container = add<GUI::Widget>();
m_calendar_tile_container->set_layout<GUI::VerticalBoxLayout>();
m_calendar_tile_container->layout()->set_spacing(0);
for (auto& row : m_week_rows) {
row = m_calendar_tile_container->add<GUI::Widget>();
row->set_layout<GUI::HorizontalBoxLayout>();
row->layout()->set_spacing(0);
}
int i = 0;
for (int j = 0; j < 6; j++)
for (int k = 0; k < 7; k++) {
m_calendar_tiles[i] = m_week_rows[j]->add<CalendarTile>(i, date_time);
m_calendar_tiles[i]->on_click = [this](int index) {
m_previous_selected_date = m_selected_date;
m_selected_date = m_calendar_tiles[index]->get_date_time();
update_tiles(m_selected_date.year(), m_selected_date.month());
if (on_calendar_tile_click)
on_calendar_tile_click();
};
m_calendar_tiles[i]->on_doubleclick = [this](int index) {
if (m_calendar_tiles[index]->get_date_time().day() != m_previous_selected_date.day())
return;
if (on_calendar_tile_doubleclick)
on_calendar_tile_doubleclick();
};
i++;
}
m_month_tile_container = add<GUI::Widget>();
m_month_tile_container->set_visible(false);
m_month_tile_container->set_layout<GUI::VerticalBoxLayout>();
m_month_tile_container->set_fill_with_background_color(true);
m_month_tile_container->set_background_role(Gfx::ColorRole::HoverHighlight);
m_month_tile_container->layout()->set_spacing(0);
for (auto& row : m_month_rows) {
row = m_month_tile_container->add<GUI::Widget>();
row->set_layout<GUI::HorizontalBoxLayout>();
row->layout()->set_spacing(0);
}
i = 0;
for (int j = 0; j < 3; j++)
for (int k = 0; k < 4; k++) {
m_month_tiles[i] = m_month_rows[j]->add<MonthTile>(i, date_time);
m_month_tiles[i]->set_button_style(Gfx::ButtonStyle::CoolBar);
m_month_tiles[i]->on_indexed_click = [this](int index) {
toggle_mode();
update_tiles(m_month_tiles[index]->get_date_time().year(), m_month_tiles[index]->get_date_time().month());
if (on_month_tile_click)
on_month_tile_click();
};
i++;
}
update_tiles(selected_year(), selected_month());
}
Calendar::~Calendar()
{
}
void Calendar::toggle_mode()
{
m_mode == Month ? m_mode = Year : m_mode = Month;
if (mode() == Month) {
m_day_name_container->set_visible(true);
m_calendar_tile_container->set_visible(true);
m_month_tile_container->set_visible(false);
} else {
m_day_name_container->set_visible(false);
m_calendar_tile_container->set_visible(false);
m_month_tile_container->set_visible(true);
}
this->resize(this->height(), this->width());
update_tiles(selected_year(), selected_month());
}
void Calendar::set_grid(bool grid)
{
if (m_grid == grid)
return;
m_grid = grid;
for (int i = 0; i < 42; i++) {
m_calendar_tiles[i]->set_grid(grid);
m_calendar_tiles[i]->update();
}
}
void Calendar::resize_event(GUI::ResizeEvent& event)
{
if (m_day_name_container->is_visible()) {
for (int i = 0; i < 7; i++) {
if (event.size().width() < 120)
m_day_names[i]->set_text(micro_day_names[i]);
else if (event.size().width() < 200)
m_day_names[i]->set_text(mini_day_names[i]);
else if (event.size().width() < 480)
m_day_names[i]->set_text(short_day_names[i]);
else
m_day_names[i]->set_text(long_day_names[i]);
}
}
if (m_month_tile_container->is_visible()) {
for (int i = 0; i < 12; i++) {
if (event.size().width() < 250)
m_month_tiles[i]->set_text(short_month_names[i]);
else
m_month_tiles[i]->set_text(long_month_names[i]);
}
}
(event.size().width() < 200) ? set_grid(false) : set_grid(true);
}
void Calendar::update_tiles(unsigned int target_year, unsigned int target_month)
{
set_selected_calendar(target_year, target_month);
if (mode() == Month) {
unsigned int i = 0;
for (int y = 0; y < 6; y++)
for (int x = 0; x < 7; x++) {
auto date_time = Core::DateTime::create(target_year, target_month, 1);
unsigned int start_of_month = date_time.weekday();
unsigned int year;
unsigned int month;
unsigned int day;
if (start_of_month > i) {
month = (target_month - 1 == 0) ? 12 : target_month - 1;
year = (month == 12) ? target_year - 1 : target_year;
date_time.set_time(year, month, 1);
day = (date_time.days_in_month() - (start_of_month) + i) + 1;
date_time.set_time(year, month, day);
} else if ((i - start_of_month) + 1 > date_time.days_in_month()) {
month = (target_month + 1) > 12 ? 1 : target_month + 1;
year = (month == 1) ? target_year + 1 : target_year;
day = ((i - start_of_month) + 1) - date_time.days_in_month();
date_time.set_time(year, month, day);
} else {
month = target_month;
year = target_year;
day = (i - start_of_month) + 1;
date_time.set_time(year, month, day);
}
m_calendar_tiles[i]->update_values(i, date_time);
m_calendar_tiles[i]->set_selected(date_time.year() == m_selected_date.year() && date_time.month() == m_selected_date.month() && date_time.day() == m_selected_date.day());
m_calendar_tiles[i]->set_outside_selection(date_time.month() != selected_month() || date_time.year() != selected_year());
m_calendar_tiles[i]->update();
i++;
}
} else {
for (int i = 0; i < 12; i++) {
auto date_time = Core::DateTime::create(target_year, i + 1, 1);
m_month_tiles[i]->update_values(date_time);
}
}
}
const String Calendar::selected_calendar_text(bool long_names)
{
if (mode() == Month)
return String::formatted("{} {}", long_names ? long_month_names[m_selected_month - 1] : short_month_names[m_selected_month - 1], m_selected_year);
else
return String::number(m_selected_year);
}
void Calendar::set_selected_calendar(unsigned int year, unsigned int month)
{
m_selected_year = year;
m_selected_month = month;
}
Calendar::MonthTile::MonthTile(int index, Core::DateTime date_time)
: m_index(index)
, m_date_time(date_time)
{
}
Calendar::MonthTile::~MonthTile()
{
}
void Calendar::MonthTile::mouseup_event(GUI::MouseEvent& event)
{
if (on_indexed_click)
on_indexed_click(m_index);
GUI::Button::mouseup_event(event);
}
Calendar::CalendarTile::CalendarTile(int index, Core::DateTime date_time)
{
set_frame_thickness(0);
update_values(index, date_time);
}
void Calendar::CalendarTile::update_values(int index, Core::DateTime date_time)
{
m_index = index;
m_date_time = date_time;
m_display_date = (m_date_time.day() == 1) ? String::formatted("{} {}", short_month_names[m_date_time.month() - 1], m_date_time.day()) : String::number(m_date_time.day());
}
Calendar::CalendarTile::~CalendarTile()
{
}
void Calendar::CalendarTile::doubleclick_event(GUI::MouseEvent&)
{
if (on_doubleclick)
on_doubleclick(m_index);
}
void Calendar::CalendarTile::mousedown_event(GUI::MouseEvent&)
{
if (on_click)
on_click(m_index);
}
void Calendar::CalendarTile::enter_event(Core::Event&)
{
m_hovered = true;
update();
}
void Calendar::CalendarTile::leave_event(Core::Event&)
{
m_hovered = false;
update();
}
bool Calendar::CalendarTile::is_today() const
{
auto current_date_time = Core::DateTime::now();
return m_date_time.day() == current_date_time.day() && m_date_time.month() == current_date_time.month() && m_date_time.year() == current_date_time.year();
}
void Calendar::CalendarTile::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(frame_inner_rect());
if (is_hovered() || is_selected())
painter.fill_rect(frame_inner_rect(), palette().hover_highlight());
else
painter.fill_rect(frame_inner_rect(), palette().base());
if (m_index < 7)
painter.draw_line(frame_inner_rect().top_left(), frame_inner_rect().top_right(), Color::NamedColor::Black);
if (!((m_index + 1) % 7 == 0) && has_grid())
painter.draw_line(frame_inner_rect().top_right(), frame_inner_rect().bottom_right(), Color::NamedColor::Black);
if (m_index < 35 && has_grid())
painter.draw_line(frame_inner_rect().bottom_left(), frame_inner_rect().bottom_right(), Color::NamedColor::Black);
Gfx::IntRect day_rect;
if (has_grid()) {
day_rect = Gfx::IntRect(frame_inner_rect().x(), frame_inner_rect().y(), frame_inner_rect().width(), font().glyph_height() + 4);
day_rect.set_y(frame_inner_rect().y() + 4);
} else {
day_rect = Gfx::IntRect(frame_inner_rect());
}
int highlight_rect_width = (font().glyph_width('0') * (m_display_date.length() + 1)) + 2;
auto display_date = (m_date_time.day() == 1 && frame_inner_rect().width() > highlight_rect_width) ? m_display_date : String::number(m_date_time.day());
if (is_today()) {
if (has_grid()) {
auto highlight_rect = Gfx::IntRect(day_rect.width() / 2 - (highlight_rect_width / 2), day_rect.y(), highlight_rect_width, font().glyph_height() + 4);
painter.draw_rect(highlight_rect, palette().base_text());
} else if (is_selected()) {
painter.draw_rect(frame_inner_rect(), palette().base_text());
}
painter.draw_text(day_rect, display_date, Gfx::FontDatabase::default_bold_font(), Gfx::TextAlignment::Center, palette().base_text());
} else if (is_outside_selection()) {
painter.draw_text(day_rect, display_date, Gfx::FontDatabase::default_font(), Gfx::TextAlignment::Center, Color::LightGray);
} else {
if (!has_grid() && is_selected())
painter.draw_rect(frame_inner_rect(), palette().base_text());
painter.draw_text(day_rect, display_date, Gfx::FontDatabase::default_font(), Gfx::TextAlignment::Center, palette().base_text());
}
}
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@gmail.com>
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/String.h>
#include <LibCore/DateTime.h>
#include <LibGUI/Button.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Label.h>
#include <LibGUI/Widget.h>
namespace GUI {
class Calendar final : public GUI::Widget {
C_OBJECT(Calendar)
public:
enum Mode {
Month,
Year
};
enum {
ShortNames,
LongNames
};
Calendar(Core::DateTime);
virtual ~Calendar() override;
unsigned int selected_year() const { return m_selected_year; }
unsigned int selected_month() const { return m_selected_month; }
const String selected_calendar_text(bool long_names = ShortNames);
void update_tiles(unsigned int target_year, unsigned int target_month);
void set_selected_calendar(unsigned int year, unsigned int month);
void set_selected_date(Core::DateTime date_time) { m_selected_date = date_time; }
Core::DateTime selected_date() const { return m_selected_date; }
void toggle_mode();
void set_grid(bool grid);
bool has_grid() { return m_grid; }
Mode mode() const { return m_mode; }
Function<void()> on_calendar_tile_click;
Function<void()> on_calendar_tile_doubleclick;
Function<void()> on_month_tile_click;
private:
virtual void resize_event(GUI::ResizeEvent&) override;
class CalendarTile final : public GUI::Frame {
C_OBJECT(CalendarTile)
public:
CalendarTile(int index, Core::DateTime m_date_time);
void update_values(int index, Core::DateTime date_time);
virtual ~CalendarTile() override;
bool is_today() const;
bool is_hovered() const { return m_hovered; }
bool is_selected() const { return m_selected; }
void set_selected(bool b) { m_selected = b; }
bool is_outside_selection() const { return m_outside_selection; }
void set_outside_selection(bool b) { m_outside_selection = b; }
bool has_grid() const { return m_grid; }
void set_grid(bool b) { m_grid = b; }
Core::DateTime get_date_time() { return m_date_time; }
Function<void(int index)> on_doubleclick;
Function<void(int index)> on_click;
private:
virtual void doubleclick_event(GUI::MouseEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void enter_event(Core::Event&) override;
virtual void leave_event(Core::Event&) override;
virtual void paint_event(GUI::PaintEvent&) override;
int m_index { 0 };
bool m_outside_selection { false };
bool m_hovered { false };
bool m_selected { false };
bool m_grid { true };
String m_display_date;
Core::DateTime m_date_time;
};
class MonthTile final : public GUI::Button {
C_OBJECT(MonthTile)
public:
MonthTile(int index, Core::DateTime m_date_time);
virtual ~MonthTile() override;
void update_values(Core::DateTime date_time) { m_date_time = date_time; }
Core::DateTime get_date_time() { return m_date_time; }
Function<void(int index)> on_indexed_click;
private:
virtual void mouseup_event(GUI::MouseEvent&) override;
int m_index { 0 };
Core::DateTime m_date_time;
};
RefPtr<MonthTile> m_month_tiles[12];
RefPtr<CalendarTile> m_calendar_tiles[42];
RefPtr<GUI::Label> m_day_names[7];
RefPtr<GUI::Widget> m_week_rows[6];
RefPtr<GUI::Widget> m_month_rows[3];
RefPtr<GUI::Widget> m_month_tile_container;
RefPtr<GUI::Widget> m_calendar_tile_container;
RefPtr<GUI::Widget> m_day_name_container;
Core::DateTime m_selected_date;
Core::DateTime m_previous_selected_date;
unsigned int m_selected_year { 0 };
unsigned int m_selected_month { 0 };
bool m_grid { true };
Mode m_mode { Month };
};
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/CheckBox.h>
#include <LibGUI/Painter.h>
#include <LibGfx/CharacterBitmap.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
REGISTER_WIDGET(GUI, CheckBox)
namespace GUI {
static const int s_box_width = 13;
static const int s_box_height = 13;
CheckBox::CheckBox(String text)
: AbstractButton(move(text))
{
set_min_width(32);
set_fixed_height(22);
}
CheckBox::~CheckBox()
{
}
void CheckBox::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
auto text_rect = rect();
text_rect.set_left(s_box_width + 4);
text_rect.set_width(font().width(text()));
text_rect.set_top(height() / 2 - font().glyph_height() / 2);
text_rect.set_height(font().glyph_height());
if (fill_with_background_color())
painter.fill_rect(rect(), palette().window());
if (is_enabled() && is_hovered())
painter.fill_rect(rect(), palette().hover_highlight());
Gfx::IntRect box_rect {
0, height() / 2 - s_box_height / 2 - 1,
s_box_width, s_box_height
};
Gfx::StylePainter::paint_check_box(painter, box_rect, palette(), is_enabled(), is_checked(), is_being_pressed());
paint_text(painter, text_rect, font(), Gfx::TextAlignment::TopLeft);
if (is_focused())
painter.draw_focus_rect(text_rect.inflated(6, 6), palette().focus_outline());
}
void CheckBox::click(unsigned)
{
if (!is_enabled())
return;
set_checked(!is_checked());
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/AbstractButton.h>
namespace GUI {
class CheckBox : public AbstractButton {
C_OBJECT(CheckBox);
public:
virtual ~CheckBox() override;
virtual void click(unsigned modifiers = 0) override;
private:
explicit CheckBox(String = {});
// These don't make sense for a check box, so hide them.
using AbstractButton::auto_repeat_interval;
using AbstractButton::set_auto_repeat_interval;
virtual void paint_event(PaintEvent&) override;
};
}

View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Badge.h>
#include <AK/SharedBuffer.h>
#include <Clipboard/ClipboardClientEndpoint.h>
#include <Clipboard/ClipboardServerEndpoint.h>
#include <LibGUI/Clipboard.h>
#include <LibIPC/ServerConnection.h>
namespace GUI {
class ClipboardServerConnection : public IPC::ServerConnection<ClipboardClientEndpoint, ClipboardServerEndpoint>
, public ClipboardClientEndpoint {
C_OBJECT(ClipboardServerConnection);
public:
virtual void handshake() override
{
auto response = send_sync<Messages::ClipboardServer::Greet>();
set_my_client_id(response->client_id());
}
private:
ClipboardServerConnection()
: IPC::ServerConnection<ClipboardClientEndpoint, ClipboardServerEndpoint>(*this, "/tmp/portal/clipboard")
{
}
virtual void handle(const Messages::ClipboardClient::ClipboardDataChanged&) override;
};
Clipboard& Clipboard::the()
{
static Clipboard* s_the;
if (!s_the)
s_the = new Clipboard;
return *s_the;
}
ClipboardServerConnection* s_connection;
static ClipboardServerConnection& connection()
{
return *s_connection;
}
void Clipboard::initialize(Badge<Application>)
{
s_connection = &ClipboardServerConnection::construct().leak_ref();
}
Clipboard::Clipboard()
{
}
Clipboard::DataAndType Clipboard::data_and_type() const
{
auto response = connection().send_sync<Messages::ClipboardServer::GetClipboardData>();
if (response->shbuf_id() < 0)
return {};
auto shared_buffer = SharedBuffer::create_from_shbuf_id(response->shbuf_id());
if (!shared_buffer) {
dbgln("GUI::Clipboard::data() failed to attach to the shared buffer");
return {};
}
if (response->data_size() > shared_buffer->size()) {
dbgln("GUI::Clipboard::data() clipping contents size is greater than shared buffer size");
return {};
}
auto data = ByteBuffer::copy(shared_buffer->data<void>(), response->data_size());
auto type = response->mime_type();
auto metadata = response->metadata().entries();
return { data, type, metadata };
}
void Clipboard::set_data(ReadonlyBytes data, const String& type, const HashMap<String, String>& metadata)
{
auto shared_buffer = SharedBuffer::create_with_size(data.size());
if (!shared_buffer) {
dbgln("GUI::Clipboard::set_data() failed to create a shared buffer");
return;
}
if (!data.is_empty())
memcpy(shared_buffer->data<void>(), data.data(), data.size());
shared_buffer->seal();
shared_buffer->share_with(connection().server_pid());
connection().send_sync<Messages::ClipboardServer::SetClipboardData>(shared_buffer->shbuf_id(), data.size(), type, metadata);
}
void ClipboardServerConnection::handle(const Messages::ClipboardClient::ClipboardDataChanged& message)
{
auto& clipboard = Clipboard::the();
if (clipboard.on_change)
clipboard.on_change(message.mime_type());
}
RefPtr<Gfx::Bitmap> Clipboard::bitmap() const
{
auto clipping = data_and_type();
if (clipping.mime_type != "image/x-serenityos")
return nullptr;
auto width = clipping.metadata.get("width").value_or("0").to_uint();
if (!width.has_value() || width.value() == 0)
return nullptr;
auto height = clipping.metadata.get("height").value_or("0").to_uint();
if (!height.has_value() || height.value() == 0)
return nullptr;
auto pitch = clipping.metadata.get("pitch").value_or("0").to_uint();
if (!pitch.has_value() || pitch.value() == 0)
return nullptr;
auto format = clipping.metadata.get("format").value_or("0").to_uint();
if (!format.has_value() || format.value() == 0)
return nullptr;
auto clipping_bitmap = Gfx::Bitmap::create_wrapper((Gfx::BitmapFormat)format.value(), { (int)width.value(), (int)height.value() }, pitch.value(), clipping.data.data());
auto bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGBA32, { (int)width.value(), (int)height.value() });
for (int y = 0; y < clipping_bitmap->height(); ++y) {
for (int x = 0; x < clipping_bitmap->width(); ++x) {
auto pixel = clipping_bitmap->get_pixel(x, y);
bitmap->set_pixel(x, y, pixel);
}
}
return bitmap;
}
void Clipboard::set_bitmap(const Gfx::Bitmap& bitmap)
{
HashMap<String, String> metadata;
metadata.set("width", String::number(bitmap.width()));
metadata.set("height", String::number(bitmap.height()));
metadata.set("format", String::number((int)bitmap.format()));
metadata.set("pitch", String::number(bitmap.pitch()));
set_data({ bitmap.scanline(0), bitmap.size_in_bytes() }, "image/x-serenityos", metadata);
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/String.h>
#include <LibGUI/Forward.h>
#include <LibGfx/Forward.h>
namespace GUI {
class Clipboard {
public:
static Clipboard& the();
ByteBuffer data() const { return data_and_type().data; }
String mime_type() const { return data_and_type().mime_type; }
void set_data(ReadonlyBytes, const String& mime_type = "text/plain", const HashMap<String, String>& metadata = {});
void set_plain_text(const String& text)
{
set_data(text.bytes());
}
void set_bitmap(const Gfx::Bitmap&);
RefPtr<Gfx::Bitmap> bitmap() const;
struct DataAndType {
ByteBuffer data;
String mime_type;
HashMap<String, String> metadata;
};
DataAndType data_and_type() const;
Function<void(const String& mime_type)> on_change;
static void initialize(Badge<Application>);
private:
Clipboard();
};
}

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibCore/Timer.h>
#include <LibGUI/ColorInput.h>
#include <LibGUI/ColorPicker.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Palette.h>
REGISTER_WIDGET(GUI, ColorInput)
namespace GUI {
ColorInput::ColorInput()
: TextEditor(TextEditor::SingleLine)
{
set_min_width(32);
set_fixed_height(22);
TextEditor::on_change = [this] {
auto parsed_color = Color::from_string(text());
if (parsed_color.has_value())
set_color_without_changing_text(parsed_color.value());
};
REGISTER_STRING_PROPERTY("color_picker_title", color_picker_title, set_color_picker_title);
REGISTER_BOOL_PROPERTY("has_alpha_channel", has_alpha_channel, set_color_has_alpha_channel);
}
ColorInput::~ColorInput()
{
}
Gfx::IntRect ColorInput::color_rect() const
{
auto color_box_padding = 3;
auto color_box_size = height() - color_box_padding - color_box_padding;
return { width() - color_box_size - color_box_padding, color_box_padding, color_box_size, color_box_size };
}
void ColorInput::set_color_without_changing_text(Color color)
{
if (m_color == color)
return;
m_color = color;
update();
if (on_change)
on_change();
}
void ColorInput::set_color(Color color)
{
if (m_color == color)
return;
set_text(m_color_has_alpha_channel ? color.to_string() : color.to_string_without_alpha());
};
void ColorInput::mousedown_event(MouseEvent& event)
{
if (event.button() == MouseButton::Left && color_rect().contains(event.position())) {
m_may_be_color_rect_click = true;
return;
}
TextEditor::mousedown_event(event);
}
void ColorInput::mouseup_event(MouseEvent& event)
{
if (event.button() == MouseButton::Left) {
bool is_color_rect_click = m_may_be_color_rect_click && color_rect().contains(event.position());
m_may_be_color_rect_click = false;
if (is_color_rect_click) {
auto dialog = GUI::ColorPicker::construct(m_color, window(), m_color_picker_title);
dialog->set_color_has_alpha_channel(m_color_has_alpha_channel);
if (dialog->exec() == GUI::Dialog::ExecOK)
set_color(dialog->color());
event.accept();
return;
}
}
TextEditor::mouseup_event(event);
}
void ColorInput::mousemove_event(MouseEvent& event)
{
if (color_rect().contains(event.position())) {
set_override_cursor(Gfx::StandardCursor::Hand);
event.accept();
return;
} else {
set_override_cursor(Gfx::StandardCursor::IBeam);
}
TextEditor::mousemove_event(event);
}
void ColorInput::paint_event(PaintEvent& event)
{
TextEditor::paint_event(event);
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.fill_rect(color_rect(), m_color);
painter.draw_rect(color_rect(), Color::Black);
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Function.h>
#include <LibGUI/TextEditor.h>
namespace GUI {
class ColorInput final : public TextEditor {
C_OBJECT(ColorInput);
public:
virtual ~ColorInput() override;
bool has_alpha_channel() const { return m_color_has_alpha_channel; }
void set_color_has_alpha_channel(bool has_alpha) { m_color_has_alpha_channel = has_alpha; }
void set_color(Color);
Color color() { return m_color; }
void set_color_picker_title(String title) { m_color_picker_title = move(title); }
String color_picker_title() { return m_color_picker_title; }
Function<void()> on_change;
protected:
virtual void mousedown_event(MouseEvent&) override;
virtual void mouseup_event(MouseEvent&) override;
virtual void mousemove_event(MouseEvent&) override;
virtual void paint_event(PaintEvent&) override;
private:
ColorInput();
Gfx::IntRect color_rect() const;
void set_color_without_changing_text(Color);
Color m_color;
String m_color_picker_title { "Select color" };
bool m_color_has_alpha_channel { true };
bool m_may_be_color_rect_click { false };
};
}

View file

@ -0,0 +1,724 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ColorPicker.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Label.h>
#include <LibGUI/Painter.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/Palette.h>
namespace GUI {
class ColorButton : public AbstractButton {
C_OBJECT(ColorButton);
public:
virtual ~ColorButton() override;
void set_selected(bool selected);
Color color() const { return m_color; }
Function<void(const Color)> on_click;
protected:
virtual void click(unsigned modifiers = 0) override;
virtual void doubleclick_event(GUI::MouseEvent&) override;
virtual void paint_event(PaintEvent&) override;
private:
explicit ColorButton(ColorPicker& picker, Color color = {});
ColorPicker& m_picker;
Color m_color;
bool m_selected { false };
};
class ColorField final : public GUI::Frame {
C_OBJECT(ColorField);
public:
Function<void(Color)> on_pick;
void set_color(Color);
void set_hue(double);
void set_hue_from_pick(double);
private:
ColorField(Color color);
Color m_color;
// save hue separately so full white color doesn't reset it to 0
double m_hue;
RefPtr<Gfx::Bitmap> m_color_bitmap;
bool m_being_pressed { false };
Gfx::IntPoint m_last_position;
void create_color_bitmap();
void pick_color_at_position(GUI::MouseEvent& event);
void recalculate_position();
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void mouseup_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void paint_event(GUI::PaintEvent&) override;
virtual void resize_event(ResizeEvent&) override;
};
class ColorSlider final : public GUI::Frame {
C_OBJECT(ColorSlider);
public:
Function<void(double)> on_pick;
void set_value(double);
private:
ColorSlider(double value);
double m_value;
RefPtr<Gfx::Bitmap> m_color_bitmap;
bool m_being_pressed { false };
int m_last_position;
void pick_value_at_position(GUI::MouseEvent& event);
void recalculate_position();
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void mouseup_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void paint_event(GUI::PaintEvent&) override;
virtual void resize_event(ResizeEvent&) override;
};
class ColorPreview final : public GUI::Widget {
C_OBJECT(ColorPreview);
public:
void set_color(Color);
private:
ColorPreview(Color);
Color m_color;
virtual void paint_event(GUI::PaintEvent&) override;
};
class CustomColorWidget final : public GUI::Widget {
C_OBJECT(CustomColorWidget);
public:
Function<void(Color)> on_pick;
void set_color(Color);
private:
CustomColorWidget(Color);
RefPtr<ColorField> m_color_field;
RefPtr<ColorSlider> m_color_slider;
};
ColorPicker::ColorPicker(Color color, Window* parent_window, String title)
: Dialog(parent_window)
, m_color(color)
{
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/color-chooser.png"));
set_title(title);
set_resizable(false);
resize(458, 326);
build_ui();
}
ColorPicker::~ColorPicker()
{
}
void ColorPicker::set_color_has_alpha_channel(bool has_alpha)
{
if (m_color_has_alpha_channel == has_alpha)
return;
m_color_has_alpha_channel = has_alpha;
update_color_widgets();
}
void ColorPicker::build_ui()
{
auto& root_container = set_main_widget<Widget>();
root_container.set_layout<VerticalBoxLayout>();
root_container.layout()->set_margins({ 4, 4, 4, 4 });
root_container.set_fill_with_background_color(true);
auto& tab_widget = root_container.add<GUI::TabWidget>();
auto& tab_palette = tab_widget.add_tab<Widget>("Palette");
tab_palette.set_layout<VerticalBoxLayout>();
tab_palette.layout()->set_margins({ 4, 4, 4, 4 });
tab_palette.layout()->set_spacing(4);
build_ui_palette(tab_palette);
auto& tab_custom_color = tab_widget.add_tab<Widget>("Custom Color");
tab_custom_color.set_layout<VerticalBoxLayout>();
tab_custom_color.layout()->set_margins({ 4, 4, 4, 4 });
tab_custom_color.layout()->set_spacing(4);
build_ui_custom(tab_custom_color);
auto& button_container = root_container.add<Widget>();
button_container.set_fixed_height(22);
button_container.set_layout<HorizontalBoxLayout>();
button_container.layout()->set_spacing(4);
button_container.layout()->add_spacer();
auto& ok_button = button_container.add<Button>();
ok_button.set_fixed_width(80);
ok_button.set_text("OK");
ok_button.on_click = [this](auto) {
done(ExecOK);
};
auto& cancel_button = button_container.add<Button>();
cancel_button.set_fixed_width(80);
cancel_button.set_text("Cancel");
cancel_button.on_click = [this](auto) {
done(ExecCancel);
};
}
void ColorPicker::build_ui_palette(Widget& root_container)
{
unsigned colors[4][9] = {
{ 0xef2929, 0xf0b143, 0xfce94f, 0x9fe13a, 0x7c9ece, 0xa680a8, 0xe1ba70, 0x888a85, 0xeeeeec },
{ 0xba1e09, 0xf57900, 0xe9d51a, 0x8bd121, 0x4164a3, 0x6f517b, 0xb77f19, 0x555753, 0xd4d7cf },
{ 0x961605, 0xbf600c, 0xe9d51a, 0x619910, 0x2b4986, 0x573666, 0x875b09, 0x2f3436, 0xbbbdb6 },
{ 0x000000, 0x2f3436, 0x555753, 0x808080, 0xbabdb6, 0xd3d7cf, 0xeeeeec, 0xf3f3f3, 0xffffff }
};
for (int r = 0; r < 4; r++) {
auto& colors_row = root_container.add<Widget>();
colors_row.set_layout<HorizontalBoxLayout>();
for (int i = 0; i < 8; i++) {
create_color_button(colors_row, colors[r][i]);
}
}
}
void ColorPicker::build_ui_custom(Widget& root_container)
{
enum RGBComponent {
Red,
Green,
Blue,
Alpha
};
auto& horizontal_container = root_container.add<Widget>();
horizontal_container.set_fill_with_background_color(true);
horizontal_container.set_layout<HorizontalBoxLayout>();
// Left Side
m_custom_color = horizontal_container.add<CustomColorWidget>(m_color);
m_custom_color->set_fixed_size(299, 260);
m_custom_color->on_pick = [this](Color color) {
if (m_color == color)
return;
m_color = color;
update_color_widgets();
};
// Right Side
auto& vertical_container = horizontal_container.add<Widget>();
vertical_container.set_layout<VerticalBoxLayout>();
vertical_container.layout()->set_margins({ 8, 0, 0, 0 });
vertical_container.set_fixed_width(128);
auto& preview_container = vertical_container.add<Frame>();
preview_container.set_layout<VerticalBoxLayout>();
preview_container.layout()->set_margins({ 2, 2, 2, 2 });
preview_container.layout()->set_spacing(0);
preview_container.set_fixed_height(128);
// Current color
preview_container.add<ColorPreview>(m_color);
// Preview selected color
m_preview_widget = preview_container.add<ColorPreview>(m_color);
vertical_container.layout()->add_spacer();
// HTML
auto& html_container = vertical_container.add<GUI::Widget>();
html_container.set_layout<GUI::HorizontalBoxLayout>();
html_container.set_fixed_height(22);
auto& html_label = html_container.add<GUI::Label>();
html_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
html_label.set_fixed_width(48);
html_label.set_text("HTML:");
m_html_text = html_container.add<GUI::TextBox>();
m_html_text->set_text(m_color_has_alpha_channel ? m_color.to_string() : m_color.to_string_without_alpha());
m_html_text->on_change = [this]() {
auto color_name = m_html_text->text();
auto optional_color = Color::from_string(color_name);
if (optional_color.has_value() && (!color_name.starts_with("#") || color_name.length() == ((m_color_has_alpha_channel) ? 9 : 7))) {
// The color length must be 9/7 (unless it is a name like red), because:
// - If we allowed 5/4 character rgb color, the field would reset to 9/7 characters after you deleted 4/3 characters.
auto color = optional_color.value();
if (m_color == color)
return;
m_color = optional_color.value();
m_custom_color->set_color(color);
update_color_widgets();
}
};
// RGB Lines
auto make_spinbox = [&](RGBComponent component, int initial_value) {
auto& rgb_container = vertical_container.add<GUI::Widget>();
rgb_container.set_layout<GUI::HorizontalBoxLayout>();
rgb_container.set_fixed_height(22);
auto& rgb_label = rgb_container.add<GUI::Label>();
rgb_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
rgb_label.set_fixed_width(48);
auto& spinbox = rgb_container.add<SpinBox>();
spinbox.set_fixed_height(20);
spinbox.set_min(0);
spinbox.set_max(255);
spinbox.set_value(initial_value);
spinbox.set_enabled(m_color_has_alpha_channel);
spinbox.on_change = [this, component](auto value) {
auto color = m_color;
if (component == Red)
color.set_red(value);
if (component == Green)
color.set_green(value);
if (component == Blue)
color.set_blue(value);
if (component == Alpha)
color.set_alpha(value);
if (m_color == color)
return;
m_color = color;
m_custom_color->set_color(color);
update_color_widgets();
};
if (component == Red) {
rgb_label.set_text("Red:");
m_red_spinbox = spinbox;
} else if (component == Green) {
rgb_label.set_text("Green:");
m_green_spinbox = spinbox;
} else if (component == Blue) {
rgb_label.set_text("Blue:");
m_blue_spinbox = spinbox;
} else if (component == Alpha) {
rgb_label.set_text("Alpha:");
m_alpha_spinbox = spinbox;
}
};
make_spinbox(Red, m_color.red());
make_spinbox(Green, m_color.green());
make_spinbox(Blue, m_color.blue());
make_spinbox(Alpha, m_color.alpha());
}
void ColorPicker::update_color_widgets()
{
m_preview_widget->set_color(m_color);
m_html_text->set_text(m_color_has_alpha_channel ? m_color.to_string() : m_color.to_string_without_alpha());
m_red_spinbox->set_value(m_color.red());
m_green_spinbox->set_value(m_color.green());
m_blue_spinbox->set_value(m_color.blue());
m_alpha_spinbox->set_value(m_color.alpha());
m_alpha_spinbox->set_enabled(m_color_has_alpha_channel);
}
void ColorPicker::create_color_button(Widget& container, unsigned rgb)
{
Color color = Color::from_rgb(rgb);
auto& widget = container.add<ColorButton>(*this, color);
widget.on_click = [this](Color color) {
for (auto& value : m_color_widgets) {
value->set_selected(false);
value->update();
}
m_color = color;
m_custom_color->set_color(color);
update_color_widgets();
};
if (color == m_color) {
widget.set_selected(true);
}
m_color_widgets.append(&widget);
}
ColorButton::ColorButton(ColorPicker& picker, Color color)
: m_picker(picker)
{
m_color = color;
}
ColorButton::~ColorButton()
{
}
void ColorButton::set_selected(bool selected)
{
m_selected = selected;
}
void ColorButton::doubleclick_event(GUI::MouseEvent&)
{
click();
m_selected = true;
m_picker.done(Dialog::ExecOK);
}
void ColorButton::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
Gfx::StylePainter::paint_button(painter, rect(), palette(), Gfx::ButtonStyle::Normal, is_being_pressed(), is_hovered(), is_checked(), is_enabled(), is_focused());
painter.fill_rect(rect().shrunken(2, 2), m_color);
if (m_selected) {
painter.fill_rect(rect().shrunken(6, 6), Color::Black);
painter.fill_rect(rect().shrunken(10, 10), Color::White);
painter.fill_rect(rect().shrunken(14, 14), m_color);
}
}
void ColorButton::click(unsigned)
{
if (on_click)
on_click(m_color);
m_selected = true;
}
CustomColorWidget::CustomColorWidget(Color color)
{
set_layout<HorizontalBoxLayout>();
m_color_field = add<ColorField>(color);
auto size = 256 + (m_color_field->frame_thickness() * 2);
m_color_field->set_fixed_size(size, size);
m_color_field->on_pick = [this](Color color) {
if (on_pick)
on_pick(color);
};
m_color_slider = add<ColorSlider>(color.to_hsv().hue);
auto slider_width = 24 + (m_color_slider->frame_thickness() * 2);
m_color_slider->set_fixed_size(slider_width, size);
m_color_slider->on_pick = [this](double value) {
m_color_field->set_hue_from_pick(value);
};
}
void CustomColorWidget::set_color(Color color)
{
m_color_field->set_color(color);
m_color_field->set_hue(color.to_hsv().hue);
}
ColorField::ColorField(Color color)
: m_color(color)
, m_hue(color.to_hsv().hue)
{
create_color_bitmap();
}
void ColorField::create_color_bitmap()
{
m_color_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, { 256, 256 });
auto painter = Gfx::Painter(*m_color_bitmap);
Gfx::HSV hsv;
hsv.hue = m_hue;
for (int x = 0; x < 256; x++) {
hsv.saturation = x / 255.0f;
for (int y = 0; y < 256; y++) {
hsv.value = (255 - y) / 255.0f;
Color color = Color::from_hsv(hsv);
painter.set_pixel({ x, y }, color);
}
}
}
void ColorField::set_color(Color color)
{
if (m_color == color)
return;
m_color = color;
// don't save m_hue here by default, we don't want to set it to 0 in case color is full white
// m_hue = color.to_hsv().hue;
recalculate_position();
}
void ColorField::recalculate_position()
{
Gfx::HSV hsv = m_color.to_hsv();
auto x = hsv.saturation * width();
auto y = (1 - hsv.value) * height();
m_last_position = Gfx::IntPoint(x, y);
update();
}
void ColorField::set_hue(double hue)
{
if (m_hue == hue)
return;
auto hsv = m_color.to_hsv();
hsv.hue = hue;
m_hue = hue;
create_color_bitmap();
auto color = Color::from_hsv(hsv);
color.set_alpha(m_color.alpha());
set_color(color);
}
void ColorField::set_hue_from_pick(double hue)
{
set_hue(hue);
if (on_pick)
on_pick(m_color);
}
void ColorField::pick_color_at_position(GUI::MouseEvent& event)
{
if (!m_being_pressed)
return;
auto inner_rect = frame_inner_rect();
auto position = event.position().constrained(inner_rect).translated(-frame_thickness(), -frame_thickness());
auto color = Color::from_hsv(m_hue, (double)position.x() / inner_rect.width(), (double)(inner_rect.height() - position.y()) / inner_rect.height());
color.set_alpha(m_color.alpha());
m_last_position = position;
m_color = color;
if (on_pick)
on_pick(color);
update();
}
void ColorField::mousedown_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Left) {
m_being_pressed = true;
pick_color_at_position(event);
}
}
void ColorField::mouseup_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Left) {
m_being_pressed = false;
pick_color_at_position(event);
}
}
void ColorField::mousemove_event(GUI::MouseEvent& event)
{
if (event.buttons() & GUI::MouseButton::Left)
pick_color_at_position(event);
}
void ColorField::paint_event(GUI::PaintEvent& event)
{
Frame::paint_event(event);
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.add_clip_rect(frame_inner_rect());
painter.draw_scaled_bitmap(frame_inner_rect(), *m_color_bitmap, m_color_bitmap->rect());
painter.translate(frame_thickness(), frame_thickness());
painter.draw_line({ m_last_position.x() - 1, 0 }, { m_last_position.x() - 1, height() }, Color::White);
painter.draw_line({ m_last_position.x() + 1, 0 }, { m_last_position.x() + 1, height() }, Color::White);
painter.draw_line({ 0, m_last_position.y() - 1 }, { width(), m_last_position.y() - 1 }, Color::White);
painter.draw_line({ 0, m_last_position.y() + 1 }, { width(), m_last_position.y() + 1 }, Color::White);
painter.draw_line({ m_last_position.x(), 0 }, { m_last_position.x(), height() }, Color::Black);
painter.draw_line({ 0, m_last_position.y() }, { width(), m_last_position.y() }, Color::Black);
}
void ColorField::resize_event(ResizeEvent&)
{
recalculate_position();
}
ColorSlider::ColorSlider(double value)
: m_value(value)
{
m_color_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::RGB32, { 32, 360 });
auto painter = Gfx::Painter(*m_color_bitmap);
for (int h = 0; h < 360; h++) {
Gfx::HSV hsv;
hsv.hue = h;
hsv.saturation = 1.0;
hsv.value = 1.0;
Color color = Color::from_hsv(hsv);
painter.draw_line({ 0, h }, { 32, h }, color);
}
}
void ColorSlider::set_value(double value)
{
if (m_value == value)
return;
m_value = value;
recalculate_position();
}
void ColorSlider::recalculate_position()
{
m_last_position = (m_value / 360.0) * height();
update();
}
void ColorSlider::pick_value_at_position(GUI::MouseEvent& event)
{
if (!m_being_pressed)
return;
auto inner_rect = frame_inner_rect();
auto position = event.position().constrained(inner_rect).translated(-frame_thickness(), -frame_thickness());
auto hue = (double)position.y() / inner_rect.height() * 360;
m_last_position = position.y();
m_value = hue;
if (on_pick)
on_pick(m_value);
update();
}
void ColorSlider::mousedown_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Left) {
m_being_pressed = true;
pick_value_at_position(event);
}
}
void ColorSlider::mouseup_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Left) {
m_being_pressed = false;
pick_value_at_position(event);
}
}
void ColorSlider::mousemove_event(GUI::MouseEvent& event)
{
if (event.buttons() & GUI::MouseButton::Left)
pick_value_at_position(event);
}
void ColorSlider::paint_event(GUI::PaintEvent& event)
{
Frame::paint_event(event);
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.add_clip_rect(frame_inner_rect());
painter.draw_scaled_bitmap(frame_inner_rect(), *m_color_bitmap, m_color_bitmap->rect());
painter.translate(frame_thickness(), frame_thickness());
painter.draw_line({ 0, m_last_position - 1 }, { width(), m_last_position - 1 }, Color::White);
painter.draw_line({ 0, m_last_position + 1 }, { width(), m_last_position + 1 }, Color::White);
painter.draw_line({ 0, m_last_position }, { width(), m_last_position }, Color::Black);
}
void ColorSlider::resize_event(ResizeEvent&)
{
recalculate_position();
}
ColorPreview::ColorPreview(Color color)
: m_color(color)
{
}
void ColorPreview::set_color(Color color)
{
if (m_color == color)
return;
m_color = color;
update();
}
void ColorPreview::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
if (m_color.alpha() < 255) {
Gfx::StylePainter::paint_transparency_grid(painter, rect(), palette());
painter.fill_rect(rect(), m_color);
painter.fill_rect({ 0, 0, rect().width() / 4, rect().height() }, m_color.with_alpha(255));
} else {
painter.fill_rect(rect(), m_color);
}
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/AbstractButton.h>
#include <LibGUI/Dialog.h>
namespace GUI {
class ColorButton;
class ColorPreview;
class CustomColorWidget;
class ColorPicker final : public Dialog {
C_OBJECT(ColorPicker)
public:
virtual ~ColorPicker() override;
bool color_has_alpha_channel() const { return m_color_has_alpha_channel; }
void set_color_has_alpha_channel(bool);
Color color() const { return m_color; }
private:
explicit ColorPicker(Color, Window* parent_window = nullptr, String title = "Edit Color");
void build_ui();
void build_ui_custom(Widget& root_container);
void build_ui_palette(Widget& root_container);
void update_color_widgets();
void create_color_button(Widget& container, unsigned rgb);
Color m_color;
bool m_color_has_alpha_channel { true };
Vector<ColorButton*> m_color_widgets;
RefPtr<CustomColorWidget> m_custom_color;
RefPtr<ColorPreview> m_preview_widget;
RefPtr<TextBox> m_html_text;
RefPtr<SpinBox> m_red_spinbox;
RefPtr<SpinBox> m_green_spinbox;
RefPtr<SpinBox> m_blue_spinbox;
RefPtr<SpinBox> m_alpha_spinbox;
};
}

View file

@ -0,0 +1,347 @@
/*
* Copyright (c) 2020, Sergey Bugaev <bugaevc@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/ColumnsView.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGfx/CharacterBitmap.h>
#include <LibGfx/Palette.h>
namespace GUI {
static const char* s_arrow_bitmap_data = {
" "
" # "
" ## "
" ### "
" #### "
" ### "
" ## "
" # "
" "
};
static const int s_arrow_bitmap_width = 9;
static const int s_arrow_bitmap_height = 9;
ColumnsView::ColumnsView()
{
set_fill_with_background_color(true);
set_background_role(ColorRole::Base);
set_foreground_role(ColorRole::BaseText);
m_columns.append({ {}, 0 });
}
ColumnsView::~ColumnsView()
{
}
void ColumnsView::select_all()
{
Vector<Column> columns_for_selection;
selection().for_each_index([&](auto& index) {
for (auto& column : m_columns) {
if (column.parent_index == index.parent()) {
columns_for_selection.append(column);
return;
}
}
ASSERT_NOT_REACHED();
});
for (Column& column : columns_for_selection) {
int row_count = model()->row_count(column.parent_index);
for (int row = 0; row < row_count; row++) {
ModelIndex index = model()->index(row, m_model_column, column.parent_index);
selection().add(index);
}
}
}
void ColumnsView::paint_event(PaintEvent& event)
{
AbstractView::paint_event(event);
if (!model())
return;
Painter painter(*this);
painter.add_clip_rect(frame_inner_rect());
painter.add_clip_rect(event.rect());
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
int column_x = 0;
auto selection_color = is_focused() ? palette().selection() : palette().inactive_selection();
for (size_t i = 0; i < m_columns.size(); i++) {
auto& column = m_columns[i];
auto* next_column = i + 1 == m_columns.size() ? nullptr : &m_columns[i + 1];
ASSERT(column.width > 0);
int row_count = model()->row_count(column.parent_index);
for (int row = 0; row < row_count; row++) {
ModelIndex index = model()->index(row, m_model_column, column.parent_index);
ASSERT(index.is_valid());
bool is_selected_row = selection().contains(index);
Color background_color = palette().color(background_role());
Color text_color = palette().color(foreground_role());
if (next_column != nullptr && next_column->parent_index == index) {
background_color = palette().inactive_selection();
text_color = palette().inactive_selection_text();
}
if (is_selected_row) {
background_color = selection_color;
text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text();
}
Gfx::IntRect row_rect { column_x, row * item_height(), column.width, item_height() };
painter.fill_rect(row_rect, background_color);
auto icon = index.data(ModelRole::Icon);
Gfx::IntRect icon_rect = { column_x + icon_spacing(), 0, icon_size(), icon_size() };
icon_rect.center_vertically_within(row_rect);
if (icon.is_icon()) {
if (auto* bitmap = icon.as_icon().bitmap_for_size(icon_size())) {
if (is_selected_row) {
auto tint = selection_color.with_alpha(100);
painter.blit_filtered(icon_rect.location(), *bitmap, bitmap->rect(), [&](auto src) { return src.blend(tint); });
} else if (m_hovered_index.is_valid() && m_hovered_index.parent() == index.parent() && m_hovered_index.row() == index.row()) {
painter.blit_brightened(icon_rect.location(), *bitmap, bitmap->rect());
} else {
painter.blit(icon_rect.location(), *bitmap, bitmap->rect());
}
}
}
Gfx::IntRect text_rect = {
icon_rect.right() + 1 + icon_spacing(), row * item_height(),
column.width - icon_spacing() - icon_size() - icon_spacing() - icon_spacing() - s_arrow_bitmap_width - icon_spacing(), item_height()
};
draw_item_text(painter, index, is_selected_row, text_rect, index.data().to_string(), font_for_index(index), Gfx::TextAlignment::CenterLeft, Gfx::TextElision::None);
if (is_focused() && index == cursor_index()) {
painter.draw_rect(row_rect, palette().color(background_role()));
painter.draw_focus_rect(row_rect, palette().focus_outline());
}
if (has_pending_drop() && index == drop_candidate_index()) {
painter.draw_rect(row_rect, palette().selection(), true);
}
bool expandable = model()->row_count(index) > 0;
if (expandable) {
Gfx::IntRect arrow_rect = {
text_rect.right() + 1 + icon_spacing(), 0,
s_arrow_bitmap_width, s_arrow_bitmap_height
};
arrow_rect.center_vertically_within(row_rect);
static auto& arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_arrow_bitmap_data, s_arrow_bitmap_width, s_arrow_bitmap_height).leak_ref();
painter.draw_bitmap(arrow_rect.location(), arrow_bitmap, text_color);
}
}
int separator_height = content_size().height();
if (height() > separator_height)
separator_height = height();
painter.draw_line({ column_x + column.width, 0 }, { column_x + column.width, separator_height }, palette().button());
column_x += column.width + 1;
}
}
void ColumnsView::push_column(const ModelIndex& parent_index)
{
ASSERT(model());
// Drop columns at the end.
ModelIndex grandparent = model()->parent_index(parent_index);
for (int i = m_columns.size() - 1; i > 0; i--) {
if (m_columns[i].parent_index == grandparent)
break;
m_columns.shrink(i);
dbgln("Dropping column {}", i);
}
// Add the new column.
dbgln("Adding a new column");
m_columns.append({ parent_index, 0 });
update_column_sizes();
update();
}
void ColumnsView::update_column_sizes()
{
if (!model())
return;
int total_width = 0;
int total_height = 0;
for (auto& column : m_columns) {
int row_count = model()->row_count(column.parent_index);
int column_height = row_count * item_height();
if (column_height > total_height)
total_height = column_height;
column.width = 10;
for (int row = 0; row < row_count; row++) {
ModelIndex index = model()->index(row, m_model_column, column.parent_index);
ASSERT(index.is_valid());
auto text = index.data().to_string();
int row_width = icon_spacing() + icon_size() + icon_spacing() + font().width(text) + icon_spacing() + s_arrow_bitmap_width + icon_spacing();
if (row_width > column.width)
column.width = row_width;
}
total_width += column.width + 1;
}
set_content_size({ total_width, total_height });
}
ModelIndex ColumnsView::index_at_event_position(const Gfx::IntPoint& a_position) const
{
if (!model())
return {};
auto position = a_position.translated(horizontal_scrollbar().value() - frame_thickness(), vertical_scrollbar().value() - frame_thickness());
int column_x = 0;
for (auto& column : m_columns) {
if (position.x() < column_x)
break;
if (position.x() > column_x + column.width) {
column_x += column.width;
continue;
}
int row = position.y() / item_height();
int row_count = model()->row_count(column.parent_index);
if (row >= row_count)
return {};
return model()->index(row, m_model_column, column.parent_index);
}
return {};
}
void ColumnsView::mousedown_event(MouseEvent& event)
{
AbstractView::mousedown_event(event);
if (!model())
return;
if (event.button() != MouseButton::Left)
return;
auto index = index_at_event_position(event.position());
if (index.is_valid() && !(event.modifiers() & Mod_Ctrl)) {
if (model()->row_count(index))
push_column(index);
}
}
void ColumnsView::model_did_update(unsigned flags)
{
AbstractView::model_did_update(flags);
// FIXME: Don't drop the columns on minor updates.
dbgln("Model was updated; dropping columns :(");
m_columns.clear();
m_columns.append({ {}, 0 });
update_column_sizes();
update();
}
void ColumnsView::move_cursor(CursorMovement movement, SelectionUpdate selection_update)
{
if (!model())
return;
auto& model = *this->model();
if (!cursor_index().is_valid()) {
set_cursor(model.index(0, m_model_column, {}), SelectionUpdate::Set);
return;
}
ModelIndex new_index;
auto cursor_parent = model.parent_index(cursor_index());
switch (movement) {
case CursorMovement::Up: {
int row = cursor_index().row() > 0 ? cursor_index().row() - 1 : 0;
new_index = model.index(row, cursor_index().column(), cursor_parent);
break;
}
case CursorMovement::Down: {
int row = cursor_index().row() + 1;
new_index = model.index(row, cursor_index().column(), cursor_parent);
break;
}
case CursorMovement::Left:
new_index = cursor_parent;
break;
case CursorMovement::Right:
new_index = model.index(0, m_model_column, cursor_index());
if (model.is_valid(new_index)) {
if (model.is_valid(cursor_index()))
push_column(cursor_index());
update();
break;
}
default:
break;
}
if (new_index.is_valid())
set_cursor(new_index, selection_update);
}
Gfx::IntRect ColumnsView::content_rect(const ModelIndex& index) const
{
if (!index.is_valid())
return {};
int column_x = 0;
for (auto& column : m_columns) {
if (column.parent_index == index.parent())
return { column_x + icon_size(), index.row() * item_height(), column.width - icon_size(), item_height() };
column_x += column.width + 1;
}
return {};
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2020, Sergey Bugaev <bugaevc@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Vector.h>
#include <LibGUI/AbstractView.h>
namespace GUI {
class ColumnsView : public AbstractView {
C_OBJECT(ColumnsView)
public:
int model_column() const { return m_model_column; }
void set_model_column(int column) { m_model_column = column; }
virtual ModelIndex index_at_event_position(const Gfx::IntPoint&) const override;
virtual Gfx::IntRect content_rect(const ModelIndex&) const override;
private:
ColumnsView();
virtual ~ColumnsView() override;
void push_column(const ModelIndex& parent_index);
void update_column_sizes();
int item_height() const { return 16; }
int icon_size() const { return 16; }
int icon_spacing() const { return 2; }
int text_padding() const { return 2; }
virtual void model_did_update(unsigned flags) override;
virtual void paint_event(PaintEvent&) override;
virtual void mousedown_event(MouseEvent& event) override;
void move_cursor(CursorMovement, SelectionUpdate) override;
virtual void select_all() override;
struct Column {
ModelIndex parent_index;
int width;
// TODO: per-column vertical scroll?
};
Vector<Column> m_columns;
int m_model_column { 0 };
};
}

View file

@ -0,0 +1,296 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/ComboBox.h>
#include <LibGUI/ControlBoxButton.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/ListView.h>
#include <LibGUI/Model.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
REGISTER_WIDGET(GUI, ComboBox)
namespace GUI {
class ComboBoxEditor final : public TextEditor {
C_OBJECT(ComboBoxEditor);
public:
Function<void(int delta)> on_mousewheel;
private:
ComboBoxEditor()
: TextEditor(TextEditor::SingleLine)
{
}
virtual void mousewheel_event(MouseEvent& event) override
{
if (!is_focused())
set_focus(true);
if (on_mousewheel)
on_mousewheel(event.wheel_delta());
}
};
ComboBox::ComboBox()
{
set_min_width(32);
set_fixed_height(22);
m_editor = add<ComboBoxEditor>();
m_editor->set_frame_thickness(0);
m_editor->on_return_pressed = [this] {
if (on_return_pressed)
on_return_pressed();
};
m_editor->on_up_pressed = [this] {
navigate(AbstractView::CursorMovement::Up);
};
m_editor->on_down_pressed = [this] {
navigate(AbstractView::CursorMovement::Down);
};
m_editor->on_pageup_pressed = [this] {
navigate(AbstractView::CursorMovement::PageUp);
};
m_editor->on_pagedown_pressed = [this] {
navigate(AbstractView::CursorMovement::PageDown);
};
m_editor->on_mousewheel = [this](int delta) {
// Since we can only show one item at a time we don't want to
// skip any. So just move one item at a time.
navigate_relative(delta > 0 ? 1 : -1);
};
m_editor->on_mousedown = [this] {
if (only_allow_values_from_model())
m_open_button->click();
};
m_open_button = add<ControlBoxButton>(ControlBoxButton::DownArrow);
m_open_button->set_focus_policy(GUI::FocusPolicy::NoFocus);
m_open_button->on_click = [this](auto) {
if (m_list_window->is_visible())
close();
else
open();
};
m_list_window = add<Window>(window());
m_list_window->set_frameless(true);
m_list_window->set_accessory(true);
m_list_window->on_active_input_change = [this](bool is_active_input) {
if (!is_active_input) {
m_open_button->set_enabled(false);
close();
}
m_open_button->set_enabled(true);
};
m_list_view = m_list_window->set_main_widget<ListView>();
m_list_view->horizontal_scrollbar().set_visible(false);
m_list_view->set_alternating_row_colors(false);
m_list_view->set_hover_highlighting(true);
m_list_view->set_frame_thickness(1);
m_list_view->set_frame_shadow(Gfx::FrameShadow::Plain);
m_list_view->on_selection = [this](auto& index) {
ASSERT(model());
m_list_view->set_activates_on_selection(true);
if (m_updating_model)
selection_updated(index);
};
m_list_view->on_activation = [this](auto& index) {
deferred_invoke([this, index](auto&) {
selection_updated(index);
if (on_change)
on_change(m_editor->text(), index);
});
m_list_view->set_activates_on_selection(false);
close();
};
m_list_view->on_escape_pressed = [this] {
close();
};
}
ComboBox::~ComboBox()
{
}
void ComboBox::navigate(AbstractView::CursorMovement cursor_movement)
{
auto previous_selected = m_list_view->cursor_index();
m_list_view->move_cursor(cursor_movement, AbstractView::SelectionUpdate::Set);
auto current_selected = m_list_view->cursor_index();
selection_updated(current_selected);
if (previous_selected.row() != current_selected.row() && on_change)
on_change(m_editor->text(), current_selected);
}
void ComboBox::navigate_relative(int delta)
{
auto previous_selected = m_list_view->cursor_index();
m_list_view->move_cursor_relative(delta, AbstractView::SelectionUpdate::Set);
auto current_selected = m_list_view->cursor_index();
selection_updated(current_selected);
if (previous_selected.row() != current_selected.row() && on_change)
on_change(m_editor->text(), current_selected);
}
void ComboBox::selection_updated(const ModelIndex& index)
{
if (index.is_valid())
m_selected_index = index;
else
m_selected_index.clear();
auto new_value = index.data().to_string();
m_editor->set_text(new_value);
if (!m_only_allow_values_from_model)
m_editor->select_all();
}
void ComboBox::resize_event(ResizeEvent& event)
{
Widget::resize_event(event);
int button_height = event.size().height() - frame_thickness() * 2;
int button_width = 15;
m_open_button->set_relative_rect(width() - button_width - frame_thickness(), frame_thickness(), button_width, button_height);
auto editor_rect = frame_inner_rect();
editor_rect.set_width(editor_rect.width() - button_width);
m_editor->set_relative_rect(editor_rect);
}
void ComboBox::set_model(NonnullRefPtr<Model> model)
{
TemporaryChange change(m_updating_model, true);
m_selected_index.clear();
m_list_view->set_model(move(model));
}
void ComboBox::set_selected_index(size_t index)
{
if (!m_list_view->model())
return;
TemporaryChange change(m_updating_model, true);
m_list_view->set_cursor(m_list_view->model()->index(index, 0), AbstractView::SelectionUpdate::Set);
}
size_t ComboBox::selected_index() const
{
return m_selected_index.has_value() ? m_selected_index.value().row() : 0;
}
void ComboBox::select_all()
{
m_editor->select_all();
}
void ComboBox::open()
{
if (!model())
return;
auto my_screen_rect = screen_relative_rect();
int longest_item_width = 0;
for (int i = 0; i < model()->row_count(); ++i) {
auto index = model()->index(i);
auto item_text = index.data().to_string();
longest_item_width = max(longest_item_width, m_list_view->font().width(item_text));
}
Gfx::IntSize size {
max(width(), longest_item_width + m_list_view->width_occupied_by_vertical_scrollbar() + m_list_view->frame_thickness() * 2 + m_list_view->horizontal_padding()),
model()->row_count() * m_list_view->item_height() + m_list_view->frame_thickness() * 2
};
auto taskbar_height = GUI::Desktop::the().taskbar_height();
auto menubar_height = GUI::Desktop::the().menubar_height();
// NOTE: This is so the combobox bottom edge exactly fits the taskbar's
// top edge - the value was found through trial and error though.
auto offset = 8;
Gfx::IntRect list_window_rect { my_screen_rect.bottom_left(), size };
list_window_rect.intersect(Desktop::the().rect().shrunken(0, taskbar_height + menubar_height + offset));
m_editor->set_has_visible_list(true);
m_editor->set_focus(true);
if (m_selected_index.has_value()) {
// Don't set m_updating_model to true here because we only want to
// change the list view's selected item without triggering a change to it.
m_list_view->set_cursor(m_selected_index.value(), AbstractView::SelectionUpdate::Set);
}
m_list_window->set_rect(list_window_rect);
m_list_window->show();
}
void ComboBox::close()
{
m_list_window->hide();
m_editor->set_has_visible_list(false);
m_editor->set_focus(true);
}
String ComboBox::text() const
{
return m_editor->text();
}
void ComboBox::set_text(const String& text)
{
m_editor->set_text(text);
}
void ComboBox::set_only_allow_values_from_model(bool b)
{
if (m_only_allow_values_from_model == b)
return;
m_only_allow_values_from_model = b;
m_editor->set_mode(m_only_allow_values_from_model ? TextEditor::DisplayOnly : TextEditor::Editable);
}
Model* ComboBox::model()
{
return m_list_view->model();
}
const Model* ComboBox::model() const
{
return m_list_view->model();
}
int ComboBox::model_column() const
{
return m_list_view->model_column();
}
void ComboBox::set_model_column(int column)
{
m_list_view->set_model_column(column);
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/AbstractView.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Model.h>
namespace GUI {
class ComboBoxEditor;
class ControlBoxButton;
class ComboBox : public Frame {
C_OBJECT(ComboBox);
public:
virtual ~ComboBox() override;
String text() const;
void set_text(const String&);
void open();
void close();
void select_all();
Model* model();
const Model* model() const;
void set_model(NonnullRefPtr<Model>);
size_t selected_index() const;
void set_selected_index(size_t index);
bool only_allow_values_from_model() const { return m_only_allow_values_from_model; }
void set_only_allow_values_from_model(bool);
int model_column() const;
void set_model_column(int);
Function<void(const String&, const ModelIndex&)> on_change;
Function<void()> on_return_pressed;
protected:
ComboBox();
virtual void resize_event(ResizeEvent&) override;
private:
void selection_updated(const ModelIndex&);
void navigate(AbstractView::CursorMovement);
void navigate_relative(int);
RefPtr<ComboBoxEditor> m_editor;
RefPtr<ControlBoxButton> m_open_button;
RefPtr<Window> m_list_window;
RefPtr<ListView> m_list_view;
Optional<ModelIndex> m_selected_index;
bool m_only_allow_values_from_model { false };
bool m_updating_model { false };
};
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/Command.h>
namespace GUI {
Command::~Command()
{
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/String.h>
namespace GUI {
class Command {
public:
virtual ~Command();
virtual void undo() { }
virtual void redo() { }
String action_text() const { return m_action_text; }
virtual bool is_insert_text() const { return false; }
virtual bool is_remove_text() const { return false; }
protected:
Command() { }
void set_action_text(const String& text) { m_action_text = text; }
private:
String m_action_text;
};
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2020, Charles Chaucer <thankyouverycool@github>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/ControlBoxButton.h>
#include <LibGUI/Painter.h>
#include <LibGfx/CharacterBitmap.h>
#include <LibGfx/Palette.h>
namespace GUI {
static const char* s_up_arrow_bitmap_data = {
" "
" # "
" ### "
" ##### "
" ####### "
" "
};
static const char* s_down_arrow_bitmap_data = {
" "
" ####### "
" ##### "
" ### "
" # "
" "
};
static Gfx::CharacterBitmap* s_up_arrow_bitmap;
static Gfx::CharacterBitmap* s_down_arrow_bitmap;
static const int s_bitmap_width = 9;
static const int s_bitmap_height = 6;
ControlBoxButton::ControlBoxButton(Type type)
: m_type(type)
{
}
ControlBoxButton::~ControlBoxButton()
{
}
void ControlBoxButton::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
Gfx::StylePainter::paint_button(painter, rect(), palette(), Gfx::ButtonStyle::Normal, is_being_pressed(), is_hovered(), is_checked(), is_enabled(), is_focused());
auto button_location = rect().location().translated((width() - s_bitmap_width) / 2, (height() - s_bitmap_height) / 2);
if (is_being_pressed())
button_location.move_by(1, 1);
if (type() == UpArrow) {
if (!s_up_arrow_bitmap)
s_up_arrow_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_up_arrow_bitmap_data, s_bitmap_width, s_bitmap_height).leak_ref();
if (!is_enabled())
painter.draw_bitmap(button_location.translated(1, 1), *s_up_arrow_bitmap, palette().threed_highlight());
painter.draw_bitmap(button_location, *s_up_arrow_bitmap, is_enabled() ? palette().button_text() : palette().threed_shadow1());
} else {
if (!s_down_arrow_bitmap)
s_down_arrow_bitmap = &Gfx::CharacterBitmap::create_from_ascii(s_down_arrow_bitmap_data, s_bitmap_width, s_bitmap_height).leak_ref();
if (!is_enabled())
painter.draw_bitmap(button_location.translated(1, 1), *s_down_arrow_bitmap, palette().threed_highlight());
painter.draw_bitmap(button_location, *s_down_arrow_bitmap, is_enabled() ? palette().button_text() : palette().threed_shadow1());
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020, Charles Chaucer <thankyouverycool@github>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Button.h>
namespace GUI {
class ControlBoxButton final : public Button {
C_OBJECT(ControlBoxButton);
public:
enum Type {
UpArrow,
DownArrow
};
virtual ~ControlBoxButton() override;
private:
explicit ControlBoxButton(const Type type);
virtual void paint_event(PaintEvent& event) override;
Type m_type { DownArrow };
Type type() const { return m_type; }
};
}

View file

@ -0,0 +1,127 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibCpp/Lexer.h>
#include <LibGUI/CppSyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
namespace GUI {
static TextStyle style_for_token_type(Gfx::Palette palette, Cpp::Token::Type type)
{
switch (type) {
case Cpp::Token::Type::Keyword:
return { palette.syntax_keyword(), true };
case Cpp::Token::Type::KnownType:
return { palette.syntax_type(), true };
case Cpp::Token::Type::Identifier:
return { palette.syntax_identifier(), false };
case Cpp::Token::Type::DoubleQuotedString:
case Cpp::Token::Type::SingleQuotedString:
case Cpp::Token::Type::RawString:
return { palette.syntax_string(), false };
case Cpp::Token::Type::Integer:
case Cpp::Token::Type::Float:
return { palette.syntax_number(), false };
case Cpp::Token::Type::IncludePath:
return { palette.syntax_preprocessor_value(), false };
case Cpp::Token::Type::EscapeSequence:
return { palette.syntax_keyword(), true };
case Cpp::Token::Type::PreprocessorStatement:
case Cpp::Token::Type::IncludeStatement:
return { palette.syntax_preprocessor_statement(), false };
case Cpp::Token::Type::Comment:
return { palette.syntax_comment(), false };
default:
return { palette.base_text(), false };
}
}
bool CppSyntaxHighlighter::is_identifier(void* token) const
{
auto cpp_token = static_cast<Cpp::Token::Type>(reinterpret_cast<size_t>(token));
return cpp_token == Cpp::Token::Type::Identifier;
}
bool CppSyntaxHighlighter::is_navigatable(void* token) const
{
auto cpp_token = static_cast<Cpp::Token::Type>(reinterpret_cast<size_t>(token));
return cpp_token == Cpp::Token::Type::IncludePath;
}
void CppSyntaxHighlighter::rehighlight(Gfx::Palette palette)
{
ASSERT(m_editor);
auto text = m_editor->text();
Cpp::Lexer lexer(text);
auto tokens = lexer.lex();
Vector<GUI::TextDocumentSpan> spans;
for (auto& token : tokens) {
#ifdef DEBUG_SYNTAX_HIGHLIGHTING
dbg() << token.to_string() << " @ " << token.m_start.line << ":" << token.m_start.column << " - " << token.m_end.line << ":" << token.m_end.column;
#endif
GUI::TextDocumentSpan span;
span.range.set_start({ token.m_start.line, token.m_start.column });
span.range.set_end({ token.m_end.line, token.m_end.column });
auto style = style_for_token_type(palette, token.m_type);
span.attributes.color = style.color;
span.attributes.bold = style.bold;
span.is_skippable = token.m_type == Cpp::Token::Type::Whitespace;
span.data = reinterpret_cast<void*>(token.m_type);
spans.append(span);
}
m_editor->document().set_spans(spans);
m_has_brace_buddies = false;
highlight_matching_token_pair();
m_editor->update();
}
Vector<SyntaxHighlighter::MatchingTokenPair> CppSyntaxHighlighter::matching_token_pairs() const
{
static Vector<SyntaxHighlighter::MatchingTokenPair> pairs;
if (pairs.is_empty()) {
pairs.append({ reinterpret_cast<void*>(Cpp::Token::Type::LeftCurly), reinterpret_cast<void*>(Cpp::Token::Type::RightCurly) });
pairs.append({ reinterpret_cast<void*>(Cpp::Token::Type::LeftParen), reinterpret_cast<void*>(Cpp::Token::Type::RightParen) });
pairs.append({ reinterpret_cast<void*>(Cpp::Token::Type::LeftBracket), reinterpret_cast<void*>(Cpp::Token::Type::RightBracket) });
}
return pairs;
}
bool CppSyntaxHighlighter::token_types_equal(void* token1, void* token2) const
{
return static_cast<Cpp::Token::Type>(reinterpret_cast<size_t>(token1)) == static_cast<Cpp::Token::Type>(reinterpret_cast<size_t>(token2));
}
CppSyntaxHighlighter::~CppSyntaxHighlighter()
{
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/SyntaxHighlighter.h>
namespace GUI {
class CppSyntaxHighlighter final : public SyntaxHighlighter {
public:
CppSyntaxHighlighter() { }
virtual ~CppSyntaxHighlighter() override;
virtual bool is_identifier(void*) const override;
virtual bool is_navigatable(void*) const override;
virtual SyntaxLanguage language() const override { return SyntaxLanguage::Cpp; }
virtual void rehighlight(Gfx::Palette) override;
protected:
virtual Vector<MatchingTokenPair> matching_token_pairs() const override;
virtual bool token_types_equal(void*, void*) const override;
};
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Badge.h>
#include <LibCore/ConfigFile.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/WindowServerConnection.h>
#include <string.h>
#include <unistd.h>
namespace GUI {
Desktop& Desktop::the()
{
static Desktop* the;
if (!the)
the = new Desktop;
return *the;
}
Desktop::Desktop()
{
}
void Desktop::did_receive_screen_rect(Badge<WindowServerConnection>, const Gfx::IntRect& rect)
{
if (m_rect == rect)
return;
m_rect = rect;
if (on_rect_change)
on_rect_change(rect);
}
void Desktop::set_background_color(const StringView& background_color)
{
WindowServerConnection::the().post_message(Messages::WindowServer::SetBackgroundColor(background_color));
}
void Desktop::set_wallpaper_mode(const StringView& mode)
{
WindowServerConnection::the().post_message(Messages::WindowServer::SetWallpaperMode(mode));
}
bool Desktop::set_wallpaper(const StringView& path, bool save_config)
{
WindowServerConnection::the().post_message(Messages::WindowServer::AsyncSetWallpaper(path));
auto ret_val = WindowServerConnection::the().wait_for_specific_message<Messages::WindowClient::AsyncSetWallpaperFinished>()->success();
if (ret_val && save_config) {
RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("WindowManager");
dbgln("Saving wallpaper path '{}' to config file at {}", path, config->file_name());
config->write_entry("Background", "Wallpaper", path);
config->sync();
}
return ret_val;
}
String Desktop::wallpaper() const
{
return WindowServerConnection::the().send_sync<Messages::WindowServer::GetWallpaper>()->path();
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Function.h>
#include <AK/String.h>
#include <LibGUI/Forward.h>
#include <LibGfx/Rect.h>
namespace GUI {
class Desktop {
public:
static Desktop& the();
Desktop();
void set_background_color(const StringView& background_color);
void set_wallpaper_mode(const StringView& mode);
String wallpaper() const;
bool set_wallpaper(const StringView& path, bool save_config = true);
Gfx::IntRect rect() const { return m_rect; }
int taskbar_height() const { return 28; }
int menubar_height() const { return 19; }
void did_receive_screen_rect(Badge<WindowServerConnection>, const Gfx::IntRect&);
Function<void(const Gfx::IntRect&)> on_rect_change;
private:
Gfx::IntRect m_rect;
};
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibCore/EventLoop.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/Event.h>
namespace GUI {
Dialog::Dialog(Window* parent_window)
: Window(parent_window)
{
set_modal(true);
set_minimizable(false);
}
Dialog::~Dialog()
{
}
int Dialog::exec()
{
ASSERT(!m_event_loop);
m_event_loop = make<Core::EventLoop>();
if (parent() && is<Window>(parent())) {
auto& parent_window = *static_cast<Window*>(parent());
if (parent_window.is_visible()) {
center_within(parent_window);
} else {
center_on_screen();
}
} else {
center_on_screen();
}
show();
auto result = m_event_loop->exec();
m_event_loop = nullptr;
dbgln("{}: Event loop returned with result {}", *this, result);
remove_from_parent();
return result;
}
void Dialog::done(int result)
{
if (!m_event_loop)
return;
m_result = result;
dbgln("{}: Quit event loop with result {}", *this, result);
m_event_loop->quit(result);
}
void Dialog::event(Core::Event& event)
{
if (event.type() == Event::KeyUp) {
auto& key_event = static_cast<KeyEvent&>(event);
if (key_event.key() == KeyCode::Key_Escape) {
done(ExecCancel);
event.accept();
return;
}
}
Window::event(event);
}
void Dialog::close()
{
Window::close();
m_event_loop->quit(ExecCancel);
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Window.h>
namespace GUI {
class Dialog : public Window {
C_OBJECT(Dialog)
public:
enum ExecResult {
ExecOK = 0,
ExecCancel = 1,
ExecAborted = 2,
ExecYes = 3,
ExecNo = 4,
};
virtual ~Dialog() override;
int exec();
int result() const { return m_result; }
void done(int result);
virtual void event(Core::Event&) override;
virtual void close() override;
protected:
explicit Dialog(Window* parent_window);
private:
OwnPtr<Core::EventLoop> m_event_loop;
int m_result { ExecAborted };
};
}
template<>
struct AK::Formatter<GUI::Dialog> : Formatter<Core::Object> {
};

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Badge.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <LibGUI/DisplayLink.h>
#include <LibGUI/WindowServerConnection.h>
namespace GUI {
class DisplayLinkCallback : public RefCounted<DisplayLinkCallback> {
public:
DisplayLinkCallback(i32 link_id, Function<void(i32)> callback)
: m_link_id(link_id)
, m_callback(move(callback))
{
}
void invoke()
{
m_callback(m_link_id);
}
private:
i32 m_link_id { 0 };
Function<void(i32)> m_callback;
};
static HashMap<i32, RefPtr<DisplayLinkCallback>>& callbacks()
{
static HashMap<i32, RefPtr<DisplayLinkCallback>>* map;
if (!map)
map = new HashMap<i32, RefPtr<DisplayLinkCallback>>;
return *map;
}
static i32 s_next_callback_id = 1;
i32 DisplayLink::register_callback(Function<void(i32)> callback)
{
if (callbacks().is_empty())
WindowServerConnection::the().post_message(Messages::WindowServer::EnableDisplayLink());
i32 callback_id = s_next_callback_id++;
callbacks().set(callback_id, adopt(*new DisplayLinkCallback(callback_id, move(callback))));
return callback_id;
}
bool DisplayLink::unregister_callback(i32 callback_id)
{
ASSERT(callbacks().contains(callback_id));
callbacks().remove(callback_id);
if (callbacks().is_empty())
WindowServerConnection::the().post_message(Messages::WindowServer::DisableDisplayLink());
return true;
}
void DisplayLink::notify(Badge<WindowServerConnection>)
{
auto copy_of_callbacks = callbacks();
for (auto& it : copy_of_callbacks)
it.value->invoke();
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Forward.h>
#include <LibGUI/Forward.h>
namespace GUI {
class DisplayLink {
public:
static i32 register_callback(Function<void(i32)>);
static bool unregister_callback(i32 callback_id);
static void notify(Badge<WindowServerConnection>);
};
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Badge.h>
#include <AK/SharedBuffer.h>
#include <LibCore/EventLoop.h>
#include <LibCore/MimeData.h>
#include <LibGUI/DragOperation.h>
#include <LibGUI/WindowServerConnection.h>
#include <LibGfx/Bitmap.h>
namespace GUI {
static DragOperation* s_current_drag_operation;
DragOperation::DragOperation(Core::Object* parent)
: Core::Object(parent)
{
}
DragOperation::~DragOperation()
{
}
DragOperation::Outcome DragOperation::exec()
{
ASSERT(!s_current_drag_operation);
ASSERT(!m_event_loop);
ASSERT(m_mime_data);
int bitmap_id = -1;
Gfx::IntSize bitmap_size;
RefPtr<Gfx::Bitmap> shared_bitmap;
if (m_mime_data->has_format("image/x-raw-bitmap")) {
auto data = m_mime_data->data("image/x-raw-bitmap");
auto bitmap = Gfx::Bitmap::create_from_serialized_byte_buffer(move(data));
shared_bitmap = bitmap->to_bitmap_backed_by_shared_buffer();
shared_bitmap->shared_buffer()->share_with(WindowServerConnection::the().server_pid());
bitmap_id = shared_bitmap->shbuf_id();
bitmap_size = shared_bitmap->size();
}
auto response = WindowServerConnection::the().send_sync<Messages::WindowServer::StartDrag>(
m_mime_data->text(),
m_mime_data->all_data(),
bitmap_id, bitmap_size);
if (!response->started()) {
m_outcome = Outcome::Cancelled;
return m_outcome;
}
s_current_drag_operation = this;
m_event_loop = make<Core::EventLoop>();
auto result = m_event_loop->exec();
m_event_loop = nullptr;
dbgln("{}: event loop returned with result {}", class_name(), result);
remove_from_parent();
s_current_drag_operation = nullptr;
return m_outcome;
}
void DragOperation::done(Outcome outcome)
{
ASSERT(m_outcome == Outcome::None);
m_outcome = outcome;
m_event_loop->quit(0);
}
void DragOperation::notify_accepted(Badge<WindowServerConnection>)
{
ASSERT(s_current_drag_operation);
s_current_drag_operation->done(Outcome::Accepted);
}
void DragOperation::notify_cancelled(Badge<WindowServerConnection>)
{
if (s_current_drag_operation)
s_current_drag_operation->done(Outcome::Cancelled);
}
void DragOperation::set_text(const String& text)
{
if (!m_mime_data)
m_mime_data = Core::MimeData::construct();
m_mime_data->set_text(text);
}
void DragOperation::set_bitmap(const Gfx::Bitmap* bitmap)
{
if (!m_mime_data)
m_mime_data = Core::MimeData::construct();
if (bitmap)
m_mime_data->set_data("image/x-raw-bitmap", bitmap->serialize_to_byte_buffer());
}
void DragOperation::set_data(const String& data_type, const String& data)
{
if (!m_mime_data)
m_mime_data = Core::MimeData::construct();
m_mime_data->set_data(data_type, data.to_byte_buffer());
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/OwnPtr.h>
#include <LibCore/Object.h>
#include <LibGUI/Forward.h>
#include <LibGfx/Forward.h>
namespace GUI {
class DragOperation : public Core::Object {
C_OBJECT(DragOperation)
public:
enum class Outcome {
None,
Accepted,
Cancelled,
};
virtual ~DragOperation() override;
void set_mime_data(RefPtr<Core::MimeData> mime_data) { m_mime_data = move(mime_data); }
void set_text(const String& text);
void set_bitmap(const Gfx::Bitmap* bitmap);
void set_data(const String& data_type, const String& data);
Outcome exec();
Outcome outcome() const { return m_outcome; }
static void notify_accepted(Badge<WindowServerConnection>);
static void notify_cancelled(Badge<WindowServerConnection>);
protected:
explicit DragOperation(Core::Object* parent = nullptr);
private:
void done(Outcome);
OwnPtr<Core::EventLoop> m_event_loop;
Outcome m_outcome { Outcome::None };
RefPtr<Core::MimeData> m_mime_data;
};
}

View file

@ -0,0 +1,453 @@
/*
* Copyright (c) 2021, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/EditingEngine.h>
#include <LibGUI/Event.h>
#include <LibGUI/TextEditor.h>
namespace GUI {
EditingEngine::~EditingEngine()
{
}
void EditingEngine::attach(TextEditor& editor)
{
ASSERT(!m_editor);
m_editor = editor;
}
void EditingEngine::detach()
{
ASSERT(m_editor);
m_editor = nullptr;
}
bool EditingEngine::on_key(const KeyEvent& event)
{
if (event.key() == KeyCode::Key_Left) {
if (!event.shift() && m_editor->selection()->is_valid()) {
m_editor->set_cursor(m_editor->selection()->normalized().start());
m_editor->selection()->clear();
m_editor->did_update_selection();
if (!event.ctrl()) {
m_editor->update();
return true;
}
}
if (event.ctrl()) {
move_to_previous_span(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
move_one_left(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
if (event.key() == KeyCode::Key_Right) {
if (!event.shift() && m_editor->selection()->is_valid()) {
m_editor->set_cursor(m_editor->selection()->normalized().end());
m_editor->selection()->clear();
m_editor->did_update_selection();
if (!event.ctrl()) {
m_editor->update();
return true;
}
}
if (event.ctrl()) {
move_to_next_span(event);
return true;
}
move_one_right(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
if (event.key() == KeyCode::Key_Up) {
move_one_up(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
if (event.key() == KeyCode::Key_Down) {
move_one_down(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
if (event.key() == KeyCode::Key_Home) {
if (event.ctrl()) {
m_editor->toggle_selection_if_needed_for_event(event.shift());
move_to_first_line();
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
} else {
move_to_line_beginning(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
}
return true;
}
if (event.key() == KeyCode::Key_End) {
if (event.ctrl()) {
m_editor->toggle_selection_if_needed_for_event(event.shift());
move_to_last_line();
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
} else {
move_to_line_end(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
}
return true;
}
if (event.key() == KeyCode::Key_PageUp) {
move_page_up(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
if (event.key() == KeyCode::Key_PageDown) {
move_page_down(event);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
return true;
}
return false;
}
void EditingEngine::move_one_left(const KeyEvent& event)
{
if (m_editor->cursor().column() > 0) {
int new_column = m_editor->cursor().column() - 1;
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(m_editor->cursor().line(), new_column);
} else if (m_editor->cursor().line() > 0) {
int new_line = m_editor->cursor().line() - 1;
int new_column = m_editor->lines()[new_line].length();
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_line, new_column);
}
}
void EditingEngine::move_one_right(const KeyEvent& event)
{
int new_line = m_editor->cursor().line();
int new_column = m_editor->cursor().column();
if (m_editor->cursor().column() < m_editor->current_line().length()) {
new_line = m_editor->cursor().line();
new_column = m_editor->cursor().column() + 1;
} else if (m_editor->cursor().line() != m_editor->line_count() - 1) {
new_line = m_editor->cursor().line() + 1;
new_column = 0;
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_line, new_column);
}
void EditingEngine::move_to_previous_span(const KeyEvent& event)
{
TextPosition new_cursor;
if (m_editor->document().has_spans()) {
auto span = m_editor->document().first_non_skippable_span_before(m_editor->cursor());
if (span.has_value()) {
new_cursor = span.value().range.start();
} else {
// No remaining spans, just use word break calculation
new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
}
} else {
new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
}
void EditingEngine::move_to_next_span(const KeyEvent& event)
{
TextPosition new_cursor;
if (m_editor->document().has_spans()) {
auto span = m_editor->document().first_non_skippable_span_after(m_editor->cursor());
if (span.has_value()) {
new_cursor = span.value().range.start();
} else {
// No remaining spans, just use word break calculation
new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
}
} else {
new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
if (event.shift() && m_editor->selection()->start().is_valid()) {
m_editor->selection()->set_end(m_editor->cursor());
m_editor->did_update_selection();
}
}
void EditingEngine::move_to_line_beginning(const KeyEvent& event)
{
TextPosition new_cursor;
m_editor->toggle_selection_if_needed_for_event(event.shift());
if (m_editor->is_line_wrapping_enabled()) {
// FIXME: Replicate the first_nonspace_column behavior in wrapping mode.
auto home_position = m_editor->cursor_content_rect().location().translated(-m_editor->width(), 0);
new_cursor = m_editor->text_position_at_content_position(home_position);
} else {
size_t first_nonspace_column = m_editor->current_line().first_non_whitespace_column();
if (m_editor->cursor().column() == first_nonspace_column) {
new_cursor = { m_editor->cursor().line(), 0 };
} else {
new_cursor = { m_editor->cursor().line(), first_nonspace_column };
}
}
m_editor->set_cursor(new_cursor);
}
void EditingEngine::move_to_line_end(const KeyEvent& event)
{
TextPosition new_cursor;
if (m_editor->is_line_wrapping_enabled()) {
auto end_position = m_editor->cursor_content_rect().location().translated(m_editor->width(), 0);
new_cursor = m_editor->text_position_at_content_position(end_position);
} else {
new_cursor = { m_editor->cursor().line(), m_editor->current_line().length() };
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
}
void EditingEngine::move_one_up(const KeyEvent& event)
{
if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) {
if (event.ctrl() && event.shift()) {
move_selected_lines_up();
return;
}
TextPosition new_cursor;
if (m_editor->is_line_wrapping_enabled()) {
auto position_above = m_editor->cursor_content_rect().location().translated(0, -m_editor->line_height());
new_cursor = m_editor->text_position_at_content_position(position_above);
} else {
size_t new_line = m_editor->cursor().line() - 1;
size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
new_cursor = { new_line, new_column };
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
}
};
void EditingEngine::move_one_down(const KeyEvent& event)
{
if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) {
if (event.ctrl() && event.shift()) {
move_selected_lines_down();
return;
}
TextPosition new_cursor;
if (m_editor->is_line_wrapping_enabled()) {
new_cursor = m_editor->text_position_at_content_position(m_editor->cursor_content_rect().location().translated(0, m_editor->line_height()));
auto position_below = m_editor->cursor_content_rect().location().translated(0, m_editor->line_height());
new_cursor = m_editor->text_position_at_content_position(position_below);
} else {
size_t new_line = m_editor->cursor().line() + 1;
size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
new_cursor = { new_line, new_column };
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
}
};
void EditingEngine::move_up(const KeyEvent& event, double page_height_factor)
{
if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) {
int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
TextPosition new_cursor;
if (m_editor->is_line_wrapping_enabled()) {
auto position_above = m_editor->cursor_content_rect().location().translated(0, -pixels);
new_cursor = m_editor->text_position_at_content_position(position_above);
} else {
size_t page_step = (size_t)pixels / (size_t)m_editor->line_height();
size_t new_line = m_editor->cursor().line() < page_step ? 0 : m_editor->cursor().line() - page_step;
size_t new_column = min(m_editor->cursor().column(), m_editor->line(new_line).length());
new_cursor = { new_line, new_column };
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
}
};
void EditingEngine::move_down(const KeyEvent& event, double page_height_factor)
{
if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) {
int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
TextPosition new_cursor;
if (m_editor->is_line_wrapping_enabled()) {
auto position_below = m_editor->cursor_content_rect().location().translated(0, pixels);
new_cursor = m_editor->text_position_at_content_position(position_below);
} else {
size_t new_line = min(m_editor->line_count() - 1, m_editor->cursor().line() + pixels / m_editor->line_height());
size_t new_column = min(m_editor->cursor().column(), m_editor->lines()[new_line].length());
new_cursor = { new_line, new_column };
}
m_editor->toggle_selection_if_needed_for_event(event.shift());
m_editor->set_cursor(new_cursor);
};
}
void EditingEngine::move_page_up(const KeyEvent& event)
{
move_up(event, 1);
};
void EditingEngine::move_page_down(const KeyEvent& event)
{
move_down(event, 1);
};
void EditingEngine::move_to_first_line()
{
m_editor->set_cursor(0, 0);
};
void EditingEngine::move_to_last_line()
{
m_editor->set_cursor(m_editor->line_count() - 1, m_editor->lines()[m_editor->line_count() - 1].length());
};
void EditingEngine::get_selection_line_boundaries(size_t& first_line, size_t& last_line)
{
auto selection = m_editor->normalized_selection();
if (!selection.is_valid()) {
first_line = m_editor->cursor().line();
last_line = m_editor->cursor().line();
return;
}
first_line = selection.start().line();
last_line = selection.end().line();
if (first_line != last_line && selection.end().column() == 0)
last_line -= 1;
}
void EditingEngine::move_selected_lines_up()
{
if (!m_editor->is_editable())
return;
size_t first_line;
size_t last_line;
get_selection_line_boundaries(first_line, last_line);
if (first_line == 0)
return;
auto& lines = m_editor->document().lines();
lines.insert((int)last_line, lines.take((int)first_line - 1));
m_editor->set_cursor({ first_line - 1, 0 });
if (m_editor->has_selection()) {
m_editor->selection()->set_start({ first_line - 1, 0 });
m_editor->selection()->set_end({ last_line - 1, m_editor->line(last_line - 1).length() });
}
m_editor->did_change();
m_editor->update();
}
void EditingEngine::move_selected_lines_down()
{
if (!m_editor->is_editable())
return;
size_t first_line;
size_t last_line;
get_selection_line_boundaries(first_line, last_line);
auto& lines = m_editor->document().lines();
ASSERT(lines.size() != 0);
if (last_line >= lines.size() - 1)
return;
lines.insert((int)first_line, lines.take((int)last_line + 1));
m_editor->set_cursor({ first_line + 1, 0 });
if (m_editor->has_selection()) {
m_editor->selection()->set_start({ first_line + 1, 0 });
m_editor->selection()->set_end({ last_line + 1, m_editor->line(last_line + 1).length() });
}
m_editor->did_change();
m_editor->update();
}
void EditingEngine::delete_char()
{
if (!m_editor->is_editable())
return;
m_editor->do_delete();
};
void EditingEngine::delete_line()
{
if (!m_editor->is_editable())
return;
m_editor->delete_current_line();
};
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Noncopyable.h>
#include <LibGUI/Event.h>
#include <LibGUI/TextDocument.h>
namespace GUI {
enum CursorWidth {
NARROW,
WIDE
};
class EditingEngine {
AK_MAKE_NONCOPYABLE(EditingEngine);
AK_MAKE_NONMOVABLE(EditingEngine);
public:
virtual ~EditingEngine();
virtual CursorWidth cursor_width() const { return NARROW; }
void attach(TextEditor& editor);
void detach();
virtual bool on_key(const KeyEvent& event);
protected:
EditingEngine() { }
WeakPtr<TextEditor> m_editor;
void move_one_left(const KeyEvent& event);
void move_one_right(const KeyEvent& event);
void move_one_up(const KeyEvent& event);
void move_one_down(const KeyEvent& event);
void move_to_previous_span(const KeyEvent& event);
void move_to_next_span(const KeyEvent& event);
void move_to_line_beginning(const KeyEvent& event);
void move_to_line_end(const KeyEvent& event);
void move_page_up(const KeyEvent& event);
void move_page_down(const KeyEvent& event);
void move_to_first_line();
void move_to_last_line();
void move_up(const KeyEvent& event, double page_height_factor);
void move_down(const KeyEvent& event, double page_height_factor);
void get_selection_line_boundaries(size_t& first_line, size_t& last_line);
void delete_line();
void delete_char();
private:
void move_selected_lines_up();
void move_selected_lines_down();
};
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <AK/Utf32View.h>
#include <LibCore/DirIterator.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/EmojiInputDialog.h>
#include <LibGUI/Event.h>
#include <LibGUI/Frame.h>
#include <stdlib.h>
namespace GUI {
static Vector<u32> supported_emoji_code_points()
{
Vector<u32> code_points;
Core::DirIterator dt("/res/emoji", Core::DirIterator::SkipDots);
while (dt.has_next()) {
auto filename = dt.next_path();
auto lexical_path = LexicalPath(filename);
if (lexical_path.extension() != "png")
continue;
auto basename = lexical_path.basename();
if (!basename.starts_with("U+"))
continue;
u32 code_point = strtoul(basename.characters() + 2, nullptr, 16);
code_points.append(code_point);
}
return code_points;
}
EmojiInputDialog::EmojiInputDialog(Window* parent_window)
: Dialog(parent_window)
{
set_frameless(true);
auto& main_widget = set_main_widget<Frame>();
main_widget.set_frame_shape(Gfx::FrameShape::Container);
main_widget.set_frame_shadow(Gfx::FrameShadow::Raised);
main_widget.set_fill_with_background_color(true);
auto& main_layout = main_widget.set_layout<VerticalBoxLayout>();
main_layout.set_margins({ 1, 1, 1, 1 });
main_layout.set_spacing(0);
auto code_points = supported_emoji_code_points();
size_t index = 0;
size_t columns = 6;
size_t rows = ceil_div(code_points.size(), columns);
for (size_t row = 0; row < rows && index < code_points.size(); ++row) {
auto& horizontal_container = main_widget.add<Widget>();
auto& horizontal_layout = horizontal_container.set_layout<HorizontalBoxLayout>();
horizontal_layout.set_spacing(0);
for (size_t column = 0; column < columns; ++column) {
if (index < code_points.size()) {
StringBuilder builder;
builder.append(Utf32View(&code_points[index++], 1));
auto emoji_text = builder.to_string();
auto& button = horizontal_container.add<Button>(emoji_text);
button.set_min_size(16, 16);
button.set_button_style(Gfx::ButtonStyle::CoolBar);
button.on_click = [this, button = &button](auto) {
m_selected_emoji_text = button->text();
done(ExecOK);
};
} else {
horizontal_container.add<Widget>();
}
}
}
}
void EmojiInputDialog::event(Core::Event& event)
{
if (event.type() == Event::KeyDown) {
auto& key_event = static_cast<KeyEvent&>(event);
if (key_event.key() == Key_Escape) {
done(ExecCancel);
return;
}
}
Dialog::event(event);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Dialog.h>
namespace GUI {
class EmojiInputDialog final : public Dialog {
C_OBJECT(EmojiInputDialog);
public:
const String& selected_emoji_text() const { return m_selected_emoji_text; }
private:
virtual void event(Core::Event&) override;
explicit EmojiInputDialog(Window* parent_window);
String m_selected_emoji_text;
};
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <LibCore/MimeData.h>
#include <LibGUI/Event.h>
namespace GUI {
DropEvent::DropEvent(const Gfx::IntPoint& position, const String& text, NonnullRefPtr<Core::MimeData> mime_data)
: Event(Event::Drop)
, m_position(position)
, m_text(text)
, m_mime_data(move(mime_data))
{
}
DropEvent::~DropEvent()
{
}
String KeyEvent::to_string() const
{
Vector<String, 8> parts;
if (m_modifiers & Mod_Ctrl)
parts.append("Ctrl");
if (m_modifiers & Mod_Shift)
parts.append("Shift");
if (m_modifiers & Mod_Alt)
parts.append("Alt");
if (m_modifiers & Mod_Logo)
parts.append("Logo");
if (auto* key_name = key_code_to_string(static_cast<KeyCode>(m_key)))
parts.append(key_name);
else
parts.append("(Invalid)");
StringBuilder builder;
for (size_t i = 0; i < parts.size(); ++i) {
builder.append(parts[i]);
if (i != parts.size() - 1)
builder.append('+');
}
return builder.to_string();
}
}

View file

@ -0,0 +1,403 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/StringBuilder.h>
#include <AK/Vector.h>
#include <Kernel/API/KeyCode.h>
#include <LibCore/Event.h>
#include <LibGUI/FocusSource.h>
#include <LibGUI/WindowType.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
namespace GUI {
class Event : public Core::Event {
public:
enum Type {
Show = 1000,
Hide,
Paint,
MultiPaint,
Resize,
MouseMove,
MouseDown,
MouseDoubleClick,
MouseUp,
MouseWheel,
Enter,
Leave,
KeyDown,
KeyUp,
WindowEntered,
WindowLeft,
WindowBecameInactive,
WindowBecameActive,
WindowInputEntered,
WindowInputLeft,
FocusIn,
FocusOut,
WindowCloseRequest,
ContextMenu,
EnabledChange,
DragEnter,
DragLeave,
DragMove,
Drop,
ThemeChange,
__Begin_WM_Events,
WM_WindowRemoved,
WM_WindowStateChanged,
WM_WindowRectChanged,
WM_WindowIconBitmapChanged,
__End_WM_Events,
};
Event() { }
explicit Event(Type type)
: Core::Event(type)
{
}
virtual ~Event() { }
bool is_key_event() const { return type() == KeyUp || type() == KeyDown; }
bool is_paint_event() const { return type() == Paint; }
};
class WMEvent : public Event {
public:
WMEvent(Type type, int client_id, int window_id)
: Event(type)
, m_client_id(client_id)
, m_window_id(window_id)
{
}
int client_id() const { return m_client_id; }
int window_id() const { return m_window_id; }
private:
int m_client_id { -1 };
int m_window_id { -1 };
};
class WMWindowRemovedEvent : public WMEvent {
public:
WMWindowRemovedEvent(int client_id, int window_id)
: WMEvent(Event::Type::WM_WindowRemoved, client_id, window_id)
{
}
};
class WMWindowStateChangedEvent : public WMEvent {
public:
WMWindowStateChangedEvent(int client_id, int window_id, int parent_client_id, int parent_window_id, const StringView& title, const Gfx::IntRect& rect, bool is_active, bool is_modal, WindowType window_type, bool is_minimized, bool is_frameless, int progress)
: WMEvent(Event::Type::WM_WindowStateChanged, client_id, window_id)
, m_parent_client_id(parent_client_id)
, m_parent_window_id(parent_window_id)
, m_title(title)
, m_rect(rect)
, m_window_type(window_type)
, m_active(is_active)
, m_modal(is_modal)
, m_minimized(is_minimized)
, m_frameless(is_frameless)
, m_progress(progress)
{
}
int parent_client_id() const { return m_parent_client_id; }
int parent_window_id() const { return m_parent_window_id; }
const String& title() const { return m_title; }
const Gfx::IntRect& rect() const { return m_rect; }
bool is_active() const { return m_active; }
bool is_modal() const { return m_modal; }
WindowType window_type() const { return m_window_type; }
bool is_minimized() const { return m_minimized; }
bool is_frameless() const { return m_frameless; }
int progress() const { return m_progress; }
private:
int m_parent_client_id;
int m_parent_window_id;
String m_title;
Gfx::IntRect m_rect;
WindowType m_window_type;
bool m_active;
bool m_modal;
bool m_minimized;
bool m_frameless;
int m_progress;
};
class WMWindowRectChangedEvent : public WMEvent {
public:
WMWindowRectChangedEvent(int client_id, int window_id, const Gfx::IntRect& rect)
: WMEvent(Event::Type::WM_WindowRectChanged, client_id, window_id)
, m_rect(rect)
{
}
const Gfx::IntRect& rect() const { return m_rect; }
private:
Gfx::IntRect m_rect;
};
class WMWindowIconBitmapChangedEvent : public WMEvent {
public:
WMWindowIconBitmapChangedEvent(int client_id, int window_id, int icon_buffer_id, const Gfx::IntSize& icon_size)
: WMEvent(Event::Type::WM_WindowIconBitmapChanged, client_id, window_id)
, m_icon_buffer_id(icon_buffer_id)
, m_icon_size(icon_size)
{
}
int icon_buffer_id() const { return m_icon_buffer_id; }
const Gfx::IntSize& icon_size() const { return m_icon_size; }
private:
int m_icon_buffer_id;
Gfx::IntSize m_icon_size;
};
class MultiPaintEvent final : public Event {
public:
explicit MultiPaintEvent(const Vector<Gfx::IntRect, 32>& rects, const Gfx::IntSize& window_size)
: Event(Event::MultiPaint)
, m_rects(rects)
, m_window_size(window_size)
{
}
const Vector<Gfx::IntRect, 32>& rects() const { return m_rects; }
const Gfx::IntSize& window_size() const { return m_window_size; }
private:
Vector<Gfx::IntRect, 32> m_rects;
Gfx::IntSize m_window_size;
};
class PaintEvent final : public Event {
public:
explicit PaintEvent(const Gfx::IntRect& rect, const Gfx::IntSize& window_size = {})
: Event(Event::Paint)
, m_rect(rect)
, m_window_size(window_size)
{
}
const Gfx::IntRect& rect() const { return m_rect; }
const Gfx::IntSize& window_size() const { return m_window_size; }
private:
Gfx::IntRect m_rect;
Gfx::IntSize m_window_size;
};
class ResizeEvent final : public Event {
public:
explicit ResizeEvent(const Gfx::IntSize& size)
: Event(Event::Resize)
, m_size(size)
{
}
const Gfx::IntSize& size() const { return m_size; }
private:
Gfx::IntSize m_size;
};
class ContextMenuEvent final : public Event {
public:
explicit ContextMenuEvent(const Gfx::IntPoint& position, const Gfx::IntPoint& screen_position)
: Event(Event::ContextMenu)
, m_position(position)
, m_screen_position(screen_position)
{
}
const Gfx::IntPoint& position() const { return m_position; }
const Gfx::IntPoint& screen_position() const { return m_screen_position; }
private:
Gfx::IntPoint m_position;
Gfx::IntPoint m_screen_position;
};
class ShowEvent final : public Event {
public:
ShowEvent()
: Event(Event::Show)
{
}
};
class HideEvent final : public Event {
public:
HideEvent()
: Event(Event::Hide)
{
}
};
enum MouseButton : u8 {
None = 0,
Left = 1,
Right = 2,
Middle = 4,
Back = 8,
Forward = 16,
};
class KeyEvent final : public Event {
public:
KeyEvent(Type type, KeyCode key, u8 modifiers, u32 code_point, u32 scancode)
: Event(type)
, m_key(key)
, m_modifiers(modifiers)
, m_code_point(code_point)
, m_scancode(scancode)
{
}
KeyCode key() const { return m_key; }
bool ctrl() const { return m_modifiers & Mod_Ctrl; }
bool alt() const { return m_modifiers & Mod_Alt; }
bool shift() const { return m_modifiers & Mod_Shift; }
bool logo() const { return m_modifiers & Mod_Logo; }
u8 modifiers() const { return m_modifiers; }
u32 code_point() const { return m_code_point; }
String text() const
{
StringBuilder sb;
sb.append_code_point(m_code_point);
return sb.to_string();
}
u32 scancode() const { return m_scancode; }
String to_string() const;
private:
friend class WindowServerConnection;
KeyCode m_key { KeyCode::Key_Invalid };
u8 m_modifiers { 0 };
u32 m_code_point { 0 };
u32 m_scancode { 0 };
};
class MouseEvent final : public Event {
public:
MouseEvent(Type type, const Gfx::IntPoint& position, unsigned buttons, MouseButton button, unsigned modifiers, int wheel_delta)
: Event(type)
, m_position(position)
, m_buttons(buttons)
, m_button(button)
, m_modifiers(modifiers)
, m_wheel_delta(wheel_delta)
{
}
const Gfx::IntPoint& position() const { return m_position; }
int x() const { return m_position.x(); }
int y() const { return m_position.y(); }
MouseButton button() const { return m_button; }
unsigned buttons() const { return m_buttons; }
bool ctrl() const { return m_modifiers & Mod_Ctrl; }
bool alt() const { return m_modifiers & Mod_Alt; }
bool shift() const { return m_modifiers & Mod_Shift; }
bool logo() const { return m_modifiers & Mod_Logo; }
unsigned modifiers() const { return m_modifiers; }
int wheel_delta() const { return m_wheel_delta; }
private:
Gfx::IntPoint m_position;
unsigned m_buttons { 0 };
MouseButton m_button { MouseButton::None };
unsigned m_modifiers { 0 };
int m_wheel_delta { 0 };
};
class DragEvent final : public Event {
public:
DragEvent(Type type, const Gfx::IntPoint& position, Vector<String> mime_types)
: Event(type)
, m_position(position)
, m_mime_types(move(mime_types))
{
}
const Gfx::IntPoint& position() const { return m_position; }
const Vector<String>& mime_types() const { return m_mime_types; }
private:
Gfx::IntPoint m_position;
Vector<String> m_mime_types;
};
class DropEvent final : public Event {
public:
DropEvent(const Gfx::IntPoint&, const String& text, NonnullRefPtr<Core::MimeData> mime_data);
~DropEvent();
const Gfx::IntPoint& position() const { return m_position; }
const String& text() const { return m_text; }
const Core::MimeData& mime_data() const { return m_mime_data; }
private:
Gfx::IntPoint m_position;
String m_text;
NonnullRefPtr<Core::MimeData> m_mime_data;
};
class ThemeChangeEvent final : public Event {
public:
ThemeChangeEvent()
: Event(Type::ThemeChange)
{
}
};
class FocusEvent final : public Event {
public:
explicit FocusEvent(Type type, FocusSource source)
: Event(type)
, m_source(source)
{
}
FocusSource source() const { return m_source; }
private:
FocusSource m_source { FocusSource::Programmatic };
};
}

View file

@ -0,0 +1,276 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/LexicalPath.h>
#include <AK/MappedFile.h>
#include <AK/String.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibCore/StandardPaths.h>
#include <LibELF/Image.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/PNGLoader.h>
#include <sys/stat.h>
namespace GUI {
static Icon s_hard_disk_icon;
static Icon s_directory_icon;
static Icon s_directory_open_icon;
static Icon s_inaccessible_directory_icon;
static Icon s_home_directory_icon;
static Icon s_home_directory_open_icon;
static Icon s_file_icon;
static Icon s_symlink_icon;
static Icon s_socket_icon;
static Icon s_executable_icon;
static Icon s_filetype_image_icon;
static RefPtr<Gfx::Bitmap> s_symlink_emblem;
static RefPtr<Gfx::Bitmap> s_symlink_emblem_small;
static HashMap<String, Icon> s_filetype_icons;
static HashMap<String, Vector<String>> s_filetype_patterns;
static void initialize_executable_icon_if_needed()
{
static bool initialized = false;
if (initialized)
return;
initialized = true;
s_executable_icon = Icon::default_icon("filetype-executable");
}
static void initialize_if_needed()
{
static bool s_initialized = false;
if (s_initialized)
return;
auto config = Core::ConfigFile::open("/etc/FileIconProvider.ini");
s_symlink_emblem = Gfx::Bitmap::load_from_file("/res/icons/symlink-emblem.png");
s_symlink_emblem_small = Gfx::Bitmap::load_from_file("/res/icons/symlink-emblem-small.png");
s_hard_disk_icon = Icon::default_icon("hard-disk");
s_directory_icon = Icon::default_icon("filetype-folder");
s_directory_open_icon = Icon::default_icon("filetype-folder-open");
s_inaccessible_directory_icon = Icon::default_icon("filetype-folder-inaccessible");
s_home_directory_icon = Icon::default_icon("home-directory");
s_home_directory_open_icon = Icon::default_icon("home-directory-open");
s_file_icon = Icon::default_icon("filetype-unknown");
s_symlink_icon = Icon::default_icon("filetype-symlink");
s_socket_icon = Icon::default_icon("filetype-socket");
s_filetype_image_icon = Icon::default_icon("filetype-image");
initialize_executable_icon_if_needed();
for (auto& filetype : config->keys("Icons")) {
s_filetype_icons.set(filetype, Icon::default_icon(String::formatted("filetype-{}", filetype)));
s_filetype_patterns.set(filetype, config->read_entry("Icons", filetype).split(','));
}
s_initialized = true;
}
Icon FileIconProvider::directory_icon()
{
initialize_if_needed();
return s_directory_icon;
}
Icon FileIconProvider::directory_open_icon()
{
initialize_if_needed();
return s_directory_open_icon;
}
Icon FileIconProvider::home_directory_icon()
{
initialize_if_needed();
return s_home_directory_icon;
}
Icon FileIconProvider::home_directory_open_icon()
{
initialize_if_needed();
return s_home_directory_open_icon;
}
Icon FileIconProvider::filetype_image_icon()
{
initialize_if_needed();
return s_filetype_image_icon;
}
Icon FileIconProvider::icon_for_path(const String& path)
{
struct stat stat;
if (::stat(path.characters(), &stat) < 0)
return {};
return icon_for_path(path, stat.st_mode);
}
Icon FileIconProvider::icon_for_executable(const String& path)
{
static HashMap<String, Icon> app_icon_cache;
if (auto it = app_icon_cache.find(path); it != app_icon_cache.end())
return it->value;
initialize_executable_icon_if_needed();
// If the icon for an app isn't in the cache we attempt to load the file as an ELF image and extract
// the serenity_app_icon_* sections which should contain the icons as raw PNG data. In the future it would
// be better if the binary signalled the image format being used or we deduced it, e.g. using magic bytes.
auto file_or_error = MappedFile::map(path);
if (file_or_error.is_error()) {
app_icon_cache.set(path, s_executable_icon);
return s_executable_icon;
}
auto& mapped_file = file_or_error.value();
if (mapped_file->size() < SELFMAG) {
app_icon_cache.set(path, s_executable_icon);
return s_executable_icon;
}
if (memcmp(mapped_file->data(), ELFMAG, SELFMAG) != 0) {
app_icon_cache.set(path, s_executable_icon);
return s_executable_icon;
}
auto image = ELF::Image((const u8*)mapped_file->data(), mapped_file->size());
if (!image.is_valid()) {
app_icon_cache.set(path, s_executable_icon);
return s_executable_icon;
}
// If any of the required sections are missing then use the defaults
Icon icon;
struct IconSection {
const char* section_name;
int image_size;
};
static const IconSection icon_sections[] = { { .section_name = "serenity_icon_s", .image_size = 16 }, { .section_name = "serenity_icon_m", .image_size = 32 } };
bool had_error = false;
for (const auto& icon_section : icon_sections) {
auto section = image.lookup_section(icon_section.section_name);
RefPtr<Gfx::Bitmap> bitmap;
if (section.is_undefined()) {
bitmap = s_executable_icon.bitmap_for_size(icon_section.image_size)->clone();
} else {
bitmap = Gfx::load_png_from_memory(reinterpret_cast<const u8*>(section.raw_data()), section.size());
}
if (!bitmap) {
dbgln("Failed to find embedded icon and failed to clone default icon for application {} at icon size {}", path, icon_section.image_size);
had_error = true;
continue;
}
icon.set_bitmap_for_size(icon_section.image_size, std::move(bitmap));
}
if (had_error) {
app_icon_cache.set(path, s_executable_icon);
return s_executable_icon;
}
app_icon_cache.set(path, icon);
return icon;
}
Icon FileIconProvider::icon_for_path(const String& path, mode_t mode)
{
initialize_if_needed();
if (path == "/")
return s_hard_disk_icon;
if (S_ISDIR(mode)) {
if (path == Core::StandardPaths::home_directory())
return s_home_directory_icon;
if (access(path.characters(), R_OK | X_OK) < 0)
return s_inaccessible_directory_icon;
return s_directory_icon;
}
if (S_ISLNK(mode)) {
auto raw_symlink_target = Core::File::read_link(path);
if (raw_symlink_target.is_null())
return s_symlink_icon;
String target_path;
if (raw_symlink_target.starts_with('/')) {
target_path = raw_symlink_target;
} else {
target_path = Core::File::real_path_for(String::formatted("{}/{}", LexicalPath(path).dirname(), raw_symlink_target));
}
auto target_icon = icon_for_path(target_path);
Icon generated_icon;
for (auto size : target_icon.sizes()) {
auto& emblem = size < 32 ? *s_symlink_emblem_small : *s_symlink_emblem;
auto original_bitmap = target_icon.bitmap_for_size(size);
ASSERT(original_bitmap);
auto generated_bitmap = original_bitmap->clone();
if (!generated_bitmap) {
dbgln("Failed to clone {}x{} icon for symlink variant", size, size);
return s_symlink_icon;
}
GUI::Painter painter(*generated_bitmap);
painter.blit({ size - emblem.width(), size - emblem.height() }, emblem, emblem.rect());
generated_icon.set_bitmap_for_size(size, move(generated_bitmap));
}
return generated_icon;
}
if (S_ISSOCK(mode))
return s_socket_icon;
if (mode & (S_IXUSR | S_IXGRP | S_IXOTH))
return icon_for_executable(path);
if (Gfx::Bitmap::is_path_a_supported_image_format(path.view()))
return s_filetype_image_icon;
for (auto& filetype : s_filetype_icons.keys()) {
auto patterns = s_filetype_patterns.get(filetype).value();
for (auto& pattern : patterns) {
if (path.matches(pattern, CaseSensitivity::CaseInsensitive))
return s_filetype_icons.get(filetype).value();
}
}
return s_file_icon;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Forward.h>
#include <LibGUI/Forward.h>
#include <sys/types.h>
namespace GUI {
class FileIconProvider {
public:
static Icon icon_for_path(const String&, mode_t);
static Icon icon_for_path(const String&);
static Icon icon_for_executable(const String&);
static Icon filetype_image_icon();
static Icon directory_icon();
static Icon directory_open_icon();
static Icon home_directory_icon();
static Icon home_directory_open_icon();
};
}

View file

@ -0,0 +1,334 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/Function.h>
#include <AK/LexicalPath.h>
#include <LibCore/StandardPaths.h>
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/MultiView.h>
#include <LibGUI/SortingProxyModel.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ToolBar.h>
#include <LibGfx/FontDatabase.h>
#include <string.h>
namespace GUI {
Optional<String> FilePicker::get_open_filepath(Window* parent_window, const String& window_title, Options options)
{
auto picker = FilePicker::construct(parent_window, Mode::Open, options);
if (!window_title.is_null())
picker->set_title(window_title);
if (picker->exec() == Dialog::ExecOK) {
String file_path = picker->selected_file().string();
if (file_path.is_null())
return {};
return file_path;
}
return {};
}
Optional<String> FilePicker::get_save_filepath(Window* parent_window, const String& title, const String& extension, Options options)
{
auto picker = FilePicker::construct(parent_window, Mode::Save, options, String::formatted("{}.{}", title, extension));
if (picker->exec() == Dialog::ExecOK) {
String file_path = picker->selected_file().string();
if (file_path.is_null())
return {};
return file_path;
}
return {};
}
FilePicker::FilePicker(Window* parent_window, Mode mode, Options options, const StringView& file_name, const StringView& path)
: Dialog(parent_window)
, m_model(FileSystemModel::create())
, m_mode(mode)
{
switch (m_mode) {
case Mode::Open:
case Mode::OpenMultiple:
set_title("Open");
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"));
break;
case Mode::Save:
set_title("Save as");
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"));
break;
}
resize(560, 320);
auto& horizontal_container = set_main_widget<Widget>();
horizontal_container.set_layout<HorizontalBoxLayout>();
horizontal_container.layout()->set_margins({ 4, 4, 4, 4 });
horizontal_container.set_fill_with_background_color(true);
auto& vertical_container = horizontal_container.add<Widget>();
vertical_container.set_layout<VerticalBoxLayout>();
vertical_container.layout()->set_spacing(4);
auto& upper_container = vertical_container.add<Widget>();
upper_container.set_layout<HorizontalBoxLayout>();
upper_container.layout()->set_spacing(2);
upper_container.set_fixed_height(26);
auto& toolbar = upper_container.add<ToolBar>();
toolbar.set_fixed_width(165);
toolbar.set_has_frame(false);
m_location_textbox = upper_container.add<TextBox>();
m_location_textbox->set_text(path);
m_view = vertical_container.add<MultiView>();
m_view->set_selection_mode(m_mode == Mode::OpenMultiple ? GUI::AbstractView::SelectionMode::MultiSelection : GUI::AbstractView::SelectionMode::SingleSelection);
m_view->set_model(SortingProxyModel::create(*m_model));
m_view->set_model_column(FileSystemModel::Column::Name);
m_view->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
m_view->set_column_hidden(FileSystemModel::Column::Owner, true);
m_view->set_column_hidden(FileSystemModel::Column::Group, true);
m_view->set_column_hidden(FileSystemModel::Column::Permissions, true);
m_view->set_column_hidden(FileSystemModel::Column::Inode, true);
m_view->set_column_hidden(FileSystemModel::Column::SymlinkTarget, true);
set_path(path);
m_model->register_client(*this);
m_location_textbox->on_return_pressed = [this] {
set_path(m_location_textbox->text());
};
auto open_parent_directory_action = Action::create("Open parent directory", { Mod_Alt, Key_Up }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [this](const Action&) {
set_path(String::formatted("{}/..", m_model->root_path()));
});
toolbar.add_action(*open_parent_directory_action);
auto go_home_action = CommonActions::make_go_home_action([this](auto&) {
set_path(Core::StandardPaths::home_directory());
});
toolbar.add_action(go_home_action);
toolbar.add_separator();
auto mkdir_action = Action::create("New directory...", Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [this](const Action&) {
String value;
if (InputBox::show(value, this, "Enter name:", "New directory") == InputBox::ExecOK && !value.is_empty()) {
auto new_dir_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", m_model->root_path(), value));
int rc = mkdir(new_dir_path.characters(), 0777);
if (rc < 0) {
MessageBox::show(this, String::formatted("mkdir(\"{}\") failed: {}", new_dir_path, strerror(errno)), "Error", MessageBox::Type::Error);
} else {
m_model->update();
}
}
});
toolbar.add_action(*mkdir_action);
toolbar.add_separator();
toolbar.add_action(m_view->view_as_icons_action());
toolbar.add_action(m_view->view_as_table_action());
toolbar.add_action(m_view->view_as_columns_action());
auto& lower_container = vertical_container.add<Widget>();
lower_container.set_layout<VerticalBoxLayout>();
lower_container.layout()->set_spacing(4);
lower_container.set_fixed_height(48);
auto& filename_container = lower_container.add<Widget>();
filename_container.set_fixed_height(22);
filename_container.set_layout<HorizontalBoxLayout>();
auto& filename_label = filename_container.add<Label>("File name:");
filename_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
filename_label.set_fixed_width(60);
m_filename_textbox = filename_container.add<TextBox>();
m_filename_textbox->set_focus(true);
if (m_mode == Mode::Save) {
m_filename_textbox->set_text(file_name);
m_filename_textbox->select_all();
}
m_filename_textbox->on_return_pressed = [&] {
on_file_return();
};
m_view->on_selection_change = [this] {
auto index = m_view->selection().first();
auto& filter_model = (SortingProxyModel&)*m_view->model();
auto local_index = filter_model.map_to_source(index);
const FileSystemModel::Node& node = m_model->node(local_index);
LexicalPath path { node.full_path() };
if (have_preview())
clear_preview();
if (!node.is_directory())
m_filename_textbox->set_text(node.name);
if (have_preview())
set_preview(path);
};
auto& button_container = lower_container.add<Widget>();
button_container.set_fixed_height(22);
button_container.set_layout<HorizontalBoxLayout>();
button_container.layout()->set_spacing(4);
button_container.layout()->add_spacer();
auto& cancel_button = button_container.add<Button>();
cancel_button.set_fixed_width(80);
cancel_button.set_text("Cancel");
cancel_button.on_click = [this](auto) {
done(ExecCancel);
};
auto& ok_button = button_container.add<Button>();
ok_button.set_fixed_width(80);
ok_button.set_text(ok_button_name(m_mode));
ok_button.on_click = [this](auto) {
on_file_return();
};
m_view->on_activation = [this](auto& index) {
auto& filter_model = (SortingProxyModel&)*m_view->model();
auto local_index = filter_model.map_to_source(index);
const FileSystemModel::Node& node = m_model->node(local_index);
auto path = node.full_path();
if (node.is_directory()) {
set_path(path);
// NOTE: 'node' is invalid from here on
} else {
on_file_return();
}
};
if (!((unsigned)options & (unsigned)Options::DisablePreview)) {
m_preview_container = horizontal_container.add<Frame>();
m_preview_container->set_visible(false);
m_preview_container->set_fixed_width(180);
m_preview_container->set_layout<VerticalBoxLayout>();
m_preview_container->layout()->set_margins({ 8, 8, 8, 8 });
m_preview_image = m_preview_container->add<ImageWidget>();
m_preview_image->set_should_stretch(true);
m_preview_image->set_auto_resize(false);
m_preview_image->set_fixed_size(160, 160);
m_preview_name_label = m_preview_container->add<Label>();
m_preview_name_label->set_font(Gfx::FontDatabase::default_bold_font());
m_preview_name_label->set_fixed_height(m_preview_name_label->font().glyph_height());
m_preview_geometry_label = m_preview_container->add<Label>();
m_preview_geometry_label->set_fixed_height(m_preview_name_label->font().glyph_height());
}
}
FilePicker::~FilePicker()
{
m_model->unregister_client(*this);
}
void FilePicker::model_did_update(unsigned)
{
m_location_textbox->set_text(m_model->root_path());
if (have_preview())
clear_preview();
}
void FilePicker::set_preview(const LexicalPath& path)
{
if (Gfx::Bitmap::is_path_a_supported_image_format(path.string())) {
auto bitmap = Gfx::Bitmap::load_from_file(path.string());
if (!bitmap) {
clear_preview();
return;
}
bool should_stretch = bitmap->width() > m_preview_image->width() || bitmap->height() > m_preview_image->height();
m_preview_name_label->set_text(path.basename());
m_preview_geometry_label->set_text(bitmap->size().to_string());
m_preview_image->set_should_stretch(should_stretch);
m_preview_image->set_bitmap(move(bitmap));
m_preview_container->set_visible(true);
}
}
void FilePicker::clear_preview()
{
m_preview_image->set_bitmap(nullptr);
m_preview_name_label->set_text(String::empty());
m_preview_geometry_label->set_text(String::empty());
m_preview_container->set_visible(false);
}
void FilePicker::on_file_return()
{
LexicalPath path(String::formatted("{}/{}", m_model->root_path(), m_filename_textbox->text()));
if (FilePicker::file_exists(path.string()) && m_mode == Mode::Save) {
auto result = MessageBox::show(this, "File already exists, overwrite?", "Existing File", MessageBox::Type::Warning, MessageBox::InputType::OKCancel);
if (result == MessageBox::ExecCancel)
return;
}
m_selected_file = path;
done(ExecOK);
}
bool FilePicker::file_exists(const StringView& path)
{
struct stat st;
int rc = stat(path.to_string().characters(), &st);
if (rc < 0) {
if (errno == ENOENT)
return false;
}
if (rc == 0) {
return true;
}
return false;
}
void FilePicker::set_path(const String& path)
{
auto new_path = LexicalPath(path).string();
m_location_textbox->set_icon(FileIconProvider::icon_for_path(new_path).bitmap_for_size(16));
m_model->set_root_path(new_path);
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <LibCore/StandardPaths.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Model.h>
namespace GUI {
class FilePicker final
: public Dialog
, private ModelClient {
C_OBJECT(FilePicker);
public:
enum class Mode {
Open,
OpenMultiple,
Save
};
enum class Options : unsigned {
None = 0,
DisablePreview = (1 << 0)
};
static Optional<String> get_open_filepath(Window* parent_window, Options options)
{
return get_open_filepath(parent_window, {}, options);
}
static Optional<String> get_open_filepath(Window* parent_window, const String& window_title = {}, Options options = Options::None);
static Optional<String> get_save_filepath(Window* parent_window, const String& title, const String& extension, Options options = Options::None);
static bool file_exists(const StringView& path);
virtual ~FilePicker() override;
LexicalPath selected_file() const { return m_selected_file; }
private:
bool have_preview() const { return m_preview_container; }
void set_preview(const LexicalPath&);
void clear_preview();
void on_file_return();
void set_path(const String&);
// ^GUI::ModelClient
virtual void model_did_update(unsigned) override;
FilePicker(Window* parent_window, Mode type = Mode::Open, Options = Options::None, const StringView& file_name = "Untitled", const StringView& path = Core::StandardPaths::home_directory());
static String ok_button_name(Mode mode)
{
switch (mode) {
case Mode::Open:
case Mode::OpenMultiple:
return "Open";
case Mode::Save:
return "Save";
default:
return "OK";
}
}
RefPtr<MultiView> m_view;
NonnullRefPtr<FileSystemModel> m_model;
LexicalPath m_selected_file;
RefPtr<TextBox> m_filename_textbox;
RefPtr<TextBox> m_location_textbox;
RefPtr<Frame> m_preview_container;
RefPtr<ImageWidget> m_preview_image;
RefPtr<Label> m_preview_name_label;
RefPtr<Label> m_preview_geometry_label;
Mode m_mode { Mode::Open };
};
}

View file

@ -0,0 +1,647 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/LexicalPath.h>
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibCore/StandardPaths.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Bitmap.h>
#include <LibThread/BackgroundAction.h>
#include <dirent.h>
#include <grp.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
namespace GUI {
ModelIndex FileSystemModel::Node::index(int column) const
{
if (!parent)
return {};
for (size_t row = 0; row < parent->children.size(); ++row) {
if (&parent->children[row] == this)
return m_model.create_index(row, column, const_cast<Node*>(this));
}
ASSERT_NOT_REACHED();
}
bool FileSystemModel::Node::fetch_data(const String& full_path, bool is_root)
{
struct stat st;
int rc;
if (is_root)
rc = stat(full_path.characters(), &st);
else
rc = lstat(full_path.characters(), &st);
if (rc < 0) {
m_error = errno;
perror("stat/lstat");
return false;
}
size = st.st_size;
mode = st.st_mode;
uid = st.st_uid;
gid = st.st_gid;
inode = st.st_ino;
mtime = st.st_mtime;
if (S_ISLNK(mode)) {
symlink_target = Core::File::read_link(full_path);
if (symlink_target.is_null())
perror("readlink");
}
if (S_ISDIR(mode)) {
is_accessible_directory = access(full_path.characters(), R_OK | X_OK) == 0;
}
return true;
}
void FileSystemModel::Node::traverse_if_needed()
{
if (!is_directory() || has_traversed)
return;
has_traversed = true;
if (m_parent_of_root) {
auto root = adopt_own(*new Node(m_model));
root->fetch_data("/", true);
root->name = "/";
root->parent = this;
children.append(move(root));
return;
}
total_size = 0;
auto full_path = this->full_path();
Core::DirIterator di(full_path, m_model.should_show_dotfiles() ? Core::DirIterator::SkipParentAndBaseDir : Core::DirIterator::SkipDots);
if (di.has_error()) {
m_error = di.error();
fprintf(stderr, "DirIterator: %s\n", di.error_string());
return;
}
Vector<String> child_names;
while (di.has_next()) {
child_names.append(di.next_path());
}
quick_sort(child_names);
for (auto& name : child_names) {
String child_path = String::formatted("{}/{}", full_path, name);
auto child = adopt_own(*new Node(m_model));
bool ok = child->fetch_data(child_path, false);
if (!ok)
continue;
if (m_model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode))
continue;
child->name = name;
child->parent = this;
total_size += child->size;
children.append(move(child));
}
if (m_watch_fd >= 0)
return;
m_watch_fd = watch_file(full_path.characters(), full_path.length());
if (m_watch_fd < 0) {
perror("watch_file");
return;
}
fcntl(m_watch_fd, F_SETFD, FD_CLOEXEC);
dbgln("Watching {} for changes, m_watch_fd={}", full_path, m_watch_fd);
m_notifier = Core::Notifier::construct(m_watch_fd, Core::Notifier::Event::Read);
m_notifier->on_ready_to_read = [this] {
char buffer[32];
int rc = read(m_notifier->fd(), buffer, sizeof(buffer));
ASSERT(rc >= 0);
has_traversed = false;
mode = 0;
children.clear();
reify_if_needed();
m_model.did_update();
};
}
void FileSystemModel::Node::reify_if_needed()
{
traverse_if_needed();
if (mode != 0)
return;
fetch_data(full_path(), parent == nullptr || parent->m_parent_of_root);
}
String FileSystemModel::Node::full_path() const
{
Vector<String, 32> lineage;
for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) {
lineage.append(ancestor->name);
}
StringBuilder builder;
builder.append(m_model.root_path());
for (int i = lineage.size() - 1; i >= 0; --i) {
builder.append('/');
builder.append(lineage[i]);
}
builder.append('/');
builder.append(name);
return LexicalPath::canonicalized_path(builder.to_string());
}
ModelIndex FileSystemModel::index(const StringView& path, int column) const
{
LexicalPath lexical_path(path);
const Node* node = m_root->m_parent_of_root ? &m_root->children.first() : m_root;
if (lexical_path.string() == "/")
return node->index(column);
for (size_t i = 0; i < lexical_path.parts().size(); ++i) {
auto& part = lexical_path.parts()[i];
bool found = false;
for (auto& child : node->children) {
if (child.name == part) {
const_cast<Node&>(child).reify_if_needed();
node = &child;
found = true;
if (i == lexical_path.parts().size() - 1)
return child.index(column);
break;
}
}
if (!found)
return {};
}
return {};
}
String FileSystemModel::full_path(const ModelIndex& index) const
{
auto& node = this->node(index);
const_cast<Node&>(node).reify_if_needed();
return node.full_path();
}
FileSystemModel::FileSystemModel(const StringView& root_path, Mode mode)
: m_root_path(LexicalPath::canonicalized_path(root_path))
, m_mode(mode)
{
setpwent();
while (auto* passwd = getpwent())
m_user_names.set(passwd->pw_uid, passwd->pw_name);
endpwent();
setgrent();
while (auto* group = getgrent())
m_group_names.set(group->gr_gid, group->gr_name);
endgrent();
update();
}
FileSystemModel::~FileSystemModel()
{
}
String FileSystemModel::name_for_uid(uid_t uid) const
{
auto it = m_user_names.find(uid);
if (it == m_user_names.end())
return String::number(uid);
return (*it).value;
}
String FileSystemModel::name_for_gid(gid_t gid) const
{
auto it = m_group_names.find(gid);
if (it == m_group_names.end())
return String::number(gid);
return (*it).value;
}
static String permission_string(mode_t mode)
{
StringBuilder builder;
if (S_ISDIR(mode))
builder.append("d");
else if (S_ISLNK(mode))
builder.append("l");
else if (S_ISBLK(mode))
builder.append("b");
else if (S_ISCHR(mode))
builder.append("c");
else if (S_ISFIFO(mode))
builder.append("f");
else if (S_ISSOCK(mode))
builder.append("s");
else if (S_ISREG(mode))
builder.append("-");
else
builder.append("?");
builder.appendf("%c%c%c%c%c%c%c%c",
mode & S_IRUSR ? 'r' : '-',
mode & S_IWUSR ? 'w' : '-',
mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
mode & S_IRGRP ? 'r' : '-',
mode & S_IWGRP ? 'w' : '-',
mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
mode & S_IROTH ? 'r' : '-',
mode & S_IWOTH ? 'w' : '-');
if (mode & S_ISVTX)
builder.append("t");
else
builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
return builder.to_string();
}
void FileSystemModel::Node::set_selected(bool selected)
{
if (m_selected == selected)
return;
m_selected = selected;
}
void FileSystemModel::update_node_on_selection(const ModelIndex& index, const bool selected)
{
Node& node = const_cast<Node&>(this->node(index));
node.set_selected(selected);
}
void FileSystemModel::set_root_path(const StringView& root_path)
{
if (root_path.is_null())
m_root_path = {};
else
m_root_path = LexicalPath::canonicalized_path(root_path);
update();
if (m_root->has_error()) {
if (on_error)
on_error(m_root->error(), m_root->error_string());
} else if (on_complete) {
on_complete();
}
}
void FileSystemModel::update()
{
m_root = adopt_own(*new Node(*this));
if (m_root_path.is_null())
m_root->m_parent_of_root = true;
m_root->reify_if_needed();
did_update();
}
int FileSystemModel::row_count(const ModelIndex& index) const
{
Node& node = const_cast<Node&>(this->node(index));
node.reify_if_needed();
if (node.is_directory())
return node.children.size();
return 0;
}
const FileSystemModel::Node& FileSystemModel::node(const ModelIndex& index) const
{
if (!index.is_valid())
return *m_root;
ASSERT(index.internal_data());
return *(Node*)index.internal_data();
}
ModelIndex FileSystemModel::index(int row, int column, const ModelIndex& parent) const
{
if (row < 0 || column < 0)
return {};
auto& node = this->node(parent);
const_cast<Node&>(node).reify_if_needed();
if (static_cast<size_t>(row) >= node.children.size())
return {};
return create_index(row, column, &node.children[row]);
}
ModelIndex FileSystemModel::parent_index(const ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto& node = this->node(index);
if (!node.parent) {
ASSERT(&node == m_root);
return {};
}
return node.parent->index(index.column());
}
Variant FileSystemModel::data(const ModelIndex& index, ModelRole role) const
{
ASSERT(index.is_valid());
if (role == ModelRole::TextAlignment) {
switch (index.column()) {
case Column::Icon:
return Gfx::TextAlignment::Center;
case Column::Size:
case Column::Inode:
return Gfx::TextAlignment::CenterRight;
case Column::Name:
case Column::Owner:
case Column::Group:
case Column::ModificationTime:
case Column::Permissions:
case Column::SymlinkTarget:
return Gfx::TextAlignment::CenterLeft;
default:
ASSERT_NOT_REACHED();
}
}
auto& node = this->node(index);
if (role == ModelRole::Custom) {
// For GUI::FileSystemModel, custom role means the full path.
ASSERT(index.column() == Column::Name);
return node.full_path();
}
if (role == ModelRole::MimeData) {
if (index.column() == Column::Name) {
StringBuilder builder;
builder.append("file://");
builder.append(node.full_path());
return builder.to_string();
}
return {};
}
if (role == ModelRole::Sort) {
switch (index.column()) {
case Column::Icon:
return node.is_directory() ? 0 : 1;
case Column::Name:
return node.name;
case Column::Size:
return (int)node.size;
case Column::Owner:
return name_for_uid(node.uid);
case Column::Group:
return name_for_gid(node.gid);
case Column::Permissions:
return permission_string(node.mode);
case Column::ModificationTime:
return node.mtime;
case Column::Inode:
return (int)node.inode;
case Column::SymlinkTarget:
return node.symlink_target;
}
ASSERT_NOT_REACHED();
}
if (role == ModelRole::Display) {
switch (index.column()) {
case Column::Icon:
return icon_for(node);
case Column::Name:
return node.name;
case Column::Size:
return (int)node.size;
case Column::Owner:
return name_for_uid(node.uid);
case Column::Group:
return name_for_gid(node.gid);
case Column::Permissions:
return permission_string(node.mode);
case Column::ModificationTime:
return timestamp_string(node.mtime);
case Column::Inode:
return (int)node.inode;
case Column::SymlinkTarget:
return node.symlink_target;
}
}
if (role == ModelRole::Icon) {
return icon_for(node);
}
return {};
}
Icon FileSystemModel::icon_for(const Node& node) const
{
if (node.full_path() == "/")
return FileIconProvider::icon_for_path("/");
if (Gfx::Bitmap::is_path_a_supported_image_format(node.name)) {
if (!node.thumbnail) {
if (!const_cast<FileSystemModel*>(this)->fetch_thumbnail_for(node))
return FileIconProvider::filetype_image_icon();
}
return GUI::Icon(FileIconProvider::filetype_image_icon().bitmap_for_size(16), *node.thumbnail);
}
if (node.is_directory()) {
if (node.full_path() == Core::StandardPaths::home_directory()) {
if (node.is_selected())
return FileIconProvider::home_directory_open_icon();
return FileIconProvider::home_directory_icon();
}
if (node.is_selected() && node.is_accessible_directory)
return FileIconProvider::directory_open_icon();
}
return FileIconProvider::icon_for_path(node.full_path(), node.mode);
}
static HashMap<String, RefPtr<Gfx::Bitmap>> s_thumbnail_cache;
static RefPtr<Gfx::Bitmap> render_thumbnail(const StringView& path)
{
auto png_bitmap = Gfx::Bitmap::load_from_file(path);
if (!png_bitmap)
return nullptr;
double scale = min(32 / (double)png_bitmap->width(), 32 / (double)png_bitmap->height());
auto thumbnail = Gfx::Bitmap::create(png_bitmap->format(), { 32, 32 });
Gfx::IntRect destination = Gfx::IntRect(0, 0, (int)(png_bitmap->width() * scale), (int)(png_bitmap->height() * scale));
destination.center_within(thumbnail->rect());
Painter painter(*thumbnail);
painter.draw_scaled_bitmap(destination, *png_bitmap, png_bitmap->rect());
return thumbnail;
}
bool FileSystemModel::fetch_thumbnail_for(const Node& node)
{
// See if we already have the thumbnail
// we're looking for in the cache.
auto path = node.full_path();
auto it = s_thumbnail_cache.find(path);
if (it != s_thumbnail_cache.end()) {
if (!(*it).value)
return false;
node.thumbnail = (*it).value;
return true;
}
// Otherwise, arrange to render the thumbnail
// in background and make it available later.
s_thumbnail_cache.set(path, nullptr);
m_thumbnail_progress_total++;
auto weak_this = make_weak_ptr();
LibThread::BackgroundAction<RefPtr<Gfx::Bitmap>>::create(
[path] {
return render_thumbnail(path);
},
[this, path, weak_this](auto thumbnail) {
s_thumbnail_cache.set(path, move(thumbnail));
// The model was destroyed, no need to update
// progress or call any event handlers.
if (weak_this.is_null())
return;
m_thumbnail_progress++;
if (on_thumbnail_progress)
on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total);
if (m_thumbnail_progress == m_thumbnail_progress_total) {
m_thumbnail_progress = 0;
m_thumbnail_progress_total = 0;
}
did_update();
});
return false;
}
int FileSystemModel::column_count(const ModelIndex&) const
{
return Column::__Count;
}
String FileSystemModel::column_name(int column) const
{
switch (column) {
case Column::Icon:
return "";
case Column::Name:
return "Name";
case Column::Size:
return "Size";
case Column::Owner:
return "Owner";
case Column::Group:
return "Group";
case Column::Permissions:
return "Mode";
case Column::ModificationTime:
return "Modified";
case Column::Inode:
return "Inode";
case Column::SymlinkTarget:
return "Symlink target";
}
ASSERT_NOT_REACHED();
}
bool FileSystemModel::accepts_drag(const ModelIndex& index, const Vector<String>& mime_types) const
{
if (!index.is_valid())
return false;
if (!mime_types.contains_slow("text/uri-list"))
return false;
auto& node = this->node(index);
return node.is_directory();
}
void FileSystemModel::set_should_show_dotfiles(bool show)
{
if (m_should_show_dotfiles == show)
return;
m_should_show_dotfiles = show;
update();
}
bool FileSystemModel::is_editable(const ModelIndex& index) const
{
if (!index.is_valid())
return false;
return index.column() == Column::Name;
}
void FileSystemModel::set_data(const ModelIndex& index, const Variant& data)
{
ASSERT(is_editable(index));
Node& node = const_cast<Node&>(this->node(index));
auto dirname = LexicalPath(node.full_path()).dirname();
auto new_full_path = String::formatted("{}/{}", dirname, data.to_string());
int rc = rename(node.full_path().characters(), new_full_path.characters());
if (rc < 0) {
if (on_error)
on_error(errno, strerror(errno));
}
}
Vector<ModelIndex, 1> FileSystemModel::matches(const StringView& searching, unsigned flags, const ModelIndex& index)
{
Node& node = const_cast<Node&>(this->node(index));
node.reify_if_needed();
Vector<ModelIndex, 1> found_indexes;
for (auto& child : node.children) {
if (string_matches(child.name, searching, flags)) {
const_cast<Node&>(child).reify_if_needed();
found_indexes.append(child.index(Column::Name));
if (flags & FirstMatchOnly)
break;
}
}
return found_indexes;
}
}

View file

@ -0,0 +1,187 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtrVector.h>
#include <LibCore/DateTime.h>
#include <LibCore/Notifier.h>
#include <LibGUI/Model.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
namespace GUI {
class FileSystemModel
: public Model
, public Weakable<FileSystemModel> {
friend struct Node;
public:
enum Mode {
Invalid,
DirectoriesOnly,
FilesAndDirectories
};
enum Column {
Icon = 0,
Name,
Size,
Owner,
Group,
Permissions,
ModificationTime,
Inode,
SymlinkTarget,
__Count,
};
struct Node {
~Node() { close(m_watch_fd); }
String name;
String symlink_target;
size_t size { 0 };
mode_t mode { 0 };
uid_t uid { 0 };
gid_t gid { 0 };
ino_t inode { 0 };
time_t mtime { 0 };
bool is_accessible_directory { false };
size_t total_size { 0 };
mutable RefPtr<Gfx::Bitmap> thumbnail;
bool is_directory() const { return S_ISDIR(mode); }
bool is_executable() const { return mode & (S_IXUSR | S_IXGRP | S_IXOTH); }
bool is_selected() const { return m_selected; }
void set_selected(bool selected);
bool has_error() const { return m_error != 0; }
int error() const { return m_error; }
const char* error_string() const { return strerror(m_error); }
String full_path() const;
private:
friend class FileSystemModel;
explicit Node(FileSystemModel& model)
: m_model(model)
{
}
FileSystemModel& m_model;
Node* parent { nullptr };
NonnullOwnPtrVector<Node> children;
bool has_traversed { false };
bool m_selected { false };
int m_watch_fd { -1 };
RefPtr<Core::Notifier> m_notifier;
int m_error { 0 };
bool m_parent_of_root { false };
ModelIndex index(int column) const;
void traverse_if_needed();
void reify_if_needed();
bool fetch_data(const String& full_path, bool is_root);
};
static NonnullRefPtr<FileSystemModel> create(const StringView& root_path = "/", Mode mode = Mode::FilesAndDirectories)
{
return adopt(*new FileSystemModel(root_path, mode));
}
virtual ~FileSystemModel() override;
String root_path() const { return m_root_path; }
void set_root_path(const StringView&);
String full_path(const ModelIndex&) const;
ModelIndex index(const StringView& path, int column) const;
void update_node_on_selection(const ModelIndex&, const bool);
ModelIndex m_previously_selected_index {};
const Node& node(const ModelIndex& index) const;
Function<void(int done, int total)> on_thumbnail_progress;
Function<void()> on_complete;
Function<void(int error, const char* error_string)> on_error;
virtual int tree_column() const override { return Column::Name; }
virtual int row_count(const ModelIndex& = ModelIndex()) const override;
virtual int column_count(const ModelIndex& = ModelIndex()) const override;
virtual String column_name(int column) const override;
virtual Variant data(const ModelIndex&, ModelRole = ModelRole::Display) const override;
virtual void update() override;
virtual ModelIndex parent_index(const ModelIndex&) const override;
virtual ModelIndex index(int row, int column = 0, const ModelIndex& parent = ModelIndex()) const override;
virtual StringView drag_data_type() const override { return "text/uri-list"; }
virtual bool accepts_drag(const ModelIndex&, const Vector<String>& mime_types) const override;
virtual bool is_column_sortable(int column_index) const override { return column_index != Column::Icon; }
virtual bool is_editable(const ModelIndex&) const override;
virtual bool is_searchable() const override { return true; }
virtual void set_data(const ModelIndex&, const Variant&) override;
virtual Vector<ModelIndex, 1> matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) override;
static String timestamp_string(time_t timestamp)
{
return Core::DateTime::from_timestamp(timestamp).to_string();
}
bool should_show_dotfiles() const { return m_should_show_dotfiles; }
void set_should_show_dotfiles(bool);
private:
FileSystemModel(const StringView& root_path, Mode);
String name_for_uid(uid_t) const;
String name_for_gid(gid_t) const;
HashMap<uid_t, String> m_user_names;
HashMap<gid_t, String> m_group_names;
bool fetch_thumbnail_for(const Node& node);
GUI::Icon icon_for(const Node& node) const;
String m_root_path;
Mode m_mode { Invalid };
OwnPtr<Node> m_root { nullptr };
unsigned m_thumbnail_progress { 0 };
unsigned m_thumbnail_progress_total { 0 };
bool m_should_show_dotfiles { false };
};
}

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/FilteringProxyModel.h>
namespace GUI {
ModelIndex FilteringProxyModel::index(int row, int column, const ModelIndex& parent_index) const
{
int parent_row = parent_index.row();
if (!parent_index.is_valid())
parent_row = 0;
return create_index(parent_row + row, column);
}
int FilteringProxyModel::row_count(const ModelIndex&) const
{
return m_matching_indices.size();
}
int FilteringProxyModel::column_count(const ModelIndex& index) const
{
if (!index.is_valid())
return {};
if ((size_t)index.row() > m_matching_indices.size() || index.row() < 0)
return 0;
return m_model.column_count(m_matching_indices[index.row()]);
}
Variant FilteringProxyModel::data(const ModelIndex& index, ModelRole role) const
{
if (!index.is_valid())
return {};
if ((size_t)index.row() > m_matching_indices.size() || index.row() < 0)
return 0;
return m_matching_indices[index.row()].data(role);
}
void FilteringProxyModel::update()
{
m_model.update();
filter();
did_update();
}
void FilteringProxyModel::filter()
{
m_matching_indices.clear();
Function<void(ModelIndex&)> add_matching = [&](ModelIndex& parent_index) {
for (auto i = 0; i < m_model.row_count(parent_index); ++i) {
auto index = m_model.index(i, 0, parent_index);
if (!index.is_valid())
continue;
auto filter_matches = m_model.data_matches(index, m_filter_term);
bool matches = filter_matches == TriState::True;
if (filter_matches == TriState::Unknown) {
auto data = index.data();
if (data.is_string() && data.as_string().contains(m_filter_term))
matches = true;
}
if (matches)
m_matching_indices.append(index);
add_matching(index);
}
};
ModelIndex parent_index;
add_matching(parent_index);
}
void FilteringProxyModel::set_filter_term(const StringView& term)
{
if (m_filter_term == term)
return;
m_filter_term = term;
update();
}
ModelIndex FilteringProxyModel::map(const ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto row = index.row();
if (m_matching_indices.size() > (size_t)row)
return m_matching_indices[row];
return {};
}
bool FilteringProxyModel::is_searchable() const
{
return m_model.is_searchable();
}
Vector<ModelIndex, 1> FilteringProxyModel::matches(const StringView& searching, unsigned flags, const ModelIndex& index)
{
auto found_indexes = m_model.matches(searching, flags, index);
for (size_t i = 0; i < found_indexes.size(); i++)
found_indexes[i] = map(found_indexes[i]);
return found_indexes;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibGUI/Model.h>
#include <LibGUI/TextBox.h>
namespace GUI {
class FilteringProxyModel final : public Model {
public:
static NonnullRefPtr<FilteringProxyModel> construct(Model& model)
{
return adopt(*new FilteringProxyModel(model));
}
virtual ~FilteringProxyModel() override {};
virtual int row_count(const ModelIndex& = ModelIndex()) const override;
virtual int column_count(const ModelIndex& = ModelIndex()) const override;
virtual Variant data(const ModelIndex&, ModelRole = ModelRole::Display) const override;
virtual void update() override;
virtual ModelIndex index(int row, int column = 0, const ModelIndex& parent = ModelIndex()) const override;
virtual bool is_searchable() const override;
virtual Vector<ModelIndex, 1> matches(const StringView&, unsigned = MatchesFlag::AllMatching, const ModelIndex& = ModelIndex()) override;
void set_filter_term(const StringView& term);
ModelIndex map(const ModelIndex&) const;
private:
void filter();
explicit FilteringProxyModel(Model& model)
: m_model(model)
{
}
Model& m_model;
// Maps row to actual model index.
Vector<ModelIndex> m_matching_indices;
String m_filter_term;
};
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
namespace GUI {
enum class FocusSource {
Programmatic,
Keyboard,
Mouse,
};
}

View file

@ -0,0 +1,226 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/QuickSort.h>
#include <LibGUI/Button.h>
#include <LibGUI/FontPicker.h>
#include <LibGUI/FontPickerDialogGML.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/ListView.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/Widget.h>
#include <LibGfx/FontDatabase.h>
namespace GUI {
struct FontWeightNameMapping {
constexpr FontWeightNameMapping(int w, const char* n)
: weight(w)
, name(n)
{
}
int weight { 0 };
StringView name;
};
static constexpr FontWeightNameMapping font_weight_names[] = {
{ 100, "Thin" },
{ 200, "Extra Light" },
{ 300, "Light" },
{ 400, "Regular" },
{ 500, "Medium" },
{ 600, "Semi Bold" },
{ 700, "Bold" },
{ 800, "Extra Bold" },
{ 900, "Black" },
{ 950, "Extra Black" },
};
static constexpr StringView weight_to_name(int weight)
{
for (auto& it : font_weight_names) {
if (it.weight == weight)
return it.name;
}
return {};
}
class FontWeightListModel : public ItemListModel<int> {
public:
FontWeightListModel(const Vector<int>& weights)
: ItemListModel(weights)
{
}
virtual Variant data(const ModelIndex& index, ModelRole role) const override
{
if (role == ModelRole::Custom)
return m_data.at(index.row());
if (role == ModelRole::Display)
return String(weight_to_name(m_data.at(index.row())));
return ItemListModel::data(index, role);
}
};
FontPicker::FontPicker(Window* parent_window, const Gfx::Font* current_font, bool fixed_width_only)
: Dialog(parent_window)
, m_fixed_width_only(fixed_width_only)
{
set_title("Font picker");
resize(430, 280);
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-font-editor.png"));
auto& widget = set_main_widget<GUI::Widget>();
if (!widget.load_from_gml(font_picker_dialog_gml))
ASSERT_NOT_REACHED();
m_family_list_view = *widget.find_descendant_of_type_named<ListView>("family_list_view");
m_family_list_view->set_model(ItemListModel<String>::create(m_families));
m_family_list_view->horizontal_scrollbar().set_visible(false);
m_weight_list_view = *widget.find_descendant_of_type_named<ListView>("weight_list_view");
m_weight_list_view->set_model(adopt(*new FontWeightListModel(m_weights)));
m_weight_list_view->horizontal_scrollbar().set_visible(false);
m_size_list_view = *widget.find_descendant_of_type_named<ListView>("size_list_view");
m_size_list_view->set_model(ItemListModel<int>::create(m_sizes));
m_size_list_view->horizontal_scrollbar().set_visible(false);
m_sample_text_label = *widget.find_descendant_of_type_named<Label>("sample_text_label");
m_families.clear();
Gfx::FontDatabase::the().for_each_font([&](auto& font) {
if (m_fixed_width_only && !font.is_fixed_width())
return;
if (!m_families.contains_slow(font.family()))
m_families.append(font.family());
});
quick_sort(m_families);
m_family_list_view->on_selection = [this](auto& index) {
m_family = index.data().to_string();
m_weights.clear();
Gfx::FontDatabase::the().for_each_font([&](auto& font) {
if (m_fixed_width_only && !font.is_fixed_width())
return;
if (font.family() == m_family.value() && !m_weights.contains_slow(font.weight())) {
m_weights.append(font.weight());
}
});
quick_sort(m_weights);
Optional<size_t> index_of_old_weight_in_new_list;
if (m_weight.has_value())
index_of_old_weight_in_new_list = m_weights.find_first_index(m_weight.value());
m_weight_list_view->model()->update();
m_weight_list_view->set_cursor(m_weight_list_view->model()->index(index_of_old_weight_in_new_list.value_or(0)), GUI::AbstractView::SelectionUpdate::Set);
update_font();
};
m_weight_list_view->on_selection = [this](auto& index) {
m_weight = index.data(ModelRole::Custom).to_i32();
m_sizes.clear();
Gfx::FontDatabase::the().for_each_font([&](auto& font) {
if (m_fixed_width_only && !font.is_fixed_width())
return;
if (font.family() == m_family.value() && font.weight() == m_weight.value()) {
m_sizes.append(font.presentation_size());
}
});
quick_sort(m_sizes);
Optional<size_t> index_of_old_size_in_new_list;
if (m_size.has_value()) {
index_of_old_size_in_new_list = m_sizes.find_first_index(m_size.value());
}
m_size_list_view->model()->update();
m_size_list_view->set_cursor(m_size_list_view->model()->index(index_of_old_size_in_new_list.value_or(0)), GUI::AbstractView::SelectionUpdate::Set);
update_font();
};
m_size_list_view->on_selection = [this](auto& index) {
m_size = index.data().to_i32();
update_font();
};
auto& ok_button = *widget.find_descendant_of_type_named<GUI::Button>("ok_button");
ok_button.on_click = [this](auto) {
done(ExecOK);
};
auto& cancel_button = *widget.find_descendant_of_type_named<GUI::Button>("cancel_button");
cancel_button.on_click = [this](auto) {
done(ExecCancel);
};
set_font(current_font);
}
FontPicker::~FontPicker()
{
}
void FontPicker::set_font(const Gfx::Font* font)
{
if (m_font == font)
return;
m_font = font;
m_sample_text_label->set_font(m_font);
if (!m_font) {
m_family = {};
m_weight = {};
m_size = {};
m_weights.clear();
m_sizes.clear();
m_weight_list_view->model()->update();
m_size_list_view->model()->update();
return;
}
m_family = font->family();
m_weight = font->weight();
m_size = font->presentation_size();
size_t family_index = m_families.find_first_index(m_font->family()).value();
m_family_list_view->set_cursor(m_family_list_view->model()->index(family_index), GUI::AbstractView::SelectionUpdate::Set);
size_t weight_index = m_weights.find_first_index(m_font->weight()).value();
m_weight_list_view->set_cursor(m_weight_list_view->model()->index(weight_index), GUI::AbstractView::SelectionUpdate::Set);
size_t size_index = m_sizes.find_first_index(m_font->presentation_size()).value();
m_size_list_view->set_cursor(m_size_list_view->model()->index(size_index), GUI::AbstractView::SelectionUpdate::Set);
}
void FontPicker::update_font()
{
if (m_family.has_value() && m_size.has_value() && m_weight.has_value()) {
m_font = Gfx::FontDatabase::the().get(m_family.value(), m_size.value(), m_weight.value());
m_sample_text_label->set_font(m_font);
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Dialog.h>
#include <LibGfx/Forward.h>
namespace GUI {
class FontPicker final : public GUI::Dialog {
C_OBJECT(FontPicker);
public:
virtual ~FontPicker() override;
RefPtr<Gfx::Font> font() const { return m_font; }
void set_font(const Gfx::Font*);
private:
FontPicker(Window* parent_window = nullptr, const Gfx::Font* current_font = nullptr, bool fixed_width_only = false);
void update_font();
const bool m_fixed_width_only;
RefPtr<Gfx::Font> m_font;
RefPtr<ListView> m_family_list_view;
RefPtr<ListView> m_weight_list_view;
RefPtr<ListView> m_size_list_view;
RefPtr<Label> m_sample_text_label;
Vector<String> m_families;
Vector<int> m_weights;
Vector<int> m_sizes;
Optional<String> m_family;
Optional<int> m_weight;
Optional<int> m_size;
};
}

View file

@ -0,0 +1,95 @@
@GUI::Widget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [4, 4, 4, 4]
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Widget {
layout: @GUI::VerticalBoxLayout {
}
@GUI::Label {
text: "Family:"
text_alignment: "CenterLeft"
fixed_height: 16
}
@GUI::ListView {
name: "family_list_view"
}
}
@GUI::Widget {
fixed_width: 100
layout: @GUI::VerticalBoxLayout {
}
@GUI::Label {
text: "Weight:"
text_alignment: "CenterLeft"
fixed_height: 16
}
@GUI::ListView {
name: "weight_list_view"
}
}
@GUI::Widget {
fixed_width: 80
layout: @GUI::VerticalBoxLayout {
}
@GUI::Label {
text: "Size:"
text_alignment: "CenterLeft"
fixed_height: 16
}
@GUI::ListView {
name: "size_list_view"
}
}
}
@GUI::GroupBox {
layout: @GUI::VerticalBoxLayout {
}
title: "Sample text"
fixed_height: 80
@GUI::Label {
name: "sample_text_label"
text: "The quick brown fox jumps over the lazy dog."
}
}
@GUI::Widget {
fixed_height: 22
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Widget {
}
@GUI::Button {
name: "ok_button"
text: "OK"
fixed_width: 80
}
@GUI::Button {
name: "cancel_button"
text: "Cancel"
fixed_width: 80
}
}
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
namespace GUI {
class AbstractButton;
class AbstractTableView;
class AbstractView;
class Action;
class ActionGroup;
class Application;
class AutocompleteBox;
class AutocompleteProvider;
class BoxLayout;
class Button;
class CheckBox;
class Command;
class DragEvent;
class DropEvent;
class EditingEngine;
class FileSystemModel;
class Frame;
class GroupBox;
class HeaderView;
class HorizontalBoxLayout;
class HorizontalSlider;
class Icon;
class IconView;
class JsonArrayModel;
class KeyEvent;
class Label;
class Layout;
class ListView;
class Menu;
class MenuBar;
class MenuItem;
class Model;
class ModelEditingDelegate;
class ModelIndex;
class MouseEvent;
class MultiPaintEvent;
class MultiView;
class OpacitySlider;
class PaintEvent;
class Painter;
class ResizeCorner;
class ResizeEvent;
class ScrollBar;
class Slider;
class SortingProxyModel;
class SpinBox;
class Splitter;
class StackWidget;
class StatusBar;
class SyntaxHighlighter;
class TabWidget;
class TableView;
class TextBox;
class TextDocument;
class TextDocumentLine;
class TextDocumentUndoCommand;
class TextEditor;
class ThemeChangeEvent;
class ToolBar;
class ToolBarContainer;
class TreeView;
class Variant;
class VerticalBoxLayout;
class VerticalSlider;
class WMEvent;
class Widget;
class WidgetClassRegistration;
class Window;
class WindowServerConnection;
enum class ModelRole;
enum class SortOrder;
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/Frame.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
REGISTER_WIDGET(GUI, Frame)
namespace GUI {
Frame::Frame()
{
set_frame_thickness(2);
set_frame_shape(Gfx::FrameShape::Container);
set_frame_shadow(Gfx::FrameShadow::Sunken);
REGISTER_INT_PROPERTY("thickness", frame_thickness, set_frame_thickness);
REGISTER_ENUM_PROPERTY("shadow", frame_shadow, set_frame_shadow, Gfx::FrameShadow,
{ Gfx::FrameShadow::Plain, "Plain" },
{ Gfx::FrameShadow::Raised, "Raised" },
{ Gfx::FrameShadow::Sunken, "Sunken" });
REGISTER_ENUM_PROPERTY("shape", frame_shape, set_frame_shape, Gfx::FrameShape,
{ Gfx::FrameShape::NoFrame, "NoFrame" },
{ Gfx::FrameShape::Box, "Box" },
{ Gfx::FrameShape::Container, "Container" },
{ Gfx::FrameShape::Panel, "Panel" },
{ Gfx::FrameShape::VerticalLine, "VerticalLine" },
{ Gfx::FrameShape::HorizontalLine, "HorizontalLine" });
}
Frame::~Frame()
{
}
void Frame::set_frame_thickness(int thickness)
{
if (m_thickness == thickness)
return;
m_thickness = thickness;
set_content_margins({ thickness, thickness, thickness, thickness });
}
void Frame::paint_event(PaintEvent& event)
{
if (m_shape == Gfx::FrameShape::NoFrame)
return;
Painter painter(*this);
painter.add_clip_rect(event.rect());
Gfx::StylePainter::paint_frame(painter, rect(), palette(), m_shape, m_shadow, m_thickness, spans_entire_window_horizontally());
}
Gfx::IntRect Frame::children_clip_rect() const
{
return frame_inner_rect();
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
#include <LibGfx/StylePainter.h>
namespace GUI {
class Frame : public Widget {
C_OBJECT(Frame)
public:
virtual ~Frame() override;
int frame_thickness() const { return m_thickness; }
void set_frame_thickness(int thickness);
Gfx::FrameShadow frame_shadow() const { return m_shadow; }
void set_frame_shadow(Gfx::FrameShadow shadow) { m_shadow = shadow; }
Gfx::FrameShape frame_shape() const { return m_shape; }
void set_frame_shape(Gfx::FrameShape shape) { m_shape = shape; }
Gfx::IntRect frame_inner_rect_for_size(const Gfx::IntSize& size) const { return { m_thickness, m_thickness, size.width() - m_thickness * 2, size.height() - m_thickness * 2 }; }
Gfx::IntRect frame_inner_rect() const { return frame_inner_rect_for_size(size()); }
virtual Gfx::IntRect children_clip_rect() const override;
protected:
Frame();
void paint_event(PaintEvent&) override;
private:
int m_thickness { 0 };
Gfx::FrameShadow m_shadow { Gfx::FrameShadow::Plain };
Gfx::FrameShape m_shape { Gfx::FrameShape::NoFrame };
};
}

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2021, Linus Groh <mail@linusgroh.de>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/StringBuilder.h>
#include <LibGUI/GMLFormatter.h>
#include <LibGUI/GMLParser.h>
namespace GUI {
static String format_gml_object(const JsonObject& node, size_t indentation = 0, bool is_inline = false)
{
StringBuilder builder;
auto indent = [&builder](size_t indentation) {
for (size_t i = 0; i < indentation; ++i)
builder.append(" ");
};
struct Property {
String key;
JsonValue value;
};
Vector<Property> properties;
node.for_each_member([&](auto& key, auto& value) {
if (key != "class" && key != "layout" && key != "children")
properties.append({ key, value });
return IterationDecision::Continue;
});
if (!is_inline)
indent(indentation);
builder.append('@');
builder.append(node.get("class").as_string());
builder.append(" {\n");
for (auto& property : properties) {
indent(indentation + 1);
builder.append(property.key);
builder.append(": ");
if (property.value.is_array()) {
// custom array serialization as AK's doesn't pretty-print
// objects and arrays (we only care about arrays (for now))
builder.append("[");
auto first = true;
property.value.as_array().for_each([&](auto& value) {
if (!first)
builder.append(", ");
first = false;
value.serialize(builder);
});
builder.append("]");
} else {
property.value.serialize(builder);
}
builder.append("\n");
}
if (node.has("layout")) {
auto layout = node.get("layout").as_object();
if (!properties.is_empty())
builder.append("\n");
indent(indentation + 1);
builder.append("layout: ");
builder.append(format_gml_object(move(layout), indentation + 1, true));
}
if (node.has("children")) {
auto children = node.get("children").as_array();
auto first = properties.is_empty() && !node.has("layout");
children.for_each([&](auto& value) {
if (!first)
builder.append("\n");
first = false;
builder.append(format_gml_object(value.as_object(), indentation + 1));
});
}
indent(indentation);
builder.append("}\n");
return builder.to_string();
}
String format_gml(const StringView& string)
{
// FIXME: Preserve comments somehow, they're not contained
// in the JSON object returned by parse_gml()
auto ast = parse_gml(string);
if (ast.is_null())
return {};
ASSERT(ast.is_object());
return format_gml_object(ast.as_object());
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2021, Linus Groh <mail@linusgroh.de>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Forward.h>
namespace GUI {
String format_gml(const StringView&);
}

View file

@ -0,0 +1,175 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include "GMLLexer.h"
#include <AK/Vector.h>
#include <ctype.h>
namespace GUI {
GMLLexer::GMLLexer(const StringView& input)
: m_input(input)
{
}
char GMLLexer::peek(size_t offset) const
{
if ((m_index + offset) >= m_input.length())
return 0;
return m_input[m_index + offset];
}
char GMLLexer::consume()
{
ASSERT(m_index < m_input.length());
char ch = m_input[m_index++];
m_previous_position = m_position;
if (ch == '\n') {
m_position.line++;
m_position.column = 0;
} else {
m_position.column++;
}
return ch;
}
static bool is_valid_identifier_start(char ch)
{
return isalpha(ch) || ch == '_';
}
static bool is_valid_identifier_character(char ch)
{
return isalnum(ch) || ch == '_';
}
static bool is_valid_class_character(char ch)
{
return isalnum(ch) || ch == '_' || ch == ':';
}
Vector<GMLToken> GMLLexer::lex()
{
Vector<GMLToken> tokens;
size_t token_start_index = 0;
GMLPosition token_start_position;
auto begin_token = [&] {
token_start_index = m_index;
token_start_position = m_position;
};
auto commit_token = [&](auto type) {
GMLToken token;
token.m_view = m_input.substring_view(token_start_index, m_index - token_start_index);
token.m_type = type;
token.m_start = token_start_position;
token.m_end = m_previous_position;
tokens.append(token);
};
auto consume_class = [&] {
begin_token();
consume();
commit_token(GMLToken::Type::ClassMarker);
begin_token();
while (is_valid_class_character(peek()))
consume();
commit_token(GMLToken::Type::ClassName);
};
while (m_index < m_input.length()) {
if (isspace(peek(0))) {
begin_token();
while (isspace(peek()))
consume();
continue;
}
// C++ style comments
if (peek(0) && peek(0) == '/' && peek(1) == '/') {
begin_token();
while (peek() && peek() != '\n')
consume();
commit_token(GMLToken::Type::Comment);
continue;
}
if (peek(0) == '{') {
begin_token();
consume();
commit_token(GMLToken::Type::LeftCurly);
continue;
}
if (peek(0) == '}') {
begin_token();
consume();
commit_token(GMLToken::Type::RightCurly);
continue;
}
if (peek(0) == '@') {
consume_class();
continue;
}
if (is_valid_identifier_start(peek(0))) {
begin_token();
consume();
while (is_valid_identifier_character(peek(0)))
consume();
commit_token(GMLToken::Type::Identifier);
continue;
}
if (peek(0) == ':') {
begin_token();
consume();
commit_token(GMLToken::Type::Colon);
while (isspace(peek()))
consume();
if (peek(0) == '@') {
consume_class();
} else {
begin_token();
while (peek() && peek() != '\n')
consume();
commit_token(GMLToken::Type::JsonValue);
}
continue;
}
consume();
commit_token(GMLToken::Type::Unknown);
}
return tokens;
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/StringView.h>
namespace GUI {
#define FOR_EACH_TOKEN_TYPE \
__TOKEN(Unknown) \
__TOKEN(Comment) \
__TOKEN(ClassMarker) \
__TOKEN(ClassName) \
__TOKEN(LeftCurly) \
__TOKEN(RightCurly) \
__TOKEN(Identifier) \
__TOKEN(Colon) \
__TOKEN(JsonValue)
struct GMLPosition {
size_t line;
size_t column;
};
struct GMLToken {
enum class Type {
#define __TOKEN(x) x,
FOR_EACH_TOKEN_TYPE
#undef __TOKEN
};
const char* to_string() const
{
switch (m_type) {
#define __TOKEN(x) \
case Type::x: \
return #x;
FOR_EACH_TOKEN_TYPE
#undef __TOKEN
}
ASSERT_NOT_REACHED();
}
Type m_type { Type::Unknown };
StringView m_view;
GMLPosition m_start;
GMLPosition m_end;
};
class GMLLexer {
public:
GMLLexer(const StringView&);
Vector<GMLToken> lex();
private:
char peek(size_t offset = 0) const;
char consume();
StringView m_input;
size_t m_index { 0 };
GMLPosition m_previous_position { 0, 0 };
GMLPosition m_position { 0, 0 };
};
}

View file

@ -0,0 +1,161 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/GenericLexer.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/Queue.h>
#include <LibGUI/GMLLexer.h>
#include <LibGUI/GMLParser.h>
#include <ctype.h>
namespace GUI {
static Optional<JsonValue> parse_core_object(Queue<GMLToken>& tokens)
{
JsonObject object;
JsonArray children;
auto peek = [&] {
if (tokens.is_empty())
return GMLToken::Type::Unknown;
return tokens.head().m_type;
};
while (peek() == GMLToken::Type::Comment)
tokens.dequeue();
if (peek() != GMLToken::Type::ClassMarker) {
dbgln("Expected class marker");
return {};
}
tokens.dequeue();
if (peek() != GMLToken::Type::ClassName) {
dbgln("Expected class name");
return {};
}
auto class_name = tokens.dequeue();
object.set("class", JsonValue(class_name.m_view));
if (peek() != GMLToken::Type::LeftCurly) {
dbgln("Expected {{");
return {};
}
tokens.dequeue();
for (;;) {
if (peek() == GMLToken::Type::RightCurly) {
// End of object
break;
}
if (peek() == GMLToken::Type::ClassMarker) {
// It's a child object.
auto value = parse_core_object(tokens);
if (!value.has_value()) {
dbgln("Parsing child object failed");
return {};
}
if (!value.value().is_object()) {
dbgln("Expected child to be Core::Object");
return {};
}
children.append(value.release_value());
} else if (peek() == GMLToken::Type::Identifier) {
// It's a property.
auto property_name = tokens.dequeue();
if (property_name.m_view.is_empty()) {
dbgln("Expected non-empty property name");
return {};
}
if (peek() != GMLToken::Type::Colon) {
dbgln("Expected ':'");
return {};
}
tokens.dequeue();
JsonValue value;
if (peek() == GMLToken::Type::ClassMarker) {
auto parsed_value = parse_core_object(tokens);
if (!parsed_value.has_value())
return {};
if (!parsed_value.value().is_object()) {
dbgln("Expected property to be Core::Object");
return {};
}
value = parsed_value.release_value();
} else if (peek() == GMLToken::Type::JsonValue) {
auto value_string = tokens.dequeue();
auto parsed_value = JsonValue::from_string(value_string.m_view);
if (!parsed_value.has_value()) {
dbgln("Expected property to be JSON value");
return {};
}
value = parsed_value.release_value();
}
object.set(property_name.m_view, move(value));
} else if (peek() == GMLToken::Type::Comment) {
tokens.dequeue();
} else {
dbgln("Expected child, property, comment, or }}");
return {};
}
}
if (peek() != GMLToken::Type::RightCurly) {
dbgln("Expected }}");
return {};
}
tokens.dequeue();
if (!children.is_empty())
object.set("children", move(children));
return object;
}
JsonValue parse_gml(const StringView& string)
{
auto lexer = GMLLexer(string);
Queue<GMLToken> tokens;
for (auto& token : lexer.lex())
tokens.enqueue(token);
auto root = parse_core_object(tokens);
if (!root.has_value())
return JsonValue();
return root.release_value();
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/Forward.h>
namespace GUI {
JsonValue parse_gml(const StringView&);
}

View file

@ -0,0 +1,107 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/GMLLexer.h>
#include <LibGUI/GMLSyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
namespace GUI {
static TextStyle style_for_token_type(Gfx::Palette palette, GMLToken::Type type)
{
switch (type) {
case GMLToken::Type::LeftCurly:
case GMLToken::Type::RightCurly:
return { palette.syntax_punctuation() };
case GMLToken::Type::ClassMarker:
return { palette.syntax_keyword() };
case GMLToken::Type::ClassName:
return { palette.syntax_identifier(), true };
case GMLToken::Type::Identifier:
return { palette.syntax_identifier() };
case GMLToken::Type::JsonValue:
return { palette.syntax_string() };
case GMLToken::Type::Comment:
return { palette.syntax_comment() };
default:
return { palette.base_text() };
}
}
bool GMLSyntaxHighlighter::is_identifier(void* token) const
{
auto ini_token = static_cast<GUI::GMLToken::Type>(reinterpret_cast<size_t>(token));
return ini_token == GUI::GMLToken::Type::Identifier;
}
void GMLSyntaxHighlighter::rehighlight(Gfx::Palette palette)
{
ASSERT(m_editor);
auto text = m_editor->text();
GMLLexer lexer(text);
auto tokens = lexer.lex();
Vector<GUI::TextDocumentSpan> spans;
for (auto& token : tokens) {
GUI::TextDocumentSpan span;
span.range.set_start({ token.m_start.line, token.m_start.column });
span.range.set_end({ token.m_end.line, token.m_end.column });
auto style = style_for_token_type(palette, token.m_type);
span.attributes.color = style.color;
span.attributes.bold = style.bold;
span.is_skippable = false;
span.data = reinterpret_cast<void*>(token.m_type);
spans.append(span);
}
m_editor->document().set_spans(spans);
m_has_brace_buddies = false;
highlight_matching_token_pair();
m_editor->update();
}
Vector<GMLSyntaxHighlighter::MatchingTokenPair> GMLSyntaxHighlighter::matching_token_pairs() const
{
static Vector<SyntaxHighlighter::MatchingTokenPair> pairs;
if (pairs.is_empty()) {
pairs.append({ reinterpret_cast<void*>(GMLToken::Type::LeftCurly), reinterpret_cast<void*>(GMLToken::Type::RightCurly) });
}
return pairs;
}
bool GMLSyntaxHighlighter::token_types_equal(void* token1, void* token2) const
{
return static_cast<GUI::GMLToken::Type>(reinterpret_cast<size_t>(token1)) == static_cast<GUI::GMLToken::Type>(reinterpret_cast<size_t>(token2));
}
GMLSyntaxHighlighter::~GMLSyntaxHighlighter()
{
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/SyntaxHighlighter.h>
namespace GUI {
class GMLSyntaxHighlighter final : public SyntaxHighlighter {
public:
GMLSyntaxHighlighter() { }
virtual ~GMLSyntaxHighlighter() override;
virtual bool is_identifier(void*) const override;
virtual SyntaxLanguage language() const override { return SyntaxLanguage::INI; }
virtual void rehighlight(Gfx::Palette) override;
protected:
virtual Vector<MatchingTokenPair> matching_token_pairs() const override;
virtual bool token_types_equal(void*, void*) const override;
};
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/GroupBox.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
REGISTER_WIDGET(GUI, GroupBox)
namespace GUI {
GroupBox::GroupBox(const StringView& title)
: m_title(title)
{
REGISTER_STRING_PROPERTY("title", title, set_title);
}
GroupBox::~GroupBox()
{
}
void GroupBox::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
Gfx::IntRect frame_rect {
0, font().glyph_height() / 2,
width(), height() - font().glyph_height() / 2
};
Gfx::StylePainter::paint_frame(painter, frame_rect, palette(), Gfx::FrameShape::Box, Gfx::FrameShadow::Sunken, 2);
if (!m_title.is_empty()) {
Gfx::IntRect text_rect { 4, 0, font().width(m_title) + 6, font().glyph_height() };
painter.fill_rect(text_rect, palette().button());
painter.draw_text(text_rect, m_title, Gfx::TextAlignment::Center, palette().button_text());
}
}
void GroupBox::set_title(const StringView& title)
{
if (m_title == title)
return;
m_title = title;
update();
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace GUI {
class GroupBox : public Widget {
C_OBJECT(GroupBox)
public:
virtual ~GroupBox() override;
String title() const { return m_title; }
void set_title(const StringView&);
protected:
explicit GroupBox(const StringView& title = {});
virtual void paint_event(PaintEvent&) override;
private:
String m_title;
};
}

View file

@ -0,0 +1,387 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/AbstractTableView.h>
#include <LibGUI/Action.h>
#include <LibGUI/HeaderView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
#include <LibGfx/StylePainter.h>
namespace GUI {
static constexpr int minimum_column_size = 2;
HeaderView::HeaderView(AbstractTableView& table_view, Gfx::Orientation orientation)
: m_table_view(table_view)
, m_orientation(orientation)
{
set_font(Gfx::FontDatabase::default_bold_font());
if (m_orientation == Gfx::Orientation::Horizontal) {
set_fixed_height(16);
} else {
set_fixed_width(40);
}
}
HeaderView::~HeaderView()
{
}
void HeaderView::set_section_size(int section, int size)
{
auto& data = section_data(section);
if (data.size == size)
return;
data.size = size;
data.has_initialized_size = true;
data.size = size;
m_table_view.header_did_change_section_size({}, m_orientation, section, size);
}
int HeaderView::section_size(int section) const
{
return section_data(section).size;
}
HeaderView::SectionData& HeaderView::section_data(int section) const
{
if (static_cast<size_t>(section) >= m_section_data.size())
m_section_data.resize(section + 1);
return m_section_data.at(section);
}
Gfx::IntRect HeaderView::section_rect(int section) const
{
if (!model())
return {};
auto& data = section_data(section);
if (!data.visibility)
return {};
int offset = 0;
for (int i = 0; i < section; ++i) {
if (!is_section_visible(i))
continue;
offset += section_data(i).size;
if (orientation() == Gfx::Orientation::Horizontal)
offset += m_table_view.horizontal_padding() * 2;
}
if (orientation() == Gfx::Orientation::Horizontal)
return { offset, 0, section_size(section) + m_table_view.horizontal_padding() * 2, height() };
return { 0, offset, width(), section_size(section) };
}
Gfx::IntRect HeaderView::section_resize_grabbable_rect(int section) const
{
if (!model())
return {};
// FIXME: Support resizable rows.
if (m_orientation == Gfx::Orientation::Vertical)
return {};
auto rect = section_rect(section);
return { rect.right() - 1, rect.top(), 4, rect.height() };
}
int HeaderView::section_count() const
{
if (!model())
return 0;
return m_orientation == Gfx::Orientation::Horizontal ? model()->column_count() : model()->row_count();
}
void HeaderView::mousedown_event(MouseEvent& event)
{
if (!model())
return;
auto& model = *this->model();
int section_count = this->section_count();
for (int i = 0; i < section_count; ++i) {
if (section_resize_grabbable_rect(i).contains(event.position())) {
m_resizing_section = i;
m_in_section_resize = true;
m_section_resize_original_width = section_size(i);
m_section_resize_origin = event.position();
return;
}
auto rect = this->section_rect(i);
if (rect.contains(event.position()) && model.is_column_sortable(i)) {
m_pressed_section = i;
m_pressed_section_is_pressed = true;
update();
return;
}
}
}
void HeaderView::mousemove_event(MouseEvent& event)
{
if (!model())
return;
if (m_in_section_resize) {
auto delta = event.position() - m_section_resize_origin;
int new_size = m_section_resize_original_width + delta.primary_offset_for_orientation(m_orientation);
if (new_size <= minimum_column_size)
new_size = minimum_column_size;
ASSERT(m_resizing_section >= 0 && m_resizing_section < model()->column_count());
set_section_size(m_resizing_section, new_size);
return;
}
if (m_pressed_section != -1) {
auto header_rect = this->section_rect(m_pressed_section);
if (header_rect.contains(event.position())) {
set_hovered_section(m_pressed_section);
if (!m_pressed_section_is_pressed)
update();
m_pressed_section_is_pressed = true;
} else {
set_hovered_section(-1);
if (m_pressed_section_is_pressed)
update();
m_pressed_section_is_pressed = false;
}
return;
}
if (event.buttons() == 0) {
int section_count = this->section_count();
bool found_hovered_header = false;
for (int i = 0; i < section_count; ++i) {
if (section_resize_grabbable_rect(i).contains(event.position())) {
set_override_cursor(Gfx::StandardCursor::ResizeColumn);
set_hovered_section(-1);
return;
}
if (section_rect(i).contains(event.position())) {
set_hovered_section(i);
found_hovered_header = true;
}
}
if (!found_hovered_header)
set_hovered_section(-1);
}
set_override_cursor(Gfx::StandardCursor::None);
}
void HeaderView::mouseup_event(MouseEvent& event)
{
if (event.button() == MouseButton::Left) {
if (m_in_section_resize) {
if (!section_resize_grabbable_rect(m_resizing_section).contains(event.position()))
set_override_cursor(Gfx::StandardCursor::None);
m_in_section_resize = false;
return;
}
if (m_pressed_section != -1) {
if (m_orientation == Gfx::Orientation::Horizontal && section_rect(m_pressed_section).contains(event.position())) {
auto new_sort_order = m_table_view.sort_order();
if (m_table_view.key_column() == m_pressed_section)
new_sort_order = m_table_view.sort_order() == SortOrder::Ascending
? SortOrder::Descending
: SortOrder::Ascending;
m_table_view.set_key_column_and_sort_order(m_pressed_section, new_sort_order);
}
m_pressed_section = -1;
m_pressed_section_is_pressed = false;
update();
return;
}
}
}
void HeaderView::paint_horizontal(Painter& painter)
{
painter.draw_line({ 0, 0 }, { rect().right(), 0 }, palette().threed_highlight());
painter.draw_line({ 0, rect().bottom() }, { rect().right(), rect().bottom() }, palette().threed_shadow1());
int x_offset = 0;
int section_count = this->section_count();
for (int section = 0; section < section_count; ++section) {
if (!is_section_visible(section))
continue;
int section_width = section_size(section);
bool is_key_column = m_table_view.key_column() == section;
Gfx::IntRect cell_rect(x_offset, 0, section_width + m_table_view.horizontal_padding() * 2, height());
bool pressed = section == m_pressed_section && m_pressed_section_is_pressed;
bool hovered = section == m_hovered_section && model()->is_column_sortable(section);
Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, pressed, hovered);
String text;
if (is_key_column) {
StringBuilder builder;
builder.append(model()->column_name(section));
if (m_table_view.sort_order() == SortOrder::Ascending)
builder.append(" \xE2\xAC\x86"); // UPWARDS BLACK ARROW
else if (m_table_view.sort_order() == SortOrder::Descending)
builder.append(" \xE2\xAC\x87"); // DOWNWARDS BLACK ARROW
text = builder.to_string();
} else {
text = model()->column_name(section);
}
auto text_rect = cell_rect.shrunken(m_table_view.horizontal_padding() * 2, 0);
if (pressed)
text_rect.move_by(1, 1);
painter.draw_text(text_rect, text, font(), section_alignment(section), palette().button_text());
x_offset += section_width + m_table_view.horizontal_padding() * 2;
}
if (x_offset < rect().right()) {
Gfx::IntRect cell_rect(x_offset, 0, width() - x_offset, height());
Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, false, false);
}
}
void HeaderView::paint_vertical(Painter& painter)
{
painter.draw_line(rect().top_left(), rect().bottom_left(), palette().threed_highlight());
painter.draw_line(rect().top_right(), rect().bottom_right(), palette().threed_shadow1());
int y_offset = 0;
int section_count = this->section_count();
for (int section = 0; section < section_count; ++section) {
if (!is_section_visible(section))
continue;
int section_size = this->section_size(section);
Gfx::IntRect cell_rect(0, y_offset, width(), section_size);
bool pressed = section == m_pressed_section && m_pressed_section_is_pressed;
bool hovered = false;
Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, pressed, hovered);
String text = String::number(section);
auto text_rect = cell_rect.shrunken(m_table_view.horizontal_padding() * 2, 0);
if (pressed)
text_rect.move_by(1, 1);
painter.draw_text(text_rect, text, font(), section_alignment(section), palette().button_text());
y_offset += section_size;
}
if (y_offset < rect().bottom()) {
Gfx::IntRect cell_rect(0, y_offset, width(), height() - y_offset);
Gfx::StylePainter::paint_button(painter, cell_rect, palette(), Gfx::ButtonStyle::Normal, false, false);
}
}
void HeaderView::paint_event(PaintEvent& event)
{
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.fill_rect(rect(), palette().button());
if (orientation() == Gfx::Orientation::Horizontal)
paint_horizontal(painter);
else
paint_vertical(painter);
}
void HeaderView::set_section_visible(int section, bool visible)
{
auto& data = section_data(section);
if (data.visibility == visible)
return;
data.visibility = visible;
if (data.visibility_action) {
data.visibility_action->set_checked(visible);
}
m_table_view.header_did_change_section_visibility({}, m_orientation, section, visible);
update();
}
Menu& HeaderView::ensure_context_menu()
{
// FIXME: This menu needs to be rebuilt if the model is swapped out,
// or if the column count/names change.
if (!m_context_menu) {
ASSERT(model());
m_context_menu = Menu::construct();
if (m_orientation == Gfx::Orientation::Vertical) {
dbgln("FIXME: Support context menus for vertical GUI::HeaderView");
return *m_context_menu;
}
int section_count = this->section_count();
for (int section = 0; section < section_count; ++section) {
auto& column_data = this->section_data(section);
auto name = model()->column_name(section);
column_data.visibility_action = Action::create_checkable(name, [this, section](auto& action) {
set_section_visible(section, action.is_checked());
});
column_data.visibility_action->set_checked(column_data.visibility);
m_context_menu->add_action(*column_data.visibility_action);
}
}
return *m_context_menu;
}
void HeaderView::context_menu_event(ContextMenuEvent& event)
{
ensure_context_menu().popup(event.screen_position());
}
void HeaderView::leave_event(Core::Event& event)
{
Widget::leave_event(event);
set_hovered_section(-1);
}
Gfx::TextAlignment HeaderView::section_alignment(int section) const
{
return section_data(section).alignment;
}
void HeaderView::set_section_alignment(int section, Gfx::TextAlignment alignment)
{
section_data(section).alignment = alignment;
}
bool HeaderView::is_section_visible(int section) const
{
return section_data(section).visibility;
}
void HeaderView::set_hovered_section(int section)
{
if (m_hovered_section == section)
return;
m_hovered_section = section;
update();
}
Model* HeaderView::model()
{
return m_table_view.model();
}
const Model* HeaderView::model() const
{
return m_table_view.model();
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Widget.h>
#include <LibGfx/Orientation.h>
namespace GUI {
class HeaderView final : public Widget {
C_OBJECT(HeaderView);
public:
virtual ~HeaderView() override;
Gfx::Orientation orientation() const { return m_orientation; }
Model* model();
const Model* model() const;
void set_section_size(int section, int size);
int section_size(int section) const;
Gfx::TextAlignment section_alignment(int section) const;
void set_section_alignment(int section, Gfx::TextAlignment);
bool is_section_visible(int section) const;
void set_section_visible(int section, bool);
int section_count() const;
Gfx::IntRect section_rect(int section) const;
private:
HeaderView(AbstractTableView&, Gfx::Orientation);
virtual void paint_event(PaintEvent&) override;
virtual void mousedown_event(MouseEvent&) override;
virtual void mousemove_event(MouseEvent&) override;
virtual void mouseup_event(MouseEvent&) override;
virtual void context_menu_event(ContextMenuEvent&) override;
virtual void leave_event(Core::Event&) override;
Gfx::IntRect section_resize_grabbable_rect(int) const;
void paint_horizontal(Painter&);
void paint_vertical(Painter&);
Menu& ensure_context_menu();
RefPtr<Menu> m_context_menu;
AbstractTableView& m_table_view;
Gfx::Orientation m_orientation { Gfx::Orientation::Horizontal };
struct SectionData {
int size { 0 };
bool has_initialized_size { false };
bool visibility { true };
RefPtr<Action> visibility_action;
Gfx::TextAlignment alignment { Gfx::TextAlignment::CenterLeft };
};
SectionData& section_data(int section) const;
void set_hovered_section(int);
mutable Vector<SectionData> m_section_data;
bool m_in_section_resize { false };
Gfx::IntPoint m_section_resize_origin;
int m_section_resize_original_width { 0 };
int m_resizing_section { -1 };
int m_pressed_section { -1 };
bool m_pressed_section_is_pressed { false };
int m_hovered_section { -1 };
};
}

View file

@ -0,0 +1,160 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include "INILexer.h"
#include <AK/Vector.h>
#include <ctype.h>
namespace GUI {
IniLexer::IniLexer(const StringView& input)
: m_input(input)
{
}
char IniLexer::peek(size_t offset) const
{
if ((m_index + offset) >= m_input.length())
return 0;
return m_input[m_index + offset];
}
char IniLexer::consume()
{
ASSERT(m_index < m_input.length());
char ch = m_input[m_index++];
m_previous_position = m_position;
if (ch == '\n') {
m_position.line++;
m_position.column = 0;
} else {
m_position.column++;
}
return ch;
}
Vector<IniToken> IniLexer::lex()
{
Vector<IniToken> tokens;
size_t token_start_index = 0;
IniPosition token_start_position;
auto emit_token = [&](auto type) {
IniToken token;
token.m_type = type;
token.m_start = m_position;
token.m_end = m_position;
tokens.append(token);
consume();
};
auto begin_token = [&] {
token_start_index = m_index;
token_start_position = m_position;
};
auto commit_token = [&](auto type) {
IniToken token;
token.m_type = type;
token.m_start = token_start_position;
token.m_end = m_previous_position;
tokens.append(token);
};
while (m_index < m_input.length()) {
auto ch = peek();
if (isspace(ch)) {
begin_token();
while (isspace(peek()))
consume();
commit_token(IniToken::Type::Whitespace);
continue;
}
// ;Comment
if (ch == ';') {
begin_token();
while (peek() && peek() != '\n')
consume();
commit_token(IniToken::Type::Comment);
continue;
}
// [Section]
if (ch == '[') {
// [ Token
begin_token();
consume();
commit_token(IniToken::Type::LeftBracket);
// Section
begin_token();
while (peek() && !(peek() == ']' || peek() == '\n'))
consume();
commit_token(IniToken::Type::section);
// ] Token
if (peek() && peek() == ']') {
begin_token();
consume();
commit_token(IniToken::Type::RightBracket);
}
continue;
}
// Empty Line
if (ch == '\n') {
consume();
emit_token(IniToken::Type::Unknown);
continue;
}
// Name=Value
begin_token();
while (peek() && !(peek() == '=' || peek() == '\n'))
consume();
commit_token(IniToken::Type::Name);
if (peek() && peek() == '=') {
begin_token();
consume();
commit_token(IniToken::Type::Equal);
}
if (peek()) {
begin_token();
while (peek() && peek() != '\n')
consume();
commit_token(IniToken::Type::Value);
}
}
return tokens;
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/StringView.h>
namespace GUI {
#define FOR_EACH_TOKEN_TYPE \
__TOKEN(Unknown) \
__TOKEN(Comment) \
__TOKEN(Whitespace) \
__TOKEN(section) \
__TOKEN(LeftBracket) \
__TOKEN(RightBracket) \
__TOKEN(Name) \
__TOKEN(Value) \
__TOKEN(Equal)
struct IniPosition {
size_t line;
size_t column;
};
struct IniToken {
enum class Type {
#define __TOKEN(x) x,
FOR_EACH_TOKEN_TYPE
#undef __TOKEN
};
const char* to_string() const
{
switch (m_type) {
#define __TOKEN(x) \
case Type::x: \
return #x;
FOR_EACH_TOKEN_TYPE
#undef __TOKEN
}
ASSERT_NOT_REACHED();
}
Type m_type { Type::Unknown };
IniPosition m_start;
IniPosition m_end;
};
class IniLexer {
public:
IniLexer(const StringView&);
Vector<IniToken> lex();
private:
char peek(size_t offset = 0) const;
char consume();
StringView m_input;
size_t m_index { 0 };
IniPosition m_previous_position { 0, 0 };
IniPosition m_position { 0, 0 };
};
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/INILexer.h>
#include <LibGUI/INISyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
namespace GUI {
static TextStyle style_for_token_type(Gfx::Palette palette, IniToken::Type type)
{
switch (type) {
case IniToken::Type::LeftBracket:
case IniToken::Type::RightBracket:
case IniToken::Type::section:
return { palette.syntax_keyword(), true };
case IniToken::Type::Name:
return { palette.syntax_identifier() };
case IniToken::Type::Value:
return { palette.syntax_string() };
case IniToken::Type::Comment:
return { palette.syntax_comment() };
case IniToken::Type::Equal:
return { palette.syntax_operator(), true };
default:
return { palette.base_text() };
}
}
bool IniSyntaxHighlighter::is_identifier(void* token) const
{
auto ini_token = static_cast<GUI::IniToken::Type>(reinterpret_cast<size_t>(token));
return ini_token == GUI::IniToken::Type::Name;
}
void IniSyntaxHighlighter::rehighlight(Gfx::Palette palette)
{
ASSERT(m_editor);
auto text = m_editor->text();
IniLexer lexer(text);
auto tokens = lexer.lex();
Vector<GUI::TextDocumentSpan> spans;
for (auto& token : tokens) {
GUI::TextDocumentSpan span;
span.range.set_start({ token.m_start.line, token.m_start.column });
span.range.set_end({ token.m_end.line, token.m_end.column });
auto style = style_for_token_type(palette, token.m_type);
span.attributes.color = style.color;
span.attributes.bold = style.bold;
span.is_skippable = token.m_type == IniToken::Type::Whitespace;
span.data = reinterpret_cast<void*>(token.m_type);
spans.append(span);
}
m_editor->document().set_spans(spans);
m_has_brace_buddies = false;
highlight_matching_token_pair();
m_editor->update();
}
Vector<IniSyntaxHighlighter::MatchingTokenPair> IniSyntaxHighlighter::matching_token_pairs() const
{
static Vector<SyntaxHighlighter::MatchingTokenPair> pairs;
if (pairs.is_empty()) {
pairs.append({ reinterpret_cast<void*>(IniToken::Type::LeftBracket), reinterpret_cast<void*>(IniToken::Type::RightBracket) });
}
return pairs;
}
bool IniSyntaxHighlighter::token_types_equal(void* token1, void* token2) const
{
return static_cast<GUI::IniToken::Type>(reinterpret_cast<size_t>(token1)) == static_cast<GUI::IniToken::Type>(reinterpret_cast<size_t>(token2));
}
IniSyntaxHighlighter::~IniSyntaxHighlighter()
{
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/SyntaxHighlighter.h>
namespace GUI {
class IniSyntaxHighlighter final : public SyntaxHighlighter {
public:
IniSyntaxHighlighter() { }
virtual ~IniSyntaxHighlighter() override;
virtual bool is_identifier(void*) const override;
virtual SyntaxLanguage language() const override { return SyntaxLanguage::INI; }
virtual void rehighlight(Gfx::Palette) override;
protected:
virtual Vector<MatchingTokenPair> matching_token_pairs() const override;
virtual bool token_types_equal(void*, void*) const override;
};
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/String.h>
#include <LibGUI/Icon.h>
#include <LibGfx/Bitmap.h>
namespace GUI {
Icon::Icon()
: m_impl(IconImpl::create())
{
}
Icon::Icon(const IconImpl& impl)
: m_impl(const_cast<IconImpl&>(impl))
{
}
Icon::Icon(const Icon& other)
: m_impl(other.m_impl)
{
}
Icon::Icon(RefPtr<Gfx::Bitmap>&& bitmap)
: Icon()
{
if (bitmap) {
ASSERT(bitmap->width() == bitmap->height());
int size = bitmap->width();
set_bitmap_for_size(size, move(bitmap));
}
}
Icon::Icon(RefPtr<Gfx::Bitmap>&& bitmap1, RefPtr<Gfx::Bitmap>&& bitmap2)
: Icon(move(bitmap1))
{
if (bitmap2) {
ASSERT(bitmap2->width() == bitmap2->height());
int size = bitmap2->width();
set_bitmap_for_size(size, move(bitmap2));
}
}
const Gfx::Bitmap* IconImpl::bitmap_for_size(int size) const
{
auto it = m_bitmaps.find(size);
if (it != m_bitmaps.end())
return it->value.ptr();
int best_diff_so_far = INT32_MAX;
const Gfx::Bitmap* best_fit = nullptr;
for (auto& it : m_bitmaps) {
int abs_diff = abs(it.key - size);
if (abs_diff < best_diff_so_far) {
best_diff_so_far = abs_diff;
best_fit = it.value.ptr();
}
}
return best_fit;
}
void IconImpl::set_bitmap_for_size(int size, RefPtr<Gfx::Bitmap>&& bitmap)
{
if (!bitmap) {
m_bitmaps.remove(size);
return;
}
m_bitmaps.set(size, move(bitmap));
}
Icon Icon::default_icon(const StringView& name)
{
auto bitmap16 = Gfx::Bitmap::load_from_file(String::formatted("/res/icons/16x16/{}.png", name));
auto bitmap32 = Gfx::Bitmap::load_from_file(String::formatted("/res/icons/32x32/{}.png", name));
return Icon(move(bitmap16), move(bitmap32));
}
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <LibGfx/Bitmap.h>
namespace GUI {
class IconImpl : public RefCounted<IconImpl> {
public:
static NonnullRefPtr<IconImpl> create() { return adopt(*new IconImpl); }
~IconImpl() { }
const Gfx::Bitmap* bitmap_for_size(int) const;
void set_bitmap_for_size(int, RefPtr<Gfx::Bitmap>&&);
Vector<int> sizes() const
{
Vector<int> sizes;
for (auto& it : m_bitmaps)
sizes.append(it.key);
return sizes;
}
private:
IconImpl() { }
HashMap<int, RefPtr<Gfx::Bitmap>> m_bitmaps;
};
class Icon {
public:
Icon();
explicit Icon(RefPtr<Gfx::Bitmap>&&);
explicit Icon(RefPtr<Gfx::Bitmap>&&, RefPtr<Gfx::Bitmap>&&);
explicit Icon(const IconImpl&);
Icon(const Icon&);
~Icon() { }
static Icon default_icon(const StringView&);
Icon& operator=(const Icon& other)
{
if (this != &other)
m_impl = other.m_impl;
return *this;
}
const Gfx::Bitmap* bitmap_for_size(int size) const { return m_impl->bitmap_for_size(size); }
void set_bitmap_for_size(int size, RefPtr<Gfx::Bitmap>&& bitmap) { m_impl->set_bitmap_for_size(size, move(bitmap)); }
const IconImpl& impl() const { return *m_impl; }
Vector<int> sizes() const { return m_impl->sizes(); }
private:
NonnullRefPtr<IconImpl> m_impl;
};
}

View file

@ -0,0 +1,821 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
#include <LibCore/Timer.h>
#include <LibGUI/DragOperation.h>
#include <LibGUI/IconView.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGfx/Palette.h>
//#define DRAGDROP_DEBUG
namespace GUI {
IconView::IconView()
{
set_fill_with_background_color(true);
set_background_role(ColorRole::Base);
set_foreground_role(ColorRole::BaseText);
horizontal_scrollbar().set_visible(false);
}
IconView::~IconView()
{
}
void IconView::select_all()
{
for (int item_index = 0; item_index < item_count(); ++item_index) {
auto& item_data = m_item_data_cache[item_index];
if (!item_data.selected) {
if (item_data.is_valid())
add_selection(item_data);
else
add_selection(model()->index(item_index, model_column()));
}
}
}
void IconView::scroll_into_view(const ModelIndex& index, bool scroll_horizontally, bool scroll_vertically)
{
if (!index.is_valid())
return;
ScrollableWidget::scroll_into_view(item_rect(index.row()), scroll_horizontally, scroll_vertically);
}
void IconView::resize_event(ResizeEvent& event)
{
AbstractView::resize_event(event);
update_content_size();
}
void IconView::reinit_item_cache() const
{
auto prev_item_count = m_item_data_cache.size();
size_t new_item_count = item_count();
auto items_to_invalidate = min(prev_item_count, new_item_count);
// if the new number of items is less, check if any of the
// ones not in the list anymore was selected
for (size_t i = new_item_count; i < m_item_data_cache.size(); i++) {
auto& item_data = m_item_data_cache[i];
if (item_data.selected) {
ASSERT(m_selected_count_cache > 0);
m_selected_count_cache--;
}
}
if ((size_t)m_first_selected_hint >= new_item_count)
m_first_selected_hint = 0;
m_item_data_cache.resize(new_item_count);
for (size_t i = 0; i < items_to_invalidate; i++) {
auto& item_data = m_item_data_cache[i];
// TODO: It's unfortunate that we have no way to know whether any
// data actually changed, so we have to invalidate *everyone*
if (item_data.is_valid() /* && !model()->is_valid(item_data.index)*/)
item_data.invalidate();
if (item_data.selected && i < (size_t)m_first_selected_hint)
m_first_selected_hint = (int)i;
}
m_item_data_cache_valid = true;
}
auto IconView::get_item_data(int item_index) const -> ItemData&
{
if (!m_item_data_cache_valid)
reinit_item_cache();
auto& item_data = m_item_data_cache[item_index];
if (item_data.is_valid())
return item_data;
item_data.index = model()->index(item_index, model_column());
item_data.text = item_data.index.data().to_string();
get_item_rects(item_index, item_data, font_for_index(item_data.index));
item_data.valid = true;
return item_data;
}
auto IconView::item_data_from_content_position(const Gfx::IntPoint& content_position) const -> ItemData*
{
if (!m_visual_row_count || !m_visual_column_count)
return nullptr;
int row, column;
column_row_from_content_position(content_position, row, column);
int item_index = (m_flow_direction == FlowDirection::LeftToRight)
? row * m_visual_column_count + column
: column * m_visual_row_count + row;
if (item_index < 0 || item_index >= item_count())
return nullptr;
return &get_item_data(item_index);
}
void IconView::model_did_update(unsigned flags)
{
AbstractView::model_did_update(flags);
if (!model() || (flags & GUI::Model::InvalidateAllIndexes)) {
m_item_data_cache.clear();
AbstractView::clear_selection();
m_selected_count_cache = 0;
m_first_selected_hint = 0;
}
m_item_data_cache_valid = false;
update_content_size();
update();
}
void IconView::update_content_size()
{
if (!model())
return set_content_size({});
int content_width;
int content_height;
if (m_flow_direction == FlowDirection::LeftToRight) {
m_visual_column_count = max(1, available_size().width() / effective_item_size().width());
if (m_visual_column_count)
m_visual_row_count = ceil_div(model()->row_count(), m_visual_column_count);
else
m_visual_row_count = 0;
content_width = available_size().width();
content_height = m_visual_row_count * effective_item_size().height();
} else {
m_visual_row_count = max(1, available_size().height() / effective_item_size().height());
if (m_visual_row_count)
m_visual_column_count = ceil_div(model()->row_count(), m_visual_row_count);
else
m_visual_column_count = 0;
content_width = m_visual_column_count * effective_item_size().width();
content_height = available_size().height();
}
set_content_size({ content_width, content_height });
if (!m_item_data_cache_valid)
reinit_item_cache();
for (int item_index = 0; item_index < item_count(); item_index++) {
auto& item_data = m_item_data_cache[item_index];
if (item_data.is_valid())
update_item_rects(item_index, item_data);
}
}
Gfx::IntRect IconView::item_rect(int item_index) const
{
if (!m_visual_row_count || !m_visual_column_count)
return {};
int visual_row_index;
int visual_column_index;
if (m_flow_direction == FlowDirection::LeftToRight) {
visual_row_index = item_index / m_visual_column_count;
visual_column_index = item_index % m_visual_column_count;
} else {
visual_row_index = item_index % m_visual_row_count;
visual_column_index = item_index / m_visual_row_count;
}
return {
visual_column_index * effective_item_size().width(),
visual_row_index * effective_item_size().height(),
effective_item_size().width(),
effective_item_size().height()
};
}
ModelIndex IconView::index_at_event_position(const Gfx::IntPoint& position) const
{
ASSERT(model());
auto adjusted_position = to_content_position(position);
if (auto item_data = item_data_from_content_position(adjusted_position)) {
if (item_data->is_containing(adjusted_position))
return item_data->index;
}
return {};
}
void IconView::mousedown_event(MouseEvent& event)
{
if (!model())
return AbstractView::mousedown_event(event);
if (event.button() != MouseButton::Left)
return AbstractView::mousedown_event(event);
auto index = index_at_event_position(event.position());
if (index.is_valid()) {
// We might start dragging this item, but not rubber-banding.
return AbstractView::mousedown_event(event);
}
if (event.modifiers() & Mod_Ctrl) {
m_rubber_banding_store_selection = true;
} else {
clear_selection();
m_rubber_banding_store_selection = false;
}
auto adjusted_position = to_content_position(event.position());
m_might_drag = false;
if (selection_mode() == SelectionMode::MultiSelection) {
m_rubber_banding = true;
m_rubber_band_origin = adjusted_position;
m_rubber_band_current = adjusted_position;
}
}
void IconView::mouseup_event(MouseEvent& event)
{
if (m_rubber_banding && event.button() == MouseButton::Left) {
m_rubber_banding = false;
if (m_out_of_view_timer)
m_out_of_view_timer->stop();
update();
}
AbstractView::mouseup_event(event);
}
bool IconView::update_rubber_banding(const Gfx::IntPoint& position)
{
auto adjusted_position = to_content_position(position);
if (m_rubber_band_current != adjusted_position) {
auto prev_rect = Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_current);
m_rubber_band_current = adjusted_position;
auto rubber_band_rect = Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_current);
// If the rectangle width or height is 0, we still want to be able
// to match the items in the path. An easy work-around for this
// is to simply set the width or height to 1
auto ensure_rect = [](Gfx::IntRect& rect) {
if (rect.width() <= 0)
rect.set_width(1);
if (rect.height() <= 0)
rect.set_height(1);
};
ensure_rect(prev_rect);
ensure_rect(rubber_band_rect);
// Clearing the entire selection every time is very expensive,
// determine what items may need to be deselected and what new
// items may need to be selected. Avoid a ton of allocations.
auto deselect_area = prev_rect.shatter(rubber_band_rect);
auto select_area = rubber_band_rect.shatter(prev_rect);
// Initialize all candidate's toggle flag. We need to know which
// items we touched because the various rectangles likely will
// contain the same item more than once
for_each_item_intersecting_rects(deselect_area, [](ItemData& item_data) -> IterationDecision {
item_data.selection_toggled = false;
return IterationDecision::Continue;
});
for_each_item_intersecting_rects(select_area, [](ItemData& item_data) -> IterationDecision {
item_data.selection_toggled = false;
return IterationDecision::Continue;
});
// Now toggle all items that are no longer in the selected area, once only
for_each_item_intersecting_rects(deselect_area, [&](ItemData& item_data) -> IterationDecision {
if (!item_data.selection_toggled && item_data.is_intersecting(prev_rect) && !item_data.is_intersecting(rubber_band_rect)) {
item_data.selection_toggled = true;
toggle_selection(item_data);
}
return IterationDecision::Continue;
});
// Now toggle all items that are in the new selected area, once only
for_each_item_intersecting_rects(select_area, [&](ItemData& item_data) -> IterationDecision {
if (!item_data.selection_toggled && !item_data.is_intersecting(prev_rect) && item_data.is_intersecting(rubber_band_rect)) {
item_data.selection_toggled = true;
toggle_selection(item_data);
}
return IterationDecision::Continue;
});
update();
return true;
}
return false;
}
#define SCROLL_OUT_OF_VIEW_HOT_MARGIN 20
void IconView::mousemove_event(MouseEvent& event)
{
if (!model())
return AbstractView::mousemove_event(event);
if (m_rubber_banding) {
auto in_view_rect = widget_inner_rect();
in_view_rect.shrink(SCROLL_OUT_OF_VIEW_HOT_MARGIN, SCROLL_OUT_OF_VIEW_HOT_MARGIN);
if (!in_view_rect.contains(event.position())) {
if (!m_out_of_view_timer) {
m_out_of_view_timer = add<Core::Timer>();
m_out_of_view_timer->set_interval(100);
m_out_of_view_timer->on_timeout = [this] {
scroll_out_of_view_timer_fired();
};
}
m_out_of_view_position = event.position();
if (!m_out_of_view_timer->is_active())
m_out_of_view_timer->start();
} else {
if (m_out_of_view_timer)
m_out_of_view_timer->stop();
}
if (update_rubber_banding(event.position()))
return;
}
AbstractView::mousemove_event(event);
}
void IconView::scroll_out_of_view_timer_fired()
{
auto scroll_to = to_content_position(m_out_of_view_position);
// Adjust the scroll-to position by SCROLL_OUT_OF_VIEW_HOT_MARGIN / 2
// depending on which direction we're scrolling. This allows us to
// start scrolling before we actually leave the visible area, which
// is important when there is no space to further move the mouse. The
// speed of scrolling is determined by the distance between the mouse
// pointer and the widget's inner rect shrunken by the hot margin
auto in_view_rect = widget_inner_rect().shrunken(SCROLL_OUT_OF_VIEW_HOT_MARGIN, SCROLL_OUT_OF_VIEW_HOT_MARGIN);
int adjust_x = 0, adjust_y = 0;
if (m_out_of_view_position.y() > in_view_rect.bottom())
adjust_y = (SCROLL_OUT_OF_VIEW_HOT_MARGIN / 2) + min(SCROLL_OUT_OF_VIEW_HOT_MARGIN, m_out_of_view_position.y() - in_view_rect.bottom());
else if (m_out_of_view_position.y() < in_view_rect.top())
adjust_y = -(SCROLL_OUT_OF_VIEW_HOT_MARGIN / 2) + max(-SCROLL_OUT_OF_VIEW_HOT_MARGIN, m_out_of_view_position.y() - in_view_rect.top());
if (m_out_of_view_position.x() > in_view_rect.right())
adjust_x = (SCROLL_OUT_OF_VIEW_HOT_MARGIN / 2) + min(SCROLL_OUT_OF_VIEW_HOT_MARGIN, m_out_of_view_position.x() - in_view_rect.right());
else if (m_out_of_view_position.x() < in_view_rect.left())
adjust_x = -(SCROLL_OUT_OF_VIEW_HOT_MARGIN / 2) + max(-SCROLL_OUT_OF_VIEW_HOT_MARGIN, m_out_of_view_position.x() - in_view_rect.left());
ScrollableWidget::scroll_into_view({ scroll_to.translated(adjust_x, adjust_y), { 1, 1 } }, true, true);
update_rubber_banding(m_out_of_view_position);
}
void IconView::update_item_rects(int item_index, ItemData& item_data) const
{
auto item_rect = this->item_rect(item_index);
item_data.icon_rect.center_within(item_rect);
item_data.icon_rect.move_by(0, item_data.icon_offset_y);
item_data.text_rect.center_horizontally_within(item_rect);
item_data.text_rect.set_top(item_rect.y() + item_data.text_offset_y);
}
Gfx::IntRect IconView::content_rect(const ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto& item_data = get_item_data(index.row());
return item_data.text_rect.inflated(4, 4);
}
void IconView::did_change_hovered_index(const ModelIndex& old_index, const ModelIndex& new_index)
{
AbstractView::did_change_hovered_index(old_index, new_index);
if (old_index.is_valid())
get_item_rects(old_index.row(), get_item_data(old_index.row()), font_for_index(old_index));
if (new_index.is_valid())
get_item_rects(new_index.row(), get_item_data(new_index.row()), font_for_index(new_index));
}
void IconView::did_change_cursor_index(const ModelIndex& old_index, const ModelIndex& new_index)
{
AbstractView::did_change_cursor_index(old_index, new_index);
if (old_index.is_valid())
get_item_rects(old_index.row(), get_item_data(old_index.row()), font_for_index(old_index));
if (new_index.is_valid())
get_item_rects(new_index.row(), get_item_data(new_index.row()), font_for_index(new_index));
}
void IconView::get_item_rects(int item_index, ItemData& item_data, const Gfx::Font& font) const
{
auto item_rect = this->item_rect(item_index);
item_data.icon_rect = { 0, 0, 32, 32 };
item_data.icon_rect.center_within(item_rect);
item_data.icon_offset_y = -font.glyph_height() - 6;
item_data.icon_rect.move_by(0, item_data.icon_offset_y);
int unwrapped_text_width = font.width(item_data.text);
int available_width = item_rect.width() - 6;
item_data.text_rect = { 0, item_data.icon_rect.bottom() + 6 + 1, 0, font.glyph_height() };
item_data.wrapped_text_lines.clear();
if ((unwrapped_text_width > available_width) && (item_data.selected || m_hovered_index == item_data.index || cursor_index() == item_data.index)) {
int current_line_width = 0;
int current_line_start = 0;
int widest_line_width = 0;
Utf8View utf8_view(item_data.text);
auto it = utf8_view.begin();
for (; it != utf8_view.end(); ++it) {
auto codepoint = *it;
auto glyph_width = font.glyph_width(codepoint);
if ((current_line_width + glyph_width + font.glyph_spacing()) > available_width) {
item_data.wrapped_text_lines.append(item_data.text.substring_view(current_line_start, utf8_view.byte_offset_of(it) - current_line_start));
current_line_start = utf8_view.byte_offset_of(it);
current_line_width = glyph_width;
} else {
current_line_width += glyph_width + font.glyph_spacing();
}
widest_line_width = max(widest_line_width, current_line_width);
}
if (current_line_width > 0) {
item_data.wrapped_text_lines.append(item_data.text.substring_view(current_line_start, utf8_view.byte_offset_of(it) - current_line_start));
}
item_data.text_rect.set_width(widest_line_width);
item_data.text_rect.center_horizontally_within(item_rect);
item_data.text_rect.intersect(item_rect);
item_data.text_rect.set_height(font.glyph_height() * item_data.wrapped_text_lines.size());
item_data.text_rect.inflate(6, 4);
} else {
item_data.text_rect.set_width(unwrapped_text_width);
item_data.text_rect.inflate(6, 4);
item_data.text_rect.center_horizontally_within(item_rect);
}
item_data.text_rect.intersect(item_rect);
item_data.text_offset_y = item_data.text_rect.y() - item_rect.y();
}
void IconView::second_paint_event(PaintEvent& event)
{
if (!m_rubber_banding)
return;
Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.add_clip_rect(widget_inner_rect());
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
auto rubber_band_rect = Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_current);
painter.fill_rect(rubber_band_rect, palette().rubber_band_fill());
painter.draw_rect(rubber_band_rect, palette().rubber_band_border());
}
void IconView::paint_event(PaintEvent& event)
{
Color widget_background_color = palette().color(background_role());
Frame::paint_event(event);
Painter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
if (fill_with_background_color())
painter.fill_rect(event.rect(), widget_background_color);
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
auto selection_color = is_focused() ? palette().selection() : palette().inactive_selection();
for_each_item_intersecting_rect(to_content_rect(event.rect()), [&](auto& item_data) -> IterationDecision {
Color background_color;
if (item_data.selected) {
background_color = selection_color;
} else {
if (fill_with_background_color())
background_color = widget_background_color;
}
auto icon = item_data.index.data(ModelRole::Icon);
if (icon.is_icon()) {
if (auto bitmap = icon.as_icon().bitmap_for_size(item_data.icon_rect.width())) {
Gfx::IntRect destination = bitmap->rect();
destination.center_within(item_data.icon_rect);
if (item_data.selected) {
auto tint = selection_color.with_alpha(100);
painter.blit_filtered(destination.location(), *bitmap, bitmap->rect(), [&](auto src) { return src.blend(tint); });
} else if (m_hovered_index.is_valid() && m_hovered_index == item_data.index) {
painter.blit_brightened(destination.location(), *bitmap, bitmap->rect());
} else {
painter.blit(destination.location(), *bitmap, bitmap->rect());
}
}
}
auto font = font_for_index(item_data.index);
const auto& text_rect = item_data.text_rect;
painter.fill_rect(text_rect, background_color);
if (is_focused() && item_data.index == cursor_index()) {
painter.draw_rect(text_rect, widget_background_color);
painter.draw_focus_rect(text_rect, palette().focus_outline());
}
if (!item_data.wrapped_text_lines.is_empty()) {
// Item text would not fit in the item text rect, let's break it up into lines..
const auto& lines = item_data.wrapped_text_lines;
size_t number_of_text_lines = min((size_t)text_rect.height() / font->glyph_height(), lines.size());
for (size_t line_index = 0; line_index < number_of_text_lines; ++line_index) {
Gfx::IntRect line_rect;
line_rect.set_width(text_rect.width());
line_rect.set_height(font->glyph_height());
line_rect.center_horizontally_within(item_data.text_rect);
line_rect.set_y(2 + item_data.text_rect.y() + line_index * font->glyph_height());
line_rect.inflate(6, 0);
// Shrink the line_rect on the last line to apply elision if there are more lines.
if (number_of_text_lines - 1 == line_index && lines.size() > number_of_text_lines)
line_rect.inflate(-(6 + 2 * font->max_glyph_width()), 0);
draw_item_text(painter, item_data.index, item_data.selected, line_rect, lines[line_index], font, Gfx::TextAlignment::Center, Gfx::TextElision::Right);
}
} else {
draw_item_text(painter, item_data.index, item_data.selected, item_data.text_rect, item_data.text, font, Gfx::TextAlignment::Center, Gfx::TextElision::Right);
}
if (has_pending_drop() && item_data.index == drop_candidate_index()) {
// FIXME: This visualization is not great, as it's also possible to drop things on the text label..
painter.draw_rect(item_data.icon_rect.inflated(8, 8), palette().selection(), true);
}
return IterationDecision::Continue;
});
}
int IconView::item_count() const
{
if (!model())
return 0;
return model()->row_count();
}
void IconView::did_update_selection()
{
AbstractView::did_update_selection();
if (m_changing_selection)
return;
// Selection was modified externally, we need to synchronize our cache
do_clear_selection();
selection().for_each_index([&](const ModelIndex& index) {
if (index.is_valid()) {
auto item_index = model_index_to_item_index(index);
if ((size_t)item_index < m_item_data_cache.size())
do_add_selection(get_item_data(item_index));
}
});
}
void IconView::do_clear_selection()
{
for (size_t item_index = m_first_selected_hint; item_index < m_item_data_cache.size(); item_index++) {
if (m_selected_count_cache == 0)
break;
auto& item_data = m_item_data_cache[item_index];
if (!item_data.selected)
continue;
item_data.selected = false;
m_selected_count_cache--;
}
m_first_selected_hint = 0;
ASSERT(m_selected_count_cache == 0);
}
void IconView::clear_selection()
{
TemporaryChange change(m_changing_selection, true);
AbstractView::clear_selection();
do_clear_selection();
}
bool IconView::do_add_selection(ItemData& item_data)
{
if (!item_data.selected) {
item_data.selected = true;
m_selected_count_cache++;
int item_index = &item_data - &m_item_data_cache[0];
if (m_first_selected_hint > item_index)
m_first_selected_hint = item_index;
return true;
}
return false;
}
void IconView::add_selection(ItemData& item_data)
{
if (do_add_selection(item_data))
AbstractView::add_selection(item_data.index);
}
void IconView::add_selection(const ModelIndex& new_index)
{
TemporaryChange change(m_changing_selection, true);
auto item_index = model_index_to_item_index(new_index);
add_selection(get_item_data(item_index));
}
void IconView::toggle_selection(ItemData& item_data)
{
if (!item_data.selected)
add_selection(item_data);
else
remove_selection(item_data);
}
void IconView::toggle_selection(const ModelIndex& new_index)
{
TemporaryChange change(m_changing_selection, true);
auto item_index = model_index_to_item_index(new_index);
toggle_selection(get_item_data(item_index));
}
void IconView::remove_selection(ItemData& item_data)
{
if (!item_data.selected)
return;
TemporaryChange change(m_changing_selection, true);
item_data.selected = false;
ASSERT(m_selected_count_cache > 0);
m_selected_count_cache--;
int item_index = &item_data - &m_item_data_cache[0];
if (m_first_selected_hint == item_index) {
m_first_selected_hint = 0;
while ((size_t)item_index < m_item_data_cache.size()) {
if (m_item_data_cache[item_index].selected) {
m_first_selected_hint = item_index;
break;
}
item_index++;
}
}
AbstractView::remove_selection(item_data.index);
}
void IconView::set_selection(const ModelIndex& new_index)
{
TemporaryChange change(m_changing_selection, true);
do_clear_selection();
auto item_index = model_index_to_item_index(new_index);
auto& item_data = get_item_data(item_index);
item_data.selected = true;
m_selected_count_cache = 1;
if (item_index < m_first_selected_hint)
m_first_selected_hint = item_index;
AbstractView::set_selection(new_index);
}
int IconView::items_per_page() const
{
if (m_flow_direction == FlowDirection::LeftToRight)
return (visible_content_rect().height() / effective_item_size().height()) * m_visual_column_count;
return (visible_content_rect().width() / effective_item_size().width()) * m_visual_row_count;
}
void IconView::move_cursor(CursorMovement movement, SelectionUpdate selection_update)
{
if (!model())
return;
auto& model = *this->model();
if (!cursor_index().is_valid()) {
set_cursor(model.index(0, model_column()), SelectionUpdate::Set);
return;
}
auto new_row = cursor_index().row();
switch (movement) {
case CursorMovement::Right:
if (m_flow_direction == FlowDirection::LeftToRight)
new_row += 1;
else
new_row += m_visual_row_count;
break;
case CursorMovement::Left:
if (m_flow_direction == FlowDirection::LeftToRight)
new_row -= 1;
else
new_row -= m_visual_row_count;
break;
case CursorMovement::Up:
if (m_flow_direction == FlowDirection::LeftToRight)
new_row -= m_visual_column_count;
else
new_row -= 1;
break;
case CursorMovement::Down:
if (m_flow_direction == FlowDirection::LeftToRight)
new_row += m_visual_column_count;
else
new_row += 1;
break;
case CursorMovement::PageUp:
new_row = max(0, cursor_index().row() - items_per_page());
break;
case CursorMovement::PageDown:
new_row = min(model.row_count() - 1, cursor_index().row() + items_per_page());
break;
case CursorMovement::Home:
new_row = 0;
break;
case CursorMovement::End:
new_row = model.row_count() - 1;
break;
default:
return;
}
auto new_index = model.index(new_row, cursor_index().column());
if (new_index.is_valid())
set_cursor(new_index, selection_update);
}
void IconView::set_flow_direction(FlowDirection flow_direction)
{
if (m_flow_direction == flow_direction)
return;
m_flow_direction = flow_direction;
m_item_data_cache.clear();
m_item_data_cache_valid = false;
update();
}
template<typename Function>
inline IterationDecision IconView::for_each_item_intersecting_rect(const Gfx::IntRect& rect, Function f) const
{
ASSERT(model());
if (rect.is_empty())
return IterationDecision::Continue;
int begin_row, begin_column;
column_row_from_content_position(rect.top_left(), begin_row, begin_column);
int end_row, end_column;
column_row_from_content_position(rect.bottom_right(), end_row, end_column);
int items_per_flow_axis_step;
int item_index;
int last_index;
if (m_flow_direction == FlowDirection::LeftToRight) {
items_per_flow_axis_step = end_column - begin_column + 1;
item_index = max(0, begin_row * m_visual_column_count + begin_column);
last_index = min(item_count(), end_row * m_visual_column_count + end_column + 1);
} else {
items_per_flow_axis_step = end_row - begin_row + 1;
item_index = max(0, begin_column * m_visual_row_count + begin_row);
last_index = min(item_count(), end_column * m_visual_row_count + end_row + 1);
}
while (item_index < last_index) {
for (int i = item_index; i < min(item_index + items_per_flow_axis_step, last_index); i++) {
auto& item_data = get_item_data(i);
if (item_data.is_intersecting(rect)) {
auto decision = f(item_data);
if (decision != IterationDecision::Continue)
return decision;
}
}
item_index += m_visual_column_count;
};
return IterationDecision::Continue;
}
template<typename Function>
inline IterationDecision IconView::for_each_item_intersecting_rects(const Vector<Gfx::IntRect>& rects, Function f) const
{
for (auto& rect : rects) {
auto decision = for_each_item_intersecting_rect(rect, f);
if (decision != IterationDecision::Continue)
return decision;
}
return IterationDecision::Continue;
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/IterationDecision.h>
#include <LibGUI/AbstractView.h>
#include <LibGUI/Forward.h>
#include <LibGUI/Variant.h>
namespace GUI {
class IconView : public AbstractView {
C_OBJECT(IconView)
public:
virtual ~IconView() override;
enum class FlowDirection {
LeftToRight,
TopToBottom,
};
FlowDirection flow_direction() const { return m_flow_direction; }
void set_flow_direction(FlowDirection);
int content_width() const;
int horizontal_padding() const { return m_horizontal_padding; }
virtual void scroll_into_view(const ModelIndex&, bool scroll_horizontally = true, bool scroll_vertically = true) override;
Gfx::IntSize effective_item_size() const { return m_effective_item_size; }
int model_column() const { return m_model_column; }
void set_model_column(int column) { m_model_column = column; }
virtual ModelIndex index_at_event_position(const Gfx::IntPoint&) const override;
virtual Gfx::IntRect content_rect(const ModelIndex&) const override;
virtual void select_all() override;
private:
IconView();
virtual void model_did_update(unsigned flags) override;
virtual void paint_event(PaintEvent&) override;
virtual void second_paint_event(PaintEvent&) override;
virtual void resize_event(ResizeEvent&) override;
virtual void mousedown_event(MouseEvent&) override;
virtual void mousemove_event(MouseEvent&) override;
virtual void mouseup_event(MouseEvent&) override;
virtual void did_change_hovered_index(const ModelIndex& old_index, const ModelIndex& new_index) override;
virtual void did_change_cursor_index(const ModelIndex& old_index, const ModelIndex& new_index) override;
virtual void move_cursor(CursorMovement, SelectionUpdate) override;
struct ItemData {
Gfx::IntRect text_rect;
Gfx::IntRect icon_rect;
int icon_offset_y;
int text_offset_y;
String text;
Vector<StringView> wrapped_text_lines;
ModelIndex index;
bool valid { false };
bool selected { false }; // always valid
bool selection_toggled; // only used as a temporary marker
bool is_valid() const { return valid; }
void invalidate()
{
valid = false;
text = {};
}
bool is_intersecting(const Gfx::IntRect& rect) const
{
ASSERT(valid);
return icon_rect.intersects(rect) || text_rect.intersects(rect);
}
bool is_containing(const Gfx::IntPoint& point) const
{
ASSERT(valid);
return icon_rect.contains(point) || text_rect.contains(point);
}
};
template<typename Function>
IterationDecision for_each_item_intersecting_rect(const Gfx::IntRect&, Function) const;
template<typename Function>
IterationDecision for_each_item_intersecting_rects(const Vector<Gfx::IntRect>&, Function) const;
void column_row_from_content_position(const Gfx::IntPoint& content_position, int& row, int& column) const
{
row = max(0, min(m_visual_row_count - 1, content_position.y() / effective_item_size().height()));
column = max(0, min(m_visual_column_count - 1, content_position.x() / effective_item_size().width()));
}
int item_count() const;
Gfx::IntRect item_rect(int item_index) const;
void update_content_size();
void update_item_rects(int item_index, ItemData& item_data) const;
void get_item_rects(int item_index, ItemData& item_data, const Gfx::Font&) const;
bool update_rubber_banding(const Gfx::IntPoint&);
void scroll_out_of_view_timer_fired();
int items_per_page() const;
void reinit_item_cache() const;
int model_index_to_item_index(const ModelIndex& model_index) const
{
ASSERT(model_index.row() < item_count());
return model_index.row();
}
virtual void did_update_selection() override;
virtual void clear_selection() override;
virtual void add_selection(const ModelIndex& new_index) override;
virtual void set_selection(const ModelIndex& new_index) override;
virtual void toggle_selection(const ModelIndex& new_index) override;
ItemData& get_item_data(int) const;
ItemData* item_data_from_content_position(const Gfx::IntPoint&) const;
void do_clear_selection();
bool do_add_selection(ItemData&);
void add_selection(ItemData&);
void remove_selection(ItemData&);
void toggle_selection(ItemData&);
int m_horizontal_padding { 5 };
int m_model_column { 0 };
int m_visual_column_count { 0 };
int m_visual_row_count { 0 };
Gfx::IntSize m_effective_item_size { 80, 80 };
bool m_rubber_banding { false };
bool m_rubber_banding_store_selection { false };
RefPtr<Core::Timer> m_out_of_view_timer;
Gfx::IntPoint m_out_of_view_position;
Gfx::IntPoint m_rubber_band_origin;
Gfx::IntPoint m_rubber_band_current;
FlowDirection m_flow_direction { FlowDirection::LeftToRight };
mutable Vector<ItemData> m_item_data_cache;
mutable int m_selected_count_cache { 0 };
mutable int m_first_selected_hint { 0 };
mutable bool m_item_data_cache_valid { false };
bool m_changing_selection { false };
};
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <AK/MappedFile.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImageDecoder.h>
REGISTER_WIDGET(GUI, ImageWidget)
namespace GUI {
ImageWidget::ImageWidget(const StringView&)
: m_timer(Core::Timer::construct())
{
set_frame_thickness(0);
set_frame_shadow(Gfx::FrameShadow::Plain);
set_frame_shape(Gfx::FrameShape::NoFrame);
set_auto_resize(true);
REGISTER_BOOL_PROPERTY("auto_resize", auto_resize, set_auto_resize);
REGISTER_BOOL_PROPERTY("should_stretch", should_stretch, set_should_stretch);
}
ImageWidget::~ImageWidget()
{
}
void ImageWidget::set_bitmap(const Gfx::Bitmap* bitmap)
{
if (m_bitmap == bitmap)
return;
m_bitmap = bitmap;
if (m_bitmap && m_auto_resize)
set_fixed_size(m_bitmap->size());
update();
}
void ImageWidget::set_auto_resize(bool value)
{
m_auto_resize = value;
if (m_bitmap)
set_fixed_size(m_bitmap->size());
}
void ImageWidget::animate()
{
m_current_frame_index = (m_current_frame_index + 1) % m_image_decoder->frame_count();
const auto& current_frame = m_image_decoder->frame(m_current_frame_index);
set_bitmap(current_frame.image);
if (current_frame.duration != m_timer->interval()) {
m_timer->restart(current_frame.duration);
}
if (m_current_frame_index == m_image_decoder->frame_count() - 1) {
++m_loops_completed;
if (m_loops_completed > 0 && m_loops_completed == m_image_decoder->loop_count()) {
m_timer->stop();
}
}
}
void ImageWidget::load_from_file(const StringView& path)
{
auto file_or_error = MappedFile::map(path);
if (file_or_error.is_error())
return;
auto& mapped_file = *file_or_error.value();
m_image_decoder = Gfx::ImageDecoder::create((const u8*)mapped_file.data(), mapped_file.size());
auto bitmap = m_image_decoder->bitmap();
ASSERT(bitmap);
set_bitmap(bitmap);
if (path.ends_with(".gif")) {
if (m_image_decoder->is_animated() && m_image_decoder->frame_count() > 1) {
const auto& first_frame = m_image_decoder->frame(0);
m_timer->set_interval(first_frame.duration);
m_timer->on_timeout = [this] { animate(); };
m_timer->start();
}
}
}
void ImageWidget::mousedown_event(GUI::MouseEvent&)
{
if (on_click)
on_click();
}
void ImageWidget::paint_event(PaintEvent& event)
{
Frame::paint_event(event);
if (!m_bitmap) {
return;
}
Painter painter(*this);
if (m_should_stretch) {
painter.draw_scaled_bitmap(frame_inner_rect(), *m_bitmap, m_bitmap->rect());
} else {
auto location = frame_inner_rect().center().translated(-(m_bitmap->width() / 2), -(m_bitmap->height() / 2));
painter.blit(location, *m_bitmap, m_bitmap->rect());
}
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibCore/Timer.h>
#include <LibGUI/Frame.h>
namespace GUI {
class ImageWidget : public Frame {
C_OBJECT(ImageWidget)
public:
virtual ~ImageWidget() override;
void set_bitmap(const Gfx::Bitmap*);
Gfx::Bitmap* bitmap() { return m_bitmap.ptr(); }
void set_should_stretch(bool value) { m_should_stretch = value; }
bool should_stretch() const { return m_should_stretch; }
void set_auto_resize(bool value);
bool auto_resize() const { return m_auto_resize; }
void animate();
void load_from_file(const StringView&);
Function<void()> on_click;
protected:
explicit ImageWidget(const StringView& text = {});
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void paint_event(PaintEvent&) override;
private:
RefPtr<Gfx::Bitmap> m_bitmap;
bool m_should_stretch { false };
bool m_auto_resize { false };
RefPtr<Gfx::ImageDecoder> m_image_decoder;
size_t m_current_frame_index { 0 };
size_t m_loops_completed { 0 };
NonnullRefPtr<Core::Timer> m_timer;
};
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/Font.h>
#include <stdio.h>
namespace GUI {
InputBox::InputBox(Window* parent_window, const StringView& prompt, const StringView& title)
: Dialog(parent_window)
, m_prompt(prompt)
{
set_title(title);
build();
}
InputBox::~InputBox()
{
}
int InputBox::show(String& text_value, Window* parent_window, const StringView& prompt, const StringView& title)
{
auto box = InputBox::construct(parent_window, prompt, title);
box->set_resizable(false);
if (parent_window)
box->set_icon(parent_window->icon());
auto result = box->exec();
text_value = box->text_value();
return result;
}
void InputBox::build()
{
auto& widget = set_main_widget<Widget>();
int text_width = widget.font().width(m_prompt);
int title_width = widget.font().width(title()) + 24 /* icon, plus a little padding -- not perfect */;
int max_width = AK::max(text_width, title_width);
set_rect(x(), y(), max_width + 140, 62);
widget.set_layout<VerticalBoxLayout>();
widget.set_fill_with_background_color(true);
widget.layout()->set_margins({ 6, 6, 6, 6 });
widget.layout()->set_spacing(6);
auto& label_editor_container = widget.add<Widget>();
label_editor_container.set_layout<HorizontalBoxLayout>();
auto& label = label_editor_container.add<Label>(m_prompt);
label.set_fixed_size(text_width, 16);
m_text_editor = label_editor_container.add<TextBox>();
m_text_editor->set_fixed_height(19);
auto& button_container_outer = widget.add<Widget>();
button_container_outer.set_fixed_height(20);
button_container_outer.set_layout<VerticalBoxLayout>();
auto& button_container_inner = button_container_outer.add<Widget>();
button_container_inner.set_layout<HorizontalBoxLayout>();
button_container_inner.layout()->set_spacing(6);
button_container_inner.layout()->set_margins({ 4, 4, 0, 4 });
button_container_inner.layout()->add_spacer();
m_ok_button = button_container_inner.add<Button>();
m_ok_button->set_fixed_height(20);
m_ok_button->set_text("OK");
m_ok_button->on_click = [this](auto) {
dbgln("GUI::InputBox: OK button clicked");
m_text_value = m_text_editor->text();
done(ExecOK);
};
m_cancel_button = button_container_inner.add<Button>();
m_cancel_button->set_fixed_height(20);
m_cancel_button->set_text("Cancel");
m_cancel_button->on_click = [this](auto) {
dbgln("GUI::InputBox: Cancel button clicked");
done(ExecCancel);
};
m_text_editor->on_return_pressed = [this] {
m_ok_button->click();
};
m_text_editor->on_escape_pressed = [this] {
m_cancel_button->click();
};
m_text_editor->set_focus(true);
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <LibGUI/Dialog.h>
namespace GUI {
class InputBox : public Dialog {
C_OBJECT(InputBox)
public:
virtual ~InputBox() override;
static int show(String& text_value, Window* parent_window, const StringView& prompt, const StringView& title);
private:
explicit InputBox(Window* parent_window, const StringView& prompt, const StringView& title);
String text_value() const { return m_text_value; }
void build();
String m_prompt;
String m_text_value;
RefPtr<Button> m_ok_button;
RefPtr<Button> m_cancel_button;
RefPtr<TextEditor> m_text_editor;
};
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2019-2020, Jesse Buhgaiar <jooster669@gmail.com>
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/Vector.h>
#include <LibGUI/Model.h>
namespace GUI {
template<typename T>
class ItemListModel : public Model {
public:
static NonnullRefPtr<ItemListModel> create(const Vector<T>& data) { return adopt(*new ItemListModel<T>(data)); }
virtual ~ItemListModel() override { }
virtual int row_count(const ModelIndex&) const override
{
return m_data.size();
}
virtual int column_count(const ModelIndex&) const override
{
return 1;
}
virtual String column_name(int) const override
{
return "Data";
}
virtual Variant data(const ModelIndex& index, ModelRole role) const override
{
if (role == ModelRole::TextAlignment)
return Gfx::TextAlignment::CenterLeft;
if (role == ModelRole::Display)
return m_data.at(index.row());
return {};
}
virtual void update() override
{
did_update();
}
protected:
explicit ItemListModel(const Vector<T>& data)
: m_data(data)
{
}
const Vector<T>& m_data;
};
}

View file

@ -0,0 +1,154 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER 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.
*/
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/TextEditor.h>
#include <LibGfx/Font.h>
#include <LibGfx/Palette.h>
#include <LibJS/Lexer.h>
#include <LibJS/Token.h>
namespace GUI {
static TextStyle style_for_token_type(Gfx::Palette palette, JS::TokenType type)
{
switch (JS::Token::category(type)) {
case JS::TokenCategory::Invalid:
return { palette.syntax_comment() };
case JS::TokenCategory::Number:
return { palette.syntax_number() };
case JS::TokenCategory::String:
return { palette.syntax_string() };
case JS::TokenCategory::Punctuation:
return { palette.syntax_punctuation() };
case JS::TokenCategory::Operator:
return { palette.syntax_operator() };
case JS::TokenCategory::Keyword:
return { palette.syntax_keyword(), true };
case JS::TokenCategory::ControlKeyword:
return { palette.syntax_control_keyword(), true };
case JS::TokenCategory::Identifier:
return { palette.syntax_identifier() };
default:
return { palette.base_text() };
}
}
bool JSSyntaxHighlighter::is_identifier(void* token) const
{
auto js_token = static_cast<JS::TokenType>(reinterpret_cast<size_t>(token));
return js_token == JS::TokenType::Identifier;
}
bool JSSyntaxHighlighter::is_navigatable([[maybe_unused]] void* token) const
{
return false;
}
void JSSyntaxHighlighter::rehighlight(Gfx::Palette palette)
{
ASSERT(m_editor);
auto text = m_editor->text();
JS::Lexer lexer(text);
Vector<GUI::TextDocumentSpan> spans;
GUI::TextPosition position { 0, 0 };
GUI::TextPosition start { 0, 0 };
auto advance_position = [&position](char ch) {
if (ch == '\n') {
position.set_line(position.line() + 1);
position.set_column(0);
} else
position.set_column(position.column() + 1);
};
auto append_token = [&](StringView str, const JS::Token& token, bool is_trivia) {
if (str.is_empty())
return;
start = position;
for (size_t i = 0; i < str.length() - 1; ++i)
advance_position(str[i]);
GUI::TextDocumentSpan span;
span.range.set_start(start);
span.range.set_end({ position.line(), position.column() });
auto type = is_trivia ? JS::TokenType::Invalid : token.type();
auto style = style_for_token_type(palette, type);
span.attributes.color = style.color;
span.attributes.bold = style.bold;
span.is_skippable = is_trivia;
span.data = reinterpret_cast<void*>(static_cast<size_t>(type));
spans.append(span);
advance_position(str[str.length() - 1]);
#ifdef DEBUG_SYNTAX_HIGHLIGHTING
dbg() << token.name() << (is_trivia ? " (trivia) @ \"" : " @ \"") << token.value() << "\" "
<< span.range.start().line() << ":" << span.range.start().column() << " - "
<< span.range.end().line() << ":" << span.range.end().column();
#endif
};
bool was_eof = false;
for (auto token = lexer.next(); !was_eof; token = lexer.next()) {
append_token(token.trivia(), token, true);
append_token(token.value(), token, false);
if (token.type() == JS::TokenType::Eof)
was_eof = true;
}
m_editor->document().set_spans(spans);
m_has_brace_buddies = false;
highlight_matching_token_pair();
m_editor->update();
}
Vector<SyntaxHighlighter::MatchingTokenPair> JSSyntaxHighlighter::matching_token_pairs() const
{
static Vector<SyntaxHighlighter::MatchingTokenPair> pairs;
if (pairs.is_empty()) {
pairs.append({ reinterpret_cast<void*>(JS::TokenType::CurlyOpen), reinterpret_cast<void*>(JS::TokenType::CurlyClose) });
pairs.append({ reinterpret_cast<void*>(JS::TokenType::ParenOpen), reinterpret_cast<void*>(JS::TokenType::ParenClose) });
pairs.append({ reinterpret_cast<void*>(JS::TokenType::BracketOpen), reinterpret_cast<void*>(JS::TokenType::BracketClose) });
}
return pairs;
}
bool JSSyntaxHighlighter::token_types_equal(void* token1, void* token2) const
{
return static_cast<JS::TokenType>(reinterpret_cast<size_t>(token1)) == static_cast<JS::TokenType>(reinterpret_cast<size_t>(token2));
}
JSSyntaxHighlighter::~JSSyntaxHighlighter()
{
}
}

Some files were not shown because too many files have changed in this diff Show more