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

Applications: Move to Userland/Applications/

This commit is contained in:
Andreas Kling 2021-01-12 12:05:23 +01:00
parent aa939c4b4b
commit dc28c07fa5
287 changed files with 1 additions and 1 deletions

View file

@ -0,0 +1,12 @@
set(SOURCES
main.cpp
)
execute_process(COMMAND "git rev-parse --short HEAD" OUTPUT_VARIABLE GIT_COMMIT)
execute_process(COMMAND "git rev-parse --abbrev-ref HEAD" OUTPUT_VARIABLE GIT_BRANCH)
execute_process(COMMAND "git diff-index --quiet HEAD -- && echo tracked || echo untracked" OUTPUT_VARIABLE GIT_CHANGES)
add_definitions(-DGIT_COMMIT="${GIT_COMMIT}" -DGIT_BRANCH="${GIT_BRANCH}" -DGIT_CHANGES="${GIT_CHANGES}")
serenity_bin(About)
target_link_libraries(About LibGUI)

View file

@ -0,0 +1,58 @@
/*
* 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/AboutDialog.h>
#include <LibGUI/Application.h>
#include <LibGUI/Icon.h>
#include <LibGfx/Bitmap.h>
#include <stdio.h>
#include <sys/utsname.h>
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer accept rpath unix cpath fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer accept rpath", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
auto app_icon = GUI::Icon::default_icon("ladybug");
GUI::AboutDialog::show("SerenityOS", nullptr, nullptr, app_icon.bitmap_for_size(32));
return app->exec();
}

View file

@ -0,0 +1,241 @@
/*
* Copyright (c) 2020, Emanuel Sprung <emanuel.sprung@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.
*/
#include "BookmarksBarWidget.h"
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/Event.h>
#include <LibGUI/JsonArrayModel.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Model.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
namespace Browser {
static BookmarksBarWidget* s_the;
BookmarksBarWidget& BookmarksBarWidget::the()
{
return *s_the;
}
BookmarksBarWidget::BookmarksBarWidget(const String& bookmarks_file, bool enabled)
{
s_the = this;
set_layout<GUI::HorizontalBoxLayout>();
layout()->set_spacing(0);
set_fixed_height(20);
if (!enabled)
set_visible(false);
m_additional = GUI::Button::construct();
m_additional->set_button_style(Gfx::ButtonStyle::CoolBar);
m_additional->set_text(">");
m_additional->set_fixed_size(14, 20);
m_additional->set_focus_policy(GUI::FocusPolicy::TabFocus);
m_additional->on_click = [this](auto) {
if (m_additional_menu) {
m_additional_menu->popup(m_additional->relative_position().translated(relative_position().translated(m_additional->window()->position())));
}
};
m_separator = GUI::Widget::construct();
m_context_menu = GUI::Menu::construct();
auto default_action = GUI::Action::create("Open", [this](auto&) {
if (on_bookmark_click)
on_bookmark_click(m_context_menu_url, Mod_None);
});
m_context_menu_default_action = default_action;
m_context_menu->add_action(default_action);
m_context_menu->add_action(GUI::Action::create("Open in new tab", [this](auto&) {
if (on_bookmark_click)
on_bookmark_click(m_context_menu_url, Mod_Ctrl);
}));
m_context_menu->add_action(GUI::Action::create("Delete", [this](auto&) {
remove_bookmark(m_context_menu_url);
}));
Vector<GUI::JsonArrayModel::FieldSpec> fields;
fields.empend("title", "Title", Gfx::TextAlignment::CenterLeft);
fields.empend("url", "Url", Gfx::TextAlignment::CenterRight);
set_model(GUI::JsonArrayModel::create(bookmarks_file, move(fields)));
model()->update();
}
BookmarksBarWidget::~BookmarksBarWidget()
{
if (m_model)
m_model->unregister_client(*this);
}
void BookmarksBarWidget::set_model(RefPtr<GUI::Model> model)
{
if (model == m_model)
return;
if (m_model)
m_model->unregister_client(*this);
m_model = move(model);
m_model->register_client(*this);
}
void BookmarksBarWidget::resize_event(GUI::ResizeEvent& event)
{
Widget::resize_event(event);
update_content_size();
}
void BookmarksBarWidget::model_did_update(unsigned)
{
remove_all_children();
m_bookmarks.clear();
int width = 0;
for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
auto title = model()->index(item_index, 0).data().to_string();
auto url = model()->index(item_index, 1).data().to_string();
Gfx::IntRect rect { width, 0, font().width(title) + 32, height() };
auto& button = add<GUI::Button>();
m_bookmarks.append(button);
button.set_button_style(Gfx::ButtonStyle::CoolBar);
button.set_text(title);
button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png"));
button.set_fixed_size(font().width(title) + 32, 20);
button.set_relative_rect(rect);
button.set_focus_policy(GUI::FocusPolicy::TabFocus);
button.set_tooltip(url);
button.on_click = [title, url, this](auto modifiers) {
if (on_bookmark_click)
on_bookmark_click(url, modifiers);
};
button.on_context_menu_request = [this, url](auto& context_menu_event) {
m_context_menu_url = url;
m_context_menu->popup(context_menu_event.screen_position(), m_context_menu_default_action);
};
width += rect.width();
}
add_child(*m_separator);
add_child(*m_additional);
update_content_size();
update();
}
void BookmarksBarWidget::update_content_size()
{
int x_position = 0;
m_last_visible_index = -1;
for (size_t i = 0; i < m_bookmarks.size(); ++i) {
auto& bookmark = m_bookmarks.at(i);
if (x_position + bookmark.width() > width()) {
m_last_visible_index = i;
break;
}
bookmark.set_x(x_position);
bookmark.set_visible(true);
x_position += bookmark.width();
}
if (m_last_visible_index < 0) {
m_additional->set_visible(false);
} else {
// hide all items > m_last_visible_index and create new bookmarks menu for them
m_additional->set_visible(true);
m_additional_menu = GUI::Menu::construct("Additional Bookmarks");
for (size_t i = m_last_visible_index; i < m_bookmarks.size(); ++i) {
auto& bookmark = m_bookmarks.at(i);
bookmark.set_visible(false);
m_additional_menu->add_action(GUI::Action::create(bookmark.text(),
Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png"),
[&](auto&) {
bookmark.on_click(0);
}));
}
}
}
bool BookmarksBarWidget::contains_bookmark(const String& url)
{
for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
auto item_title = model()->index(item_index, 0).data().to_string();
auto item_url = model()->index(item_index, 1).data().to_string();
if (item_url == url) {
return true;
}
}
return false;
}
bool BookmarksBarWidget::remove_bookmark(const String& url)
{
for (int item_index = 0; item_index < model()->row_count(); ++item_index) {
auto item_title = model()->index(item_index, 0).data().to_string();
auto item_url = model()->index(item_index, 1).data().to_string();
if (item_url == url) {
auto& json_model = *static_cast<GUI::JsonArrayModel*>(model());
const auto item_removed = json_model.remove(item_index);
if (item_removed)
json_model.store();
return item_removed;
}
}
return false;
}
bool BookmarksBarWidget::add_bookmark(const String& url, const String& title)
{
Vector<JsonValue> values;
values.append(title);
values.append(url);
auto& json_model = *static_cast<GUI::JsonArrayModel*>(model());
if (json_model.add(move(values))) {
json_model.store();
return true;
}
return false;
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2020, Emanuel Sprung <emanuel.sprung@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 <LibGUI/Forward.h>
#include <LibGUI/Model.h>
#include <LibGUI/Widget.h>
namespace Browser {
class BookmarksBarWidget final
: public GUI::Widget
, private GUI::ModelClient {
C_OBJECT(BookmarksBarWidget);
public:
static BookmarksBarWidget& the();
virtual ~BookmarksBarWidget() override;
void set_model(RefPtr<GUI::Model>);
GUI::Model* model() { return m_model.ptr(); }
const GUI::Model* model() const { return m_model.ptr(); }
Function<void(const String& url, unsigned modifiers)> on_bookmark_click;
Function<void(const String&, const String&)> on_bookmark_hover;
bool contains_bookmark(const String& url);
bool remove_bookmark(const String& url);
bool add_bookmark(const String& url, const String& title);
private:
BookmarksBarWidget(const String&, bool enabled);
// ^GUI::ModelClient
virtual void model_did_update(unsigned) override;
// ^GUI::Widget
virtual void resize_event(GUI::ResizeEvent&) override;
void update_content_size();
RefPtr<GUI::Model> m_model;
RefPtr<GUI::Button> m_additional;
RefPtr<GUI::Widget> m_separator;
RefPtr<GUI::Menu> m_additional_menu;
RefPtr<GUI::Menu> m_context_menu;
RefPtr<GUI::Action> m_context_menu_default_action;
String m_context_menu_url;
NonnullRefPtrVector<GUI::Button> m_bookmarks;
int m_last_visible_index { -1 };
};
}

View file

@ -0,0 +1,35 @@
/*
* 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>
namespace Browser {
extern String g_home_url;
}

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@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.
*/
#include "BrowserConsoleClient.h"
#include "ConsoleWidget.h"
#include <AK/StringBuilder.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/TextBox.h>
#include <LibWeb/DOM/DocumentType.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/DOMTreeModel.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
namespace Browser {
JS::Value BrowserConsoleClient::log()
{
m_console_widget.print_html(vm().join_arguments());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::info()
{
StringBuilder html;
html.append("<span class=\"info\">");
html.append("(i) ");
html.append(vm().join_arguments());
html.append("</span>");
m_console_widget.print_html(html.string_view());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::debug()
{
StringBuilder html;
html.append("<span class=\"debug\">");
html.append("(d) ");
html.append(vm().join_arguments());
html.append("</span>");
m_console_widget.print_html(html.string_view());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::warn()
{
StringBuilder html;
html.append("<span class=\"warn\">");
html.append("(w) ");
html.append(vm().join_arguments());
html.append("</span>");
m_console_widget.print_html(html.string_view());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::error()
{
StringBuilder html;
html.append("<span class=\"error\">");
html.append("(e) ");
html.append(vm().join_arguments());
html.append("</span>");
m_console_widget.print_html(html.string_view());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::clear()
{
m_console_widget.clear_output();
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::trace()
{
StringBuilder html;
html.append(vm().join_arguments());
auto trace = get_trace();
for (auto& function_name : trace) {
if (function_name.is_empty())
function_name = "&lt;anonymous&gt;";
html.appendff(" -> {}<br>", function_name);
}
m_console_widget.print_html(html.string_view());
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::count()
{
auto label = vm().argument_count() ? vm().argument(0).to_string_without_side_effects() : "default";
auto counter_value = m_console.counter_increment(label);
m_console_widget.print_html(String::formatted("{}: {}", label, counter_value));
return JS::js_undefined();
}
JS::Value BrowserConsoleClient::count_reset()
{
auto label = vm().argument_count() ? vm().argument(0).to_string_without_side_effects() : "default";
if (m_console.counter_reset(label)) {
m_console_widget.print_html(String::formatted("{}: 0", label));
} else {
m_console_widget.print_html(String::formatted("\"{}\" doesn't have a count", label));
}
return JS::js_undefined();
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@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 <LibGUI/Widget.h>
#include <LibJS/Console.h>
#include <LibJS/Forward.h>
#include <LibWeb/InProcessWebView.h>
namespace Browser {
class ConsoleWidget;
class BrowserConsoleClient final : public JS::ConsoleClient {
public:
BrowserConsoleClient(JS::Console& console, ConsoleWidget& console_widget)
: ConsoleClient(console)
, m_console_widget(console_widget)
{
}
private:
virtual JS::Value log() override;
virtual JS::Value info() override;
virtual JS::Value debug() override;
virtual JS::Value warn() override;
virtual JS::Value error() override;
virtual JS::Value clear() override;
virtual JS::Value trace() override;
virtual JS::Value count() override;
virtual JS::Value count_reset() override;
ConsoleWidget& m_console_widget;
};
}

View file

@ -0,0 +1,15 @@
@GUI::Widget {
name: "browser"
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
spacing: 2
}
@GUI::TabWidget {
name: "tab_widget"
container_padding: 0
uniform_tabs: true
text_alignment: "CenterLeft"
}
}

View file

@ -0,0 +1,19 @@
compile_gml(BrowserWindow.gml BrowserWindowGML.h browser_window_gml)
compile_gml(Tab.gml TabGML.h tab_gml)
set(SOURCES
BookmarksBarWidget.cpp
BrowserConsoleClient.cpp
ConsoleWidget.cpp
DownloadWidget.cpp
History.cpp
InspectorWidget.cpp
main.cpp
Tab.cpp
WindowActions.cpp
BrowserWindowGML.h
TabGML.h
)
serenity_app(Browser ICON app-browser)
target_link_libraries(Browser LibWeb LibProtocol LibGUI LibDesktop)

View file

@ -0,0 +1,169 @@
/*
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@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.
*/
#include "ConsoleWidget.h"
#include <AK/StringBuilder.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/FontDatabase.h>
#include <LibJS/Interpreter.h>
#include <LibJS/MarkupGenerator.h>
#include <LibJS/Parser.h>
#include <LibJS/Runtime/Error.h>
#include <LibWeb/DOM/DocumentType.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/DOMTreeModel.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
namespace Browser {
ConsoleWidget::ConsoleWidget()
{
set_layout<GUI::VerticalBoxLayout>();
set_fill_with_background_color(true);
auto base_document = Web::DOM::Document::create();
base_document->append_child(adopt(*new Web::DOM::DocumentType(base_document)));
auto html_element = base_document->create_element("html");
base_document->append_child(html_element);
auto head_element = base_document->create_element("head");
html_element->append_child(head_element);
auto body_element = base_document->create_element("body");
html_element->append_child(body_element);
m_output_container = body_element;
m_output_view = add<Web::InProcessWebView>();
m_output_view->set_document(base_document);
auto& bottom_container = add<GUI::Widget>();
bottom_container.set_layout<GUI::HorizontalBoxLayout>();
bottom_container.set_fixed_height(22);
m_input = bottom_container.add<GUI::TextBox>();
m_input->set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
// FIXME: Syntax Highlighting breaks the cursor's position on non fixed-width fonts.
m_input->set_font(Gfx::FontDatabase::default_fixed_width_font());
m_input->set_history_enabled(true);
m_input->on_return_pressed = [this] {
auto js_source = m_input->text();
// FIXME: An is_blank check to check if there is only whitespace would probably be preferable.
if (js_source.is_empty())
return;
m_input->add_current_text_to_history();
m_input->clear();
print_source_line(js_source);
auto parser = JS::Parser(JS::Lexer(js_source));
auto program = parser.parse_program();
StringBuilder output_html;
if (parser.has_errors()) {
auto error = parser.errors()[0];
auto hint = error.source_location_hint(js_source);
if (!hint.is_empty())
output_html.append(String::formatted("<pre>{}</pre>", escape_html_entities(hint)));
m_interpreter->vm().throw_exception<JS::SyntaxError>(m_interpreter->global_object(), error.to_string());
} else {
m_interpreter->run(m_interpreter->global_object(), *program);
}
if (m_interpreter->exception()) {
output_html.append("Uncaught exception: ");
output_html.append(JS::MarkupGenerator::html_from_value(m_interpreter->exception()->value()));
print_html(output_html.string_view());
m_interpreter->vm().clear_exception();
return;
}
print_html(JS::MarkupGenerator::html_from_value(m_interpreter->vm().last_value()));
};
set_focus_proxy(m_input);
auto& clear_button = bottom_container.add<GUI::Button>();
clear_button.set_fixed_size(22, 22);
clear_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/delete.png"));
clear_button.set_tooltip("Clear the console output");
clear_button.on_click = [this](auto) {
clear_output();
};
}
ConsoleWidget::~ConsoleWidget()
{
}
void ConsoleWidget::set_interpreter(WeakPtr<JS::Interpreter> interpreter)
{
if (m_interpreter.ptr() == interpreter.ptr())
return;
m_interpreter = interpreter;
m_console_client = make<BrowserConsoleClient>(interpreter->global_object().console(), *this);
interpreter->global_object().console().set_client(*m_console_client.ptr());
clear_output();
}
void ConsoleWidget::print_source_line(const StringView& source)
{
StringBuilder html;
html.append("<span class=\"repl-indicator\">");
html.append("&gt; ");
html.append("</span>");
html.append(JS::MarkupGenerator::html_from_source(source));
print_html(html.string_view());
}
void ConsoleWidget::print_html(const StringView& line)
{
auto paragraph = m_output_container->document().create_element("p");
paragraph->set_inner_html(line);
m_output_container->append_child(paragraph);
m_output_container->document().invalidate_layout();
m_output_container->document().update_layout();
m_output_view->scroll_to_bottom();
}
void ConsoleWidget::clear_output()
{
m_output_container->remove_all_children();
m_output_view->update();
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020, Hunter Salyer <thefalsehonesty@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 "BrowserConsoleClient.h"
#include "History.h"
#include <LibGUI/Widget.h>
#include <LibJS/Forward.h>
#include <LibWeb/InProcessWebView.h>
namespace Browser {
class ConsoleWidget final : public GUI::Widget {
C_OBJECT(ConsoleWidget)
public:
virtual ~ConsoleWidget();
void set_interpreter(WeakPtr<JS::Interpreter>);
void print_source_line(const StringView&);
void print_html(const StringView&);
void clear_output();
private:
ConsoleWidget();
RefPtr<GUI::TextBox> m_input;
RefPtr<Web::InProcessWebView> m_output_view;
RefPtr<Web::DOM::Element> m_output_container;
WeakPtr<JS::Interpreter> m_interpreter;
OwnPtr<BrowserConsoleClient> m_console_client;
};
}

View file

@ -0,0 +1,182 @@
/*
* 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 "DownloadWidget.h"
#include <AK/NumberFormat.h>
#include <AK/SharedBuffer.h>
#include <AK/StringBuilder.h>
#include <LibCore/File.h>
#include <LibCore/FileStream.h>
#include <LibCore/StandardPaths.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/ProgressBar.h>
#include <LibGUI/Window.h>
#include <LibProtocol/Client.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <math.h>
namespace Browser {
DownloadWidget::DownloadWidget(const URL& url)
: m_url(url)
{
{
StringBuilder builder;
builder.append(Core::StandardPaths::downloads_directory());
builder.append('/');
builder.append(m_url.basename());
m_destination_path = builder.to_string();
}
m_elapsed_timer.start();
m_download = Web::ResourceLoader::the().protocol_client().start_download("GET", url.to_string());
ASSERT(m_download);
m_download->on_progress = [this](Optional<u32> total_size, u32 downloaded_size) {
did_progress(total_size.value(), downloaded_size);
};
{
auto file_or_error = Core::File::open(m_destination_path, Core::IODevice::WriteOnly);
if (file_or_error.is_error()) {
GUI::MessageBox::show(window(), String::formatted("Cannot open {} for writing", m_destination_path), "Download failed", GUI::MessageBox::Type::Error);
window()->close();
return;
}
m_output_file_stream = make<Core::OutputFileStream>(*file_or_error.value());
}
m_download->on_finish = [this](bool success, auto) { did_finish(success); };
m_download->stream_into(*m_output_file_stream);
set_fill_with_background_color(true);
auto& layout = set_layout<GUI::VerticalBoxLayout>();
layout.set_margins({ 4, 4, 4, 4 });
auto& animation_container = add<GUI::Widget>();
animation_container.set_fixed_height(32);
auto& animation_layout = animation_container.set_layout<GUI::HorizontalBoxLayout>();
auto& browser_image = animation_container.add<GUI::ImageWidget>();
browser_image.load_from_file("/res/graphics/download-animation.gif");
animation_layout.add_spacer();
auto& source_label = add<GUI::Label>(String::formatted("From: {}", url));
source_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
source_label.set_fixed_height(16);
m_progress_bar = add<GUI::ProgressBar>();
m_progress_bar->set_fixed_height(20);
m_progress_label = add<GUI::Label>();
m_progress_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
m_progress_label->set_fixed_height(16);
auto& destination_label = add<GUI::Label>(String::formatted("To: {}", m_destination_path));
destination_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
destination_label.set_fixed_height(16);
auto& button_container = add<GUI::Widget>();
auto& button_container_layout = button_container.set_layout<GUI::HorizontalBoxLayout>();
button_container_layout.add_spacer();
m_cancel_button = button_container.add<GUI::Button>("Cancel");
m_cancel_button->set_fixed_size(100, 22);
m_cancel_button->on_click = [this](auto) {
bool success = m_download->stop();
ASSERT(success);
window()->close();
};
m_close_button = button_container.add<GUI::Button>("OK");
m_close_button->set_enabled(false);
m_close_button->set_fixed_size(100, 22);
m_close_button->on_click = [this](auto) {
window()->close();
};
}
DownloadWidget::~DownloadWidget()
{
}
void DownloadWidget::did_progress(Optional<u32> total_size, u32 downloaded_size)
{
m_progress_bar->set_min(0);
if (total_size.has_value()) {
int percent = roundf(((float)downloaded_size / (float)total_size.value()) * 100.0f);
window()->set_progress(percent);
m_progress_bar->set_max(total_size.value());
} else {
m_progress_bar->set_max(0);
}
m_progress_bar->set_value(downloaded_size);
{
StringBuilder builder;
builder.append("Downloaded ");
builder.append(human_readable_size(downloaded_size));
builder.appendff(" in {} sec", m_elapsed_timer.elapsed() / 1000);
m_progress_label->set_text(builder.to_string());
}
{
StringBuilder builder;
if (total_size.has_value()) {
int percent = roundf(((float)downloaded_size / (float)total_size.value()) * 100);
builder.appendff("{}%", percent);
} else {
builder.append(human_readable_size(downloaded_size));
}
builder.append(" of ");
builder.append(m_url.basename());
window()->set_title(builder.to_string());
}
}
void DownloadWidget::did_finish(bool success)
{
dbgln("did_finish, success={}", success);
m_close_button->set_enabled(true);
m_cancel_button->set_text("Open in Folder");
m_cancel_button->on_click = [this](auto) {
Desktop::Launcher::open(URL::create_with_file_protocol(Core::StandardPaths::downloads_directory()));
window()->close();
};
m_cancel_button->update();
if (!success) {
GUI::MessageBox::show(window(), String::formatted("Download failed for some reason"), "Download failed", GUI::MessageBox::Type::Error);
window()->close();
return;
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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/URL.h>
#include <LibCore/ElapsedTimer.h>
#include <LibCore/FileStream.h>
#include <LibGUI/ProgressBar.h>
#include <LibGUI/Widget.h>
#include <LibProtocol/Download.h>
namespace Browser {
class DownloadWidget final : public GUI::Widget {
C_OBJECT(DownloadWidget);
public:
virtual ~DownloadWidget() override;
private:
explicit DownloadWidget(const URL&);
void did_progress(Optional<u32> total_size, u32 downloaded_size);
void did_finish(bool success);
URL m_url;
String m_destination_path;
RefPtr<Protocol::Download> m_download;
RefPtr<GUI::ProgressBar> m_progress_bar;
RefPtr<GUI::Label> m_progress_label;
RefPtr<GUI::Button> m_cancel_button;
RefPtr<GUI::Button> m_close_button;
OwnPtr<Core::OutputFileStream> m_output_file_stream;
Core::ElapsedTimer m_elapsed_timer;
};
}

View file

@ -0,0 +1,73 @@
/*
* 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 "History.h"
namespace Browser {
void History::dump() const
{
dbgln("Dump {} items(s)", m_items.size());
int i = 0;
for (auto& item : m_items) {
dbgln("[{}] {} {}", i, item, m_current == i ? '*' : ' ');
++i;
}
}
void History::push(const URL& url)
{
m_items.shrink(m_current + 1);
m_items.append(url);
m_current++;
}
URL History::current() const
{
if (m_current == -1)
return {};
return m_items[m_current];
}
void History::go_back()
{
ASSERT(can_go_back());
m_current--;
}
void History::go_forward()
{
ASSERT(can_go_forward());
m_current++;
}
void History::clear()
{
m_items = {};
m_current = -1;
}
}

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 <AK/URL.h>
#include <AK/Vector.h>
namespace Browser {
class History {
public:
void dump() const;
void push(const URL&);
URL current() const;
void go_back();
void go_forward();
bool can_go_back() { return m_current > 0; }
bool can_go_forward() { return m_current + 1 < static_cast<int>(m_items.size()); }
void clear();
private:
Vector<URL> m_items;
int m_current { -1 };
};
}

View file

@ -0,0 +1,94 @@
/*
* 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 "InspectorWidget.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TreeView.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOMTreeModel.h>
#include <LibWeb/LayoutTreeModel.h>
#include <LibWeb/StylePropertiesModel.h>
namespace Browser {
void InspectorWidget::set_inspected_node(Web::DOM::Node* node)
{
m_document->set_inspected_node(node);
if (node && node->is_element()) {
auto& element = downcast<Web::DOM::Element>(*node);
if (element.specified_css_values()) {
m_style_table_view->set_model(Web::StylePropertiesModel::create(*element.specified_css_values()));
m_computed_style_table_view->set_model(Web::StylePropertiesModel::create(*element.computed_style()));
}
} else {
m_style_table_view->set_model(nullptr);
m_computed_style_table_view->set_model(nullptr);
}
}
InspectorWidget::InspectorWidget()
{
set_layout<GUI::VerticalBoxLayout>();
auto& splitter = add<GUI::VerticalSplitter>();
auto& top_tab_widget = splitter.add<GUI::TabWidget>();
m_dom_tree_view = top_tab_widget.add_tab<GUI::TreeView>("DOM");
m_dom_tree_view->on_selection = [this](auto& index) {
auto* node = static_cast<Web::DOM::Node*>(index.internal_data());
set_inspected_node(node);
};
m_layout_tree_view = top_tab_widget.add_tab<GUI::TreeView>("Layout");
m_layout_tree_view->on_selection = [this](auto& index) {
auto* node = static_cast<Web::Layout::Node*>(index.internal_data());
set_inspected_node(node->dom_node());
};
auto& bottom_tab_widget = splitter.add<GUI::TabWidget>();
m_style_table_view = bottom_tab_widget.add_tab<GUI::TableView>("Styles");
m_computed_style_table_view = bottom_tab_widget.add_tab<GUI::TableView>("Computed");
}
InspectorWidget::~InspectorWidget()
{
}
void InspectorWidget::set_document(Web::DOM::Document* document)
{
if (m_document == document)
return;
m_document = document;
m_dom_tree_view->set_model(Web::DOMTreeModel::create(*document));
m_layout_tree_view->set_model(Web::LayoutTreeModel::create(*document));
}
}

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/Widget.h>
#include <LibWeb/Forward.h>
namespace Browser {
class InspectorWidget final : public GUI::Widget {
C_OBJECT(InspectorWidget)
public:
virtual ~InspectorWidget();
void set_document(Web::DOM::Document*);
private:
InspectorWidget();
void set_inspected_node(Web::DOM::Node*);
RefPtr<GUI::TreeView> m_dom_tree_view;
RefPtr<GUI::TreeView> m_layout_tree_view;
RefPtr<GUI::TableView> m_style_table_view;
RefPtr<GUI::TableView> m_computed_style_table_view;
RefPtr<Web::DOM::Document> m_document;
};
}

View file

@ -0,0 +1,550 @@
/*
* 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 "Tab.h"
#include "BookmarksBarWidget.h"
#include "Browser.h"
#include "ConsoleWidget.h"
#include "DownloadWidget.h"
#include "InspectorWidget.h"
#include "WindowActions.h"
#include <AK/StringBuilder.h>
#include <Applications/Browser/TabGML.h>
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/StatusBar.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/Window.h>
#include <LibJS/Interpreter.h>
#include <LibWeb/CSS/Parser/CSSParser.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOMTreeModel.h>
#include <LibWeb/Dump.h>
#include <LibWeb/InProcessWebView.h>
#include <LibWeb/Layout/BlockBox.h>
#include <LibWeb/Layout/InitialContainingBlockBox.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/OutOfProcessWebView.h>
#include <LibWeb/Page/Frame.h>
namespace Browser {
URL url_from_user_input(const String& input)
{
auto url = URL(input);
if (url.is_valid())
return url;
StringBuilder builder;
builder.append("http://");
builder.append(input);
return URL(builder.build());
}
static void start_download(const URL& url)
{
auto window = GUI::Window::construct();
window->resize(300, 150);
window->set_title(String::formatted("0% of {}", url.basename()));
window->set_resizable(false);
window->set_main_widget<DownloadWidget>(url);
window->show();
[[maybe_unused]] auto& unused = window.leak_ref();
}
Tab::Tab(Type type)
: m_type(type)
{
load_from_gml(tab_gml);
m_toolbar_container = *find_descendant_of_type_named<GUI::ToolBarContainer>("toolbar_container");
auto& toolbar = *find_descendant_of_type_named<GUI::ToolBar>("toolbar");
auto& webview_container = *find_descendant_of_type_named<GUI::Widget>("webview_container");
if (m_type == Type::InProcessWebView)
m_page_view = webview_container.add<Web::InProcessWebView>();
else
m_web_content_view = webview_container.add<Web::OutOfProcessWebView>();
m_go_back_action = GUI::CommonActions::make_go_back_action([this](auto&) { go_back(); }, this);
m_go_forward_action = GUI::CommonActions::make_go_forward_action([this](auto&) { go_forward(); }, this);
toolbar.add_action(*m_go_back_action);
toolbar.add_action(*m_go_forward_action);
toolbar.add_action(GUI::CommonActions::make_go_home_action([this](auto&) { load(g_home_url); }, this));
m_reload_action = GUI::CommonActions::make_reload_action([this](auto&) { reload(); }, this);
toolbar.add_action(*m_reload_action);
m_location_box = toolbar.add<GUI::TextBox>();
m_location_box->set_placeholder("Address");
m_location_box->on_return_pressed = [this] {
auto url = url_from_user_input(m_location_box->text());
load(url);
view().set_focus(true);
};
m_location_box->add_custom_context_menu_action(GUI::Action::create("Paste & Go", [this](auto&) {
m_location_box->set_text(GUI::Clipboard::the().data());
m_location_box->on_return_pressed();
}));
m_bookmark_button = toolbar.add<GUI::Button>();
m_bookmark_button->set_button_style(Gfx::ButtonStyle::CoolBar);
m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-contour.png"));
m_bookmark_button->set_fixed_size(22, 22);
m_bookmark_button->on_click = [this](auto) {
auto url = this->url().to_string();
if (BookmarksBarWidget::the().contains_bookmark(url)) {
BookmarksBarWidget::the().remove_bookmark(url);
} else {
BookmarksBarWidget::the().add_bookmark(url, m_title);
}
update_bookmark_button(url);
};
hooks().on_load_start = [this](auto& url) {
m_location_box->set_icon(nullptr);
m_location_box->set_text(url.to_string());
// don't add to history if back or forward is pressed
if (!m_is_history_navigation)
m_history.push(url);
m_is_history_navigation = false;
update_actions();
update_bookmark_button(url.to_string());
};
hooks().on_link_click = [this](auto& url, auto& target, unsigned modifiers) {
if (target == "_blank" || modifiers == Mod_Ctrl) {
on_tab_open_request(url);
} else {
load(url);
}
};
m_link_context_menu = GUI::Menu::construct();
auto link_default_action = GUI::Action::create("Open", [this](auto&) {
hooks().on_link_click(m_link_context_menu_url, "", 0);
});
m_link_context_menu->add_action(link_default_action);
m_link_context_menu_default_action = link_default_action;
m_link_context_menu->add_action(GUI::Action::create("Open in new tab", [this](auto&) {
hooks().on_link_click(m_link_context_menu_url, "_blank", 0);
}));
m_link_context_menu->add_separator();
m_link_context_menu->add_action(GUI::Action::create("Copy link", [this](auto&) {
GUI::Clipboard::the().set_plain_text(m_link_context_menu_url.to_string());
}));
m_link_context_menu->add_separator();
m_link_context_menu->add_action(GUI::Action::create("Download", [this](auto&) {
start_download(m_link_context_menu_url);
}));
hooks().on_link_context_menu_request = [this](auto& url, auto& screen_position) {
m_link_context_menu_url = url;
m_link_context_menu->popup(screen_position, m_link_context_menu_default_action);
};
m_image_context_menu = GUI::Menu::construct();
m_image_context_menu->add_action(GUI::Action::create("Open image", [this](auto&) {
hooks().on_link_click(m_image_context_menu_url, "", 0);
}));
m_image_context_menu->add_action(GUI::Action::create("Open image in new tab", [this](auto&) {
hooks().on_link_click(m_image_context_menu_url, "_blank", 0);
}));
m_image_context_menu->add_separator();
m_image_context_menu->add_action(GUI::Action::create("Copy image", [this](auto&) {
if (m_image_context_menu_bitmap.is_valid())
GUI::Clipboard::the().set_bitmap(*m_image_context_menu_bitmap.bitmap());
}));
m_image_context_menu->add_action(GUI::Action::create("Copy image URL", [this](auto&) {
GUI::Clipboard::the().set_plain_text(m_image_context_menu_url.to_string());
}));
m_image_context_menu->add_separator();
m_image_context_menu->add_action(GUI::Action::create("Download", [this](auto&) {
start_download(m_image_context_menu_url);
}));
hooks().on_image_context_menu_request = [this](auto& image_url, auto& screen_position, const Gfx::ShareableBitmap& shareable_bitmap) {
m_image_context_menu_url = image_url;
m_image_context_menu_bitmap = shareable_bitmap;
m_image_context_menu->popup(screen_position);
};
hooks().on_link_middle_click = [this](auto& href, auto&, auto) {
hooks().on_link_click(href, "_blank", 0);
};
hooks().on_title_change = [this](auto& title) {
if (title.is_null()) {
m_title = url().to_string();
} else {
m_title = title;
}
if (on_title_change)
on_title_change(m_title);
};
hooks().on_favicon_change = [this](auto& icon) {
m_icon = icon;
m_location_box->set_icon(&icon);
if (on_favicon_change)
on_favicon_change(icon);
};
// FIXME: Support JS console in multi-process mode.
if (m_type == Type::InProcessWebView) {
hooks().on_set_document = [this](auto* document) {
if (document && m_console_window) {
auto* console_widget = static_cast<ConsoleWidget*>(m_console_window->main_widget());
console_widget->set_interpreter(document->interpreter().make_weak_ptr());
}
};
}
auto focus_location_box_action = GUI::Action::create(
"Focus location box", { Mod_Ctrl, Key_L }, [this](auto&) {
m_location_box->select_all();
m_location_box->set_focus(true);
},
this);
m_statusbar = *find_descendant_of_type_named<GUI::StatusBar>("statusbar");
hooks().on_link_hover = [this](auto& url) {
if (url.is_valid())
m_statusbar->set_text(url.to_string());
else
m_statusbar->set_text("");
};
hooks().on_url_drop = [this](auto& url) {
load(url);
};
m_menubar = GUI::MenuBar::construct();
auto& app_menu = m_menubar->add_menu("Browser");
app_menu.add_action(WindowActions::the().create_new_tab_action());
app_menu.add_action(GUI::Action::create(
"Close tab", { Mod_Ctrl, Key_W }, Gfx::Bitmap::load_from_file("/res/icons/16x16/close-tab.png"), [this](auto&) {
on_tab_close_request(*this);
},
this));
app_menu.add_action(*m_reload_action);
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
auto& view_menu = m_menubar->add_menu("View");
view_menu.add_action(GUI::CommonActions::make_fullscreen_action(
[this](auto&) {
window()->set_fullscreen(!window()->is_fullscreen());
auto is_fullscreen = window()->is_fullscreen();
auto* tab_widget = static_cast<GUI::TabWidget*>(parent_widget());
tab_widget->set_bar_visible(!is_fullscreen && tab_widget->children().size() > 1);
m_toolbar_container->set_visible(!is_fullscreen);
m_statusbar->set_visible(!is_fullscreen);
},
this));
auto view_source_action = GUI::Action::create(
"View source", { Mod_Ctrl, Key_U }, [this](auto&) {
if (m_type == Type::InProcessWebView) {
ASSERT(m_page_view->document());
auto url = m_page_view->document()->url().to_string();
auto source = m_page_view->document()->source();
auto window = GUI::Window::construct();
auto& editor = window->set_main_widget<GUI::TextEditor>();
editor.set_text(source);
editor.set_mode(GUI::TextEditor::ReadOnly);
editor.set_ruler_visible(true);
window->resize(640, 480);
window->set_title(url);
window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-text.png"));
window->show();
[[maybe_unused]] auto& unused = window.leak_ref();
} else {
TODO();
}
},
this);
auto inspect_dom_tree_action = GUI::Action::create(
"Inspect DOM tree", { Mod_None, Key_F12 }, [this](auto&) {
if (m_type == Type::InProcessWebView) {
if (!m_dom_inspector_window) {
m_dom_inspector_window = GUI::Window::construct();
m_dom_inspector_window->resize(300, 500);
m_dom_inspector_window->set_title("DOM inspector");
m_dom_inspector_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
m_dom_inspector_window->set_main_widget<InspectorWidget>();
}
auto* inspector_widget = static_cast<InspectorWidget*>(m_dom_inspector_window->main_widget());
inspector_widget->set_document(m_page_view->document());
m_dom_inspector_window->show();
m_dom_inspector_window->move_to_front();
} else {
TODO();
}
},
this);
auto& inspect_menu = m_menubar->add_menu("Inspect");
inspect_menu.add_action(*view_source_action);
inspect_menu.add_action(*inspect_dom_tree_action);
inspect_menu.add_action(GUI::Action::create(
"Open JS Console", { Mod_Ctrl, Key_I }, [this](auto&) {
if (m_type == Type::InProcessWebView) {
if (!m_console_window) {
m_console_window = GUI::Window::construct();
m_console_window->resize(500, 300);
m_console_window->set_title("JS Console");
m_console_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-javascript.png"));
m_console_window->set_main_widget<ConsoleWidget>();
}
auto* console_widget = static_cast<ConsoleWidget*>(m_console_window->main_widget());
console_widget->set_interpreter(m_page_view->document()->interpreter().make_weak_ptr());
m_console_window->show();
m_console_window->move_to_front();
} else {
TODO();
}
},
this));
auto& debug_menu = m_menubar->add_menu("Debug");
debug_menu.add_action(GUI::Action::create(
"Dump DOM tree", [this](auto&) {
if (m_type == Type::InProcessWebView) {
Web::dump_tree(*m_page_view->document());
} else {
TODO();
}
},
this));
debug_menu.add_action(GUI::Action::create(
"Dump Layout tree", [this](auto&) {
if (m_type == Type::InProcessWebView) {
Web::dump_tree(*m_page_view->document()->layout_node());
} else {
TODO();
}
},
this));
debug_menu.add_action(GUI::Action::create(
"Dump Style sheets", [this](auto&) {
if (m_type == Type::InProcessWebView) {
for (auto& sheet : m_page_view->document()->style_sheets().sheets()) {
Web::dump_sheet(sheet);
}
} else {
TODO();
}
},
this));
debug_menu.add_action(GUI::Action::create("Dump history", { Mod_Ctrl, Key_H }, [&](auto&) {
m_history.dump();
}));
debug_menu.add_separator();
auto line_box_borders_action = GUI::Action::create_checkable(
"Line box borders", [this](auto& action) {
if (m_type == Type::InProcessWebView) {
m_page_view->set_should_show_line_box_borders(action.is_checked());
m_page_view->update();
} else {
TODO();
}
},
this);
line_box_borders_action->set_checked(false);
debug_menu.add_action(line_box_borders_action);
debug_menu.add_separator();
debug_menu.add_action(GUI::Action::create("Collect garbage", { Mod_Ctrl | Mod_Shift, Key_G }, [this](auto&) {
if (m_type == Type::InProcessWebView) {
if (auto* document = m_page_view->document()) {
document->interpreter().heap().collect_garbage(JS::Heap::CollectionType::CollectGarbage, true);
}
} else {
TODO();
}
}));
auto& bookmarks_menu = m_menubar->add_menu("Bookmarks");
bookmarks_menu.add_action(WindowActions::the().show_bookmarks_bar_action());
auto& help_menu = m_menubar->add_menu("Help");
help_menu.add_action(WindowActions::the().about_action());
m_tab_context_menu = GUI::Menu::construct();
m_tab_context_menu->add_action(GUI::Action::create("Reload Tab", [this](auto&) {
m_reload_action->activate();
}));
m_tab_context_menu->add_action(GUI::Action::create("Close Tab", [this](auto&) {
on_tab_close_request(*this);
}));
m_page_context_menu = GUI::Menu::construct();
m_page_context_menu->add_action(*m_go_back_action);
m_page_context_menu->add_action(*m_go_forward_action);
m_page_context_menu->add_action(*m_reload_action);
m_page_context_menu->add_separator();
m_page_context_menu->add_action(*view_source_action);
m_page_context_menu->add_action(*inspect_dom_tree_action);
hooks().on_context_menu_request = [&](auto& screen_position) {
m_page_context_menu->popup(screen_position);
};
}
Tab::~Tab()
{
}
void Tab::load(const URL& url, LoadType load_type)
{
m_is_history_navigation = (load_type == LoadType::HistoryNavigation);
if (m_type == Type::InProcessWebView)
m_page_view->load(url);
else
m_web_content_view->load(url);
}
URL Tab::url() const
{
if (m_type == Type::InProcessWebView)
return m_page_view->url();
return m_web_content_view->url();
}
void Tab::reload()
{
load(url());
}
void Tab::go_back()
{
m_history.go_back();
update_actions();
load(m_history.current(), LoadType::HistoryNavigation);
}
void Tab::go_forward()
{
m_history.go_forward();
update_actions();
load(m_history.current(), LoadType::HistoryNavigation);
}
void Tab::update_actions()
{
m_go_back_action->set_enabled(m_history.can_go_back());
m_go_forward_action->set_enabled(m_history.can_go_forward());
}
void Tab::update_bookmark_button(const String& url)
{
if (BookmarksBarWidget::the().contains_bookmark(url)) {
m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-filled.png"));
m_bookmark_button->set_tooltip("Remove Bookmark");
} else {
m_bookmark_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/bookmark-contour.png"));
m_bookmark_button->set_tooltip("Add Bookmark");
}
}
void Tab::did_become_active()
{
Web::ResourceLoader::the().on_load_counter_change = [this] {
if (Web::ResourceLoader::the().pending_loads() == 0) {
m_statusbar->set_text("");
return;
}
m_statusbar->set_text(String::formatted("Loading ({} pending resources...)", Web::ResourceLoader::the().pending_loads()));
};
BookmarksBarWidget::the().on_bookmark_click = [this](auto& url, unsigned modifiers) {
if (modifiers & Mod_Ctrl)
on_tab_open_request(url);
else
load(url);
};
BookmarksBarWidget::the().on_bookmark_hover = [this](auto&, auto& url) {
m_statusbar->set_text(url);
};
BookmarksBarWidget::the().remove_from_parent();
m_toolbar_container->add_child(BookmarksBarWidget::the());
auto is_fullscreen = window()->is_fullscreen();
m_toolbar_container->set_visible(!is_fullscreen);
m_statusbar->set_visible(!is_fullscreen);
GUI::Application::the()->set_menubar(m_menubar);
}
void Tab::context_menu_requested(const Gfx::IntPoint& screen_position)
{
m_tab_context_menu->popup(screen_position);
}
GUI::Widget& Tab::view()
{
if (m_type == Type::InProcessWebView)
return *m_page_view;
return *m_web_content_view;
}
Web::WebViewHooks& Tab::hooks()
{
if (m_type == Type::InProcessWebView)
return *m_page_view;
return *m_web_content_view;
}
}

View file

@ -0,0 +1,22 @@
@GUI::Widget {
layout: @GUI::VerticalBoxLayout {
}
@GUI::ToolBarContainer {
name: "toolbar_container"
@GUI::ToolBar {
name: "toolbar"
}
}
@GUI::Widget {
name: "webview_container"
layout: @GUI::VerticalBoxLayout {
}
}
@GUI::StatusBar {
name: "statusbar"
}
}

View file

@ -0,0 +1,123 @@
/*
* 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 "History.h"
#include <AK/URL.h>
#include <LibGUI/Widget.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibHTTP/HttpJob.h>
#include <LibWeb/Forward.h>
namespace Web {
class OutOfProcessWebView;
class WebViewHooks;
}
namespace Browser {
class Tab final : public GUI::Widget {
C_OBJECT(Tab);
public:
enum class Type {
InProcessWebView,
OutOfProcessWebView,
};
virtual ~Tab() override;
URL url() const;
enum class LoadType {
Normal,
HistoryNavigation,
};
void load(const URL&, LoadType = LoadType::Normal);
void reload();
void go_back();
void go_forward();
void did_become_active();
void context_menu_requested(const Gfx::IntPoint& screen_position);
Function<void(String)> on_title_change;
Function<void(const URL&)> on_tab_open_request;
Function<void(Tab&)> on_tab_close_request;
Function<void(const Gfx::Bitmap&)> on_favicon_change;
const String& title() const { return m_title; }
const Gfx::Bitmap* icon() const { return m_icon; }
GUI::Widget& view();
private:
explicit Tab(Type);
Web::WebViewHooks& hooks();
void update_actions();
void update_bookmark_button(const String& url);
Type m_type;
History m_history;
RefPtr<Web::InProcessWebView> m_page_view;
RefPtr<Web::OutOfProcessWebView> m_web_content_view;
RefPtr<GUI::Action> m_go_back_action;
RefPtr<GUI::Action> m_go_forward_action;
RefPtr<GUI::Action> m_reload_action;
RefPtr<GUI::TextBox> m_location_box;
RefPtr<GUI::Button> m_bookmark_button;
RefPtr<GUI::Window> m_dom_inspector_window;
RefPtr<GUI::Window> m_console_window;
RefPtr<GUI::StatusBar> m_statusbar;
RefPtr<GUI::MenuBar> m_menubar;
RefPtr<GUI::ToolBarContainer> m_toolbar_container;
RefPtr<GUI::Menu> m_link_context_menu;
RefPtr<GUI::Action> m_link_context_menu_default_action;
URL m_link_context_menu_url;
RefPtr<GUI::Menu> m_image_context_menu;
Gfx::ShareableBitmap m_image_context_menu_bitmap;
URL m_image_context_menu_url;
RefPtr<GUI::Menu> m_tab_context_menu;
RefPtr<GUI::Menu> m_page_context_menu;
String m_title;
RefPtr<const Gfx::Bitmap> m_icon;
bool m_is_history_navigation { false };
};
URL url_from_user_input(const String& input);
}

View file

@ -0,0 +1,82 @@
/*
* 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 "WindowActions.h"
#include <LibGUI/Icon.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
namespace Browser {
static WindowActions* s_the;
WindowActions& WindowActions::the()
{
ASSERT(s_the);
return *s_the;
}
WindowActions::WindowActions(GUI::Window& window)
{
ASSERT(!s_the);
s_the = this;
m_create_new_tab_action = GUI::Action::create(
"New tab", { Mod_Ctrl, Key_T }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new-tab.png"), [this](auto&) {
if (on_create_new_tab)
on_create_new_tab();
},
&window);
m_next_tab_action = GUI::Action::create(
"Next tab", { Mod_Ctrl, Key_PageDown }, [this](auto&) {
if (on_next_tab)
on_next_tab();
},
&window);
m_previous_tab_action = GUI::Action::create(
"Previous tab", { Mod_Ctrl, Key_PageUp }, [this](auto&) {
if (on_previous_tab)
on_previous_tab();
},
&window);
m_about_action = GUI::Action::create(
"About Browser", GUI::Icon::default_icon("app-browser").bitmap_for_size(16), [this](const GUI::Action&) {
if (on_about)
on_about();
},
&window);
m_show_bookmarks_bar_action = GUI::Action::create_checkable(
"Show bookmarks bar",
[this](auto& action) {
if (on_show_bookmarks_bar)
on_show_bookmarks_bar(action);
},
&window);
}
}

View file

@ -0,0 +1,59 @@
/*
* 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/Action.h>
namespace Browser {
class WindowActions {
public:
static WindowActions& the();
WindowActions(GUI::Window&);
Function<void()> on_create_new_tab;
Function<void()> on_next_tab;
Function<void()> on_previous_tab;
Function<void()> on_about;
Function<void(GUI::Action&)> on_show_bookmarks_bar;
GUI::Action& create_new_tab_action() { return *m_create_new_tab_action; }
GUI::Action& next_tab_action() { return *m_next_tab_action; }
GUI::Action& previous_tab_action() { return *m_previous_tab_action; }
GUI::Action& about_action() { return *m_about_action; }
GUI::Action& show_bookmarks_bar_action() { return *m_show_bookmarks_bar_action; }
private:
RefPtr<GUI::Action> m_create_new_tab_action;
RefPtr<GUI::Action> m_next_tab_action;
RefPtr<GUI::Action> m_previous_tab_action;
RefPtr<GUI::Action> m_about_action;
RefPtr<GUI::Action> m_show_bookmarks_bar_action;
};
}

View file

@ -0,0 +1,254 @@
/*
* 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 "BookmarksBarWidget.h"
#include "Browser.h"
#include "InspectorWidget.h"
#include "Tab.h"
#include "WindowActions.h"
#include <AK/StringBuilder.h>
#include <Applications/Browser/BrowserWindowGML.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/File.h>
#include <LibCore/StandardPaths.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/AboutDialog.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Icon.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <stdio.h>
#include <stdlib.h>
namespace Browser {
String g_home_url;
bool g_multi_process = false;
static String bookmarks_file_path()
{
StringBuilder builder;
builder.append(Core::StandardPaths::config_directory());
builder.append("/bookmarks.json");
return builder.to_string();
}
}
int main(int argc, char** argv)
{
if (getuid() == 0) {
warnln("Refusing to run as root");
return 1;
}
if (pledge("stdio shared_buffer accept unix cpath rpath wpath fattr sendfd recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
const char* specified_url = nullptr;
Core::ArgsParser args_parser;
args_parser.add_option(Browser::g_multi_process, "Multi-process mode", "multi-process", 'm');
args_parser.add_positional_argument(specified_url, "URL to open", "url", Core::ArgsParser::Required::No);
args_parser.parse(argc, argv);
auto app = GUI::Application::construct(argc, argv);
// Connect to the ProtocolServer immediately so we can drop the "unix" pledge.
Web::ResourceLoader::the();
// Connect to LaunchServer immediately and let it know that we won't ask for anything other than opening
// the user's downloads directory.
// FIXME: This should go away with a standalone download manager at some point.
if (!Desktop::Launcher::add_allowed_url(URL::create_with_file_protocol(Core::StandardPaths::downloads_directory()))
|| !Desktop::Launcher::seal_allowlist()) {
warnln("Failed to set up allowed launch URLs");
return 1;
}
if (pledge("stdio shared_buffer accept unix cpath rpath wpath sendfd recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/home", "rwc") < 0) {
perror("unveil");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/etc/passwd", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/tmp/portal/image", "rw") < 0) {
perror("unveil");
return 1;
}
if (unveil("/tmp/portal/webcontent", "rw") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
auto app_icon = GUI::Icon::default_icon("app-browser");
auto m_config = Core::ConfigFile::get_for_app("Browser");
Browser::g_home_url = m_config->read_entry("Preferences", "Home", "about:blank");
auto ad_filter_list_or_error = Core::File::open(String::formatted("{}/BrowserContentFilters.txt", Core::StandardPaths::config_directory()), Core::IODevice::ReadOnly);
if (!ad_filter_list_or_error.is_error()) {
auto& ad_filter_list = *ad_filter_list_or_error.value();
while (!ad_filter_list.eof()) {
auto line = ad_filter_list.read_line();
if (line.is_empty())
continue;
Web::ContentFilter::the().add_pattern(line);
}
}
bool bookmarksbar_enabled = true;
auto bookmarks_bar = Browser::BookmarksBarWidget::construct(Browser::bookmarks_file_path(), bookmarksbar_enabled);
auto window = GUI::Window::construct();
window->resize(640, 480);
window->set_icon(app_icon.bitmap_for_size(16));
window->set_title("Browser");
auto& widget = window->set_main_widget<GUI::Widget>();
widget.load_from_gml(browser_window_gml);
auto& tab_widget = *widget.find_descendant_of_type_named<GUI::TabWidget>("tab_widget");
auto default_favicon = Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-html.png");
ASSERT(default_favicon);
tab_widget.on_change = [&](auto& active_widget) {
auto& tab = static_cast<Browser::Tab&>(active_widget);
window->set_title(String::formatted("{} - Browser", tab.title()));
tab.did_become_active();
};
tab_widget.on_middle_click = [&](auto& clicked_widget) {
auto& tab = static_cast<Browser::Tab&>(clicked_widget);
tab.on_tab_close_request(tab);
};
tab_widget.on_context_menu_request = [&](auto& clicked_widget, const GUI::ContextMenuEvent& context_menu_event) {
auto& tab = static_cast<Browser::Tab&>(clicked_widget);
tab.context_menu_requested(context_menu_event.screen_position());
};
Browser::WindowActions window_actions(*window);
Function<void(URL url, bool activate)> create_new_tab;
create_new_tab = [&](auto url, auto activate) {
auto type = Browser::g_multi_process ? Browser::Tab::Type::OutOfProcessWebView : Browser::Tab::Type::InProcessWebView;
auto& new_tab = tab_widget.add_tab<Browser::Tab>("New tab", type);
tab_widget.set_bar_visible(!window->is_fullscreen() && tab_widget.children().size() > 1);
tab_widget.set_tab_icon(new_tab, default_favicon);
new_tab.on_title_change = [&](auto title) {
tab_widget.set_tab_title(new_tab, title);
if (tab_widget.active_widget() == &new_tab)
window->set_title(String::formatted("{} - Browser", title));
};
new_tab.on_favicon_change = [&](auto& bitmap) {
tab_widget.set_tab_icon(new_tab, &bitmap);
};
new_tab.on_tab_open_request = [&](auto& url) {
create_new_tab(url, true);
};
new_tab.on_tab_close_request = [&](auto& tab) {
tab_widget.deferred_invoke([&](auto&) {
tab_widget.remove_tab(tab);
tab_widget.set_bar_visible(!window->is_fullscreen() && tab_widget.children().size() > 1);
if (tab_widget.children().is_empty())
app->quit();
});
};
new_tab.load(url);
dbgln("Added new tab {:p}, loading {}", &new_tab, url);
if (activate)
tab_widget.set_active_widget(&new_tab);
};
URL first_url = Browser::g_home_url;
if (specified_url) {
if (Core::File::exists(specified_url)) {
first_url = URL::create_with_file_protocol(Core::File::real_path_for(specified_url));
} else {
first_url = Browser::url_from_user_input(specified_url);
}
}
window_actions.on_create_new_tab = [&] {
create_new_tab(Browser::g_home_url, true);
};
window_actions.on_next_tab = [&] {
tab_widget.activate_next_tab();
};
window_actions.on_previous_tab = [&] {
tab_widget.activate_previous_tab();
};
window_actions.on_about = [&] {
GUI::AboutDialog::show("Browser", app_icon.bitmap_for_size(32), window);
};
window_actions.on_show_bookmarks_bar = [&](auto& action) {
Browser::BookmarksBarWidget::the().set_visible(action.is_checked());
};
window_actions.show_bookmarks_bar_action().set_checked(bookmarksbar_enabled);
create_new_tab(first_url, true);
window->show();
return app->exec();
}

View file

@ -0,0 +1,26 @@
add_subdirectory(About)
add_subdirectory(Browser)
add_subdirectory(Calculator)
add_subdirectory(Calendar)
add_subdirectory(CrashReporter)
add_subdirectory(Debugger)
add_subdirectory(DisplaySettings)
add_subdirectory(FileManager)
add_subdirectory(FontEditor)
add_subdirectory(Help)
add_subdirectory(HexEditor)
add_subdirectory(IRCClient)
add_subdirectory(KeyboardMapper)
add_subdirectory(KeyboardSettings)
add_subdirectory(MouseSettings)
add_subdirectory(Piano)
add_subdirectory(PixelPaint)
add_subdirectory(QuickShow)
add_subdirectory(SoundPlayer)
add_subdirectory(SpaceAnalyzer)
add_subdirectory(Spreadsheet)
add_subdirectory(SystemMonitor)
add_subdirectory(ThemeEditor)
add_subdirectory(Terminal)
add_subdirectory(TextEditor)
add_subdirectory(Welcome)

View file

@ -0,0 +1,11 @@
compile_gml(CalculatorWindow.gml CalculatorGML.h calculator_gml)
set(SOURCES
main.cpp
Calculator.cpp
CalculatorWidget.cpp
Keypad.cpp
CalculatorGML.h
)
serenity_app(Calculator ICON app-calculator)
target_link_libraries(Calculator LibGUI)

View file

@ -0,0 +1,143 @@
/*
* Copyright (c) 2019-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 "Calculator.h"
#include <AK/Assertions.h>
#include <math.h>
Calculator::Calculator()
{
}
Calculator::~Calculator()
{
}
double Calculator::begin_operation(Operation operation, double argument)
{
double res = 0.0;
switch (operation) {
case Operation::None:
ASSERT_NOT_REACHED();
case Operation::Add:
case Operation::Subtract:
case Operation::Multiply:
case Operation::Divide:
m_saved_argument = argument;
m_operation_in_progress = operation;
return argument;
case Operation::Sqrt:
if (argument < 0.0) {
m_has_error = true;
return argument;
}
res = sqrt(argument);
clear_operation();
break;
case Operation::Inverse:
if (argument == 0.0) {
m_has_error = true;
return argument;
}
res = 1 / argument;
clear_operation();
break;
case Operation::Percent:
res = argument * 0.01;
break;
case Operation::ToggleSign:
res = -argument;
break;
case Operation::MemClear:
m_mem = 0.0;
res = argument;
break;
case Operation::MemRecall:
res = m_mem;
break;
case Operation::MemSave:
m_mem = argument;
res = argument;
break;
case Operation::MemAdd:
m_mem += argument;
res = m_mem;
break;
}
return res;
}
double Calculator::finish_operation(double argument)
{
double res = 0.0;
switch (m_operation_in_progress) {
case Operation::None:
return argument;
case Operation::Add:
res = m_saved_argument + argument;
break;
case Operation::Subtract:
res = m_saved_argument - argument;
break;
case Operation::Multiply:
res = m_saved_argument * argument;
break;
case Operation::Divide:
if (argument == 0.0) {
m_has_error = true;
return argument;
}
res = m_saved_argument / argument;
break;
case Operation::Sqrt:
case Operation::Inverse:
case Operation::Percent:
case Operation::ToggleSign:
case Operation::MemClear:
case Operation::MemRecall:
case Operation::MemSave:
case Operation::MemAdd:
ASSERT_NOT_REACHED();
}
clear_operation();
return res;
}
void Calculator::clear_operation()
{
m_operation_in_progress = Operation::None;
m_saved_argument = 0.0;
clear_error();
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2019-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
// This type implements the regular calculator
// behavior, such as performing arithmetic
// operations and providing a memory cell.
// It does not deal with number input; you
// have to pass in already parsed double
// values.
class Calculator final {
public:
Calculator();
~Calculator();
enum class Operation {
None,
Add,
Subtract,
Multiply,
Divide,
Sqrt,
Inverse,
Percent,
ToggleSign,
MemClear,
MemRecall,
MemSave,
MemAdd
};
double begin_operation(Operation, double);
double finish_operation(double);
bool has_error() const { return m_has_error; }
void clear_operation();
void clear_error() { m_has_error = false; }
private:
Operation m_operation_in_progress { Operation::None };
double m_saved_argument { 0.0 };
double m_mem { 0.0 };
bool m_has_error { false };
};

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021 Glenford Williams <gw_dev@outlook.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 "CalculatorWidget.h"
#include "Applications/Calculator/CalculatorGML.h"
#include <AK/Assertions.h>
#include <LibGUI/Button.h>
#include <LibGUI/Label.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
CalculatorWidget::CalculatorWidget()
{
load_from_gml(calculator_gml);
m_entry = *find_descendant_of_type_named<GUI::TextBox>("entry_textbox");
m_entry->set_relative_rect(5, 5, 244, 26);
m_entry->set_text_alignment(Gfx::TextAlignment::CenterRight);
m_entry->set_font(Gfx::FontDatabase::default_fixed_width_font());
m_label = *find_descendant_of_type_named<GUI::Label>("label");
m_label->set_frame_shadow(Gfx::FrameShadow::Sunken);
m_label->set_frame_shape(Gfx::FrameShape::Container);
m_label->set_frame_thickness(2);
for (int i = 0; i < 10; i++) {
m_digit_button[i] = *find_descendant_of_type_named<GUI::Button>(String::formatted("{}_button", i));
add_digit_button(*m_digit_button[i], i);
}
m_mem_add_button = *find_descendant_of_type_named<GUI::Button>("mem_add_button");
add_operation_button(*m_mem_add_button, Calculator::Operation::MemAdd);
m_mem_save_button = *find_descendant_of_type_named<GUI::Button>("mem_save_button");
add_operation_button(*m_mem_save_button, Calculator::Operation::MemSave);
m_mem_recall_button = *find_descendant_of_type_named<GUI::Button>("mem_recall_button");
add_operation_button(*m_mem_recall_button, Calculator::Operation::MemRecall);
m_mem_clear_button = *find_descendant_of_type_named<GUI::Button>("mem_clear_button");
add_operation_button(*m_mem_clear_button, Calculator::Operation::MemClear);
m_clear_button = *find_descendant_of_type_named<GUI::Button>("clear_button");
m_clear_button->on_click = [this](auto) {
m_keypad.set_value(0.0);
m_calculator.clear_operation();
update_display();
};
m_clear_error_button = *find_descendant_of_type_named<GUI::Button>("clear_error_button");
m_clear_error_button->on_click = [this](auto) {
m_keypad.set_value(0.0);
update_display();
};
m_backspace_button = *find_descendant_of_type_named<GUI::Button>("backspace_button");
m_backspace_button->on_click = [this](auto) {
m_keypad.type_backspace();
update_display();
};
m_decimal_point_button = *find_descendant_of_type_named<GUI::Button>("decimal_button");
m_decimal_point_button->on_click = [this](auto) {
m_keypad.type_decimal_point();
update_display();
};
m_sign_button = *find_descendant_of_type_named<GUI::Button>("sign_button");
add_operation_button(*m_sign_button, Calculator::Operation::ToggleSign);
m_add_button = *find_descendant_of_type_named<GUI::Button>("add_button");
add_operation_button(*m_add_button, Calculator::Operation::Add);
m_subtract_button = *find_descendant_of_type_named<GUI::Button>("subtract_button");
add_operation_button(*m_subtract_button, Calculator::Operation::Subtract);
m_multiply_button = *find_descendant_of_type_named<GUI::Button>("multiply_button");
add_operation_button(*m_multiply_button, Calculator::Operation::Multiply);
m_divide_button = *find_descendant_of_type_named<GUI::Button>("divide_button");
add_operation_button(*m_divide_button, Calculator::Operation::Divide);
m_sqrt_button = *find_descendant_of_type_named<GUI::Button>("sqrt_button");
add_operation_button(*m_sqrt_button, Calculator::Operation::Sqrt);
m_inverse_button = *find_descendant_of_type_named<GUI::Button>("inverse_button");
add_operation_button(*m_inverse_button, Calculator::Operation::Inverse);
m_percent_button = *find_descendant_of_type_named<GUI::Button>("mod_button");
add_operation_button(*m_percent_button, Calculator::Operation::Percent);
m_equals_button = *find_descendant_of_type_named<GUI::Button>("equal_button");
m_equals_button->on_click = [this](auto) {
double argument = m_keypad.value();
double res = m_calculator.finish_operation(argument);
m_keypad.set_value(res);
update_display();
};
}
CalculatorWidget::~CalculatorWidget()
{
}
void CalculatorWidget::add_operation_button(GUI::Button& button, Calculator::Operation operation)
{
button.on_click = [this, operation](auto) {
double argument = m_keypad.value();
double res = m_calculator.begin_operation(operation, argument);
m_keypad.set_value(res);
update_display();
};
}
void CalculatorWidget::add_digit_button(GUI::Button& button, int digit)
{
button.on_click = [this, digit](auto) {
m_keypad.type_digit(digit);
update_display();
};
}
void CalculatorWidget::update_display()
{
m_entry->set_text(m_keypad.to_string());
if (m_calculator.has_error())
m_label->set_text("E");
else
m_label->set_text("");
}
void CalculatorWidget::keydown_event(GUI::KeyEvent& event)
{
//Clear button selection when we are typing
m_equals_button->set_focus(true);
m_equals_button->set_focus(false);
if (event.key() == KeyCode::Key_Return) {
m_keypad.set_value(m_calculator.finish_operation(m_keypad.value()));
} else if (event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9) {
m_keypad.type_digit(atoi(event.text().characters()));
} else if (event.key() == KeyCode::Key_Period) {
m_keypad.type_decimal_point();
} else if (event.key() == KeyCode::Key_Escape) {
m_keypad.set_value(0.0);
m_calculator.clear_operation();
} else if (event.key() == KeyCode::Key_Backspace) {
m_keypad.type_backspace();
} else {
Calculator::Operation operation;
switch (event.key()) {
case KeyCode::Key_Plus:
operation = Calculator::Operation::Add;
break;
case KeyCode::Key_Minus:
operation = Calculator::Operation::Subtract;
break;
case KeyCode::Key_Asterisk:
operation = Calculator::Operation::Multiply;
break;
case KeyCode::Key_Slash:
operation = Calculator::Operation::Divide;
break;
case KeyCode::Key_Percent:
operation = Calculator::Operation::Percent;
break;
default:
return;
}
m_keypad.set_value(m_calculator.begin_operation(operation, m_keypad.value()));
}
update_display();
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
* Copyright (c) 2021 Glenford Williams <gw_dev@outlook.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 "Calculator.h"
#include "Keypad.h"
#include <AK/Vector.h>
#include <LibGUI/Widget.h>
class CalculatorWidget final : public GUI::Widget {
C_OBJECT(CalculatorWidget)
public:
virtual ~CalculatorWidget() override;
private:
CalculatorWidget();
void add_operation_button(GUI::Button&, Calculator::Operation);
void add_digit_button(GUI::Button&, int digit);
void update_display();
virtual void keydown_event(GUI::KeyEvent&) override;
Calculator m_calculator;
Keypad m_keypad;
RefPtr<GUI::TextBox> m_entry;
RefPtr<GUI::Label> m_label;
RefPtr<GUI::Button> m_digit_button[10];
RefPtr<GUI::Button> m_mem_add_button;
RefPtr<GUI::Button> m_mem_save_button;
RefPtr<GUI::Button> m_mem_recall_button;
RefPtr<GUI::Button> m_mem_clear_button;
RefPtr<GUI::Button> m_clear_button;
RefPtr<GUI::Button> m_clear_error_button;
RefPtr<GUI::Button> m_backspace_button;
RefPtr<GUI::Button> m_decimal_point_button;
RefPtr<GUI::Button> m_sign_button;
RefPtr<GUI::Button> m_add_button;
RefPtr<GUI::Button> m_subtract_button;
RefPtr<GUI::Button> m_multiply_button;
RefPtr<GUI::Button> m_divide_button;
RefPtr<GUI::Button> m_sqrt_button;
RefPtr<GUI::Button> m_inverse_button;
RefPtr<GUI::Button> m_percent_button;
RefPtr<GUI::Button> m_equals_button;
};

View file

@ -0,0 +1,272 @@
@GUI::Widget {
fixed_width: 254
fixed_height: 213
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [10, 0, 10, 0]
}
@GUI::TextBox {
name: "entry_textbox"
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
name: "label"
fixed_width: 35
fixed_height: 27
}
@GUI::Widget {
fixed_width: 5
}
@GUI::Button {
name: "backspace_button"
text: "Backspace"
fixed_width: 65
fixed_height: 28
foreground_color: "brown"
}
@GUI::Button {
name: "clear_error_button"
text: "CE"
fixed_width: 55
fixed_height: 28
foreground_color: "brown"
}
@GUI::Button {
name: "clear_button"
text: "C"
fixed_width: 60
fixed_height: 28
foreground_color: "brown"
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Button {
name: "mem_clear_button"
text: "MC"
fixed_width: 35
fixed_height: 28
foreground_color: "red"
}
@GUI::Widget {
fixed_width: 5
}
@GUI::Button {
name: "7_button"
text: "7"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "8_button"
text: "8"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "9_button"
text: "9"
fixed_width: 35
fixed_height: 28
}
@GUI::Button {
name: "divide_button"
text: "/"
fixed_width: 35
fixed_height: 28
}
@GUI::Button {
name: "sqrt_button"
text: "sqrt"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {}
@GUI::Button {
name: "mem_recall_button"
text: "MR"
fixed_width: 35
fixed_height: 28
foreground_color: "red"
}
@GUI::Widget {
fixed_width: 5
}
@GUI::Button {
name: "4_button"
text: "4"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "5_button"
text: "5"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "6_button"
text: "6"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "multiply_button"
text: "*"
fixed_width: 35
fixed_height: 28
}
@GUI::Button {
name: "mod_button"
text: "%"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {}
@GUI::Button {
name: "mem_save_button"
text: "MS"
fixed_width: 35
fixed_height: 28
foreground_color: "red"
}
@GUI::Widget {
fixed_width: 5
}
@GUI::Button {
name: "1_button"
text: "1"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "2_button"
text: "2"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "3_button"
text: "3"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "subtract_button"
text: "-"
fixed_width: 35
fixed_height: 28
}
@GUI::Button {
name: "inverse_button"
text: "1/x"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
}
@GUI::Widget {
layout: @GUI::HorizontalBoxLayout {}
@GUI::Button {
name: "mem_add_button"
text: "M+"
fixed_width: 35
fixed_height: 28
foreground_color: "red"
}
@GUI::Widget {
fixed_width: 5
}
@GUI::Button {
name: "0_button"
text: "0"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "sign_button"
text: "+/-"
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "decimal_button"
text: "."
fixed_width: 35
fixed_height: 28
foreground_color: "blue"
}
@GUI::Button {
name: "add_button"
text: "+"
fixed_width: 35
fixed_height: 28
}
@GUI::Button {
name: "equal_button"
text: "="
fixed_width: 35
fixed_height: 28
foreground_color: "red"
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2019-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 "Keypad.h"
#include <AK/StringBuilder.h>
#include <math.h>
Keypad::Keypad()
{
}
Keypad::~Keypad()
{
}
void Keypad::type_digit(int digit)
{
switch (m_state) {
case State::External:
m_state = State::TypingInteger;
m_negative = false;
m_int_value = digit;
m_frac_value = 0;
m_frac_length = 0;
break;
case State::TypingInteger:
ASSERT(m_frac_value == 0);
ASSERT(m_frac_length == 0);
m_int_value *= 10;
m_int_value += digit;
break;
case State::TypingDecimal:
if (m_frac_length > 6)
break;
m_frac_value *= 10;
m_frac_value += digit;
m_frac_length++;
break;
}
}
void Keypad::type_decimal_point()
{
switch (m_state) {
case State::External:
m_negative = false;
m_int_value = 0;
m_frac_value = 0;
m_frac_length = 0;
break;
case State::TypingInteger:
ASSERT(m_frac_value == 0);
ASSERT(m_frac_length == 0);
m_state = State::TypingDecimal;
break;
case State::TypingDecimal:
// Ignore it.
break;
}
}
void Keypad::type_backspace()
{
switch (m_state) {
case State::External:
m_negative = false;
m_int_value = 0;
m_frac_value = 0;
m_frac_length = 0;
break;
case State::TypingDecimal:
if (m_frac_length > 0) {
m_frac_value /= 10;
m_frac_length--;
break;
}
ASSERT(m_frac_value == 0);
m_state = State::TypingInteger;
[[fallthrough]];
case State::TypingInteger:
ASSERT(m_frac_value == 0);
ASSERT(m_frac_length == 0);
m_int_value /= 10;
if (m_int_value == 0)
m_negative = false;
break;
}
}
double Keypad::value() const
{
double res = 0.0;
long frac = m_frac_value;
for (int i = 0; i < m_frac_length; i++) {
int digit = frac % 10;
res += digit;
res /= 10.0;
frac /= 10;
}
res += m_int_value;
if (m_negative)
res = -res;
return res;
}
void Keypad::set_value(double value)
{
m_state = State::External;
if (value < 0.0) {
m_negative = true;
value = -value;
} else
m_negative = false;
m_int_value = value;
value -= m_int_value;
m_frac_value = 0;
m_frac_length = 0;
while (value != 0) {
value *= 10.0;
int digit = value;
m_frac_value *= 10;
m_frac_value += digit;
m_frac_length++;
value -= digit;
if (m_frac_length > 6)
break;
}
}
String Keypad::to_string() const
{
StringBuilder builder;
if (m_negative)
builder.append("-");
builder.appendff("{}", m_int_value);
if (m_frac_length > 0)
builder.appendff(".{:0{}}", m_frac_value, m_frac_length);
return builder.to_string();
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2019-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/String.h>
// This type implements number typing and
// displaying mechanics. It does not perform
// any arithmetic operations or anything on
// the values it deals with.
class Keypad final {
public:
Keypad();
~Keypad();
void type_digit(int digit);
void type_decimal_point();
void type_backspace();
double value() const;
void set_value(double);
String to_string() const;
private:
// Internal representation of the current decimal value.
bool m_negative { false };
long m_int_value { 0 };
long m_frac_value { 0 };
int m_frac_length { 0 };
// E.g. for -35.004200,
// m_negative = true
// m_int_value = 35
// m_frac_value = 4200
// m_frac_length = 6
enum class State {
External,
TypingInteger,
TypingDecimal
};
State m_state { State::External };
};

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2019-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 "CalculatorWidget.h"
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <stdio.h>
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer rpath accept unix cpath fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer rpath accept", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
auto app_icon = GUI::Icon::default_icon("app-calculator");
auto window = GUI::Window::construct();
window->set_title("Calculator");
window->set_resizable(false);
window->resize(254, 213);
window->set_main_widget<CalculatorWidget>();
window->show();
window->set_icon(app_icon.bitmap_for_size(16));
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Calculator");
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
return;
}));
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("Calculator", app_icon));
app->set_menubar(move(menubar));
return app->exec();
}

View file

@ -0,0 +1,154 @@
/*
* Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@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.
*/
#include "AddEventDialog.h"
#include <LibCore/DateTime.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/Layout.h>
#include <LibGUI/Painter.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Color.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
static const char* short_month_names[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
AddEventDialog::AddEventDialog(Core::DateTime date_time, Window* parent_window)
: Dialog(parent_window)
, m_date_time(date_time)
{
resize(158, 100);
set_title("Add Event");
set_resizable(false);
set_icon(parent_window->icon());
auto& widget = set_main_widget<GUI::Widget>();
widget.set_fill_with_background_color(true);
widget.set_layout<GUI::VerticalBoxLayout>();
auto& top_container = widget.add<GUI::Widget>();
top_container.set_layout<GUI::VerticalBoxLayout>();
top_container.set_fixed_height(45);
top_container.layout()->set_margins({ 4, 4, 4, 4 });
auto& add_label = top_container.add<GUI::Label>("Add title & date:");
add_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
add_label.set_fixed_height(14);
add_label.set_font(Gfx::FontDatabase::default_bold_font());
auto& event_title_textbox = top_container.add<GUI::TextBox>();
event_title_textbox.set_fixed_height(20);
auto& middle_container = widget.add<GUI::Widget>();
middle_container.set_layout<GUI::HorizontalBoxLayout>();
middle_container.set_fixed_height(25);
middle_container.layout()->set_margins({ 4, 4, 4, 4 });
auto& starting_month_combo = middle_container.add<GUI::ComboBox>();
starting_month_combo.set_only_allow_values_from_model(true);
starting_month_combo.set_fixed_size(50, 20);
starting_month_combo.set_model(MonthListModel::create());
starting_month_combo.set_selected_index(m_date_time.month() - 1);
auto& starting_day_combo = middle_container.add<GUI::SpinBox>();
starting_day_combo.set_fixed_size(40, 20);
starting_day_combo.set_value(m_date_time.day());
starting_day_combo.set_min(1);
auto& starting_year_combo = middle_container.add<GUI::SpinBox>();
starting_year_combo.set_fixed_size(55, 20);
starting_year_combo.set_range(0, 9999);
starting_year_combo.set_value(m_date_time.year());
widget.layout()->add_spacer();
auto& button_container = widget.add<GUI::Widget>();
button_container.set_fixed_height(20);
button_container.set_layout<GUI::HorizontalBoxLayout>();
button_container.layout()->add_spacer();
auto& ok_button = button_container.add<GUI::Button>("OK");
ok_button.set_fixed_size(80, 20);
ok_button.on_click = [this](auto) {
dbgln("TODO: Add event icon on specific tile");
done(Dialog::ExecOK);
};
event_title_textbox.set_focus(true);
}
AddEventDialog::~AddEventDialog()
{
}
AddEventDialog::MonthListModel::MonthListModel()
{
}
AddEventDialog::MonthListModel::~MonthListModel()
{
}
void AddEventDialog::MonthListModel::update()
{
}
int AddEventDialog::MonthListModel::row_count(const GUI::ModelIndex&) const
{
return 12;
}
String AddEventDialog::MonthListModel::column_name(int column) const
{
switch (column) {
case Column::Month:
return "Month";
default:
ASSERT_NOT_REACHED();
}
}
GUI::Variant AddEventDialog::MonthListModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto& month = short_month_names[index.row()];
if (role == GUI::ModelRole::Display) {
switch (index.column()) {
case Column::Month:
return month;
default:
ASSERT_NOT_REACHED();
}
}
return {};
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@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 <LibGUI/Calendar.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/Model.h>
#include <LibGUI/Window.h>
class AddEventDialog final : public GUI::Dialog {
C_OBJECT(AddEventDialog)
public:
virtual ~AddEventDialog() override;
static void show(Core::DateTime date_time, Window* parent_window = nullptr)
{
auto dialog = AddEventDialog::construct(date_time, parent_window);
dialog->exec();
}
private:
AddEventDialog(Core::DateTime date_time, Window* parent_window = nullptr);
class MonthListModel final : public GUI::Model {
public:
enum Column {
Month,
__Count,
};
static NonnullRefPtr<MonthListModel> create() { return adopt(*new MonthListModel); }
virtual ~MonthListModel() override;
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
virtual String column_name(int) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override;
private:
MonthListModel();
};
Core::DateTime m_date_time;
};

View file

@ -0,0 +1,7 @@
set(SOURCES
AddEventDialog.cpp
main.cpp
)
serenity_app(Calendar ICON app-calendar)
target_link_libraries(Calendar LibGUI)

View file

@ -0,0 +1,182 @@
/*
* Copyright (c) 2019-2020, Ryan Grieb <ryan.m.grieb@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.
*/
#include "AddEventDialog.h"
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/Calendar.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Color.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <stdio.h>
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer rpath accept unix cpath fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer rpath accept", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
auto app_icon = GUI::Icon::default_icon("app-calendar");
auto window = GUI::Window::construct();
window->set_title("Calendar");
window->resize(600, 480);
window->set_icon(app_icon.bitmap_for_size(16));
auto& root_container = window->set_main_widget<GUI::Widget>();
root_container.set_fill_with_background_color(true);
root_container.set_layout<GUI::VerticalBoxLayout>();
auto& toolbar_container = root_container.add<GUI::ToolBarContainer>();
auto& toolbar = toolbar_container.add<GUI::ToolBar>();
auto& calendar_container = root_container.add<GUI::Frame>();
calendar_container.set_layout<GUI::VerticalBoxLayout>();
calendar_container.layout()->set_margins({ 2, 2, 2, 2 });
auto& calendar_widget = calendar_container.add<GUI::Calendar>(Core::DateTime::now());
RefPtr<GUI::Button> selected_calendar_button;
auto prev_date_action = GUI::Action::create("Previous date", { Mod_Alt, Key_Left }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"), [&](const GUI::Action&) {
unsigned int target_month = calendar_widget.selected_month();
unsigned int target_year = calendar_widget.selected_year();
if (calendar_widget.mode() == GUI::Calendar::Month) {
target_month--;
if (calendar_widget.selected_month() <= 1) {
target_month = 12;
target_year--;
}
} else {
target_year--;
}
calendar_widget.update_tiles(target_year, target_month);
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
});
auto next_date_action = GUI::Action::create("Next date", { Mod_Alt, Key_Right }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [&](const GUI::Action&) {
unsigned int target_month = calendar_widget.selected_month();
unsigned int target_year = calendar_widget.selected_year();
if (calendar_widget.mode() == GUI::Calendar::Month) {
target_month++;
if (calendar_widget.selected_month() >= 12) {
target_month = 1;
target_year++;
}
} else {
target_year++;
}
calendar_widget.update_tiles(target_year, target_month);
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
});
auto add_event_action = GUI::Action::create("Add event", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"), [&](const GUI::Action&) {
AddEventDialog::show(calendar_widget.selected_date(), window);
});
auto jump_to_action = GUI::Action::create("Jump to today", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/calendar-date.png"), [&](const GUI::Action&) {
if (calendar_widget.mode() == GUI::Calendar::Year)
calendar_widget.toggle_mode();
calendar_widget.set_selected_date(Core::DateTime::now());
calendar_widget.update_tiles(Core::DateTime::now().year(), Core::DateTime::now().month());
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
});
toolbar.add_action(prev_date_action);
selected_calendar_button = toolbar.add<GUI::Button>(calendar_widget.selected_calendar_text());
selected_calendar_button->set_fixed_width(70);
selected_calendar_button->set_button_style(Gfx::ButtonStyle::CoolBar);
selected_calendar_button->set_font(Gfx::FontDatabase::default_bold_fixed_width_font());
selected_calendar_button->on_click = [&](auto) {
calendar_widget.toggle_mode();
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
};
toolbar.add_action(next_date_action);
toolbar.add_separator();
toolbar.add_action(jump_to_action);
toolbar.add_action(add_event_action);
calendar_widget.on_calendar_tile_click = [&] {
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
};
calendar_widget.on_calendar_tile_doubleclick = [&] {
AddEventDialog::show(calendar_widget.selected_date(), window);
};
calendar_widget.on_month_tile_click = [&] {
selected_calendar_button->set_text(calendar_widget.selected_calendar_text());
};
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Calendar");
app_menu.add_action(GUI::Action::create("Add Event", { Mod_Ctrl | Mod_Shift, Key_E }, Gfx::Bitmap::load_from_file("/res/icons/16x16/add-event.png"),
[&](const GUI::Action&) {
AddEventDialog::show(calendar_widget.selected_date(), window);
return;
}));
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
return;
}));
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("Calendar", app_icon));
app->set_menubar(move(menubar));
window->show();
app->exec();
}

View file

@ -0,0 +1,10 @@
compile_gml(CrashReporterWindow.gml CrashReporterWindowGML.h crash_reporter_window_gml)
set(SOURCES
main.cpp
CrashReporterWindowGML.h
)
serenity_app(CrashReporter ICON app-crash-reporter)
target_link_libraries(CrashReporter LibCore LibCoreDump LibDesktop LibGUI)

View file

@ -0,0 +1,95 @@
@GUI::Widget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [5, 5, 5, 5]
}
@GUI::Widget {
fixed_height: 44
layout: @GUI::HorizontalBoxLayout {
spacing: 10
}
@GUI::ImageWidget {
name: "icon"
}
@GUI::Label {
name: "description"
text_alignment: "CenterLeft"
}
}
@GUI::Widget {
fixed_height: 18
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Executable path:"
text_alignment: "CenterLeft"
fixed_width: 90
}
@GUI::LinkLabel {
name: "executable_link"
text_alignment: "CenterLeft"
}
}
@GUI::Widget {
fixed_height: 18
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Coredump path:"
text_alignment: "CenterLeft"
fixed_width: 90
}
@GUI::LinkLabel {
name: "coredump_link"
text_alignment: "CenterLeft"
}
}
@GUI::Widget {
fixed_height: 18
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Backtrace:"
text_alignment: "CenterLeft"
}
}
@GUI::TextEditor {
name: "backtrace_text_editor"
mode: "ReadOnly"
}
@GUI::Widget {
fixed_height: 32
layout: @GUI::HorizontalBoxLayout {
}
// HACK: We need something like Layout::add_spacer() in GML! :^)
@GUI::Widget {
}
@GUI::Button {
name: "close_button"
text: "Close"
fixed_width: 70
fixed_height: 22
}
}
}

View file

@ -0,0 +1,179 @@
/*
* Copyright (c) 2020, 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/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <AK/Types.h>
#include <AK/URL.h>
#include <Applications/CrashReporter/CrashReporterWindowGML.h>
#include <LibCore/ArgsParser.h>
#include <LibCoreDump/Backtrace.h>
#include <LibCoreDump/Reader.h>
#include <LibDesktop/AppFile.h>
#include <LibDesktop/Launcher.h>
#include <LibELF/CoreDump.h>
#include <LibGUI/Application.h>
#include <LibGUI/Button.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/Icon.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Label.h>
#include <LibGUI/Layout.h>
#include <LibGUI/LinkLabel.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
#include <string.h>
static String build_backtrace(const CoreDump::Reader& coredump)
{
StringBuilder builder;
auto assertion = coredump.metadata().get("assertion");
if (assertion.has_value() && !assertion.value().is_empty()) {
builder.append("ASSERTION FAILED: ");
builder.append(assertion.value().characters());
builder.append('\n');
builder.append('\n');
}
auto first = true;
for (auto& entry : coredump.backtrace().entries()) {
if (first)
first = false;
else
builder.append('\n');
builder.append(entry.to_string());
}
return builder.build();
}
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer accept cpath rpath unix fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
const char* coredump_path = nullptr;
Core::ArgsParser args_parser;
args_parser.set_general_help("Show information from an application crash coredump.");
args_parser.add_positional_argument(coredump_path, "Coredump path", "coredump-path");
args_parser.parse(argc, argv);
String backtrace;
String executable_path;
int pid { 0 };
u8 termination_signal { 0 };
{
auto coredump = CoreDump::Reader::create(coredump_path);
if (!coredump) {
warnln("Could not open coredump '{}'", coredump_path);
return 1;
}
auto& process_info = coredump->process_info();
backtrace = build_backtrace(*coredump);
executable_path = String(process_info.executable_path);
pid = process_info.pid;
termination_signal = process_info.termination_signal;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer accept rpath unix", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil(executable_path.characters(), "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/tmp/portal/launch", "rw") < 0) {
perror("unveil");
return 1;
}
if (unveil(nullptr, nullptr) < 0) {
perror("unveil");
return 1;
}
auto app_icon = GUI::Icon::default_icon("app-crash-reporter");
auto window = GUI::Window::construct();
window->set_title("Crash Reporter");
window->set_icon(app_icon.bitmap_for_size(16));
window->resize(460, 340);
window->center_on_screen();
auto& widget = window->set_main_widget<GUI::Widget>();
widget.load_from_gml(crash_reporter_window_gml);
auto& icon_image_widget = *widget.find_descendant_of_type_named<GUI::ImageWidget>("icon");
icon_image_widget.set_bitmap(GUI::FileIconProvider::icon_for_executable(executable_path).bitmap_for_size(32));
auto app_name = LexicalPath(executable_path).basename();
auto af = Desktop::AppFile::get_for_app(app_name);
if (af->is_valid())
app_name = af->name();
auto& description_label = *widget.find_descendant_of_type_named<GUI::Label>("description");
description_label.set_text(String::formatted("\"{}\" (PID {}) has crashed - {} (signal {})", app_name, pid, strsignal(termination_signal), termination_signal));
auto& executable_link_label = *widget.find_descendant_of_type_named<GUI::LinkLabel>("executable_link");
executable_link_label.set_text(LexicalPath::canonicalized_path(executable_path));
executable_link_label.on_click = [&] {
Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(executable_path).dirname()));
};
auto& coredump_link_label = *widget.find_descendant_of_type_named<GUI::LinkLabel>("coredump_link");
coredump_link_label.set_text(LexicalPath::canonicalized_path(coredump_path));
coredump_link_label.on_click = [&] {
Desktop::Launcher::open(URL::create_with_file_protocol(LexicalPath(coredump_path).dirname()));
};
auto& backtrace_text_editor = *widget.find_descendant_of_type_named<GUI::TextEditor>("backtrace_text_editor");
backtrace_text_editor.set_text(backtrace);
backtrace_text_editor.set_should_hide_unnecessary_scrollbars(true);
auto& close_button = *widget.find_descendant_of_type_named<GUI::Button>("close_button");
close_button.on_click = [&](auto) {
app->quit();
};
window->show();
return app->exec();
}

View file

@ -0,0 +1,6 @@
set(SOURCES
main.cpp
)
serenity_bin(Debugger)
target_link_libraries(Debugger LibCore LibDebug LibX86 LibLine)

View file

@ -0,0 +1,313 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@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.
*/
#include <AK/Assertions.h>
#include <AK/ByteBuffer.h>
#include <AK/Demangle.h>
#include <AK/LogStream.h>
#include <AK/StringBuilder.h>
#include <AK/kmalloc.h>
#include <LibC/sys/arch/i386/regs.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibDebug/DebugInfo.h>
#include <LibDebug/DebugSession.h>
#include <LibLine/Editor.h>
#include <LibX86/Disassembler.h>
#include <LibX86/Instruction.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
RefPtr<Line::Editor> editor;
OwnPtr<Debug::DebugSession> g_debug_session;
static void handle_sigint(int)
{
outln("Debugger: SIGINT");
// The destructor of DebugSession takes care of detaching
g_debug_session = nullptr;
}
static void handle_print_registers(const PtraceRegisters& regs)
{
outln("eax={:08x} ebx={:08x} ecx={:08x} edx={:08x}", regs.eax, regs.ebx, regs.ecx, regs.edx);
outln("esp={:08x} ebp={:08x} esi={:08x} edi={:08x}", regs.esp, regs.ebp, regs.esi, regs.edi);
outln("eip={:08x} eflags={:08x}", regs.eip, regs.eflags);
}
static bool handle_disassemble_command(const String& command, void* first_instruction)
{
auto parts = command.split(' ');
size_t number_of_instructions_to_disassemble = 5;
if (parts.size() == 2) {
auto number = parts[1].to_uint();
if (!number.has_value())
return false;
number_of_instructions_to_disassemble = number.value();
}
// FIXME: Instead of using a fixed "dump_size",
// we can feed instructions to the disassembler one by one
constexpr size_t dump_size = 0x100;
ByteBuffer code;
for (size_t i = 0; i < dump_size / sizeof(u32); ++i) {
auto value = g_debug_session->peek(reinterpret_cast<u32*>(first_instruction) + i);
if (!value.has_value())
break;
code.append(&value, sizeof(u32));
}
X86::SimpleInstructionStream stream(code.data(), code.size());
X86::Disassembler disassembler(stream);
for (size_t i = 0; i < number_of_instructions_to_disassemble; ++i) {
auto offset = stream.offset();
auto insn = disassembler.next();
if (!insn.has_value())
break;
outln(" {:p} <+{}>:\t{}", offset + reinterpret_cast<size_t>(first_instruction), offset, insn.value().to_string(offset));
}
return true;
}
static bool insert_breakpoint_at_address(FlatPtr address)
{
return g_debug_session->insert_breakpoint((void*)address);
}
static bool insert_breakpoint_at_source_position(const String& file, size_t line)
{
auto result = g_debug_session->insert_breakpoint(file, line);
if (!result.has_value()) {
warnln("Could not insert breakpoint at {}:{}", file, line);
return false;
}
outln("Breakpoint inserted [{}:{} ({}:{:p})]", result.value().file_name, result.value().line_number, result.value().library_name, result.value().address);
return true;
}
static bool insert_breakpoint_at_symbol(const String& symbol)
{
auto result = g_debug_session->insert_breakpoint(symbol);
if (!result.has_value()) {
warnln("Could not insert breakpoint at symbol: {}", symbol);
return false;
}
outln("Breakpoint inserted [{}:{:p}]", result.value().library_name, result.value().address);
return true;
}
static bool handle_breakpoint_command(const String& command)
{
auto parts = command.split(' ');
if (parts.size() != 2)
return false;
auto argument = parts[1];
if (argument.is_empty())
return false;
if (argument.contains(":")) {
auto source_arguments = argument.split(':');
if (source_arguments.size() != 2)
return false;
auto line = source_arguments[1].to_uint();
if (!line.has_value())
return false;
auto file = source_arguments[0];
return insert_breakpoint_at_source_position(file, line.value());
}
if ((argument.starts_with("0x"))) {
return insert_breakpoint_at_address(strtoul(argument.characters() + 2, nullptr, 16));
}
return insert_breakpoint_at_symbol(argument);
}
static bool handle_examine_command(const String& command)
{
auto parts = command.split(' ');
if (parts.size() != 2)
return false;
auto argument = parts[1];
if (argument.is_empty())
return false;
if (!(argument.starts_with("0x"))) {
return false;
}
u32 address = strtoul(argument.characters() + 2, nullptr, 16);
auto res = g_debug_session->peek((u32*)address);
if (!res.has_value()) {
printf("could not examine memory at address 0x%x\n", address);
return true;
}
printf("0x%x\n", res.value());
return true;
}
static void print_help()
{
out("Options:\n"
"cont - Continue execution\n"
"si - step to the next instruction\n"
"sl - step to the next source line\n"
"line - show the position of the current instruction in the source code\n"
"regs - Print registers\n"
"dis [number of instructions] - Print disassembly\n"
"bp <address/symbol/file:line> - Insert a breakpoint\n"
"x <address> - examine dword in memory\n");
}
int main(int argc, char** argv)
{
editor = Line::Editor::construct();
if (pledge("stdio proc ptrace exec rpath tty sigaction cpath unix fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
const char* command = nullptr;
Core::ArgsParser args_parser;
args_parser.add_positional_argument(command,
"The program to be debugged, along with its arguments",
"program", Core::ArgsParser::Required::Yes);
args_parser.parse(argc, argv);
auto result = Debug::DebugSession::exec_and_attach(command);
if (!result) {
warnln("Failed to start debugging session for: \"{}\"", command);
exit(1);
}
g_debug_session = result.release_nonnull();
struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler = handle_sigint;
sigaction(SIGINT, &sa, nullptr);
Debug::DebugInfo::SourcePosition previous_source_position;
bool in_step_line = false;
g_debug_session->run(Debug::DebugSession::DesiredInitialDebugeeState::Stopped, [&](Debug::DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) {
if (reason == Debug::DebugSession::DebugBreakReason::Exited) {
outln("Program exited.");
return Debug::DebugSession::DebugDecision::Detach;
}
ASSERT(optional_regs.has_value());
const PtraceRegisters& regs = optional_regs.value();
auto symbol_at_ip = g_debug_session->symbolicate(regs.eip);
auto source_position = g_debug_session->get_source_position(regs.eip);
if (in_step_line) {
bool no_source_info = !source_position.has_value();
if (no_source_info || source_position.value() != previous_source_position) {
if (no_source_info)
outln("No source information for current instruction! stoppoing.");
in_step_line = false;
} else {
return Debug::DebugSession::DebugDecision::SingleStep;
}
}
if (symbol_at_ip.has_value())
outln("Program is stopped at: {:p} ({}:{})", regs.eip, symbol_at_ip.value().library_name, symbol_at_ip.value().symbol);
else
outln("Program is stopped at: {:p}", regs.eip);
if (source_position.has_value()) {
previous_source_position = source_position.value();
outln("Source location: {}:{}", source_position.value().file_path, source_position.value().line_number);
} else {
outln("(No source location information for the current instruction)");
}
for (;;) {
auto command_result = editor->get_line("(sdb) ");
if (command_result.is_error())
return Debug::DebugSession::DebugDecision::Detach;
auto& command = command_result.value();
bool success = false;
Optional<Debug::DebugSession::DebugDecision> decision;
if (command.is_empty() && !editor->history().is_empty()) {
command = editor->history().last().entry;
}
if (command == "cont") {
decision = Debug::DebugSession::DebugDecision::Continue;
success = true;
} else if (command == "si") {
decision = Debug::DebugSession::DebugDecision::SingleStep;
success = true;
} else if (command == "sl") {
if (source_position.has_value()) {
decision = Debug::DebugSession::DebugDecision::SingleStep;
in_step_line = true;
success = true;
} else {
outln("No source location information for the current instruction");
}
} else if (command == "regs") {
handle_print_registers(regs);
success = true;
} else if (command.starts_with("dis")) {
success = handle_disassemble_command(command, reinterpret_cast<void*>(regs.eip));
} else if (command.starts_with("bp")) {
success = handle_breakpoint_command(command);
} else if (command.starts_with("x")) {
success = handle_examine_command(command);
}
if (success && !command.is_empty()) {
// Don't add repeated commands to history
if (editor->history().is_empty() || editor->history().last().entry != command)
editor->add_to_history(command);
}
if (!success) {
print_help();
}
if (decision.has_value())
return decision.value();
}
});
}

View file

@ -0,0 +1,11 @@
compile_gml(DisplaySettingsWindow.gml DisplaySettingsWindowGML.h display_settings_window_gml)
set(SOURCES
DisplaySettings.cpp
DisplaySettingsWindowGML.h
main.cpp
MonitorWidget.cpp
)
serenity_app(DisplaySettings ICON app-display-settings)
target_link_libraries(DisplaySettings LibGUI)

View file

@ -0,0 +1,264 @@
/*
* Copyright (c) 2019-2020, Jesse Buhagiar <jooster669@gmail.com>
* 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 "DisplaySettings.h"
#include <AK/StringBuilder.h>
#include <Applications/DisplaySettings/DisplaySettingsWindowGML.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/DirIterator.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/WindowServerConnection.h>
#include <LibGfx/Palette.h>
#include <LibGfx/SystemTheme.h>
REGISTER_WIDGET(DisplaySettings, MonitorWidget)
DisplaySettingsWidget::DisplaySettingsWidget()
{
create_resolution_list();
create_wallpaper_list();
create_frame();
load_current_settings();
}
void DisplaySettingsWidget::create_resolution_list()
{
// TODO: Find a better way to get the default resolution
m_resolutions.append({ 640, 480 });
m_resolutions.append({ 800, 600 });
m_resolutions.append({ 1024, 768 });
m_resolutions.append({ 1280, 720 });
m_resolutions.append({ 1280, 768 });
m_resolutions.append({ 1280, 1024 });
m_resolutions.append({ 1360, 768 });
m_resolutions.append({ 1368, 768 });
m_resolutions.append({ 1440, 900 });
m_resolutions.append({ 1600, 900 });
m_resolutions.append({ 1920, 1080 });
m_resolutions.append({ 2560, 1080 });
}
void DisplaySettingsWidget::create_wallpaper_list()
{
Core::DirIterator iterator("/res/wallpapers/", Core::DirIterator::Flags::SkipDots);
m_wallpapers.append("Use background color");
while (iterator.has_next()) {
m_wallpapers.append(iterator.next_path());
}
m_modes.append("simple");
m_modes.append("tile");
m_modes.append("center");
m_modes.append("scaled");
}
void DisplaySettingsWidget::create_frame()
{
load_from_gml(display_settings_window_gml);
m_monitor_widget = *find_descendant_of_type_named<DisplaySettings::MonitorWidget>("monitor_widget");
m_wallpaper_combo = *find_descendant_of_type_named<GUI::ComboBox>("wallpaper_combo");
m_wallpaper_combo->set_only_allow_values_from_model(true);
m_wallpaper_combo->set_model(*GUI::ItemListModel<AK::String>::create(m_wallpapers));
m_wallpaper_combo->on_change = [this](auto& text, const GUI::ModelIndex& index) {
String path = text;
if (path.starts_with("/") && m_monitor_widget->set_wallpaper(path)) {
m_monitor_widget->update();
return;
}
if (index.row() == 0) {
path = "";
} else {
if (index.is_valid()) {
StringBuilder builder;
builder.append("/res/wallpapers/");
builder.append(path);
path = builder.to_string();
}
}
m_monitor_widget->set_wallpaper(path);
m_monitor_widget->update();
};
auto& button = *find_descendant_of_type_named<GUI::Button>("wallpaper_open_button");
button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"));
button.on_click = [this](auto) {
Optional<String> open_path = GUI::FilePicker::get_open_filepath(nullptr, "Select wallpaper from file system.");
if (!open_path.has_value())
return;
m_wallpaper_combo->set_only_allow_values_from_model(false);
m_wallpaper_combo->set_text(open_path.value());
m_wallpaper_combo->set_only_allow_values_from_model(true);
};
m_mode_combo = *find_descendant_of_type_named<GUI::ComboBox>("mode_combo");
m_mode_combo->set_only_allow_values_from_model(true);
m_mode_combo->set_model(*GUI::ItemListModel<AK::String>::create(m_modes));
m_mode_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
m_monitor_widget->set_wallpaper_mode(m_modes.at(index.row()));
m_monitor_widget->update();
};
m_resolution_combo = *find_descendant_of_type_named<GUI::ComboBox>("resolution_combo");
m_resolution_combo->set_only_allow_values_from_model(true);
m_resolution_combo->set_model(*GUI::ItemListModel<Gfx::IntSize>::create(m_resolutions));
m_resolution_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
m_monitor_widget->set_desktop_resolution(m_resolutions.at(index.row()));
m_monitor_widget->update();
};
m_color_input = *find_descendant_of_type_named<GUI::ColorInput>("color_input");
m_color_input->set_color_has_alpha_channel(false);
m_color_input->set_color_picker_title("Select color for desktop");
m_color_input->on_change = [this] {
m_monitor_widget->set_background_color(m_color_input->color());
m_monitor_widget->update();
};
auto& ok_button = *find_descendant_of_type_named<GUI::Button>("ok_button");
ok_button.on_click = [this](auto) {
send_settings_to_window_server();
GUI::Application::the()->quit();
};
auto& cancel_button = *find_descendant_of_type_named<GUI::Button>("cancel_button");
cancel_button.on_click = [](auto) {
GUI::Application::the()->quit();
};
auto& apply_button = *find_descendant_of_type_named<GUI::Button>("apply_button");
apply_button.on_click = [this](auto) {
send_settings_to_window_server();
};
}
void DisplaySettingsWidget::load_current_settings()
{
auto ws_config(Core::ConfigFile::open("/etc/WindowServer/WindowServer.ini"));
auto wm_config = Core::ConfigFile::get_for_app("WindowManager");
/// Wallpaper path ////////////////////////////////////////////////////////////////////////////
/// Read wallpaper path from config file and set value to monitor widget and combo box.
auto selected_wallpaper = wm_config->read_entry("Background", "Wallpaper", "");
if (!selected_wallpaper.is_empty()) {
m_monitor_widget->set_wallpaper(selected_wallpaper);
Optional<size_t> optional_index;
if (selected_wallpaper.starts_with("/res/wallpapers/")) {
auto name_parts = selected_wallpaper.split('/', true);
optional_index = m_wallpapers.find_first_index(name_parts[2]);
if (optional_index.has_value()) {
m_wallpaper_combo->set_selected_index(optional_index.value());
}
}
if (!optional_index.has_value()) {
m_wallpaper_combo->set_only_allow_values_from_model(false);
m_wallpaper_combo->set_text(selected_wallpaper);
m_wallpaper_combo->set_only_allow_values_from_model(true);
}
} else {
m_wallpaper_combo->set_selected_index(0);
}
size_t index;
/// Mode //////////////////////////////////////////////////////////////////////////////////////
auto mode = ws_config->read_entry("Background", "Mode", "simple");
if (!m_modes.contains_slow(mode)) {
warnln("Invalid background mode '{}' in WindowServer config, falling back to 'simple'", mode);
mode = "simple";
}
m_monitor_widget->set_wallpaper_mode(mode);
index = m_modes.find_first_index(mode).value();
m_mode_combo->set_selected_index(index);
/// Resolution ////////////////////////////////////////////////////////////////////////////////
Gfx::IntSize find_size;
// Let's attempt to find the current resolution and select it!
find_size.set_width(ws_config->read_num_entry("Screen", "Width", 1024));
find_size.set_height(ws_config->read_num_entry("Screen", "Height", 768));
index = m_resolutions.find_first_index(find_size).value_or(0);
Gfx::IntSize m_current_resolution = m_resolutions.at(index);
m_monitor_widget->set_desktop_resolution(m_current_resolution);
m_resolution_combo->set_selected_index(index);
/// Color /////////////////////////////////////////////////////////////////////////////////////
/// If presend read from config file. If not paint with palet color.
Color palette_desktop_color = palette().desktop_background();
auto background_color = ws_config->read_entry("Background", "Color", "");
if (!background_color.is_empty()) {
auto opt_color = Color::from_string(background_color);
if (opt_color.has_value())
palette_desktop_color = opt_color.value();
}
m_color_input->set_color(palette_desktop_color);
m_monitor_widget->set_background_color(palette_desktop_color);
m_monitor_widget->update();
}
void DisplaySettingsWidget::send_settings_to_window_server()
{
auto result = GUI::WindowServerConnection::the().send_sync<Messages::WindowServer::SetResolution>(m_monitor_widget->desktop_resolution());
if (!result->success()) {
GUI::MessageBox::show(nullptr, String::formatted("Reverting to resolution {}x{}", result->resolution().width(), result->resolution().height()),
"Unable to set resolution", GUI::MessageBox::Type::Error);
}
if (!m_monitor_widget->wallpaper().is_empty()) {
GUI::Desktop::the().set_wallpaper(m_monitor_widget->wallpaper());
} else {
dbgln("Setting color input: __{}__", m_color_input->text());
GUI::Desktop::the().set_wallpaper("");
GUI::Desktop::the().set_background_color(m_color_input->text());
}
GUI::Desktop::the().set_wallpaper_mode(m_monitor_widget->wallpaper_mode());
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2019-2020, Jesse Buhagiar <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 "MonitorWidget.h"
#include <LibGUI/ColorInput.h>
#include <LibGUI/ComboBox.h>
class DisplaySettingsWidget : public GUI::Widget {
C_OBJECT(DisplaySettingsWidget);
public:
DisplaySettingsWidget();
private:
void create_frame();
void create_wallpaper_list();
void create_resolution_list();
void load_current_settings();
void send_settings_to_window_server(); // Apply the settings to the Window Server
Vector<String> m_wallpapers;
Vector<String> m_modes;
Vector<Gfx::IntSize> m_resolutions;
RefPtr<DisplaySettings::MonitorWidget> m_monitor_widget;
RefPtr<GUI::ComboBox> m_wallpaper_combo;
RefPtr<GUI::ComboBox> m_mode_combo;
RefPtr<GUI::ComboBox> m_resolution_combo;
RefPtr<GUI::ColorInput> m_color_input;
};

View file

@ -0,0 +1,117 @@
@GUI::Widget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
margins: [4, 4, 4, 4]
}
@DisplaySettings::MonitorWidget {
name: "monitor_widget"
fixed_width: 338
fixed_height: 248
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Wallpaper:"
text_alignment: "CenterLeft"
fixed_width: 70
}
@GUI::ComboBox {
name: "wallpaper_combo"
}
@GUI::Button {
name: "wallpaper_open_button"
tooltip: "Select wallpaper from file system."
button_style: "CoolBar"
fixed_width: 22
fixed_height: 22
}
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Modes:"
text_alignment: "CenterLeft"
fixed_width: 70
}
@GUI::ComboBox {
name: "mode_combo"
}
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Resolution:"
text_alignment: "CenterLeft"
fixed_width: 70
}
@GUI::ComboBox {
name: "resolution_combo"
}
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Label {
text: "Color:"
text_alignment: "CenterLeft"
fixed_width: 70
}
@GUI::ColorInput {
name: "color_input"
fixed_width: 90
}
}
@GUI::Widget {
}
@GUI::Widget {
shrink_to_fit: true
layout: @GUI::HorizontalBoxLayout {
}
@GUI::Widget {
}
@GUI::Button {
name: "ok_button"
text: "OK"
fixed_width: 60
}
@GUI::Button {
name: "cancel_button"
text: "Cancel"
fixed_width: 60
}
@GUI::Button {
name: "apply_button"
text: "Apply"
fixed_width: 60
}
}
}

View file

@ -0,0 +1,118 @@
/*
* 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 "MonitorWidget.h"
#include <LibGUI/Painter.h>
#include <LibGfx/Bitmap.h>
namespace DisplaySettings {
MonitorWidget::MonitorWidget()
{
m_monitor_bitmap = Gfx::Bitmap::load_from_file("/res/graphics/monitor.png");
m_monitor_rect = { 8, 9, 320, 180 };
}
bool MonitorWidget::set_wallpaper(String path)
{
auto bitmap_ptr = Gfx::Bitmap::load_from_file(path);
if (!bitmap_ptr && !path.is_empty())
return false;
m_desktop_wallpaper_path = path;
m_desktop_wallpaper_bitmap = bitmap_ptr;
return true;
}
String MonitorWidget::wallpaper()
{
return m_desktop_wallpaper_path;
}
void MonitorWidget::set_wallpaper_mode(String mode)
{
m_desktop_wallpaper_mode = mode;
}
String MonitorWidget::wallpaper_mode()
{
return m_desktop_wallpaper_mode;
}
void MonitorWidget::set_desktop_resolution(Gfx::IntSize resolution)
{
m_desktop_resolution = resolution;
}
Gfx::IntSize MonitorWidget::desktop_resolution()
{
return m_desktop_resolution;
}
void MonitorWidget::set_background_color(Gfx::Color color)
{
m_desktop_color = color;
}
Gfx::Color MonitorWidget::background_color()
{
return m_desktop_color;
}
void MonitorWidget::paint_event(GUI::PaintEvent& event)
{
Gfx::IntRect screen_rect = { 0, 0, m_desktop_resolution.width(), m_desktop_resolution.height() };
auto screen_bitmap = Gfx::Bitmap::create(m_monitor_bitmap->format(), m_desktop_resolution);
GUI::Painter screen_painter(*screen_bitmap);
screen_painter.fill_rect(screen_rect, m_desktop_color);
if (!m_desktop_wallpaper_bitmap.is_null()) {
if (m_desktop_wallpaper_mode == "simple") {
screen_painter.blit({ 0, 0 }, *m_desktop_wallpaper_bitmap, m_desktop_wallpaper_bitmap->rect());
} else if (m_desktop_wallpaper_mode == "center") {
Gfx::IntPoint offset { screen_rect.width() / 2 - m_desktop_wallpaper_bitmap->size().width() / 2, screen_rect.height() / 2 - m_desktop_wallpaper_bitmap->size().height() / 2 };
screen_painter.blit_offset(screen_rect.location(), *m_desktop_wallpaper_bitmap, screen_rect, offset);
} else if (m_desktop_wallpaper_mode == "tile") {
screen_painter.draw_tiled_bitmap(screen_bitmap->rect(), *m_desktop_wallpaper_bitmap);
} else if (m_desktop_wallpaper_mode == "scaled") {
screen_painter.draw_scaled_bitmap(screen_bitmap->rect(), *m_desktop_wallpaper_bitmap, m_desktop_wallpaper_bitmap->rect());
} else {
ASSERT_NOT_REACHED();
}
}
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.blit({ 0, 0 }, *m_monitor_bitmap, m_monitor_bitmap->rect());
painter.draw_scaled_bitmap(m_monitor_rect, *screen_bitmap, screen_bitmap->rect());
if (!m_desktop_resolution.is_null()) {
painter.draw_text(m_monitor_rect.translated(1, 1), m_desktop_resolution.to_string(), Gfx::TextAlignment::Center, Color::Black);
painter.draw_text(m_monitor_rect, m_desktop_resolution.to_string(), Gfx::TextAlignment::Center, Color::White);
}
}
}

View file

@ -0,0 +1,64 @@
/*
* 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/Widget.h>
namespace DisplaySettings {
class MonitorWidget final : public GUI::Widget {
C_OBJECT(MonitorWidget);
public:
bool set_wallpaper(String path);
String wallpaper();
void set_wallpaper_mode(String mode);
String wallpaper_mode();
void set_desktop_resolution(Gfx::IntSize resolution);
Gfx::IntSize desktop_resolution();
void set_background_color(Gfx::Color background_color);
Gfx::Color background_color();
private:
MonitorWidget();
virtual void paint_event(GUI::PaintEvent& event) override;
Gfx::IntRect m_monitor_rect;
RefPtr<Gfx::Bitmap> m_monitor_bitmap;
String m_desktop_wallpaper_path;
RefPtr<Gfx::Bitmap> m_desktop_wallpaper_bitmap;
String m_desktop_wallpaper_mode;
Gfx::IntSize m_desktop_resolution;
Gfx::Color m_desktop_color;
};
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2019-2020, Jesse Buhagiar <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.
*/
#include "DisplaySettings.h"
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <stdio.h>
int main(int argc, char** argv)
{
if (pledge("stdio thread shared_buffer rpath accept cpath wpath unix fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio thread shared_buffer rpath accept cpath wpath", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app_icon = GUI::Icon::default_icon("app-display-settings");
// Let's create the tab pane that we'll hook our widgets up to :^)
auto tab_widget = GUI::TabWidget::construct();
tab_widget->add_tab<DisplaySettingsWidget>("Display Settings");
tab_widget->set_fill_with_background_color(true); // No black backgrounds!
auto window = GUI::Window::construct();
dbgln("main window: {}", window);
window->set_title("Display Settings");
window->resize(360, 410);
window->set_resizable(false);
window->set_main_widget(tab_widget.ptr());
window->set_icon(app_icon.bitmap_for_size(16));
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Display Settings");
app_menu.add_action(GUI::CommonActions::make_quit_action([&](const GUI::Action&) {
app->quit();
}));
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("Display Settings", app_icon));
app->set_menubar(move(menubar));
window->show();
return app->exec();
}

View file

@ -0,0 +1,13 @@
compile_gml(FileManagerWindow.gml FileManagerWindowGML.h file_manager_window_gml)
set(SOURCES
DesktopWidget.cpp
DirectoryView.cpp
FileManagerWindowGML.h
FileUtils.cpp
main.cpp
PropertiesWindow.cpp
)
serenity_app(FileManager ICON filetype-folder)
target_link_libraries(FileManager LibGUI LibDesktop)

View file

@ -0,0 +1,47 @@
/*
* 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 "DesktopWidget.h"
#include <LibGUI/Painter.h>
namespace FileManager {
DesktopWidget::DesktopWidget()
{
}
DesktopWidget::~DesktopWidget()
{
}
void DesktopWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.clear_rect(event.rect(), Color(0, 0, 0, 0));
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 FileManager {
class DesktopWidget final : public GUI::Widget {
C_OBJECT(DesktopWidget);
public:
virtual ~DesktopWidget() override;
private:
virtual void paint_event(GUI::PaintEvent&) override;
DesktopWidget();
};
}

View file

@ -0,0 +1,591 @@
/*
* 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 "DirectoryView.h"
#include "FileUtils.h"
#include <AK/LexicalPath.h>
#include <AK/NumberFormat.h>
#include <AK/StringBuilder.h>
#include <LibCore/MimeData.h>
#include <LibCore/StandardPaths.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/SortingProxyModel.h>
#include <serenity.h>
#include <spawn.h>
#include <stdio.h>
#include <unistd.h>
namespace FileManager {
NonnullRefPtr<GUI::Action> LauncherHandler::create_launch_action(Function<void(const LauncherHandler&)> launch_handler)
{
auto icon = GUI::FileIconProvider::icon_for_executable(details().executable).bitmap_for_size(16);
return GUI::Action::create(details().name, move(icon), [this, launch_handler = move(launch_handler)](auto&) {
launch_handler(*this);
});
}
RefPtr<LauncherHandler> DirectoryView::get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers)
{
// If this is an application, pick it first
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::Application)
return handlers[i];
}
// If there's a handler preferred by the user, pick this first
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserPreferred)
return handlers[i];
}
// Otherwise, use the user's default, if available
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserDefault)
return handlers[i];
}
// If still no match, use the first one we find
if (!handlers.is_empty()) {
return handlers[0];
}
return {};
}
NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const URL& url)
{
NonnullRefPtrVector<LauncherHandler> handlers;
for (auto& h : Desktop::Launcher::get_handlers_with_details_for_url(url)) {
handlers.append(adopt(*new LauncherHandler(h)));
}
return handlers;
}
NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const String& path)
{
return get_launch_handlers(URL::create_with_file_protocol(path));
}
void DirectoryView::handle_activation(const GUI::ModelIndex& index)
{
if (!index.is_valid())
return;
dbgln("on activation: {},{}, this={:p}, m_model={:p}", index.row(), index.column(), this, m_model.ptr());
auto& node = this->node(index);
auto path = node.full_path();
struct stat st;
if (stat(path.characters(), &st) < 0) {
perror("stat");
return;
}
if (S_ISDIR(st.st_mode)) {
if (is_desktop()) {
Desktop::Launcher::open(URL::create_with_file_protocol(path));
return;
}
open(path);
return;
}
auto url = URL::create_with_file_protocol(path);
auto launcher_handlers = get_launch_handlers(url);
auto default_launcher = get_default_launch_handler(launcher_handlers);
if (default_launcher) {
launch(url, *default_launcher);
} else {
auto error_message = String::format("Could not open %s", path.characters());
GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
}
}
DirectoryView::DirectoryView(Mode mode)
: m_mode(mode)
, m_model(GUI::FileSystemModel::create({}))
, m_sorting_model(GUI::SortingProxyModel::create(m_model))
{
set_active_widget(nullptr);
set_content_margins({ 2, 2, 2, 2 });
setup_actions();
m_error_label = add<GUI::Label>();
m_error_label->set_font(m_error_label->font().bold_variant());
setup_model();
setup_icon_view();
if (mode != Mode::Desktop) {
setup_columns_view();
setup_table_view();
}
set_view_mode(ViewMode::Icon);
}
const GUI::FileSystemModel::Node& DirectoryView::node(const GUI::ModelIndex& index) const
{
return model().node(m_sorting_model->map_to_source(index));
}
void DirectoryView::setup_model()
{
m_model->on_error = [this](int, const char* error_string) {
auto failed_path = m_model->root_path();
auto error_message = String::formatted("Could not read {}:\n{}", failed_path, error_string);
m_error_label->set_text(error_message);
set_active_widget(m_error_label);
m_mkdir_action->set_enabled(false);
m_touch_action->set_enabled(false);
add_path_to_history(model().root_path());
if (on_path_change)
on_path_change(failed_path, false);
};
m_model->on_complete = [this] {
if (m_table_view)
m_table_view->selection().clear();
if (m_icon_view)
m_icon_view->selection().clear();
add_path_to_history(model().root_path());
bool can_write_in_path = access(model().root_path().characters(), W_OK) == 0;
m_mkdir_action->set_enabled(can_write_in_path);
m_touch_action->set_enabled(can_write_in_path);
if (on_path_change)
on_path_change(model().root_path(), can_write_in_path);
};
m_model->register_client(*this);
m_model->on_thumbnail_progress = [this](int done, int total) {
if (on_thumbnail_progress)
on_thumbnail_progress(done, total);
};
if (is_desktop())
m_model->set_root_path(Core::StandardPaths::desktop_directory());
}
void DirectoryView::setup_icon_view()
{
m_icon_view = add<GUI::IconView>();
m_icon_view->set_should_hide_unnecessary_scrollbars(true);
m_icon_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_icon_view->set_editable(true);
m_icon_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_icon_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
if (is_desktop()) {
m_icon_view->set_frame_shape(Gfx::FrameShape::NoFrame);
m_icon_view->set_scrollbars_enabled(false);
m_icon_view->set_fill_with_background_color(false);
m_icon_view->set_draw_item_text_with_shadow(true);
m_icon_view->set_flow_direction(GUI::IconView::FlowDirection::TopToBottom);
}
m_icon_view->set_model(m_sorting_model);
m_icon_view->set_model_column(GUI::FileSystemModel::Column::Name);
m_icon_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_icon_view->on_selection_change = [this] {
handle_selection_change();
};
m_icon_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_icon_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
void DirectoryView::setup_columns_view()
{
m_columns_view = add<GUI::ColumnsView>();
m_columns_view->set_should_hide_unnecessary_scrollbars(true);
m_columns_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_columns_view->set_editable(true);
m_columns_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_columns_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
m_columns_view->set_model(m_sorting_model);
m_columns_view->set_model_column(GUI::FileSystemModel::Column::Name);
m_columns_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_columns_view->on_selection_change = [this] {
handle_selection_change();
};
m_columns_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_columns_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
void DirectoryView::setup_table_view()
{
m_table_view = add<GUI::TableView>();
m_table_view->set_should_hide_unnecessary_scrollbars(true);
m_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_table_view->set_editable(true);
m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_table_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
m_table_view->set_model(m_sorting_model);
m_table_view->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
m_table_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_table_view->on_selection_change = [this] {
handle_selection_change();
};
m_table_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_table_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
DirectoryView::~DirectoryView()
{
m_model->unregister_client(*this);
}
void DirectoryView::model_did_update(unsigned flags)
{
if (flags & GUI::Model::UpdateFlag::InvalidateAllIndexes) {
for_each_view_implementation([](auto& view) {
view.selection().clear();
});
}
update_statusbar();
}
void DirectoryView::set_view_mode(ViewMode mode)
{
if (m_view_mode == mode)
return;
m_view_mode = mode;
update();
if (mode == ViewMode::Table) {
set_active_widget(m_table_view);
return;
}
if (mode == ViewMode::Columns) {
set_active_widget(m_columns_view);
return;
}
if (mode == ViewMode::Icon) {
set_active_widget(m_icon_view);
return;
}
ASSERT_NOT_REACHED();
}
void DirectoryView::add_path_to_history(const StringView& path)
{
if (m_path_history.size() && m_path_history.at(m_path_history_position) == path)
return;
if (m_path_history_position < m_path_history.size())
m_path_history.resize(m_path_history_position + 1);
m_path_history.append(path);
m_path_history_position = m_path_history.size() - 1;
}
void DirectoryView::open(const StringView& path)
{
if (model().root_path() == path) {
model().update();
return;
}
set_active_widget(&current_view());
model().set_root_path(path);
}
void DirectoryView::set_status_message(const StringView& message)
{
if (on_status_message)
on_status_message(message);
}
void DirectoryView::open_parent_directory()
{
auto path = String::formatted("{}/..", model().root_path());
model().set_root_path(path);
}
void DirectoryView::refresh()
{
model().update();
}
void DirectoryView::open_previous_directory()
{
if (m_path_history_position > 0) {
set_active_widget(&current_view());
m_path_history_position--;
model().set_root_path(m_path_history[m_path_history_position]);
}
}
void DirectoryView::open_next_directory()
{
if (m_path_history_position < m_path_history.size() - 1) {
set_active_widget(&current_view());
m_path_history_position++;
model().set_root_path(m_path_history[m_path_history_position]);
}
}
void DirectoryView::update_statusbar()
{
// If we're triggered during widget construction, just ignore it.
if (m_view_mode == ViewMode::Invalid)
return;
size_t total_size = model().node({}).total_size;
if (current_view().selection().is_empty()) {
set_status_message(String::formatted("{} item(s) ({})",
model().row_count(),
human_readable_size(total_size)));
return;
}
int selected_item_count = current_view().selection().size();
size_t selected_byte_count = 0;
current_view().selection().for_each_index([&](auto& index) {
auto& model = *current_view().model();
auto size_index = model.index(index.row(), GUI::FileSystemModel::Column::Size, model.parent_index(index));
auto file_size = size_index.data().to_i32();
selected_byte_count += file_size;
});
StringBuilder builder;
builder.append(String::number(selected_item_count));
builder.append(" item");
if (selected_item_count != 1)
builder.append('s');
builder.append(" selected (");
builder.append(human_readable_size(selected_byte_count).characters());
builder.append(')');
if (selected_item_count == 1) {
auto& node = this->node(current_view().selection().first());
if (!node.symlink_target.is_empty()) {
builder.append(" -> ");
builder.append(node.symlink_target);
}
}
set_status_message(builder.to_string());
}
void DirectoryView::set_should_show_dotfiles(bool show_dotfiles)
{
m_model->set_should_show_dotfiles(show_dotfiles);
}
void DirectoryView::launch(const URL&, const LauncherHandler& launcher_handler)
{
pid_t child;
if (launcher_handler.details().launcher_type == Desktop::Launcher::LauncherType::Application) {
const char* argv[] = { launcher_handler.details().name.characters(), nullptr };
posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
if (disown(child) < 0)
perror("disown");
} else {
for (auto& path : selected_file_paths()) {
const char* argv[] = { launcher_handler.details().name.characters(), path.characters(), nullptr };
posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
if (disown(child) < 0)
perror("disown");
}
}
}
Vector<String> DirectoryView::selected_file_paths() const
{
Vector<String> paths;
auto& view = current_view();
auto& model = *view.model();
view.selection().for_each_index([&](const GUI::ModelIndex& index) {
auto parent_index = model.parent_index(index);
auto name_index = model.index(index.row(), GUI::FileSystemModel::Column::Name, parent_index);
auto path = name_index.data(GUI::ModelRole::Custom).to_string();
paths.append(path);
});
return paths;
}
void DirectoryView::do_delete(bool should_confirm)
{
auto paths = selected_file_paths();
ASSERT(!paths.is_empty());
FileUtils::delete_paths(paths, should_confirm, window());
}
void DirectoryView::handle_selection_change()
{
update_statusbar();
bool can_delete = !current_view().selection().is_empty() && access(path().characters(), W_OK) == 0;
m_delete_action->set_enabled(can_delete);
m_force_delete_action->set_enabled(can_delete);
if (on_selection_change)
on_selection_change(current_view());
}
void DirectoryView::setup_actions()
{
m_mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter name:", "New directory") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_dir_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
int rc = mkdir(new_dir_path.characters(), 0777);
if (rc < 0) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("mkdir(\"{}\") failed: {}", new_dir_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
}
}
});
m_touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter name:", "New file") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_file_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
struct stat st;
int rc = stat(new_file_path.characters(), &st);
if ((rc < 0 && errno != ENOENT)) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("stat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
return;
}
if (rc == 0) {
GUI::MessageBox::show(window(), String::formatted("{}: Already exists", new_file_path), "Error", GUI::MessageBox::Type::Error);
return;
}
int fd = creat(new_file_path.characters(), 0666);
if (fd < 0) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("creat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
return;
}
rc = close(fd);
ASSERT(rc >= 0);
}
});
m_open_terminal_action = GUI::Action::create("Open Terminal here", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
posix_spawn_file_actions_t spawn_actions;
posix_spawn_file_actions_init(&spawn_actions);
posix_spawn_file_actions_addchdir(&spawn_actions, path().characters());
pid_t pid;
const char* argv[] = { "Terminal", nullptr };
if ((errno = posix_spawn(&pid, "/bin/Terminal", &spawn_actions, nullptr, const_cast<char**>(argv), environ))) {
perror("posix_spawn");
} else {
if (disown(pid) < 0)
perror("disown");
}
posix_spawn_file_actions_destroy(&spawn_actions);
});
m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { do_delete(true); }, window());
m_force_delete_action = GUI::Action::create(
"Delete without confirmation", { Mod_Shift, Key_Delete },
[this](auto&) { do_delete(false); },
window());
}
void DirectoryView::handle_drop(const GUI::ModelIndex& index, const GUI::DropEvent& event)
{
if (!event.mime_data().has_urls())
return;
auto urls = event.mime_data().urls();
if (urls.is_empty()) {
dbgln("No files to drop");
return;
}
auto& target_node = node(index);
if (!target_node.is_directory())
return;
bool had_accepted_drop = false;
for (auto& url_to_copy : urls) {
if (!url_to_copy.is_valid() || url_to_copy.path() == target_node.full_path())
continue;
auto new_path = String::formatted("{}/{}", target_node.full_path(), LexicalPath(url_to_copy.path()).basename());
if (url_to_copy.path() == new_path)
continue;
if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
} else {
had_accepted_drop = true;
}
}
if (had_accepted_drop && on_accepted_drop)
on_accepted_drop();
}
}

View file

@ -0,0 +1,190 @@
/*
* 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/URL.h>
#include <AK/Vector.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/ColumnsView.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/IconView.h>
#include <LibGUI/StackWidget.h>
#include <LibGUI/TableView.h>
#include <sys/stat.h>
namespace FileManager {
class LauncherHandler : public RefCounted<LauncherHandler> {
public:
LauncherHandler(const NonnullRefPtr<Desktop::Launcher::Details>& details)
: m_details(details)
{
}
NonnullRefPtr<GUI::Action> create_launch_action(Function<void(const LauncherHandler&)>);
const Desktop::Launcher::Details& details() const { return *m_details; }
private:
NonnullRefPtr<Desktop::Launcher::Details> m_details;
};
class DirectoryView final
: public GUI::StackWidget
, private GUI::ModelClient {
C_OBJECT(DirectoryView);
public:
enum class Mode {
Desktop,
Normal,
};
virtual ~DirectoryView() override;
void open(const StringView& path);
String path() const { return model().root_path(); }
void open_parent_directory();
void open_previous_directory();
void open_next_directory();
int path_history_size() const { return m_path_history.size(); }
int path_history_position() const { return m_path_history_position; }
static RefPtr<LauncherHandler> get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers);
NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const URL& url);
NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const String& path);
void refresh();
void launch(const AK::URL&, const LauncherHandler&);
Function<void(const StringView& path, bool can_write_in_path)> on_path_change;
Function<void(GUI::AbstractView&)> on_selection_change;
Function<void(const GUI::ModelIndex&, const GUI::ContextMenuEvent&)> on_context_menu_request;
Function<void(const StringView&)> on_status_message;
Function<void(int done, int total)> on_thumbnail_progress;
Function<void()> on_accepted_drop;
enum ViewMode {
Invalid,
Table,
Columns,
Icon
};
void set_view_mode(ViewMode);
ViewMode view_mode() const { return m_view_mode; }
GUI::AbstractView& current_view()
{
switch (m_view_mode) {
case ViewMode::Table:
return *m_table_view;
case ViewMode::Columns:
return *m_columns_view;
case ViewMode::Icon:
return *m_icon_view;
default:
ASSERT_NOT_REACHED();
}
}
const GUI::AbstractView& current_view() const
{
return const_cast<DirectoryView*>(this)->current_view();
}
template<typename Callback>
void for_each_view_implementation(Callback callback)
{
if (m_icon_view)
callback(*m_icon_view);
if (m_table_view)
callback(*m_table_view);
if (m_columns_view)
callback(*m_columns_view);
}
void set_should_show_dotfiles(bool);
const GUI::FileSystemModel::Node& node(const GUI::ModelIndex&) const;
bool is_desktop() const { return m_mode == Mode::Desktop; }
Vector<String> selected_file_paths() const;
GUI::Action& mkdir_action() { return *m_mkdir_action; }
GUI::Action& touch_action() { return *m_touch_action; }
GUI::Action& open_terminal_action() { return *m_open_terminal_action; }
GUI::Action& delete_action() { return *m_delete_action; }
GUI::Action& force_delete_action() { return *m_force_delete_action; }
private:
explicit DirectoryView(Mode);
const GUI::FileSystemModel& model() const { return *m_model; }
GUI::FileSystemModel& model() { return *m_model; }
void handle_selection_change();
void handle_drop(const GUI::ModelIndex&, const GUI::DropEvent&);
void do_delete(bool should_confirm);
// ^GUI::ModelClient
virtual void model_did_update(unsigned) override;
void setup_actions();
void setup_model();
void setup_icon_view();
void setup_columns_view();
void setup_table_view();
void handle_activation(const GUI::ModelIndex&);
void set_status_message(const StringView&);
void update_statusbar();
Mode m_mode { Mode::Normal };
ViewMode m_view_mode { Invalid };
NonnullRefPtr<GUI::FileSystemModel> m_model;
NonnullRefPtr<GUI::SortingProxyModel> m_sorting_model;
size_t m_path_history_position { 0 };
Vector<String> m_path_history;
void add_path_to_history(const StringView& path);
RefPtr<GUI::Label> m_error_label;
RefPtr<GUI::TableView> m_table_view;
RefPtr<GUI::IconView> m_icon_view;
RefPtr<GUI::ColumnsView> m_columns_view;
RefPtr<GUI::Action> m_mkdir_action;
RefPtr<GUI::Action> m_touch_action;
RefPtr<GUI::Action> m_open_terminal_action;
RefPtr<GUI::Action> m_delete_action;
RefPtr<GUI::Action> m_force_delete_action;
};
}

View file

@ -0,0 +1,53 @@
@GUI::Widget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
spacing: 2
}
@GUI::ToolBarContainer {
@GUI::ToolBar {
name: "main_toolbar"
}
@GUI::ToolBar {
name: "location_toolbar"
visible: false
@GUI::Label {
text: "Location: "
autosize: true
}
@GUI::TextBox {
name: "location_textbox"
fixed_height: 22
}
}
@GUI::ToolBar {
name: "breadcrumb_toolbar"
@GUI::BreadcrumbBar {
name: "breadcrumb_bar"
}
}
}
@GUI::HorizontalSplitter {
name: "splitter"
@GUI::TreeView {
name: "tree_view"
fixed_width: 175
}
}
@GUI::StatusBar {
name: "statusbar"
@GUI::ProgressBar {
name: "progressbar"
text: "Generating thumbnails: "
visible: false
}
}
}

View file

@ -0,0 +1,280 @@
/*
* 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 "FileUtils.h"
#include <AK/LexicalPath.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibGUI/MessageBox.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
namespace FileUtils {
void delete_path(const String& path, GUI::Window* parent_window)
{
struct stat st;
if (lstat(path.characters(), &st)) {
GUI::MessageBox::show(parent_window,
String::formatted("lstat({}) failed: {}", path, strerror(errno)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
if (S_ISDIR(st.st_mode)) {
String error_path;
int error = FileUtils::delete_directory(path, error_path);
if (error) {
GUI::MessageBox::show(parent_window,
String::formatted("Failed to delete directory \"{}\": {}", error_path, strerror(error)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
} else if (unlink(path.characters()) < 0) {
int saved_errno = errno;
GUI::MessageBox::show(parent_window,
String::formatted("unlink(\"{}\") failed: {}", path, strerror(saved_errno)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
}
void delete_paths(const Vector<String>& paths, bool should_confirm, GUI::Window* parent_window)
{
String message;
if (paths.size() == 1) {
message = String::formatted("Really delete {}?", LexicalPath(paths[0]).basename());
} else {
message = String::formatted("Really delete {} files?", paths.size());
}
if (should_confirm) {
auto result = GUI::MessageBox::show(parent_window,
message,
"Confirm deletion",
GUI::MessageBox::Type::Warning,
GUI::MessageBox::InputType::OKCancel);
if (result == GUI::MessageBox::ExecCancel)
return;
}
for (auto& path : paths) {
delete_path(path, parent_window);
}
}
int delete_directory(String directory, String& file_that_caused_error)
{
Core::DirIterator iterator(directory, Core::DirIterator::SkipDots);
if (iterator.has_error()) {
file_that_caused_error = directory;
return -1;
}
while (iterator.has_next()) {
auto file_to_delete = String::formatted("{}/{}", directory, iterator.next_path());
struct stat st;
if (lstat(file_to_delete.characters(), &st)) {
file_that_caused_error = file_to_delete;
return errno;
}
if (S_ISDIR(st.st_mode)) {
if (delete_directory(file_to_delete, file_to_delete)) {
file_that_caused_error = file_to_delete;
return errno;
}
} else if (unlink(file_to_delete.characters())) {
file_that_caused_error = file_to_delete;
return errno;
}
}
if (rmdir(directory.characters())) {
file_that_caused_error = directory;
return errno;
}
return 0;
}
bool copy_file_or_directory(const String& src_path, const String& dst_path)
{
int duplicate_count = 0;
while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
++duplicate_count;
}
if (duplicate_count != 0) {
return copy_file_or_directory(src_path, get_duplicate_name(dst_path, duplicate_count));
}
auto source_or_error = Core::File::open(src_path, Core::IODevice::ReadOnly);
if (source_or_error.is_error())
return false;
auto& source = *source_or_error.value();
struct stat src_stat;
int rc = fstat(source.fd(), &src_stat);
if (rc < 0)
return false;
if (source.is_directory())
return copy_directory(src_path, dst_path, src_stat);
return copy_file(dst_path, src_stat, source);
}
bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat)
{
int rc = mkdir(dst_path.characters(), 0755);
if (rc < 0) {
return false;
}
Core::DirIterator di(src_path, Core::DirIterator::SkipDots);
if (di.has_error()) {
return false;
}
while (di.has_next()) {
String filename = di.next_path();
bool is_copied = copy_file_or_directory(
String::formatted("{}/{}", src_path, filename),
String::formatted("{}/{}", dst_path, filename));
if (!is_copied) {
return false;
}
}
auto my_umask = umask(0);
umask(my_umask);
rc = chmod(dst_path.characters(), src_stat.st_mode & ~my_umask);
if (rc < 0) {
return false;
}
return true;
}
bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File& source)
{
int dst_fd = creat(dst_path.characters(), 0666);
if (dst_fd < 0) {
if (errno != EISDIR) {
return false;
}
auto dst_dir_path = String::formatted("{}/{}", dst_path, LexicalPath(source.filename()).basename());
dst_fd = creat(dst_dir_path.characters(), 0666);
if (dst_fd < 0) {
return false;
}
}
ScopeGuard close_fd_guard([dst_fd]() { close(dst_fd); });
if (src_stat.st_size > 0) {
if (ftruncate(dst_fd, src_stat.st_size) < 0) {
perror("cp: ftruncate");
return false;
}
}
for (;;) {
char buffer[32768];
ssize_t nread = read(source.fd(), buffer, sizeof(buffer));
if (nread < 0) {
return false;
}
if (nread == 0)
break;
ssize_t remaining_to_write = nread;
char* bufptr = buffer;
while (remaining_to_write) {
ssize_t nwritten = write(dst_fd, bufptr, remaining_to_write);
if (nwritten < 0) {
return false;
}
assert(nwritten > 0);
remaining_to_write -= nwritten;
bufptr += nwritten;
}
}
auto my_umask = umask(0);
umask(my_umask);
int rc = fchmod(dst_fd, src_stat.st_mode & ~my_umask);
if (rc < 0) {
return false;
}
return true;
}
bool link_file(const String& src_path, const String& dst_path)
{
int duplicate_count = 0;
while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
++duplicate_count;
}
if (duplicate_count != 0) {
return link_file(src_path, get_duplicate_name(dst_path, duplicate_count));
}
int rc = symlink(src_path.characters(), dst_path.characters());
if (rc < 0) {
return false;
}
return true;
}
String get_duplicate_name(const String& path, int duplicate_count)
{
if (duplicate_count == 0) {
return path;
}
LexicalPath lexical_path(path);
StringBuilder duplicated_name;
duplicated_name.append('/');
for (size_t i = 0; i < lexical_path.parts().size() - 1; ++i) {
duplicated_name.appendff("{}/", lexical_path.parts()[i]);
}
auto prev_duplicate_tag = String::formatted("({})", duplicate_count);
auto title = lexical_path.title();
if (title.ends_with(prev_duplicate_tag)) {
// remove the previous duplicate tag "(n)" so we can add a new tag.
title = title.substring(0, title.length() - prev_duplicate_tag.length());
}
duplicated_name.appendff("{} ({})", lexical_path.title(), duplicate_count);
if (!lexical_path.extension().is_empty()) {
duplicated_name.appendff(".{}", lexical_path.extension());
}
return duplicated_name.build();
}
}

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 <AK/String.h>
#include <LibCore/Forward.h>
#include <LibGUI/Forward.h>
#include <sys/stat.h>
namespace FileUtils {
enum class FileOperation {
Copy = 0,
Cut
};
void delete_path(const String&, GUI::Window*);
void delete_paths(const Vector<String>&, bool should_confirm, GUI::Window*);
int delete_directory(String directory, String& file_that_caused_error);
bool copy_file_or_directory(const String& src_path, const String& dst_path);
String get_duplicate_name(const String& path, int duplicate_count);
bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File&);
bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat);
bool link_file(const String& src_path, const String& dst_path);
}

View file

@ -0,0 +1,304 @@
/*
* 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 "PropertiesWindow.h"
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/LinkLabel.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/SeparatorWidget.h>
#include <LibGUI/TabWidget.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
PropertiesWindow::PropertiesWindow(const String& path, bool disable_rename, Window* parent_window)
: Window(parent_window)
{
auto lexical_path = LexicalPath(path);
ASSERT(lexical_path.is_valid());
auto& main_widget = set_main_widget<GUI::Widget>();
main_widget.set_layout<GUI::VerticalBoxLayout>();
main_widget.layout()->set_margins({ 4, 4, 4, 4 });
main_widget.set_fill_with_background_color(true);
set_rect({ 0, 0, 360, 420 });
set_resizable(false);
auto& tab_widget = main_widget.add<GUI::TabWidget>();
auto& general_tab = tab_widget.add_tab<GUI::Widget>("General");
general_tab.set_layout<GUI::VerticalBoxLayout>();
general_tab.layout()->set_margins({ 12, 8, 12, 8 });
general_tab.layout()->set_spacing(10);
auto& file_container = general_tab.add<GUI::Widget>();
file_container.set_layout<GUI::HorizontalBoxLayout>();
file_container.layout()->set_spacing(20);
file_container.set_fixed_height(34);
m_icon = file_container.add<GUI::ImageWidget>();
m_icon->set_fixed_size(32, 32);
m_name = lexical_path.basename();
m_path = lexical_path.string();
m_parent_path = lexical_path.dirname();
m_name_box = file_container.add<GUI::TextBox>();
m_name_box->set_text(m_name);
m_name_box->set_mode(disable_rename ? GUI::TextBox::Mode::DisplayOnly : GUI::TextBox::Mode::Editable);
m_name_box->on_change = [&]() {
m_name_dirty = m_name != m_name_box->text();
m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
};
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"));
general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
struct stat st;
if (lstat(path.characters(), &st)) {
perror("stat");
return;
}
String owner_name;
String group_name;
if (auto* pw = getpwuid(st.st_uid)) {
owner_name = pw->pw_name;
} else {
owner_name = "n/a";
}
if (auto* gr = getgrgid(st.st_gid)) {
group_name = gr->gr_name;
} else {
group_name = "n/a";
}
m_mode = st.st_mode;
m_old_mode = st.st_mode;
auto properties = Vector<PropertyValuePair>();
properties.append({ "Type:", get_description(m_mode) });
auto parent_link = URL::create_with_file_protocol(m_parent_path);
properties.append(PropertyValuePair { "Location:", path, Optional(parent_link) });
if (S_ISLNK(m_mode)) {
auto link_destination = Core::File::read_link(path);
if (link_destination.is_null()) {
perror("readlink");
} else {
auto link_directory = LexicalPath(link_destination);
ASSERT(link_directory.is_valid());
auto link_parent = URL::create_with_file_protocol(link_directory.dirname());
properties.append({ "Link target:", link_destination, Optional(link_parent) });
}
}
properties.append({ "Size:", String::formatted("{} bytes", st.st_size) });
properties.append({ "Owner:", String::formatted("{} ({})", owner_name, st.st_uid) });
properties.append({ "Group:", String::formatted("{} ({})", group_name, st.st_gid) });
properties.append({ "Created at:", GUI::FileSystemModel::timestamp_string(st.st_ctime) });
properties.append({ "Last modified:", GUI::FileSystemModel::timestamp_string(st.st_mtime) });
make_property_value_pairs(properties, general_tab);
general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
make_permission_checkboxes(general_tab, { S_IRUSR, S_IWUSR, S_IXUSR }, "Owner:", m_mode);
make_permission_checkboxes(general_tab, { S_IRGRP, S_IWGRP, S_IXGRP }, "Group:", m_mode);
make_permission_checkboxes(general_tab, { S_IROTH, S_IWOTH, S_IXOTH }, "Others:", m_mode);
general_tab.layout()->add_spacer();
auto& button_widget = main_widget.add<GUI::Widget>();
button_widget.set_layout<GUI::HorizontalBoxLayout>();
button_widget.set_fixed_height(24);
button_widget.layout()->set_spacing(5);
button_widget.layout()->add_spacer();
make_button("OK", button_widget).on_click = [this](auto) {
if (apply_changes())
close();
};
make_button("Cancel", button_widget).on_click = [this](auto) {
close();
};
m_apply_button = make_button("Apply", button_widget);
m_apply_button->on_click = [this](auto) { apply_changes(); };
m_apply_button->set_enabled(false);
update();
}
PropertiesWindow::~PropertiesWindow()
{
}
void PropertiesWindow::update()
{
m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32));
set_title(String::formatted("{} - Properties", m_name));
}
void PropertiesWindow::permission_changed(mode_t mask, bool set)
{
if (set) {
m_mode |= mask;
} else {
m_mode &= ~mask;
}
m_permissions_dirty = m_mode != m_old_mode;
m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
}
String PropertiesWindow::make_full_path(const String& name)
{
return String::formatted("{}/{}", m_parent_path, name);
}
bool PropertiesWindow::apply_changes()
{
if (m_name_dirty) {
String new_name = m_name_box->text();
String new_file = make_full_path(new_name).characters();
if (GUI::FilePicker::file_exists(new_file)) {
GUI::MessageBox::show(this, String::formatted("A file \"{}\" already exists!", new_name), "Error", GUI::MessageBox::Type::Error);
return false;
}
if (rename(make_full_path(m_name).characters(), new_file.characters())) {
GUI::MessageBox::show(this, String::formatted("Could not rename file: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
return false;
}
m_name = new_name;
m_name_dirty = false;
update();
}
if (m_permissions_dirty) {
if (chmod(make_full_path(m_name).characters(), m_mode)) {
GUI::MessageBox::show(this, String::formatted("Could not update permissions: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
return false;
}
m_old_mode = m_mode;
m_permissions_dirty = false;
}
update();
m_apply_button->set_enabled(false);
return true;
}
void PropertiesWindow::make_permission_checkboxes(GUI::Widget& parent, PermissionMasks masks, String label_string, mode_t mode)
{
auto& widget = parent.add<GUI::Widget>();
widget.set_layout<GUI::HorizontalBoxLayout>();
widget.set_fixed_height(16);
widget.layout()->set_spacing(10);
auto& label = widget.add<GUI::Label>(label_string);
label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
struct stat st;
if (lstat(m_path.characters(), &st)) {
perror("stat");
return;
}
auto can_edit_checkboxes = st.st_uid == getuid();
auto& box_read = widget.add<GUI::CheckBox>("Read");
box_read.set_checked(mode & masks.read);
box_read.on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); };
box_read.set_enabled(can_edit_checkboxes);
auto& box_write = widget.add<GUI::CheckBox>("Write");
box_write.set_checked(mode & masks.write);
box_write.on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); };
box_write.set_enabled(can_edit_checkboxes);
auto& box_execute = widget.add<GUI::CheckBox>("Execute");
box_execute.set_checked(mode & masks.execute);
box_execute.on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); };
box_execute.set_enabled(can_edit_checkboxes);
}
void PropertiesWindow::make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent)
{
int max_width = 0;
Vector<NonnullRefPtr<GUI::Label>> property_labels;
property_labels.ensure_capacity(pairs.size());
for (auto pair : pairs) {
auto& label_container = parent.add<GUI::Widget>();
label_container.set_layout<GUI::HorizontalBoxLayout>();
label_container.set_fixed_height(14);
label_container.layout()->set_spacing(12);
auto& label_property = label_container.add<GUI::Label>(pair.property);
label_property.set_text_alignment(Gfx::TextAlignment::CenterLeft);
if (!pair.link.has_value()) {
label_container.add<GUI::Label>(pair.value).set_text_alignment(Gfx::TextAlignment::CenterLeft);
} else {
auto& link = label_container.add<GUI::LinkLabel>(pair.value);
link.set_text_alignment(Gfx::TextAlignment::CenterLeft);
link.on_click = [pair]() {
Desktop::Launcher::open(pair.link.value());
};
}
max_width = max(max_width, label_property.font().width(pair.property));
property_labels.append(label_property);
}
for (auto label : property_labels)
label->set_fixed_width(max_width);
}
GUI::Button& PropertiesWindow::make_button(String text, GUI::Widget& parent)
{
auto& button = parent.add<GUI::Button>(text);
button.set_fixed_size(70, 22);
return button;
}

View file

@ -0,0 +1,98 @@
/*
* 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 <LibCore/File.h>
#include <LibGUI/Button.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Label.h>
#include <LibGUI/TextBox.h>
class PropertiesWindow final : public GUI::Window {
C_OBJECT(PropertiesWindow);
public:
virtual ~PropertiesWindow() override;
private:
PropertiesWindow(const String& path, bool disable_rename, Window* parent = nullptr);
struct PropertyValuePair {
String property;
String value;
Optional<URL> link = {};
};
struct PermissionMasks {
mode_t read;
mode_t write;
mode_t execute;
};
static const String get_description(const mode_t mode)
{
if (S_ISREG(mode))
return "File";
if (S_ISDIR(mode))
return "Directory";
if (S_ISLNK(mode))
return "Symbolic link";
if (S_ISCHR(mode))
return "Character device";
if (S_ISBLK(mode))
return "Block device";
if (S_ISFIFO(mode))
return "FIFO (named pipe)";
if (S_ISSOCK(mode))
return "Socket";
if (mode & S_IXUSR)
return "Executable";
return "Unknown";
}
GUI::Button& make_button(String, GUI::Widget& parent);
void make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent);
void make_permission_checkboxes(GUI::Widget& parent, PermissionMasks, String label_string, mode_t mode);
void permission_changed(mode_t mask, bool set);
bool apply_changes();
void update();
String make_full_path(const String& name);
RefPtr<GUI::Button> m_apply_button;
RefPtr<GUI::TextBox> m_name_box;
RefPtr<GUI::ImageWidget> m_icon;
String m_name;
String m_parent_path;
String m_path;
mode_t m_mode { 0 };
mode_t m_old_mode { 0 };
bool m_permissions_dirty { false };
bool m_name_dirty { false };
};

View file

@ -0,0 +1,974 @@
/*
* 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 "DesktopWidget.h"
#include "DirectoryView.h"
#include "FileUtils.h"
#include "PropertiesWindow.h"
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <AK/URL.h>
#include <Applications/FileManager/FileManagerWindowGML.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/MimeData.h>
#include <LibCore/StandardPaths.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/BreadcrumbBar.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ProgressBar.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/StatusBar.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/TreeView.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
#include <pthread.h>
#include <serenity.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
using namespace FileManager;
static int run_in_desktop_mode(RefPtr<Core::ConfigFile>);
static int run_in_windowed_mode(RefPtr<Core::ConfigFile>, String initial_location);
static void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation);
static void do_paste(const String& target_directory, GUI::Window* window);
static void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window);
static void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window);
int main(int argc, char** argv)
{
if (pledge("stdio thread shared_buffer accept unix cpath rpath wpath fattr proc exec sigaction", nullptr) < 0) {
perror("pledge");
return 1;
}
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_flags = SA_NOCLDWAIT;
act.sa_handler = SIG_IGN;
int rc = sigaction(SIGCHLD, &act, nullptr);
if (rc < 0) {
perror("sigaction");
return 1;
}
RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("FileManager");
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio thread shared_buffer accept cpath rpath wpath fattr proc exec unix", nullptr) < 0) {
perror("pledge");
return 1;
}
if (app->args().contains_slow("--desktop") || app->args().contains_slow("-d"))
return run_in_desktop_mode(move(config));
// our initial location is defined as, in order of precedence:
// 1. the first command-line argument (e.g. FileManager /bin)
// 2. the user's home directory
// 3. the root directory
String initial_location;
if (argc >= 2) {
char* buffer = realpath(argv[1], nullptr);
initial_location = buffer;
free(buffer);
}
if (initial_location.is_empty())
initial_location = Core::StandardPaths::home_directory();
if (initial_location.is_empty())
initial_location = "/";
return run_in_windowed_mode(move(config), initial_location);
}
void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation)
{
if (selected_file_paths.is_empty())
ASSERT_NOT_REACHED();
StringBuilder copy_text;
if (file_operation == FileUtils::FileOperation::Cut) {
copy_text.append("#cut\n"); // This exploits the comment lines in the text/uri-list specification, which might be a bit hackish
}
for (auto& path : selected_file_paths) {
auto url = URL::create_with_file_protocol(path);
copy_text.appendff("{}\n", url);
}
GUI::Clipboard::the().set_data(copy_text.build().bytes(), "text/uri-list");
}
void do_paste(const String& target_directory, GUI::Window* window)
{
auto data_and_type = GUI::Clipboard::the().data_and_type();
if (data_and_type.mime_type != "text/uri-list") {
dbgln("Cannot paste clipboard type {}", data_and_type.mime_type);
return;
}
auto copied_lines = String::copy(data_and_type.data).split('\n');
if (copied_lines.is_empty()) {
dbgln("No files to paste");
return;
}
bool should_delete_src = false;
if (copied_lines[0] == "#cut") { // cut operation encoded as a text/uri-list commen
should_delete_src = true;
copied_lines.remove(0);
}
for (auto& uri_as_string : copied_lines) {
if (uri_as_string.is_empty())
continue;
URL url = uri_as_string;
if (!url.is_valid() || url.protocol() != "file") {
dbgln("Cannot paste URI {}", uri_as_string);
continue;
}
auto new_path = String::formatted("{}/{}", target_directory, url.basename());
if (!FileUtils::copy_file_or_directory(url.path(), new_path)) {
auto error_message = String::formatted("Could not paste {}.", url.path());
GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
} else if (should_delete_src) {
FileUtils::delete_path(url.path(), window);
}
}
}
void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window)
{
auto path = selected_file_paths.first();
auto destination = String::formatted("{}/{}", Core::StandardPaths::desktop_directory(), LexicalPath { path }.basename());
if (!FileUtils::link_file(path, destination)) {
GUI::MessageBox::show(window, "Could not create desktop shortcut", "File Manager",
GUI::MessageBox::Type::Error);
}
}
void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window)
{
RefPtr<PropertiesWindow> properties;
if (selected.is_empty()) {
properties = window->add<PropertiesWindow>(path, true);
} else {
properties = window->add<PropertiesWindow>(selected.first(), access(container_dir_path.characters(), W_OK) != 0);
}
properties->on_close = [properties = properties.ptr()] {
properties->remove_from_parent();
};
properties->center_on_screen();
properties->show();
}
int run_in_desktop_mode([[maybe_unused]] RefPtr<Core::ConfigFile> config)
{
static constexpr const char* process_name = "FileManager (Desktop)";
set_process_name(process_name, strlen(process_name));
pthread_setname_np(pthread_self(), process_name);
auto window = GUI::Window::construct();
window->set_title("Desktop Manager");
window->set_window_type(GUI::WindowType::Desktop);
window->set_has_alpha_channel(true);
auto& desktop_widget = window->set_main_widget<FileManager::DesktopWidget>();
desktop_widget.set_layout<GUI::VerticalBoxLayout>();
[[maybe_unused]] auto& directory_view = desktop_widget.add<DirectoryView>(DirectoryView::Mode::Desktop);
auto copy_action = GUI::CommonActions::make_copy_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Copy);
},
window);
copy_action->set_enabled(false);
auto cut_action = GUI::CommonActions::make_cut_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Cut);
},
window);
cut_action->set_enabled(false);
directory_view.on_selection_change = [&](const GUI::AbstractView& view) {
copy_action->set_enabled(!view.selection().is_empty());
cut_action->set_enabled(!view.selection().is_empty());
};
auto properties_action
= GUI::Action::create(
"Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action&) {
String path = directory_view.path();
Vector<String> selected = directory_view.selected_file_paths();
show_properties(path, path, selected, directory_view.window());
},
window);
auto paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action&) {
do_paste(directory_view.path(), directory_view.window());
},
window);
paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
GUI::Clipboard::the().on_change = [&](const String& data_type) {
paste_action->set_enabled(data_type == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
};
auto desktop_view_context_menu = GUI::Menu::construct("Directory View");
auto file_manager_action = GUI::Action::create("Show in File Manager", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-folder.png"), [&](const GUI::Action&) {
Desktop::Launcher::open(URL::create_with_file_protocol(directory_view.path()));
});
auto display_properties_action = GUI::Action::create("Display Settings", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"), [&](const GUI::Action&) {
Desktop::Launcher::open(URL::create_with_file_protocol("/bin/DisplaySettings"));
});
desktop_view_context_menu->add_action(directory_view.mkdir_action());
desktop_view_context_menu->add_action(directory_view.touch_action());
desktop_view_context_menu->add_action(paste_action);
desktop_view_context_menu->add_separator();
desktop_view_context_menu->add_action(file_manager_action);
desktop_view_context_menu->add_action(directory_view.open_terminal_action());
desktop_view_context_menu->add_separator();
desktop_view_context_menu->add_action(display_properties_action);
auto desktop_context_menu = GUI::Menu::construct("Directory View Directory");
desktop_context_menu->add_action(copy_action);
desktop_context_menu->add_action(cut_action);
desktop_context_menu->add_action(paste_action);
desktop_context_menu->add_action(directory_view.delete_action());
desktop_context_menu->add_separator();
desktop_context_menu->add_action(properties_action);
directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (!index.is_valid())
desktop_view_context_menu->popup(event.screen_position());
else
desktop_context_menu->popup(event.screen_position());
};
auto wm_config = Core::ConfigFile::get_for_app("WindowManager");
auto selected_wallpaper = wm_config->read_entry("Background", "Wallpaper", "");
if (!selected_wallpaper.is_empty()) {
GUI::Desktop::the().set_wallpaper(selected_wallpaper, false);
}
window->show();
return GUI::Application::the()->exec();
}
int run_in_windowed_mode(RefPtr<Core::ConfigFile> config, String initial_location)
{
auto window = GUI::Window::construct();
window->set_title("File Manager");
auto left = config->read_num_entry("Window", "Left", 150);
auto top = config->read_num_entry("Window", "Top", 75);
auto width = config->read_num_entry("Window", "Width", 640);
auto height = config->read_num_entry("Window", "Height", 480);
window->set_rect({ left, top, width, height });
auto& widget = window->set_main_widget<GUI::Widget>();
widget.load_from_gml(file_manager_window_gml);
auto& main_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("main_toolbar");
auto& location_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("location_toolbar");
location_toolbar.layout()->set_margins({ 6, 3, 6, 3 });
auto& location_textbox = *widget.find_descendant_of_type_named<GUI::TextBox>("location_textbox");
auto& breadcrumb_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("breadcrumb_toolbar");
breadcrumb_toolbar.layout()->set_margins({});
auto& breadcrumb_bar = *widget.find_descendant_of_type_named<GUI::BreadcrumbBar>("breadcrumb_bar");
location_textbox.on_focusout = [&] {
location_toolbar.set_visible(false);
breadcrumb_toolbar.set_visible(true);
};
auto& splitter = *widget.find_descendant_of_type_named<GUI::HorizontalSplitter>("splitter");
auto& tree_view = *widget.find_descendant_of_type_named<GUI::TreeView>("tree_view");
auto directories_model = GUI::FileSystemModel::create({}, GUI::FileSystemModel::Mode::DirectoriesOnly);
tree_view.set_model(directories_model);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Icon, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Size, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Owner, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Group, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Permissions, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::ModificationTime, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Inode, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::SymlinkTarget, true);
bool is_reacting_to_tree_view_selection_change = false;
auto& directory_view = splitter.add<DirectoryView>(DirectoryView::Mode::Normal);
location_textbox.on_escape_pressed = [&] {
directory_view.set_focus(true);
};
// Open the root directory. FIXME: This is awkward.
tree_view.toggle_index(directories_model->index(0, 0, {}));
auto& statusbar = *widget.find_descendant_of_type_named<GUI::StatusBar>("statusbar");
auto& progressbar = *widget.find_descendant_of_type_named<GUI::ProgressBar>("progressbar");
progressbar.set_format(GUI::ProgressBar::Format::ValueSlashMax);
progressbar.set_frame_shape(Gfx::FrameShape::Panel);
progressbar.set_frame_shadow(Gfx::FrameShadow::Sunken);
progressbar.set_frame_thickness(1);
location_textbox.on_return_pressed = [&] {
directory_view.open(location_textbox.text());
};
auto refresh_tree_view = [&] {
directories_model->update();
auto current_path = directory_view.path();
struct stat st;
// If the directory no longer exists, we find a parent that does.
while (stat(current_path.characters(), &st) != 0) {
directory_view.open_parent_directory();
current_path = directory_view.path();
if (current_path == directories_model->root_path()) {
break;
}
}
// Reselect the existing folder in the tree.
auto new_index = directories_model->index(current_path, GUI::FileSystemModel::Column::Name);
if (new_index.is_valid()) {
tree_view.expand_all_parents_of(new_index);
tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set, true);
}
directory_view.refresh();
};
auto directory_context_menu = GUI::Menu::construct("Directory View Directory");
auto directory_view_context_menu = GUI::Menu::construct("Directory View");
auto tree_view_directory_context_menu = GUI::Menu::construct("Tree View Directory");
auto tree_view_context_menu = GUI::Menu::construct("Tree View");
auto open_parent_directory_action = GUI::Action::create("Open parent directory", { Mod_Alt, Key_Up }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [&](const GUI::Action&) {
directory_view.open_parent_directory();
});
RefPtr<GUI::Action> view_as_table_action;
RefPtr<GUI::Action> view_as_icons_action;
RefPtr<GUI::Action> view_as_columns_action;
view_as_icons_action = GUI::Action::create_checkable(
"Icon view", { Mod_Ctrl, KeyCode::Key_1 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
config->write_entry("DirectoryView", "ViewMode", "Icon");
config->sync();
},
window);
view_as_table_action = GUI::Action::create_checkable(
"Table view", { Mod_Ctrl, KeyCode::Key_2 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/table-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Table);
config->write_entry("DirectoryView", "ViewMode", "Table");
config->sync();
},
window);
view_as_columns_action = GUI::Action::create_checkable(
"Columns view", { Mod_Ctrl, KeyCode::Key_3 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/columns-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
config->write_entry("DirectoryView", "ViewMode", "Columns");
config->sync();
},
window);
auto view_type_action_group = make<GUI::ActionGroup>();
view_type_action_group->set_exclusive(true);
view_type_action_group->add_action(*view_as_icons_action);
view_type_action_group->add_action(*view_as_table_action);
view_type_action_group->add_action(*view_as_columns_action);
auto tree_view_selected_file_paths = [&] {
Vector<String> paths;
auto& view = tree_view;
view.selection().for_each_index([&](const GUI::ModelIndex& index) {
paths.append(directories_model->full_path(index));
});
return paths;
};
auto select_all_action = GUI::Action::create("Select all", { Mod_Ctrl, KeyCode::Key_A }, [&](const GUI::Action&) {
directory_view.current_view().select_all();
});
auto copy_action = GUI::CommonActions::make_copy_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
paths = tree_view_selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Copy);
refresh_tree_view();
},
window);
copy_action->set_enabled(false);
auto cut_action = GUI::CommonActions::make_cut_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
paths = tree_view_selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Cut);
refresh_tree_view();
},
window);
cut_action->set_enabled(false);
auto shortcut_action
= GUI::Action::create(
"Create desktop shortcut",
{},
Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-symlink.png"),
[&](const GUI::Action&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty()) {
return;
}
do_create_link(paths, directory_view.window());
},
window);
auto properties_action
= GUI::Action::create(
"Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action& action) {
String container_dir_path;
String path;
Vector<String> selected;
if (action.activator() == directory_context_menu || directory_view.active_widget()->is_focused()) {
path = directory_view.path();
container_dir_path = path;
selected = directory_view.selected_file_paths();
} else {
path = directories_model->full_path(tree_view.selection().first());
container_dir_path = LexicalPath(path).basename();
selected = tree_view_selected_file_paths();
}
show_properties(container_dir_path, path, selected, directory_view.window());
},
window);
auto paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action& action) {
String target_directory;
if (action.activator() == directory_context_menu)
target_directory = directory_view.selected_file_paths()[0];
else
target_directory = directory_view.path();
do_paste(target_directory, directory_view.window());
refresh_tree_view();
},
window);
auto folder_specific_paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action& action) {
String target_directory;
if (action.activator() == directory_context_menu)
target_directory = directory_view.selected_file_paths()[0];
else
target_directory = directory_view.path();
do_paste(target_directory, directory_view.window());
refresh_tree_view();
},
window);
auto go_back_action = GUI::CommonActions::make_go_back_action(
[&](auto&) {
directory_view.open_previous_directory();
},
window);
auto go_forward_action = GUI::CommonActions::make_go_forward_action(
[&](auto&) {
directory_view.open_next_directory();
},
window);
auto go_home_action = GUI::CommonActions::make_go_home_action(
[&](auto&) {
directory_view.open(Core::StandardPaths::home_directory());
},
window);
GUI::Clipboard::the().on_change = [&](const String& data_type) {
auto current_location = directory_view.path();
paste_action->set_enabled(data_type == "text/uri-list" && access(current_location.characters(), W_OK) == 0);
};
auto tree_view_delete_action = GUI::CommonActions::make_delete_action(
[&](auto&) {
FileUtils::delete_paths(tree_view_selected_file_paths(), true, window);
refresh_tree_view();
},
&tree_view);
// This is a little awkward. The menu action does something different depending on which view has focus.
// It would be nice to find a good abstraction for this instead of creating a branching action like this.
auto focus_dependent_delete_action = GUI::CommonActions::make_delete_action([&](auto&) {
if (tree_view.is_focused())
tree_view_delete_action->activate();
else
directory_view.delete_action().activate();
refresh_tree_view();
});
focus_dependent_delete_action->set_enabled(false);
auto mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
directory_view.mkdir_action().activate();
refresh_tree_view();
});
auto touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
directory_view.touch_action().activate();
refresh_tree_view();
});
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("File Manager");
app_menu.add_action(mkdir_action);
app_menu.add_action(touch_action);
app_menu.add_action(copy_action);
app_menu.add_action(cut_action);
app_menu.add_action(paste_action);
app_menu.add_action(focus_dependent_delete_action);
app_menu.add_action(directory_view.open_terminal_action());
app_menu.add_separator();
app_menu.add_action(properties_action);
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
auto action_show_dotfiles = GUI::Action::create_checkable("Show dotfiles", { Mod_Ctrl, Key_H }, [&](auto& action) {
directory_view.set_should_show_dotfiles(action.is_checked());
refresh_tree_view();
});
auto& view_menu = menubar->add_menu("View");
view_menu.add_action(*view_as_icons_action);
view_menu.add_action(*view_as_table_action);
view_menu.add_action(*view_as_columns_action);
view_menu.add_separator();
view_menu.add_action(action_show_dotfiles);
auto go_to_location_action = GUI::Action::create("Go to location...", { Mod_Ctrl, Key_L }, [&](auto&) {
location_toolbar.set_visible(true);
breadcrumb_toolbar.set_visible(false);
location_textbox.select_all();
location_textbox.set_focus(true);
});
auto& go_menu = menubar->add_menu("Go");
go_menu.add_action(go_back_action);
go_menu.add_action(go_forward_action);
go_menu.add_action(open_parent_directory_action);
go_menu.add_action(go_home_action);
go_menu.add_action(go_to_location_action);
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("File Manager", GUI::Icon::default_icon("filetype-folder")));
GUI::Application::the()->set_menubar(move(menubar));
main_toolbar.add_action(go_back_action);
main_toolbar.add_action(go_forward_action);
main_toolbar.add_action(open_parent_directory_action);
main_toolbar.add_action(go_home_action);
main_toolbar.add_separator();
main_toolbar.add_action(mkdir_action);
main_toolbar.add_action(touch_action);
main_toolbar.add_action(copy_action);
main_toolbar.add_action(cut_action);
main_toolbar.add_action(paste_action);
main_toolbar.add_action(focus_dependent_delete_action);
main_toolbar.add_action(directory_view.open_terminal_action());
main_toolbar.add_separator();
main_toolbar.add_action(*view_as_icons_action);
main_toolbar.add_action(*view_as_table_action);
main_toolbar.add_action(*view_as_columns_action);
directory_view.on_path_change = [&](const String& new_path, bool can_write_in_path) {
auto icon = GUI::FileIconProvider::icon_for_path(new_path);
auto* bitmap = icon.bitmap_for_size(16);
window->set_icon(bitmap);
location_textbox.set_icon(bitmap);
window->set_title(String::formatted("{} - File Manager", new_path));
location_textbox.set_text(new_path);
{
LexicalPath lexical_path(new_path);
auto segment_index_of_new_path_in_breadcrumb_bar = [&]() -> Optional<size_t> {
for (size_t i = 0; i < breadcrumb_bar.segment_count(); ++i) {
if (breadcrumb_bar.segment_data(i) == new_path)
return i;
}
return {};
}();
if (segment_index_of_new_path_in_breadcrumb_bar.has_value()) {
breadcrumb_bar.set_selected_segment(segment_index_of_new_path_in_breadcrumb_bar.value());
} else {
breadcrumb_bar.clear_segments();
breadcrumb_bar.append_segment("/", GUI::FileIconProvider::icon_for_path("/").bitmap_for_size(16), "/");
StringBuilder builder;
for (auto& part : lexical_path.parts()) {
// NOTE: We rebuild the path as we go, so we have something to pass to GUI::FileIconProvider.
builder.append('/');
builder.append(part);
breadcrumb_bar.append_segment(part, GUI::FileIconProvider::icon_for_path(builder.string_view()).bitmap_for_size(16), builder.string_view());
}
breadcrumb_bar.set_selected_segment(breadcrumb_bar.segment_count() - 1);
breadcrumb_bar.on_segment_click = [&](size_t segment_index) {
directory_view.open(breadcrumb_bar.segment_data(segment_index));
};
}
}
if (!is_reacting_to_tree_view_selection_change) {
auto new_index = directories_model->index(new_path, GUI::FileSystemModel::Column::Name);
if (new_index.is_valid()) {
tree_view.expand_all_parents_of(new_index);
tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set);
}
}
struct stat st;
if (lstat(new_path.characters(), &st)) {
perror("stat");
return;
}
paste_action->set_enabled(can_write_in_path && GUI::Clipboard::the().mime_type() == "text/uri-list");
go_forward_action->set_enabled(directory_view.path_history_position() < directory_view.path_history_size() - 1);
go_back_action->set_enabled(directory_view.path_history_position() > 0);
open_parent_directory_action->set_enabled(new_path != "/");
};
directory_view.on_accepted_drop = [&]() {
refresh_tree_view();
};
directory_view.on_status_message = [&](const StringView& message) {
statusbar.set_text(message);
};
directory_view.on_thumbnail_progress = [&](int done, int total) {
if (done == total) {
progressbar.set_visible(false);
return;
}
progressbar.set_range(0, total);
progressbar.set_value(done);
progressbar.set_visible(true);
};
directory_view.on_selection_change = [&](GUI::AbstractView& view) {
auto& selection = view.selection();
copy_action->set_enabled(!selection.is_empty());
cut_action->set_enabled(!selection.is_empty());
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
|| !directory_view.current_view().selection().is_empty());
};
directory_context_menu->add_action(copy_action);
directory_context_menu->add_action(cut_action);
directory_context_menu->add_action(folder_specific_paste_action);
directory_context_menu->add_action(directory_view.delete_action());
directory_context_menu->add_action(shortcut_action);
directory_context_menu->add_separator();
directory_context_menu->add_action(properties_action);
directory_view_context_menu->add_action(mkdir_action);
directory_view_context_menu->add_action(touch_action);
directory_view_context_menu->add_action(paste_action);
directory_view_context_menu->add_action(directory_view.open_terminal_action());
directory_view_context_menu->add_separator();
directory_view_context_menu->add_action(action_show_dotfiles);
directory_view_context_menu->add_separator();
directory_view_context_menu->add_action(properties_action);
tree_view_directory_context_menu->add_action(copy_action);
tree_view_directory_context_menu->add_action(cut_action);
tree_view_directory_context_menu->add_action(paste_action);
tree_view_directory_context_menu->add_action(tree_view_delete_action);
tree_view_directory_context_menu->add_separator();
tree_view_directory_context_menu->add_action(properties_action);
tree_view_directory_context_menu->add_separator();
tree_view_directory_context_menu->add_action(mkdir_action);
tree_view_directory_context_menu->add_action(touch_action);
RefPtr<GUI::Menu> file_context_menu;
NonnullRefPtrVector<LauncherHandler> current_file_handlers;
RefPtr<GUI::Action> file_context_menu_action_default_action;
directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (index.is_valid()) {
auto& node = directory_view.node(index);
if (node.is_directory()) {
auto should_get_enabled = access(node.full_path().characters(), W_OK) == 0 && GUI::Clipboard::the().mime_type() == "text/uri-list";
folder_specific_paste_action->set_enabled(should_get_enabled);
directory_context_menu->popup(event.screen_position());
} else {
auto full_path = node.full_path();
current_file_handlers = directory_view.get_launch_handlers(full_path);
file_context_menu = GUI::Menu::construct("Directory View File");
file_context_menu->add_action(copy_action);
file_context_menu->add_action(cut_action);
file_context_menu->add_action(paste_action);
file_context_menu->add_action(directory_view.delete_action());
file_context_menu->add_action(shortcut_action);
file_context_menu->add_separator();
bool added_open_menu_items = false;
auto default_file_handler = directory_view.get_default_launch_handler(current_file_handlers);
if (default_file_handler) {
auto file_open_action = default_file_handler->create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
});
if (default_file_handler->details().launcher_type == Desktop::Launcher::LauncherType::Application)
file_open_action->set_text(String::formatted("Run {}", file_open_action->text()));
else
file_open_action->set_text(String::formatted("Open in {}", file_open_action->text()));
file_context_menu_action_default_action = file_open_action;
file_context_menu->add_action(move(file_open_action));
added_open_menu_items = true;
} else {
file_context_menu_action_default_action.clear();
}
if (current_file_handlers.size() > 1) {
added_open_menu_items = true;
auto& file_open_with_menu = file_context_menu->add_submenu("Open with");
for (auto& handler : current_file_handlers) {
if (&handler == default_file_handler.ptr())
continue;
file_open_with_menu.add_action(handler.create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
}));
}
}
if (added_open_menu_items)
file_context_menu->add_separator();
file_context_menu->add_action(properties_action);
file_context_menu->popup(event.screen_position(), file_context_menu_action_default_action);
}
} else {
directory_view_context_menu->popup(event.screen_position());
}
};
tree_view.on_selection = [&](const GUI::ModelIndex& index) {
if (directories_model->m_previously_selected_index.is_valid())
directories_model->update_node_on_selection(directories_model->m_previously_selected_index, false);
directories_model->update_node_on_selection(index, true);
directories_model->m_previously_selected_index = index;
};
tree_view.on_selection_change = [&] {
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
|| !directory_view.current_view().selection().is_empty());
if (tree_view.selection().is_empty())
return;
auto path = directories_model->full_path(tree_view.selection().first());
if (directory_view.path() == path)
return;
TemporaryChange change(is_reacting_to_tree_view_selection_change, true);
directory_view.open(path);
copy_action->set_enabled(!tree_view.selection().is_empty());
cut_action->set_enabled(!tree_view.selection().is_empty());
directory_view.delete_action().set_enabled(!tree_view.selection().is_empty());
};
tree_view.on_focus_change = [&]([[maybe_unused]] const bool has_focus, [[maybe_unused]] const GUI::FocusSource source) {
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && has_focus)
|| !directory_view.current_view().selection().is_empty());
};
tree_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (index.is_valid()) {
tree_view_directory_context_menu->popup(event.screen_position());
}
};
auto copy_urls_to_directory = [&](const Vector<URL>& urls, const String& directory) {
if (urls.is_empty()) {
dbgln("No files to copy");
return;
}
bool had_accepted_copy = false;
for (auto& url_to_copy : urls) {
if (!url_to_copy.is_valid() || url_to_copy.path() == directory)
continue;
auto new_path = String::formatted("{}/{}", directory, LexicalPath(url_to_copy.path()).basename());
if (url_to_copy.path() == new_path)
continue;
if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
} else {
had_accepted_copy = true;
}
}
if (had_accepted_copy)
refresh_tree_view();
};
breadcrumb_bar.on_segment_drop = [&](size_t segment_index, const GUI::DropEvent& event) {
if (!event.mime_data().has_urls())
return;
copy_urls_to_directory(event.mime_data().urls(), breadcrumb_bar.segment_data(segment_index));
};
breadcrumb_bar.on_segment_drag_enter = [&](size_t, GUI::DragEvent& event) {
if (event.mime_types().contains_slow("text/uri-list"))
event.accept();
};
breadcrumb_bar.on_doubleclick = [&](const GUI::MouseEvent&) {
go_to_location_action->activate();
};
tree_view.on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
if (!event.mime_data().has_urls())
return;
auto& target_node = directories_model->node(index);
if (!target_node.is_directory())
return;
copy_urls_to_directory(event.mime_data().urls(), target_node.full_path());
};
directory_view.open(initial_location);
directory_view.set_focus(true);
paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(initial_location.characters(), W_OK) == 0);
window->show();
// Read directory read mode from config.
auto dir_view_mode = config->read_entry("DirectoryView", "ViewMode", "Icon");
if (dir_view_mode.contains("Table")) {
directory_view.set_view_mode(DirectoryView::ViewMode::Table);
view_as_table_action->set_checked(true);
} else if (dir_view_mode.contains("Columns")) {
directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
view_as_columns_action->set_checked(true);
} else {
directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
view_as_icons_action->set_checked(true);
}
// Write window position to config file on close request.
window->on_close_request = [&] {
config->write_num_entry("Window", "Left", window->x());
config->write_num_entry("Window", "Top", window->y());
config->write_num_entry("Window", "Width", window->width());
config->write_num_entry("Window", "Height", window->height());
config->sync();
return GUI::Window::CloseRequestDecision::Close;
};
return GUI::Application::the()->exec();
}

View file

@ -0,0 +1 @@
UI_*.h

View file

@ -0,0 +1,11 @@
include_directories(${CMAKE_CURRENT_BINARY_DIR})
set(SOURCES
FontEditor.cpp
GlyphEditorWidget.cpp
GlyphMapWidget.cpp
main.cpp
)
serenity_app(FontEditor ICON app-font-editor)
target_link_libraries(FontEditor LibGUI LibDesktop LibGfx)

View file

@ -0,0 +1,372 @@
/*
* 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 "FontEditor.h"
#include "GlyphEditorWidget.h"
#include "GlyphMapWidget.h"
#include <AK/StringBuilder.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/GroupBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibGUI/SpinBox.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
#include <LibGfx/BitmapFont.h>
#include <LibGfx/Palette.h>
#include <stdlib.h>
FontEditorWidget::FontEditorWidget(const String& path, RefPtr<Gfx::BitmapFont>&& edited_font)
: m_edited_font(move(edited_font))
, m_path(path)
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>();
// Top
auto& main_container = add<GUI::Widget>();
main_container.set_layout<GUI::HorizontalBoxLayout>();
main_container.layout()->set_margins({ 4, 4, 4, 4 });
main_container.set_background_role(Gfx::ColorRole::SyntaxKeyword);
// Top-Left Glyph Editor and info
auto& editor_container = main_container.add<GUI::Widget>();
editor_container.set_layout<GUI::VerticalBoxLayout>();
editor_container.layout()->set_margins({ 4, 4, 4, 4 });
editor_container.set_background_role(Gfx::ColorRole::SyntaxKeyword);
m_glyph_editor_widget = editor_container.add<GlyphEditorWidget>(*m_edited_font);
m_glyph_editor_widget->set_fixed_size(m_glyph_editor_widget->preferred_width(), m_glyph_editor_widget->preferred_height());
editor_container.set_fixed_width(m_glyph_editor_widget->preferred_width());
auto& glyph_width_label = editor_container.add<GUI::Label>();
glyph_width_label.set_fixed_height(22);
glyph_width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
glyph_width_label.set_text("Glyph width:");
auto& glyph_width_spinbox = editor_container.add<GUI::SpinBox>();
glyph_width_spinbox.set_min(0);
glyph_width_spinbox.set_max(32);
glyph_width_spinbox.set_value(0);
glyph_width_spinbox.set_enabled(!m_edited_font->is_fixed_width());
auto& info_label = editor_container.add<GUI::Label>();
info_label.set_fixed_height(22);
info_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
info_label.set_text("info_label");
/// Top-Right glyph map and font meta data
auto& map_and_test_container = main_container.add<GUI::Widget>();
map_and_test_container.set_layout<GUI::VerticalBoxLayout>();
map_and_test_container.layout()->set_margins({ 4, 4, 4, 4 });
m_glyph_map_widget = map_and_test_container.add<GlyphMapWidget>(*m_edited_font);
m_glyph_map_widget->set_fixed_size(m_glyph_map_widget->preferred_width(), m_glyph_map_widget->preferred_height());
auto& font_mtest_group_box = map_and_test_container.add<GUI::GroupBox>();
font_mtest_group_box.set_layout<GUI::VerticalBoxLayout>();
font_mtest_group_box.layout()->set_margins({ 5, 15, 5, 5 });
font_mtest_group_box.set_fixed_height(2 * m_edited_font->glyph_height() + 50);
font_mtest_group_box.set_title("Test");
auto& demo_label_1 = font_mtest_group_box.add<GUI::Label>();
demo_label_1.set_font(m_edited_font);
demo_label_1.set_text("quick fox jumps nightly above wizard.");
auto& demo_label_2 = font_mtest_group_box.add<GUI::Label>();
demo_label_2.set_font(m_edited_font);
demo_label_2.set_text("QUICK FOX JUMPS NIGHTLY ABOVE WIZARD!");
auto& font_metadata_group_box = map_and_test_container.add<GUI::GroupBox>();
font_metadata_group_box.set_layout<GUI::VerticalBoxLayout>();
font_metadata_group_box.layout()->set_margins({ 5, 15, 5, 5 });
font_metadata_group_box.set_fixed_height(275);
font_metadata_group_box.set_title("Font metadata");
//// Name Row
auto& namecontainer = font_metadata_group_box.add<GUI::Widget>();
namecontainer.set_layout<GUI::HorizontalBoxLayout>();
namecontainer.set_fixed_height(22);
auto& name_label = namecontainer.add<GUI::Label>();
name_label.set_fixed_width(100);
name_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
name_label.set_text("Name:");
auto& name_textbox = namecontainer.add<GUI::TextBox>();
name_textbox.set_text(m_edited_font->name());
name_textbox.on_change = [&] {
m_edited_font->set_name(name_textbox.text());
};
//// Family Row
auto& family_container = font_metadata_group_box.add<GUI::Widget>();
family_container.set_layout<GUI::HorizontalBoxLayout>();
family_container.set_fixed_height(22);
auto& family_label = family_container.add<GUI::Label>();
family_label.set_fixed_width(100);
family_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
family_label.set_text("Family:");
auto& family_textbox = family_container.add<GUI::TextBox>();
family_textbox.set_text(m_edited_font->family());
family_textbox.on_change = [&] {
m_edited_font->set_family(family_textbox.text());
};
//// Presentation size Row
auto& presentation_size_container = font_metadata_group_box.add<GUI::Widget>();
presentation_size_container.set_layout<GUI::HorizontalBoxLayout>();
presentation_size_container.set_fixed_height(22);
auto& presentation_size_label = presentation_size_container.add<GUI::Label>();
presentation_size_label.set_fixed_width(100);
presentation_size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
presentation_size_label.set_text("Presentation size:");
auto& presentation_size_spinbox = presentation_size_container.add<GUI::SpinBox>();
presentation_size_spinbox.set_min(0);
presentation_size_spinbox.set_max(255);
presentation_size_spinbox.set_value(m_edited_font->presentation_size());
//// Weight Row
auto& weight_container = font_metadata_group_box.add<GUI::Widget>();
weight_container.set_layout<GUI::HorizontalBoxLayout>();
weight_container.set_fixed_height(22);
auto& weight_label = weight_container.add<GUI::Label>();
weight_label.set_fixed_width(100);
weight_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
weight_label.set_text("Weight:");
auto& weight_spinbox = weight_container.add<GUI::SpinBox>();
weight_spinbox.set_min(0);
weight_spinbox.set_max(65535);
weight_spinbox.set_value(m_edited_font->weight());
//// Glyph spacing Row
auto& glyph_spacing_container = font_metadata_group_box.add<GUI::Widget>();
glyph_spacing_container.set_layout<GUI::HorizontalBoxLayout>();
glyph_spacing_container.set_fixed_height(22);
auto& glyph_spacing = glyph_spacing_container.add<GUI::Label>();
glyph_spacing.set_fixed_width(100);
glyph_spacing.set_text_alignment(Gfx::TextAlignment::CenterLeft);
glyph_spacing.set_text("Glyph spacing:");
auto& spacing_spinbox = glyph_spacing_container.add<GUI::SpinBox>();
spacing_spinbox.set_min(0);
spacing_spinbox.set_max(255);
spacing_spinbox.set_value(m_edited_font->glyph_spacing());
//// Glyph Height Row
auto& glyph_height_container = font_metadata_group_box.add<GUI::Widget>();
glyph_height_container.set_layout<GUI::HorizontalBoxLayout>();
glyph_height_container.set_fixed_height(22);
auto& glyph_height = glyph_height_container.add<GUI::Label>();
glyph_height.set_fixed_width(100);
glyph_height.set_text_alignment(Gfx::TextAlignment::CenterLeft);
glyph_height.set_text("Glyph height:");
auto& glyph_height_spinbox = glyph_height_container.add<GUI::SpinBox>();
glyph_height_spinbox.set_min(0);
glyph_height_spinbox.set_max(255);
glyph_height_spinbox.set_value(m_edited_font->glyph_height());
glyph_height_spinbox.set_enabled(false);
//// Glyph width Row
auto& glyph_weight_container = font_metadata_group_box.add<GUI::Widget>();
glyph_weight_container.set_layout<GUI::HorizontalBoxLayout>();
glyph_weight_container.set_fixed_height(22);
auto& glyph_header_width_label = glyph_weight_container.add<GUI::Label>();
glyph_header_width_label.set_fixed_width(100);
glyph_header_width_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
glyph_header_width_label.set_text("Glyph width:");
auto& glyph_header_width_spinbox = glyph_weight_container.add<GUI::SpinBox>();
glyph_header_width_spinbox.set_min(0);
glyph_header_width_spinbox.set_max(255);
glyph_header_width_spinbox.set_value(m_edited_font->glyph_fixed_width());
glyph_header_width_spinbox.set_enabled(false);
//// Baseline Row
auto& baseline_container = font_metadata_group_box.add<GUI::Widget>();
baseline_container.set_layout<GUI::HorizontalBoxLayout>();
baseline_container.set_fixed_height(22);
auto& baseline_label = baseline_container.add<GUI::Label>();
baseline_label.set_fixed_width(100);
baseline_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
baseline_label.set_text("Baseline:");
auto& baseline_spinbox = baseline_container.add<GUI::SpinBox>();
baseline_spinbox.set_min(0);
baseline_spinbox.set_max(m_edited_font->glyph_height() - 1);
baseline_spinbox.set_value(m_edited_font->baseline());
//// Mean line Row
auto& mean_line_container = font_metadata_group_box.add<GUI::Widget>();
mean_line_container.set_layout<GUI::HorizontalBoxLayout>();
mean_line_container.set_fixed_height(22);
auto& mean_line_label = mean_line_container.add<GUI::Label>();
mean_line_label.set_fixed_width(100);
mean_line_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
mean_line_label.set_text("Mean Line:");
auto& mean_line_spinbox = mean_line_container.add<GUI::SpinBox>();
mean_line_spinbox.set_min(0);
mean_line_spinbox.set_max(m_edited_font->glyph_height() - 1);
mean_line_spinbox.set_value(m_edited_font->mean_line());
//// Fixed checkbox Row
auto& fixed_width_checkbox = font_metadata_group_box.add<GUI::CheckBox>();
fixed_width_checkbox.set_text("Fixed width");
fixed_width_checkbox.set_checked(m_edited_font->is_fixed_width());
// Bottom
auto& bottom_container = add<GUI::Widget>();
bottom_container.set_layout<GUI::HorizontalBoxLayout>();
bottom_container.layout()->set_margins({ 8, 0, 8, 8 });
bottom_container.set_fixed_height(32);
bottom_container.layout()->add_spacer();
auto& save_button = bottom_container.add<GUI::Button>();
save_button.set_fixed_size(80, 22);
save_button.set_text("Save");
save_button.on_click = [this](auto) { save_as(m_path); };
auto& quit_button = bottom_container.add<GUI::Button>();
quit_button.set_fixed_size(80, 22);
quit_button.set_text("Quit");
quit_button.on_click = [](auto) {
exit(0);
};
// Event hanglers
auto update_demo = [&] {
demo_label_1.update();
demo_label_2.update();
};
auto calculate_prefed_sizes = [&] {
int right_site_width = m_edited_font->width("QUICK FOX JUMPS NIGHTLY ABOVE WIZARD!") + 20;
right_site_width = max(right_site_width, m_glyph_map_widget->preferred_width());
m_preferred_width = m_glyph_editor_widget->width() + right_site_width + 20;
m_preferred_height = m_glyph_map_widget->relative_rect().height() + 2 * m_edited_font->glyph_height() + 380;
};
m_glyph_editor_widget->on_glyph_altered = [this, update_demo](u8 glyph) {
m_glyph_map_widget->update_glyph(glyph);
update_demo();
};
m_glyph_map_widget->on_glyph_selected = [&](size_t glyph) {
m_glyph_editor_widget->set_glyph(glyph);
glyph_width_spinbox.set_value(m_edited_font->glyph_width(m_glyph_map_widget->selected_glyph()));
StringBuilder builder;
builder.appendff("{:#02x} (", glyph);
if (glyph < 128) {
builder.append(glyph);
} else {
builder.append(128 | 64 | (glyph / 64));
builder.append(128 | (glyph % 64));
}
builder.append(')');
info_label.set_text(builder.to_string());
};
fixed_width_checkbox.on_checked = [&, update_demo](bool checked) {
m_edited_font->set_fixed_width(checked);
glyph_width_spinbox.set_enabled(!m_edited_font->is_fixed_width());
glyph_width_spinbox.set_value(m_edited_font->glyph_width(m_glyph_map_widget->selected_glyph()));
m_glyph_editor_widget->update();
update_demo();
};
glyph_width_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_glyph_width(m_glyph_map_widget->selected_glyph(), value);
m_glyph_editor_widget->update();
m_glyph_map_widget->update_glyph(m_glyph_map_widget->selected_glyph());
update_demo();
};
weight_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_weight(value);
update_demo();
};
presentation_size_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_presentation_size(value);
update_demo();
};
spacing_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_glyph_spacing(value);
update_demo();
};
baseline_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_baseline(value);
m_glyph_editor_widget->update();
update_demo();
};
mean_line_spinbox.on_change = [this, update_demo](int value) {
m_edited_font->set_mean_line(value);
m_glyph_editor_widget->update();
update_demo();
};
// init widget
calculate_prefed_sizes();
m_glyph_map_widget->set_selected_glyph('A');
}
FontEditorWidget::~FontEditorWidget()
{
}
bool FontEditorWidget::save_as(const String& path)
{
auto ret_val = m_edited_font->write_to_file(path);
if (!ret_val) {
GUI::MessageBox::show(window(), "The font file could not be saved.", "Save failed", GUI::MessageBox::Type::Error);
return false;
}
m_path = path;
return true;
}

View file

@ -0,0 +1,58 @@
/*
* 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/Widget.h>
#include <LibGfx/BitmapFont.h>
class GlyphEditorWidget;
class GlyphMapWidget;
class FontEditorWidget final : public GUI::Widget {
C_OBJECT(FontEditorWidget)
public:
virtual ~FontEditorWidget() override;
int preferred_width() { return m_preferred_width; }
int preferred_height() { return m_preferred_height; }
bool save_as(const String&);
const String& path() { return m_path; }
private:
FontEditorWidget(const String& path, RefPtr<Gfx::BitmapFont>&&);
RefPtr<Gfx::BitmapFont> m_edited_font;
RefPtr<GlyphMapWidget> m_glyph_map_widget;
RefPtr<GlyphEditorWidget> m_glyph_editor_widget;
String m_path;
int m_preferred_width;
int m_preferred_height;
};

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 "GlyphEditorWidget.h"
#include <LibGUI/Painter.h>
#include <LibGfx/BitmapFont.h>
#include <LibGfx/Palette.h>
GlyphEditorWidget::GlyphEditorWidget(Gfx::BitmapFont& mutable_font)
: m_font(mutable_font)
{
set_relative_rect({ 0, 0, preferred_width(), preferred_height() });
}
GlyphEditorWidget::~GlyphEditorWidget()
{
}
void GlyphEditorWidget::set_glyph(int glyph)
{
if (m_glyph == glyph)
return;
m_glyph = glyph;
update();
}
void GlyphEditorWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(frame_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(frame_inner_rect(), palette().base());
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-1, -1);
for (int y = 1; y < font().glyph_height(); ++y) {
int y_below = y - 1;
bool bold_line = y_below == font().baseline() || y_below == font().mean_line();
painter.draw_line({ 0, y * m_scale }, { font().max_glyph_width() * m_scale, y * m_scale }, palette().threed_shadow2(), bold_line ? 2 : 1);
}
for (int x = 1; x < font().max_glyph_width(); ++x)
painter.draw_line({ x * m_scale, 0 }, { x * m_scale, font().glyph_height() * m_scale }, palette().threed_shadow2());
auto bitmap = font().glyph_bitmap(m_glyph);
for (int y = 0; y < font().glyph_height(); ++y) {
for (int x = 0; x < font().max_glyph_width(); ++x) {
Gfx::IntRect rect { x * m_scale, y * m_scale, m_scale, m_scale };
if (x >= font().glyph_width(m_glyph)) {
painter.fill_rect(rect, palette().threed_shadow1());
} else {
if (bitmap.bit_at(x, y))
painter.fill_rect(rect, palette().base_text());
}
}
}
}
void GlyphEditorWidget::mousedown_event(GUI::MouseEvent& event)
{
draw_at_mouse(event);
}
void GlyphEditorWidget::mousemove_event(GUI::MouseEvent& event)
{
if (event.buttons() & (GUI::MouseButton::Left | GUI::MouseButton::Right))
draw_at_mouse(event);
}
void GlyphEditorWidget::draw_at_mouse(const GUI::MouseEvent& event)
{
bool set = event.buttons() & GUI::MouseButton::Left;
bool unset = event.buttons() & GUI::MouseButton::Right;
if (!(set ^ unset))
return;
int x = (event.x() - 1) / m_scale;
int y = (event.y() - 1) / m_scale;
auto bitmap = font().glyph_bitmap(m_glyph);
if (x < 0 || x >= bitmap.width())
return;
if (y < 0 || y >= bitmap.height())
return;
if (bitmap.bit_at(x, y) == set)
return;
bitmap.set_bit_at(x, y, set);
if (on_glyph_altered)
on_glyph_altered(m_glyph);
update();
}
int GlyphEditorWidget::preferred_width() const
{
return frame_thickness() * 2 + font().max_glyph_width() * m_scale - 1;
}
int GlyphEditorWidget::preferred_height() const
{
return frame_thickness() * 2 + font().glyph_height() * m_scale - 1;
}

View file

@ -0,0 +1,60 @@
/*
* 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/Frame.h>
#include <LibGfx/BitmapFont.h>
class GlyphEditorWidget final : public GUI::Frame {
C_OBJECT(GlyphEditorWidget)
public:
virtual ~GlyphEditorWidget() override;
int glyph() const { return m_glyph; }
void set_glyph(int);
int preferred_width() const;
int preferred_height() const;
Gfx::BitmapFont& font() { return *m_font; }
const Gfx::BitmapFont& font() const { return *m_font; }
Function<void(u8)> on_glyph_altered;
private:
GlyphEditorWidget(Gfx::BitmapFont&);
virtual void paint_event(GUI::PaintEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
void draw_at_mouse(const GUI::MouseEvent&);
RefPtr<Gfx::BitmapFont> m_font;
int m_glyph { 0 };
int m_scale { 10 };
};

View file

@ -0,0 +1,168 @@
/*
* 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 "GlyphMapWidget.h"
#include <LibGUI/Painter.h>
#include <LibGfx/BitmapFont.h>
#include <LibGfx/Palette.h>
GlyphMapWidget::GlyphMapWidget(Gfx::BitmapFont& mutable_font)
: m_font(mutable_font)
{
m_glyph_count = mutable_font.glyph_count();
set_relative_rect({ 0, 0, preferred_width(), preferred_height() });
set_focus_policy(GUI::FocusPolicy::StrongFocus);
}
GlyphMapWidget::~GlyphMapWidget()
{
}
int GlyphMapWidget::preferred_width() const
{
return columns() * (font().max_glyph_width() + m_horizontal_spacing) + 2 + frame_thickness() * 2;
}
int GlyphMapWidget::preferred_height() const
{
return rows() * (font().glyph_height() + m_vertical_spacing) + 2 + frame_thickness() * 2;
}
void GlyphMapWidget::set_selected_glyph(int glyph)
{
if (m_selected_glyph == glyph)
return;
m_selected_glyph = glyph;
if (on_glyph_selected)
on_glyph_selected(glyph);
update();
}
Gfx::IntRect GlyphMapWidget::get_outer_rect(int glyph) const
{
int row = glyph / columns();
int column = glyph % columns();
return Gfx::IntRect {
column * (font().max_glyph_width() + m_horizontal_spacing) + 1,
row * (font().glyph_height() + m_vertical_spacing) + 1,
font().max_glyph_width() + m_horizontal_spacing,
font().glyph_height() + m_horizontal_spacing
}
.translated(frame_thickness(), frame_thickness());
}
void GlyphMapWidget::update_glyph(int glyph)
{
update(get_outer_rect(glyph));
}
void GlyphMapWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.set_font(font());
painter.fill_rect(frame_inner_rect(), palette().base());
for (int glyph = 0; glyph < m_glyph_count; ++glyph) {
Gfx::IntRect outer_rect = get_outer_rect(glyph);
Gfx::IntRect inner_rect(
outer_rect.x() + m_horizontal_spacing / 2,
outer_rect.y() + m_vertical_spacing / 2,
font().max_glyph_width(),
font().glyph_height());
if (glyph == m_selected_glyph) {
painter.fill_rect(outer_rect, is_focused() ? palette().selection() : palette().inactive_selection());
painter.draw_glyph(inner_rect.location(), glyph, is_focused() ? palette().selection_text() : palette().inactive_selection_text());
} else {
painter.draw_glyph(inner_rect.location(), glyph, palette().base_text());
}
}
}
void GlyphMapWidget::mousedown_event(GUI::MouseEvent& event)
{
GUI::Frame::mousedown_event(event);
// FIXME: This is a silly loop.
for (int glyph = 0; glyph < m_glyph_count; ++glyph) {
if (get_outer_rect(glyph).contains(event.position())) {
set_selected_glyph(glyph);
break;
}
}
}
void GlyphMapWidget::keydown_event(GUI::KeyEvent& event)
{
GUI::Frame::keydown_event(event);
if (event.key() == KeyCode::Key_Up) {
if (selected_glyph() >= m_columns) {
set_selected_glyph(selected_glyph() - m_columns);
return;
}
}
if (event.key() == KeyCode::Key_Down) {
if (selected_glyph() < m_glyph_count - m_columns) {
set_selected_glyph(selected_glyph() + m_columns);
return;
}
}
if (event.key() == KeyCode::Key_Left) {
if (selected_glyph() > 0) {
set_selected_glyph(selected_glyph() - 1);
return;
}
}
if (event.key() == KeyCode::Key_Right) {
if (selected_glyph() < m_glyph_count - 1) {
set_selected_glyph(selected_glyph() + 1);
return;
}
}
if (event.ctrl() && event.key() == KeyCode::Key_Home) {
set_selected_glyph(0);
return;
}
if (event.ctrl() && event.key() == KeyCode::Key_End) {
set_selected_glyph(m_glyph_count - 1);
return;
}
if (!event.ctrl() && event.key() == KeyCode::Key_Home) {
set_selected_glyph(selected_glyph() / m_columns * m_columns);
return;
}
if (!event.ctrl() && event.key() == KeyCode::Key_End) {
int new_selection = selected_glyph() / m_columns * m_columns + (m_columns - 1);
int max = m_glyph_count - 1;
new_selection = clamp(new_selection, 0, max);
set_selected_glyph(new_selection);
return;
}
}

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/Function.h>
#include <AK/StdLibExtras.h>
#include <LibGUI/Frame.h>
#include <LibGfx/BitmapFont.h>
class GlyphMapWidget final : public GUI::Frame {
C_OBJECT(GlyphMapWidget)
public:
virtual ~GlyphMapWidget() override;
int selected_glyph() const { return m_selected_glyph; }
void set_selected_glyph(int);
int rows() const { return ceil_div(m_glyph_count, m_columns); }
int columns() const { return m_columns; }
int preferred_width() const;
int preferred_height() const;
Gfx::BitmapFont& font() { return *m_font; }
const Gfx::BitmapFont& font() const { return *m_font; }
void update_glyph(int);
Function<void(int)> on_glyph_selected;
private:
explicit GlyphMapWidget(Gfx::BitmapFont&);
virtual void paint_event(GUI::PaintEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void keydown_event(GUI::KeyEvent&) override;
Gfx::IntRect get_outer_rect(int glyph) const;
RefPtr<Gfx::BitmapFont> m_font;
int m_glyph_count;
int m_columns { 32 };
int m_horizontal_spacing { 2 };
int m_vertical_spacing { 2 };
int m_selected_glyph { 0 };
};

View file

@ -0,0 +1,155 @@
/*
* 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 "FontEditor.h"
#include <AK/URL.h>
#include <LibCore/ArgsParser.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/Icon.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/BitmapFont.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Point.h>
#include <stdio.h>
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer thread rpath accept unix cpath wpath fattr unix", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer thread rpath accept cpath wpath unix", nullptr) < 0) {
perror("pledge");
return 1;
}
if (!Desktop::Launcher::add_allowed_handler_with_only_specific_urls(
"/bin/Help",
{ URL::create_with_file_protocol("/usr/share/man/man1/FontEditor.md") })
|| !Desktop::Launcher::seal_allowlist()) {
warnln("Failed to set up allowed launch URLs");
return 1;
}
if (pledge("stdio shared_buffer thread rpath accept cpath wpath", nullptr) < 0) {
perror("pledge");
return 1;
}
const char* path = nullptr;
Core::ArgsParser args_parser;
args_parser.add_positional_argument(path, "The font file for editing.", "file", Core::ArgsParser::Required::No);
args_parser.parse(argc, argv);
RefPtr<Gfx::BitmapFont> edited_font;
if (path == nullptr) {
path = "/tmp/saved.font";
edited_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::FontDatabase::default_font().clone());
} else {
edited_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::Font::load_from_file(path)->clone());
if (!edited_font) {
String message = String::formatted("Couldn't load font: {}\n", path);
GUI::MessageBox::show(nullptr, message, "Font Editor", GUI::MessageBox::Type::Error);
return 1;
}
}
auto app_icon = GUI::Icon::default_icon("app-font-editor");
auto window = GUI::Window::construct();
window->set_icon(app_icon.bitmap_for_size(16));
auto set_edited_font = [&](const String& path, RefPtr<Gfx::BitmapFont>&& font, Gfx::IntPoint point) {
// Convert 256 char font to 384 char font.
if (font->type() == Gfx::FontTypes::Default)
font->set_type(Gfx::FontTypes::LatinExtendedA);
window->set_title(String::formatted("{} - Font Editor", path));
auto& font_editor_widget = window->set_main_widget<FontEditorWidget>(path, move(font));
window->set_rect({ point, { font_editor_widget.preferred_width(), font_editor_widget.preferred_height() } });
};
set_edited_font(path, move(edited_font), window->position());
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Font Editor");
app_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
Optional<String> open_path = GUI::FilePicker::get_open_filepath(window);
if (!open_path.has_value())
return;
RefPtr<Gfx::BitmapFont> new_font = static_ptr_cast<Gfx::BitmapFont>(Gfx::Font::load_from_file(open_path.value())->clone());
if (!new_font) {
String message = String::formatted("Couldn't load font: {}\n", open_path.value());
GUI::MessageBox::show(window, message, "Font Editor", GUI::MessageBox::Type::Error);
return;
}
set_edited_font(open_path.value(), move(new_font), window->position());
}));
app_menu.add_action(GUI::CommonActions::make_save_action([&](auto&) {
FontEditorWidget* editor = static_cast<FontEditorWidget*>(window->main_widget());
editor->save_as(editor->path());
}));
app_menu.add_action(GUI::CommonActions::make_save_as_action([&](auto&) {
FontEditorWidget* editor = static_cast<FontEditorWidget*>(window->main_widget());
LexicalPath lexical_path(editor->path());
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window, lexical_path.title(), lexical_path.extension());
if (!save_path.has_value())
return;
if (editor->save_as(save_path.value()))
window->set_title(String::formatted("{} - Font Editor", save_path.value()));
}));
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
app->quit();
return;
}));
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) {
Desktop::Launcher::open(URL::create_with_file_protocol("/usr/share/man/man1/FontEditor.md"), "/bin/Help");
}));
help_menu.add_action(GUI::CommonActions::make_about_action("Font Editor", app_icon, window));
app->set_menubar(move(menubar));
window->show();
return app->exec();
}

View file

@ -0,0 +1,10 @@
set(SOURCES
History.cpp
main.cpp
ManualModel.cpp
ManualPageNode.cpp
ManualSectionNode.cpp
)
serenity_app(Help ICON app-help)
target_link_libraries(Help LibWeb LibMarkdown LibGUI LibDesktop)

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2019-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 "History.h"
void History::push(const StringView& history_item)
{
m_items.shrink(m_current_history_item + 1);
m_items.append(history_item);
m_current_history_item++;
}
String History::current()
{
if (m_current_history_item == -1)
return {};
return m_items[m_current_history_item];
}
void History::go_back()
{
ASSERT(can_go_back());
m_current_history_item--;
}
void History::go_forward()
{
ASSERT(can_go_forward());
m_current_history_item++;
}
void History::clear()
{
m_items = {};
m_current_history_item = -1;
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2019-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/String.h>
#include <AK/Vector.h>
class History final {
public:
void push(const StringView& history_item);
String current();
void go_back();
void go_forward();
bool can_go_back() { return m_current_history_item > 0; }
bool can_go_forward() { return m_current_history_item + 1 < static_cast<int>(m_items.size()); }
void clear();
private:
Vector<String> m_items;
int m_current_history_item { -1 };
};

View file

@ -0,0 +1,199 @@
/*
* Copyright (c) 2019-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 "ManualModel.h"
#include "ManualNode.h"
#include "ManualPageNode.h"
#include "ManualSectionNode.h"
#include <AK/ByteBuffer.h>
#include <LibCore/File.h>
#include <LibGUI/FilteringProxyModel.h>
static ManualSectionNode s_sections[] = {
{ "1", "User programs" },
{ "2", "System calls" },
{ "3", "Libraries" },
{ "4", "Special files" },
{ "5", "File formats" },
{ "6", "Games" },
{ "7", "Miscellanea" },
{ "8", "Sysadmin tools" }
};
ManualModel::ManualModel()
{
m_section_open_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book-open.png"));
m_section_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/book.png"));
m_page_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"));
}
Optional<GUI::ModelIndex> ManualModel::index_from_path(const StringView& path) const
{
for (int section = 0; section < row_count(); ++section) {
auto parent_index = index(section, 0);
for (int row = 0; row < row_count(parent_index); ++row) {
auto child_index = index(row, 0, parent_index);
auto* node = static_cast<const ManualNode*>(child_index.internal_data());
if (!node->is_page())
continue;
auto* page = static_cast<const ManualPageNode*>(node);
if (page->path() != path)
continue;
return child_index;
}
}
return {};
}
String ManualModel::page_path(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto* node = static_cast<const ManualNode*>(index.internal_data());
if (!node->is_page())
return {};
auto* page = static_cast<const ManualPageNode*>(node);
return page->path();
}
Result<StringView, OSError> ManualModel::page_view(const String& path) const
{
if (path.is_empty())
return StringView {};
{
// Check if we've got it cached already.
auto mapped_file = m_mapped_files.get(path);
if (mapped_file.has_value())
return StringView { mapped_file.value()->bytes() };
}
auto file_or_error = MappedFile::map(path);
if (file_or_error.is_error())
return file_or_error.error();
StringView view { file_or_error.value()->bytes() };
m_mapped_files.set(path, file_or_error.release_value());
return view;
}
String ManualModel::page_and_section(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto* node = static_cast<const ManualNode*>(index.internal_data());
if (!node->is_page())
return {};
auto* page = static_cast<const ManualPageNode*>(node);
auto* section = static_cast<const ManualSectionNode*>(page->parent());
return String::formatted("{}({})", page->name(), section->section_name());
}
GUI::ModelIndex ManualModel::index(int row, int column, const GUI::ModelIndex& parent_index) const
{
if (!parent_index.is_valid())
return create_index(row, column, &s_sections[row]);
auto* parent = static_cast<const ManualNode*>(parent_index.internal_data());
auto* child = &parent->children()[row];
return create_index(row, column, child);
}
GUI::ModelIndex ManualModel::parent_index(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto* child = static_cast<const ManualNode*>(index.internal_data());
auto* parent = child->parent();
if (parent == nullptr)
return {};
if (parent->parent() == nullptr) {
for (size_t row = 0; row < sizeof(s_sections) / sizeof(s_sections[0]); row++)
if (&s_sections[row] == parent)
return create_index(row, 0, parent);
ASSERT_NOT_REACHED();
}
for (size_t row = 0; row < parent->parent()->children().size(); row++) {
ManualNode* child_at_row = &parent->parent()->children()[row];
if (child_at_row == parent)
return create_index(row, 0, parent);
}
ASSERT_NOT_REACHED();
}
int ManualModel::row_count(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return sizeof(s_sections) / sizeof(s_sections[0]);
auto* node = static_cast<const ManualNode*>(index.internal_data());
return node->children().size();
}
int ManualModel::column_count(const GUI::ModelIndex&) const
{
return 1;
}
GUI::Variant ManualModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto* node = static_cast<const ManualNode*>(index.internal_data());
switch (role) {
case GUI::ModelRole::Search:
if (!node->is_page())
return {};
return String(page_view(page_path(index)).value());
case GUI::ModelRole::Display:
return node->name();
case GUI::ModelRole::Icon:
if (node->is_page())
return m_page_icon;
if (node->is_open())
return m_section_open_icon;
return m_section_icon;
default:
return {};
}
}
void ManualModel::update_section_node_on_toggle(const GUI::ModelIndex& index, const bool open)
{
auto* node = static_cast<ManualSectionNode*>(index.internal_data());
node->set_open(open);
}
TriState ManualModel::data_matches(const GUI::ModelIndex& index, GUI::Variant term) const
{
auto view_result = page_view(page_path(index));
if (view_result.is_error() || view_result.value().is_empty())
return TriState::False;
return view_result.value().contains(term.as_string()) ? TriState::True : TriState::False;
}
void ManualModel::update()
{
did_update();
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2019-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/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/Result.h>
#include <AK/String.h>
#include <LibGUI/Model.h>
class ManualModel final : public GUI::Model {
public:
static NonnullRefPtr<ManualModel> create()
{
return adopt(*new ManualModel);
}
virtual ~ManualModel() override {};
Optional<GUI::ModelIndex> index_from_path(const StringView&) const;
String page_path(const GUI::ModelIndex&) const;
String page_and_section(const GUI::ModelIndex&) const;
Result<StringView, OSError> page_view(const String& path) const;
void update_section_node_on_toggle(const GUI::ModelIndex&, const bool);
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual TriState data_matches(const GUI::ModelIndex&, GUI::Variant) const override;
virtual void update() override;
virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override;
private:
ManualModel();
GUI::Icon m_section_open_icon;
GUI::Icon m_section_icon;
GUI::Icon m_page_icon;
mutable HashMap<String, NonnullRefPtr<MappedFile>> m_mapped_files;
};

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2019-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/NonnullOwnPtrVector.h>
#include <AK/String.h>
class ManualNode {
public:
virtual ~ManualNode() { }
virtual NonnullOwnPtrVector<ManualNode>& children() const = 0;
virtual const ManualNode* parent() const = 0;
virtual String name() const = 0;
virtual bool is_page() const { return false; }
virtual bool is_open() const { return false; }
};

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2019-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 "ManualPageNode.h"
#include "ManualSectionNode.h"
const ManualNode* ManualPageNode::parent() const
{
return &m_section;
}
NonnullOwnPtrVector<ManualNode>& ManualPageNode::children() const
{
static NonnullOwnPtrVector<ManualNode> empty_vector;
return empty_vector;
}
String ManualPageNode::path() const
{
return String::formatted("{}/{}.md", m_section.path(), m_page);
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019-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 "ManualNode.h"
class ManualSectionNode;
class ManualPageNode : public ManualNode {
public:
virtual ~ManualPageNode() override { }
ManualPageNode(const ManualSectionNode& section, const StringView& page)
: m_section(section)
, m_page(page)
{
}
virtual NonnullOwnPtrVector<ManualNode>& children() const override;
virtual const ManualNode* parent() const override;
virtual String name() const override { return m_page; };
virtual bool is_page() const override { return true; }
String path() const;
private:
const ManualSectionNode& m_section;
String m_page;
};

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2019-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 "ManualSectionNode.h"
#include "ManualPageNode.h"
#include <AK/LexicalPath.h>
#include <AK/QuickSort.h>
#include <AK/String.h>
#include <LibCore/DirIterator.h>
String ManualSectionNode::path() const
{
return String::formatted("/usr/share/man/man{}", m_section);
}
void ManualSectionNode::reify_if_needed() const
{
if (m_reified)
return;
m_reified = true;
Core::DirIterator dir_iter { path(), Core::DirIterator::Flags::SkipDots };
Vector<String> page_names;
while (dir_iter.has_next()) {
LexicalPath lexical_path(dir_iter.next_path());
if (lexical_path.extension() != "md")
continue;
page_names.append(lexical_path.title());
}
quick_sort(page_names);
for (auto& page_name : page_names)
m_children.append(make<ManualPageNode>(*this, move(page_name)));
}
void ManualSectionNode::set_open(bool open)
{
if (m_open == open)
return;
m_open = open;
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2019-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 "ManualNode.h"
class ManualSectionNode : public ManualNode {
public:
virtual ~ManualSectionNode() override { }
ManualSectionNode(String section, String name)
: m_section(section)
, m_full_name(String::formatted("{}. {}", section, name))
{
}
virtual NonnullOwnPtrVector<ManualNode>& children() const override
{
reify_if_needed();
return m_children;
}
virtual const ManualNode* parent() const override { return nullptr; }
virtual String name() const override { return m_full_name; }
virtual bool is_open() const override { return m_open; }
void set_open(bool open);
const String& section_name() const { return m_section; }
String path() const;
private:
void reify_if_needed() const;
String m_section;
String m_full_name;
mutable NonnullOwnPtrVector<ManualNode> m_children;
mutable bool m_reified { false };
bool m_open { false };
};

View file

@ -0,0 +1,312 @@
/*
* Copyright (c) 2019-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 "History.h"
#include "ManualModel.h"
#include <AK/URL.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/FilteringProxyModel.h>
#include <LibGUI/ListView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/TreeView.h>
#include <LibGUI/Window.h>
#include <LibMarkdown/Document.h>
#include <LibWeb/OutOfProcessWebView.h>
#include <libgen.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
if (pledge("stdio shared_buffer accept rpath unix cpath fattr", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer accept rpath unix", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/usr/share/man", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/tmp/portal/launch", "rw") < 0) {
perror("unveil");
return 1;
}
if (unveil("/tmp/portal/webcontent", "rw") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
const char* start_page = nullptr;
Core::ArgsParser args_parser;
args_parser.add_positional_argument(start_page, "Page to open at launch", "page", Core::ArgsParser::Required::No);
args_parser.parse(argc, argv);
auto app_icon = GUI::Icon::default_icon("app-help");
auto window = GUI::Window::construct();
window->set_icon(app_icon.bitmap_for_size(16));
window->set_title("Help");
window->resize(570, 500);
auto& widget = window->set_main_widget<GUI::Widget>();
widget.set_layout<GUI::VerticalBoxLayout>();
widget.set_fill_with_background_color(true);
widget.layout()->set_spacing(2);
auto& toolbar_container = widget.add<GUI::ToolBarContainer>();
auto& toolbar = toolbar_container.add<GUI::ToolBar>();
auto& splitter = widget.add<GUI::HorizontalSplitter>();
auto model = ManualModel::create();
auto& left_tab_bar = splitter.add<GUI::TabWidget>();
auto& tree_view_container = left_tab_bar.add_tab<GUI::Widget>("Browse");
tree_view_container.set_layout<GUI::VerticalBoxLayout>();
tree_view_container.layout()->set_margins({ 4, 4, 4, 4 });
auto& tree_view = tree_view_container.add<GUI::TreeView>();
auto& search_view = left_tab_bar.add_tab<GUI::Widget>("Search");
search_view.set_layout<GUI::VerticalBoxLayout>();
search_view.layout()->set_margins({ 4, 4, 4, 4 });
auto& search_box = search_view.add<GUI::TextBox>();
auto& search_list_view = search_view.add<GUI::ListView>();
search_box.set_fixed_height(20);
search_box.set_placeholder("Search...");
search_box.on_change = [&] {
if (auto model = search_list_view.model()) {
auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
search_model.set_filter_term(search_box.text());
search_model.update();
}
};
search_list_view.set_model(GUI::FilteringProxyModel::construct(model));
search_list_view.model()->update();
tree_view.set_model(model);
left_tab_bar.set_fixed_width(200);
auto& page_view = splitter.add<Web::OutOfProcessWebView>();
History history;
RefPtr<GUI::Action> go_back_action;
RefPtr<GUI::Action> go_forward_action;
auto update_actions = [&]() {
go_back_action->set_enabled(history.can_go_back());
go_forward_action->set_enabled(history.can_go_forward());
};
auto open_page = [&](const String& path) {
if (path.is_null()) {
window->set_title("Help");
page_view.load_empty_document();
return;
}
auto source_result = model->page_view(path);
if (source_result.is_error()) {
GUI::MessageBox::show(window, source_result.error().string(), "Failed to open man page", GUI::MessageBox::Type::Error);
return;
}
auto source = source_result.value();
String html;
{
auto md_document = Markdown::Document::parse(source);
ASSERT(md_document);
html = md_document->render_to_html();
}
auto url = URL::create_with_file_protocol(path);
page_view.load_html(html, url);
auto tree_view_index = model->index_from_path(path);
if (tree_view_index.has_value())
tree_view.expand_tree(tree_view_index.value().parent());
String page_and_section = model->page_and_section(tree_view_index.value());
window->set_title(String::formatted("{} - Help", page_and_section));
};
tree_view.on_selection_change = [&] {
String path = model->page_path(tree_view.selection().first());
history.push(path);
update_actions();
open_page(path);
};
tree_view.on_toggle = [&](const GUI::ModelIndex& index, const bool open) {
model->update_section_node_on_toggle(index, open);
};
auto open_external = [&](auto& url) {
if (!Desktop::Launcher::open(url)) {
GUI::MessageBox::show(window,
String::formatted("The link to '{}' could not be opened.", url),
"Failed to open link",
GUI::MessageBox::Type::Error);
}
};
search_list_view.on_selection = [&](auto index) {
if (!index.is_valid())
return;
if (auto model = search_list_view.model()) {
auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
index = search_model.map(index);
} else {
page_view.load_empty_document();
return;
}
String path = model->page_path(index);
if (path.is_null()) {
page_view.load_empty_document();
return;
}
tree_view.selection().clear();
tree_view.selection().add(index);
history.push(path);
update_actions();
open_page(path);
};
page_view.on_link_click = [&](auto& url, auto&, unsigned) {
if (url.protocol() != "file") {
open_external(url);
return;
}
auto path = Core::File::real_path_for(url.path());
if (!path.starts_with("/usr/share/man/")) {
open_external(url);
return;
}
auto tree_view_index = model->index_from_path(path);
if (tree_view_index.has_value()) {
dbgln("Found path _{}_ in model at index {}", path, tree_view_index.value());
tree_view.selection().set(tree_view_index.value());
return;
}
history.push(path);
update_actions();
open_page(path);
};
go_back_action = GUI::CommonActions::make_go_back_action([&](auto&) {
history.go_back();
update_actions();
open_page(history.current());
});
go_forward_action = GUI::CommonActions::make_go_forward_action([&](auto&) {
history.go_forward();
update_actions();
open_page(history.current());
});
go_back_action->set_enabled(false);
go_forward_action->set_enabled(false);
auto go_home_action = GUI::CommonActions::make_go_home_action([&](auto&) {
String path = "/usr/share/man/man7/Help-index.md";
history.push(path);
update_actions();
open_page(path);
});
toolbar.add_action(*go_back_action);
toolbar.add_action(*go_forward_action);
toolbar.add_action(*go_home_action);
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Help");
app_menu.add_action(GUI::CommonActions::make_about_action("Help", app_icon, window));
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
auto& go_menu = menubar->add_menu("Go");
go_menu.add_action(*go_back_action);
go_menu.add_action(*go_forward_action);
go_menu.add_action(*go_home_action);
app->set_menubar(move(menubar));
if (start_page) {
URL url = URL::create_with_url_or_path(start_page);
if (url.is_valid() && url.path().ends_with(".md")) {
history.push(url.path());
update_actions();
open_page(url.path());
} else {
left_tab_bar.set_active_widget(&search_view);
search_box.set_text(start_page);
if (auto model = search_list_view.model()) {
auto& search_model = *static_cast<GUI::FilteringProxyModel*>(model);
search_model.set_filter_term(search_box.text());
}
}
} else {
go_home_action->activate();
}
window->set_focused_widget(&left_tab_bar);
window->show();
return app->exec();
}

View file

@ -0,0 +1,8 @@
set(SOURCES
HexEditor.cpp
HexEditorWidget.cpp
main.cpp
)
serenity_app(HexEditor ICON app-hexeditor)
target_link_libraries(HexEditor LibGUI)

View file

@ -0,0 +1,583 @@
/*
* 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 "HexEditor.h"
#include <AK/StringBuilder.h>
#include <LibGUI/Action.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
HexEditor::HexEditor()
{
set_focus_policy(GUI::FocusPolicy::StrongFocus);
set_scrollbars_enabled(true);
set_font(Gfx::FontDatabase::default_fixed_width_font());
set_background_role(ColorRole::Base);
set_foreground_role(ColorRole::BaseText);
vertical_scrollbar().set_step(line_height());
}
HexEditor::~HexEditor()
{
}
void HexEditor::set_readonly(bool readonly)
{
if (m_readonly == readonly)
return;
m_readonly = readonly;
}
void HexEditor::set_buffer(const ByteBuffer& buffer)
{
m_buffer = buffer;
set_content_length(buffer.size());
m_tracked_changes.clear();
m_position = 0;
m_byte_position = 0;
update();
update_status();
}
void HexEditor::fill_selection(u8 fill_byte)
{
if (!has_selection())
return;
for (int i = m_selection_start; i <= m_selection_end; i++) {
m_tracked_changes.set(i, m_buffer.data()[i]);
m_buffer.data()[i] = fill_byte;
}
update();
did_change();
}
void HexEditor::set_position(int position)
{
if (position > static_cast<int>(m_buffer.size()))
return;
m_position = position;
m_byte_position = 0;
scroll_position_into_view(position);
update_status();
}
bool HexEditor::write_to_file(const StringView& path)
{
if (m_buffer.is_empty())
return true;
int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return false;
}
int rc = ftruncate(fd, m_buffer.size());
if (rc < 0) {
perror("ftruncate");
return false;
}
ssize_t nwritten = write(fd, m_buffer.data(), m_buffer.size());
if (nwritten < 0) {
perror("write");
close(fd);
return false;
}
if (static_cast<size_t>(nwritten) == m_buffer.size()) {
m_tracked_changes.clear();
update();
}
close(fd);
return true;
}
bool HexEditor::copy_selected_hex_to_clipboard()
{
if (!has_selection())
return false;
StringBuilder output_string_builder;
for (int i = m_selection_start; i <= m_selection_end; i++)
output_string_builder.appendff("{:02X} ", m_buffer.data()[i]);
GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
return true;
}
bool HexEditor::copy_selected_text_to_clipboard()
{
if (!has_selection())
return false;
StringBuilder output_string_builder;
for (int i = m_selection_start; i <= m_selection_end; i++)
output_string_builder.append(isprint(m_buffer.data()[i]) ? m_buffer[i] : '.');
GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
return true;
}
bool HexEditor::copy_selected_hex_to_clipboard_as_c_code()
{
if (!has_selection())
return false;
StringBuilder output_string_builder;
output_string_builder.appendff("unsigned char raw_data[{}] = {{\n", (m_selection_end - m_selection_start) + 1);
output_string_builder.append(" ");
for (int i = m_selection_start, j = 1; i <= m_selection_end; i++, j++) {
output_string_builder.appendff("{:#02X}", m_buffer.data()[i]);
if (i != m_selection_end)
output_string_builder.append(", ");
if ((j % 12) == 0) {
output_string_builder.append("\n");
output_string_builder.append(" ");
}
}
output_string_builder.append("\n};\n");
GUI::Clipboard::the().set_plain_text(output_string_builder.to_string());
return true;
}
void HexEditor::set_bytes_per_row(int bytes_per_row)
{
m_bytes_per_row = bytes_per_row;
set_content_size({ offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 10 + (m_bytes_per_row * character_width()) + 20, total_rows() * line_height() + 10 });
update();
}
void HexEditor::set_content_length(int length)
{
if (length == m_content_length)
return;
m_content_length = length;
set_content_size({ offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 10 + (m_bytes_per_row * character_width()) + 20, total_rows() * line_height() + 10 });
}
void HexEditor::mousedown_event(GUI::MouseEvent& event)
{
if (event.button() != GUI::MouseButton::Left) {
return;
}
auto absolute_x = horizontal_scrollbar().value() + event.x();
auto absolute_y = vertical_scrollbar().value() + event.y();
auto hex_start_x = frame_thickness() + 90;
auto hex_start_y = frame_thickness() + 5;
auto hex_end_x = hex_start_x + (bytes_per_row() * (character_width() * 3));
auto hex_end_y = hex_start_y + 5 + (total_rows() * line_height());
auto text_start_x = frame_thickness() + 100 + (bytes_per_row() * (character_width() * 3));
auto text_start_y = frame_thickness() + 5;
auto text_end_x = text_start_x + (bytes_per_row() * character_width());
auto text_end_y = text_start_y + 5 + (total_rows() * line_height());
if (absolute_x >= hex_start_x && absolute_x <= hex_end_x && absolute_y >= hex_start_y && absolute_y <= hex_end_y) {
auto byte_x = (absolute_x - hex_start_x) / (character_width() * 3);
auto byte_y = (absolute_y - hex_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset < 0 || offset >= static_cast<int>(m_buffer.size()))
return;
#ifdef HEX_DEBUG
outln("HexEditor::mousedown_event(hex): offset={}", offset);
#endif
m_edit_mode = EditMode::Hex;
m_byte_position = 0;
m_position = offset;
m_in_drag_select = true;
m_selection_start = offset;
m_selection_end = offset;
update();
update_status();
}
if (absolute_x >= text_start_x && absolute_x <= text_end_x && absolute_y >= text_start_y && absolute_y <= text_end_y) {
auto byte_x = (absolute_x - text_start_x) / character_width();
auto byte_y = (absolute_y - text_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset < 0 || offset >= static_cast<int>(m_buffer.size()))
return;
#ifdef HEX_DEBUG
outln("HexEditor::mousedown_event(text): offset={}", offset);
#endif
m_position = offset;
m_byte_position = 0;
m_in_drag_select = true;
m_selection_start = offset;
m_selection_end = offset;
m_edit_mode = EditMode::Text;
update();
update_status();
}
}
void HexEditor::mousemove_event(GUI::MouseEvent& event)
{
auto absolute_x = horizontal_scrollbar().value() + event.x();
auto absolute_y = vertical_scrollbar().value() + event.y();
auto hex_start_x = frame_thickness() + 90;
auto hex_start_y = frame_thickness() + 5;
auto hex_end_x = hex_start_x + (bytes_per_row() * (character_width() * 3));
auto hex_end_y = hex_start_y + 5 + (total_rows() * line_height());
auto text_start_x = frame_thickness() + 100 + (bytes_per_row() * (character_width() * 3));
auto text_start_y = frame_thickness() + 5;
auto text_end_x = text_start_x + (bytes_per_row() * character_width());
auto text_end_y = text_start_y + 5 + (total_rows() * line_height());
if ((absolute_x >= hex_start_x && absolute_x <= hex_end_x
&& absolute_y >= hex_start_y && absolute_y <= hex_end_y)
|| (absolute_x >= text_start_x && absolute_x <= text_end_x
&& absolute_y >= text_start_y && absolute_y <= text_end_y)) {
set_override_cursor(Gfx::StandardCursor::IBeam);
} else {
set_override_cursor(Gfx::StandardCursor::None);
}
if (m_in_drag_select) {
if (absolute_x >= hex_start_x && absolute_x <= hex_end_x && absolute_y >= hex_start_y && absolute_y <= hex_end_y) {
auto byte_x = (absolute_x - hex_start_x) / (character_width() * 3);
auto byte_y = (absolute_y - hex_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset < 0 || offset > static_cast<int>(m_buffer.size()))
return;
m_selection_end = offset;
scroll_position_into_view(offset);
}
if (absolute_x >= text_start_x && absolute_x <= text_end_x && absolute_y >= text_start_y && absolute_y <= text_end_y) {
auto byte_x = (absolute_x - text_start_x) / character_width();
auto byte_y = (absolute_y - text_start_y) / line_height();
auto offset = (byte_y * m_bytes_per_row) + byte_x;
if (offset < 0 || offset > static_cast<int>(m_buffer.size()))
return;
m_selection_end = offset;
scroll_position_into_view(offset);
}
update_status();
update();
return;
}
}
void HexEditor::mouseup_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Left) {
if (m_in_drag_select) {
if (m_selection_end < m_selection_start) {
// lets flip these around
auto start = m_selection_end;
m_selection_end = m_selection_start;
m_selection_start = start;
}
m_in_drag_select = false;
}
update();
update_status();
}
}
void HexEditor::scroll_position_into_view(int position)
{
int y = position / bytes_per_row();
int x = position % bytes_per_row();
Gfx::IntRect rect {
frame_thickness() + offset_margin_width() + (x * (character_width() * 3)) + 10,
frame_thickness() + 5 + (y * line_height()),
(character_width() * 3),
line_height() - m_line_spacing
};
scroll_into_view(rect, true, true);
}
void HexEditor::keydown_event(GUI::KeyEvent& event)
{
#ifdef HEX_DEBUG
outln("HexEditor::keydown_event key={}", static_cast<u8>(event.key()));
#endif
if (event.key() == KeyCode::Key_Up) {
if (m_position - bytes_per_row() >= 0) {
m_position -= bytes_per_row();
m_byte_position = 0;
scroll_position_into_view(m_position);
update();
update_status();
}
return;
}
if (event.key() == KeyCode::Key_Down) {
if (m_position + bytes_per_row() < static_cast<int>(m_buffer.size())) {
m_position += bytes_per_row();
m_byte_position = 0;
scroll_position_into_view(m_position);
update();
update_status();
}
return;
}
if (event.key() == KeyCode::Key_Left) {
if (m_position - 1 >= 0) {
m_position--;
m_byte_position = 0;
scroll_position_into_view(m_position);
update();
update_status();
}
return;
}
if (event.key() == KeyCode::Key_Right) {
if (m_position + 1 < static_cast<int>(m_buffer.size())) {
m_position++;
m_byte_position = 0;
scroll_position_into_view(m_position);
update();
update_status();
}
return;
}
if (event.key() == KeyCode::Key_Backspace) {
if (m_position > 0) {
m_position--;
m_byte_position = 0;
scroll_position_into_view(m_position);
update();
update_status();
}
return;
}
if (!is_readonly() && !event.ctrl() && !event.alt() && !event.text().is_empty()) {
if (m_edit_mode == EditMode::Hex) {
hex_mode_keydown_event(event);
} else {
text_mode_keydown_event(event);
}
}
}
void HexEditor::hex_mode_keydown_event(GUI::KeyEvent& event)
{
if ((event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9) || (event.key() >= KeyCode::Key_A && event.key() <= KeyCode::Key_F)) {
if (m_buffer.is_empty())
return;
ASSERT(m_position >= 0);
ASSERT(m_position < static_cast<int>(m_buffer.size()));
// yes, this is terrible... but it works.
auto value = (event.key() >= KeyCode::Key_0 && event.key() <= KeyCode::Key_9)
? event.key() - KeyCode::Key_0
: (event.key() - KeyCode::Key_A) + 0xA;
if (m_byte_position == 0) {
m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
m_buffer.data()[m_position] = value << 4 | (m_buffer.data()[m_position] & 0xF); // shift new value left 4 bits, OR with existing last 4 bits
m_byte_position++;
} else {
m_buffer.data()[m_position] = (m_buffer.data()[m_position] & 0xF0) | value; // save the first 4 bits, OR the new value in the last 4
if (m_position + 1 < static_cast<int>(m_buffer.size()))
m_position++;
m_byte_position = 0;
}
update();
update_status();
did_change();
}
}
void HexEditor::text_mode_keydown_event(GUI::KeyEvent& event)
{
if (m_buffer.is_empty())
return;
ASSERT(m_position >= 0);
ASSERT(m_position < static_cast<int>(m_buffer.size()));
if (event.code_point() == 0) // This is a control key
return;
m_tracked_changes.set(m_position, m_buffer.data()[m_position]);
m_buffer.data()[m_position] = event.code_point();
if (m_position + 1 < static_cast<int>(m_buffer.size()))
m_position++;
m_byte_position = 0;
update();
update_status();
did_change();
}
void HexEditor::update_status()
{
if (on_status_change)
on_status_change(m_position, m_edit_mode, m_selection_start, m_selection_end);
}
void HexEditor::did_change()
{
if (on_change)
on_change();
}
void HexEditor::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), palette().color(background_role()));
if (m_buffer.is_empty())
return;
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
Gfx::IntRect offset_clip_rect {
0,
vertical_scrollbar().value(),
85,
height() - height_occupied_by_horizontal_scrollbar() //(total_rows() * line_height()) + 5
};
painter.fill_rect(offset_clip_rect, palette().ruler());
painter.draw_line(offset_clip_rect.top_right(), offset_clip_rect.bottom_right(), palette().ruler_border());
auto margin_and_hex_width = offset_margin_width() + (m_bytes_per_row * (character_width() * 3)) + 15;
painter.draw_line({ margin_and_hex_width, 0 },
{ margin_and_hex_width, vertical_scrollbar().value() + (height() - height_occupied_by_horizontal_scrollbar()) },
palette().ruler_border());
auto view_height = (height() - height_occupied_by_horizontal_scrollbar());
auto min_row = max(0, vertical_scrollbar().value() / line_height()); // if below 0 then use 0
auto max_row = min(total_rows(), min_row + ceil_div(view_height, line_height())); // if above calculated rows, use calculated rows
// paint offsets
for (int i = min_row; i < max_row; i++) {
Gfx::IntRect side_offset_rect {
frame_thickness() + 5,
frame_thickness() + 5 + (i * line_height()),
width() - width_occupied_by_vertical_scrollbar(),
height() - height_occupied_by_horizontal_scrollbar()
};
bool is_current_line = (m_position / bytes_per_row()) == i;
auto line = String::formatted("{:#08X}", i * bytes_per_row());
painter.draw_text(
side_offset_rect,
line,
is_current_line ? Gfx::FontDatabase::default_bold_font() : font(),
Gfx::TextAlignment::TopLeft,
is_current_line ? palette().ruler_active_text() : palette().ruler_inactive_text());
}
for (int i = min_row; i < max_row; i++) {
for (int j = 0; j < bytes_per_row(); j++) {
auto byte_position = (i * bytes_per_row()) + j;
if (byte_position >= static_cast<int>(m_buffer.size()))
return;
Color text_color = palette().color(foreground_role());
if (m_tracked_changes.contains(byte_position)) {
text_color = Color::Red;
}
auto highlight_flag = false;
if (m_selection_start > -1 && m_selection_end > -1) {
if (byte_position >= m_selection_start && byte_position <= m_selection_end) {
highlight_flag = true;
}
if (byte_position >= m_selection_end && byte_position <= m_selection_start) {
highlight_flag = true;
}
}
Gfx::IntRect hex_display_rect {
frame_thickness() + offset_margin_width() + (j * (character_width() * 3)) + 10,
frame_thickness() + 5 + (i * line_height()),
(character_width() * 3),
line_height() - m_line_spacing
};
if (highlight_flag) {
painter.fill_rect(hex_display_rect, palette().selection());
text_color = text_color == Color::Red ? Color::from_rgb(0xFFC0CB) : palette().selection_text();
} else if (byte_position == m_position) {
painter.fill_rect(hex_display_rect, palette().inactive_selection());
text_color = palette().inactive_selection_text();
}
auto line = String::formatted("{:02X}", m_buffer[byte_position]);
painter.draw_text(hex_display_rect, line, Gfx::TextAlignment::TopLeft, text_color);
Gfx::IntRect text_display_rect {
frame_thickness() + offset_margin_width() + (bytes_per_row() * (character_width() * 3)) + (j * character_width()) + 20,
frame_thickness() + 5 + (i * line_height()),
character_width(),
line_height() - m_line_spacing
};
// selection highlighting.
if (highlight_flag) {
painter.fill_rect(text_display_rect, palette().selection());
} else if (byte_position == m_position) {
painter.fill_rect(text_display_rect, palette().inactive_selection());
}
painter.draw_text(text_display_rect, String::formatted("{:c}", isprint(m_buffer[byte_position]) ? m_buffer[byte_position] : '.'), Gfx::TextAlignment::TopLeft, text_color);
}
}
}

View file

@ -0,0 +1,105 @@
/*
* 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/NonnullOwnPtrVector.h>
#include <AK/NonnullRefPtrVector.h>
#include <AK/StdLibExtras.h>
#include <LibGUI/ScrollableWidget.h>
#include <LibGfx/Font.h>
#include <LibGfx/TextAlignment.h>
class HexEditor : public GUI::ScrollableWidget {
C_OBJECT(HexEditor)
public:
enum EditMode {
Hex,
Text
};
virtual ~HexEditor() override;
bool is_readonly() const { return m_readonly; }
void set_readonly(bool);
void set_buffer(const ByteBuffer&);
void fill_selection(u8 fill_byte);
bool write_to_file(const StringView& path);
bool has_selection() const { return !(m_selection_start == -1 || m_selection_end == -1 || (m_selection_end - m_selection_start) < 0 || m_buffer.is_empty()); }
bool copy_selected_text_to_clipboard();
bool copy_selected_hex_to_clipboard();
bool copy_selected_hex_to_clipboard_as_c_code();
int bytes_per_row() const { return m_bytes_per_row; }
void set_bytes_per_row(int);
void set_position(int position);
Function<void(int, EditMode, int, int)> on_status_change; // position, edit mode, selection start, selection end
Function<void()> on_change;
protected:
HexEditor();
virtual void paint_event(GUI::PaintEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void mouseup_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void keydown_event(GUI::KeyEvent&) override;
private:
bool m_readonly { false };
int m_line_spacing { 4 };
int m_content_length { 0 };
int m_bytes_per_row { 16 };
ByteBuffer m_buffer;
bool m_in_drag_select { false };
int m_selection_start { 0 };
int m_selection_end { 0 };
HashMap<int, u8> m_tracked_changes;
int m_position { 0 };
int m_byte_position { 0 }; // 0 or 1
EditMode m_edit_mode { Hex };
void scroll_position_into_view(int position);
int total_rows() const { return ceil_div(m_content_length, m_bytes_per_row); }
int line_height() const { return font().glyph_height() + m_line_spacing; }
int character_width() const { return font().glyph_width('W'); }
int offset_margin_width() const { return 80; }
void hex_mode_keydown_event(GUI::KeyEvent&);
void text_mode_keydown_event(GUI::KeyEvent&);
void set_content_length(int); // I might make this public if I add fetching data on demand.
void update_status();
void did_change();
};

View file

@ -0,0 +1,241 @@
/*
* 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 "HexEditorWidget.h"
#include <AK/Optional.h>
#include <AK/StringBuilder.h>
#include <LibCore/File.h>
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/StatusBar.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/ToolBar.h>
#include <stdio.h>
#include <string.h>
HexEditorWidget::HexEditorWidget()
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>();
layout()->set_spacing(2);
m_editor = add<HexEditor>();
m_editor->on_status_change = [this](int position, HexEditor::EditMode edit_mode, int selection_start, int selection_end) {
m_statusbar->set_text(0, String::formatted("Offset: {:#08X}", position));
m_statusbar->set_text(1, String::formatted("Edit Mode: {}", edit_mode == HexEditor::EditMode::Hex ? "Hex" : "Text"));
m_statusbar->set_text(2, String::formatted("Selection Start: {}", selection_start));
m_statusbar->set_text(3, String::formatted("Selection End: {}", selection_end));
m_statusbar->set_text(4, String::formatted("Selected Bytes: {}", abs(selection_end - selection_start) + 1));
};
m_editor->on_change = [this] {
bool was_dirty = m_document_dirty;
m_document_dirty = true;
if (!was_dirty)
update_title();
};
m_statusbar = add<GUI::StatusBar>(5);
m_new_action = GUI::Action::create("New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
if (m_document_dirty) {
if (GUI::MessageBox::show(window(), "Save changes to current file first?", "Warning", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel) != GUI::Dialog::ExecResult::ExecOK)
return;
m_save_action->activate();
}
String value;
if (GUI::InputBox::show(value, window(), "Enter new file size:", "New file size") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto file_size = value.to_int();
if (file_size.has_value() && file_size.value() > 0) {
m_document_dirty = false;
m_editor->set_buffer(ByteBuffer::create_zeroed(file_size.value()));
set_path(LexicalPath());
update_title();
} else {
GUI::MessageBox::show(window(), "Invalid file size entered.", "Error", GUI::MessageBox::Type::Error);
}
}
});
m_open_action = GUI::CommonActions::make_open_action([this](auto&) {
Optional<String> open_path = GUI::FilePicker::get_open_filepath(window());
if (!open_path.has_value())
return;
open_file(open_path.value());
});
m_save_action = GUI::CommonActions::make_save_action([&](auto&) {
if (!m_path.is_empty()) {
if (!m_editor->write_to_file(m_path)) {
GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
} else {
m_document_dirty = false;
update_title();
}
return;
}
m_save_as_action->activate();
});
m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) {
Optional<String> save_path = GUI::FilePicker::get_save_filepath(window(), m_name.is_null() ? "Untitled" : m_name, m_extension.is_null() ? "bin" : m_extension);
if (!save_path.has_value())
return;
if (!m_editor->write_to_file(save_path.value())) {
GUI::MessageBox::show(window(), "Unable to save file.\n", "Error", GUI::MessageBox::Type::Error);
return;
}
m_document_dirty = false;
set_path(LexicalPath(save_path.value()));
dbgln("Wrote document to {}", save_path.value());
});
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Hex Editor");
app_menu.add_action(*m_new_action);
app_menu.add_action(*m_open_action);
app_menu.add_action(*m_save_action);
app_menu.add_action(*m_save_as_action);
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) {
if (!request_close())
return;
GUI::Application::the()->quit();
}));
m_goto_decimal_offset_action = GUI::Action::create("Go To Offset (Decimal)...", { Mod_Ctrl | Mod_Shift, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter Decimal offset:", "Go To") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_offset = value.to_int();
if (new_offset.has_value())
m_editor->set_position(new_offset.value());
}
});
m_goto_hex_offset_action = GUI::Action::create("Go To Offset (Hex)...", { Mod_Ctrl, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter Hex offset:", "Go To") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_offset = strtol(value.characters(), nullptr, 16);
m_editor->set_position(new_offset);
}
});
auto& edit_menu = menubar->add_menu("Edit");
edit_menu.add_action(GUI::Action::create("Fill selection...", { Mod_Ctrl, Key_B }, [&](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Fill byte (hex):", "Fill Selection") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto fill_byte = strtol(value.characters(), nullptr, 16);
m_editor->fill_selection(fill_byte);
}
}));
edit_menu.add_separator();
edit_menu.add_action(*m_goto_decimal_offset_action);
edit_menu.add_action(*m_goto_hex_offset_action);
edit_menu.add_separator();
edit_menu.add_action(GUI::Action::create("Copy Hex", { Mod_Ctrl, Key_C }, [&](const GUI::Action&) {
m_editor->copy_selected_hex_to_clipboard();
}));
edit_menu.add_action(GUI::Action::create("Copy Text", { Mod_Ctrl | Mod_Shift, Key_C }, [&](const GUI::Action&) {
m_editor->copy_selected_text_to_clipboard();
}));
edit_menu.add_separator();
edit_menu.add_action(GUI::Action::create("Copy As C Code", { Mod_Alt | Mod_Shift, Key_C }, [&](const GUI::Action&) {
m_editor->copy_selected_hex_to_clipboard_as_c_code();
}));
auto& view_menu = menubar->add_menu("View");
auto& bytes_per_row_menu = view_menu.add_submenu("Bytes per row");
for (int i = 8; i <= 32; i += 8) {
bytes_per_row_menu.add_action(GUI::Action::create(String::number(i), [this, i](auto&) {
m_editor->set_bytes_per_row(i);
m_editor->update();
}));
}
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("Hex Editor", GUI::Icon::default_icon("Hex Editor"), window()));
GUI::Application::the()->set_menubar(move(menubar));
m_editor->set_focus(true);
}
HexEditorWidget::~HexEditorWidget()
{
}
void HexEditorWidget::set_path(const LexicalPath& lexical_path)
{
m_path = lexical_path.string();
m_name = lexical_path.title();
m_extension = lexical_path.extension();
update_title();
}
void HexEditorWidget::update_title()
{
StringBuilder builder;
builder.append(m_path);
if (m_document_dirty)
builder.append(" (*)");
builder.append(" - Hex Editor");
window()->set_title(builder.to_string());
}
void HexEditorWidget::open_file(const String& path)
{
auto file = Core::File::construct(path);
if (!file->open(Core::IODevice::ReadOnly)) {
GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: {}", path, strerror(errno)), "Error", GUI::MessageBox::Type::Error);
return;
}
m_document_dirty = false;
m_editor->set_buffer(file->read_all()); // FIXME: On really huge files, this is never going to work. Should really create a framework to fetch data from the file on-demand.
set_path(LexicalPath(path));
}
bool HexEditorWidget::request_close()
{
if (!m_document_dirty)
return true;
auto result = GUI::MessageBox::show(window(), "The file has been modified. Quit without saving?", "Quit without saving?", GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::OKCancel);
return result == GUI::MessageBox::ExecOK;
}

View file

@ -0,0 +1,65 @@
/*
* 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 "HexEditor.h"
#include <AK/Function.h>
#include <AK/LexicalPath.h>
#include <LibGUI/Application.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
class HexEditor;
class HexEditorWidget final : public GUI::Widget {
C_OBJECT(HexEditorWidget)
public:
virtual ~HexEditorWidget() override;
void open_file(const String& path);
bool request_close();
private:
HexEditorWidget();
void set_path(const LexicalPath& file);
void update_title();
RefPtr<HexEditor> m_editor;
String m_path;
String m_name;
String m_extension;
RefPtr<GUI::Action> m_new_action;
RefPtr<GUI::Action> m_open_action;
RefPtr<GUI::Action> m_save_action;
RefPtr<GUI::Action> m_save_as_action;
RefPtr<GUI::Action> m_goto_decimal_offset_action;
RefPtr<GUI::Action> m_goto_hex_offset_action;
RefPtr<GUI::StatusBar> m_statusbar;
bool m_document_dirty { false };
};

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.
*/
#include "HexEditorWidget.h"
#include <LibGUI/Icon.h>
#include <LibGfx/Bitmap.h>
#include <stdio.h>
int main(int argc, char** argv)
{
if (pledge("stdio shared_buffer accept rpath unix cpath wpath fattr thread", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio shared_buffer accept rpath cpath wpath thread", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app_icon = GUI::Icon::default_icon("app-hexeditor");
auto window = GUI::Window::construct();
window->set_title("Hex Editor");
window->resize(640, 400);
auto& hex_editor_widget = window->set_main_widget<HexEditorWidget>();
window->on_close_request = [&]() -> GUI::Window::CloseRequestDecision {
if (hex_editor_widget.request_close())
return GUI::Window::CloseRequestDecision::Close;
return GUI::Window::CloseRequestDecision::StayOpen;
};
window->show();
window->set_icon(app_icon.bitmap_for_size(16));
if (argc >= 2)
hex_editor_widget.open_file(argv[1]);
return app->exec();
}

View file

@ -0,0 +1,14 @@
set(SOURCES
IRCAppWindow.cpp
IRCChannel.cpp
IRCChannelMemberListModel.cpp
IRCClient.cpp
IRCLogBuffer.cpp
IRCQuery.cpp
IRCWindow.cpp
IRCWindowListModel.cpp
main.cpp
)
serenity_app(IRCClient ICON app-irc-client)
target_link_libraries(IRCClient LibWeb LibGUI)

View file

@ -0,0 +1,375 @@
/*
* 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 "IRCAppWindow.h"
#include "IRCChannel.h"
#include "IRCWindow.h"
#include "IRCWindowListModel.h"
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/StackWidget.h>
#include <LibGUI/TableView.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
static IRCAppWindow* s_the;
IRCAppWindow& IRCAppWindow::the()
{
return *s_the;
}
IRCAppWindow::IRCAppWindow(String server, int port)
: m_client(IRCClient::construct(server, port))
{
ASSERT(!s_the);
s_the = this;
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-irc-client.png"));
update_title();
resize(600, 400);
setup_actions();
setup_menus();
setup_widgets();
setup_client();
}
IRCAppWindow::~IRCAppWindow()
{
}
void IRCAppWindow::update_title()
{
set_title(String::formatted("{}@{}:{} - IRC Client", m_client->nickname(), m_client->hostname(), m_client->port()));
}
void IRCAppWindow::setup_client()
{
m_client->aid_create_window = [this](void* owner, IRCWindow::Type type, const String& name) {
return create_window(owner, type, name);
};
m_client->aid_get_active_window = [this] {
return static_cast<IRCWindow*>(m_container->active_widget());
};
m_client->aid_update_window_list = [this] {
m_window_list->model()->update();
};
m_client->on_nickname_changed = [this](const String&) {
update_title();
};
m_client->on_part_from_channel = [this](auto&) {
update_gui_actions();
};
if (m_client->hostname().is_empty()) {
String value;
if (GUI::InputBox::show(value, this, "Enter server:", "Connect to server") == GUI::InputBox::ExecCancel)
::exit(0);
m_client->set_server(value, 6667);
}
update_title();
bool success = m_client->connect();
ASSERT(success);
}
void IRCAppWindow::setup_actions()
{
m_join_action = GUI::Action::create("Join channel", { Mod_Ctrl, Key_J }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-join.png"), [&](auto&) {
String value;
if (GUI::InputBox::show(value, this, "Enter channel name:", "Join channel") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_join_action(value);
});
m_list_channels_action = GUI::Action::create("List channels", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-list.png"), [&](auto&) {
m_client->handle_list_channels_action();
});
m_part_action = GUI::Action::create("Part from channel", { Mod_Ctrl, Key_P }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-part.png"), [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
m_client->handle_part_action(window->channel().name());
});
m_whois_action = GUI::Action::create("Whois user", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-whois.png"), [&](auto&) {
String value;
if (GUI::InputBox::show(value, this, "Enter nickname:", "IRC WHOIS lookup") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_whois_action(value);
});
m_open_query_action = GUI::Action::create("Open query", { Mod_Ctrl, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-open-query.png"), [&](auto&) {
String value;
if (GUI::InputBox::show(value, this, "Enter nickname:", "Open IRC query with...") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_open_query_action(value);
});
m_close_query_action = GUI::Action::create("Close query", { Mod_Ctrl, Key_D }, Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-close-query.png"), [](auto&) {
outln("FIXME: Implement close-query action");
});
m_change_nick_action = GUI::Action::create("Change nickname", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-nick.png"), [this](auto&) {
String value;
if (GUI::InputBox::show(value, this, "Enter nickname:", "Change nickname") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_change_nick_action(value);
});
m_change_topic_action = GUI::Action::create("Change topic", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-topic.png"), [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter topic:", "Change topic") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_change_topic_action(window->channel().name(), value);
});
m_invite_user_action = GUI::Action::create("Invite user", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-invite.png"), [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "Invite user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_invite_user_action(window->channel().name(), value);
});
m_banlist_action = GUI::Action::create("Ban list", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
m_client->handle_banlist_action(window->channel().name());
});
m_voice_user_action = GUI::Action::create("Voice user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "Voice user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_voice_user_action(window->channel().name(), value);
});
m_devoice_user_action = GUI::Action::create("DeVoice user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "DeVoice user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_devoice_user_action(window->channel().name(), value);
});
m_hop_user_action = GUI::Action::create("Hop user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "Hop user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_hop_user_action(window->channel().name(), value);
});
m_dehop_user_action = GUI::Action::create("DeHop user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "DeHop user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_dehop_user_action(window->channel().name(), value);
});
m_op_user_action = GUI::Action::create("Op user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "Op user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_op_user_action(window->channel().name(), value);
});
m_deop_user_action = GUI::Action::create("DeOp user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String value;
if (GUI::InputBox::show(value, this, "Enter nick:", "DeOp user") == GUI::InputBox::ExecOK && !value.is_empty())
m_client->handle_deop_user_action(window->channel().name(), value);
});
m_kick_user_action = GUI::Action::create("Kick user", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
String nick_value;
if (GUI::InputBox::show(nick_value, this, "Enter nick:", "Kick user") != GUI::InputBox::ExecOK || nick_value.is_empty())
return;
String reason_value;
if (GUI::InputBox::show(reason_value, this, "Enter reason:", "Reason") == GUI::InputBox::ExecOK)
m_client->handle_kick_user_action(window->channel().name(), nick_value, reason_value.characters());
});
m_cycle_channel_action = GUI::Action::create("Cycle channel", [this](auto&) {
auto* window = m_client->current_window();
if (!window || window->type() != IRCWindow::Type::Channel) {
return;
}
m_client->handle_cycle_channel_action(window->channel().name());
});
}
void IRCAppWindow::setup_menus()
{
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("IRC Client");
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
dbgln("Terminal: Quit menu activated!");
GUI::Application::the()->quit();
return;
}));
auto& server_menu = menubar->add_menu("Server");
server_menu.add_action(*m_change_nick_action);
server_menu.add_separator();
server_menu.add_action(*m_join_action);
server_menu.add_action(*m_list_channels_action);
server_menu.add_separator();
server_menu.add_action(*m_whois_action);
server_menu.add_action(*m_open_query_action);
server_menu.add_action(*m_close_query_action);
auto& channel_menu = menubar->add_menu("Channel");
channel_menu.add_action(*m_change_topic_action);
channel_menu.add_action(*m_invite_user_action);
channel_menu.add_action(*m_banlist_action);
auto& channel_control_menu = channel_menu.add_submenu("Control");
channel_control_menu.add_action(*m_voice_user_action);
channel_control_menu.add_action(*m_devoice_user_action);
channel_control_menu.add_action(*m_hop_user_action);
channel_control_menu.add_action(*m_dehop_user_action);
channel_control_menu.add_action(*m_op_user_action);
channel_control_menu.add_action(*m_deop_user_action);
channel_control_menu.add_separator();
channel_control_menu.add_action(*m_kick_user_action);
channel_menu.add_separator();
channel_menu.add_action(*m_cycle_channel_action);
channel_menu.add_action(*m_part_action);
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("IRC Client", GUI::Icon::default_icon("app-irc-client"), this));
GUI::Application::the()->set_menubar(move(menubar));
}
void IRCAppWindow::setup_widgets()
{
auto& widget = set_main_widget<GUI::Widget>();
widget.set_fill_with_background_color(true);
widget.set_layout<GUI::VerticalBoxLayout>();
widget.layout()->set_spacing(0);
auto& toolbar_container = widget.add<GUI::ToolBarContainer>();
auto& toolbar = toolbar_container.add<GUI::ToolBar>();
toolbar.set_has_frame(false);
toolbar.add_action(*m_change_nick_action);
toolbar.add_separator();
toolbar.add_action(*m_join_action);
toolbar.add_action(*m_part_action);
toolbar.add_separator();
toolbar.add_action(*m_whois_action);
toolbar.add_action(*m_open_query_action);
toolbar.add_action(*m_close_query_action);
auto& outer_container = widget.add<GUI::Widget>();
outer_container.set_layout<GUI::VerticalBoxLayout>();
outer_container.layout()->set_margins({ 2, 0, 2, 2 });
auto& horizontal_container = outer_container.add<GUI::HorizontalSplitter>();
m_window_list = horizontal_container.add<GUI::TableView>();
m_window_list->set_column_headers_visible(false);
m_window_list->set_alternating_row_colors(false);
m_window_list->set_model(m_client->client_window_list_model());
m_window_list->set_activates_on_selection(true);
m_window_list->set_fixed_width(100);
m_window_list->on_activation = [this](auto& index) {
set_active_window(m_client->window_at(index.row()));
};
m_container = horizontal_container.add<GUI::StackWidget>();
m_container->on_active_widget_change = [this](auto*) {
update_gui_actions();
};
create_window(&m_client, IRCWindow::Server, "Server");
}
void IRCAppWindow::set_active_window(IRCWindow& window)
{
m_container->set_active_widget(&window);
window.clear_unread_count();
auto index = m_window_list->model()->index(m_client->window_index(window));
m_window_list->selection().set(index);
}
void IRCAppWindow::update_gui_actions()
{
auto* window = static_cast<IRCWindow*>(m_container->active_widget());
bool is_open_channel = window && window->type() == IRCWindow::Type::Channel && window->channel().is_open();
m_change_topic_action->set_enabled(is_open_channel);
m_invite_user_action->set_enabled(is_open_channel);
m_banlist_action->set_enabled(is_open_channel);
m_voice_user_action->set_enabled(is_open_channel);
m_devoice_user_action->set_enabled(is_open_channel);
m_hop_user_action->set_enabled(is_open_channel);
m_dehop_user_action->set_enabled(is_open_channel);
m_op_user_action->set_enabled(is_open_channel);
m_deop_user_action->set_enabled(is_open_channel);
m_kick_user_action->set_enabled(is_open_channel);
m_cycle_channel_action->set_enabled(is_open_channel);
m_part_action->set_enabled(is_open_channel);
}
NonnullRefPtr<IRCWindow> IRCAppWindow::create_window(void* owner, IRCWindow::Type type, const String& name)
{
return m_container->add<IRCWindow>(m_client, owner, type, name);
}

View file

@ -0,0 +1,76 @@
/*
* 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 "IRCClient.h"
#include "IRCWindow.h"
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
class IRCAppWindow : public GUI::Window {
C_OBJECT(IRCAppWindow);
public:
virtual ~IRCAppWindow() override;
static IRCAppWindow& the();
void set_active_window(IRCWindow&);
private:
IRCAppWindow(String server, int port);
void setup_client();
void setup_actions();
void setup_menus();
void setup_widgets();
void update_title();
void update_gui_actions();
NonnullRefPtr<IRCWindow> create_window(void* owner, IRCWindow::Type, const String& name);
NonnullRefPtr<IRCClient> m_client;
RefPtr<GUI::StackWidget> m_container;
RefPtr<GUI::TableView> m_window_list;
RefPtr<GUI::Action> m_join_action;
RefPtr<GUI::Action> m_list_channels_action;
RefPtr<GUI::Action> m_part_action;
RefPtr<GUI::Action> m_cycle_channel_action;
RefPtr<GUI::Action> m_whois_action;
RefPtr<GUI::Action> m_open_query_action;
RefPtr<GUI::Action> m_close_query_action;
RefPtr<GUI::Action> m_change_nick_action;
RefPtr<GUI::Action> m_change_topic_action;
RefPtr<GUI::Action> m_invite_user_action;
RefPtr<GUI::Action> m_banlist_action;
RefPtr<GUI::Action> m_voice_user_action;
RefPtr<GUI::Action> m_devoice_user_action;
RefPtr<GUI::Action> m_hop_user_action;
RefPtr<GUI::Action> m_dehop_user_action;
RefPtr<GUI::Action> m_op_user_action;
RefPtr<GUI::Action> m_deop_user_action;
RefPtr<GUI::Action> m_kick_user_action;
};

View file

@ -0,0 +1,144 @@
/*
* 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 "IRCChannel.h"
#include "IRCChannelMemberListModel.h"
#include "IRCClient.h"
#include <stdio.h>
IRCChannel::IRCChannel(IRCClient& client, const String& name)
: m_client(client)
, m_name(name)
, m_log(IRCLogBuffer::create())
, m_member_model(IRCChannelMemberListModel::create(*this))
{
m_window = m_client.aid_create_window(this, IRCWindow::Channel, m_name);
m_window->set_log_buffer(*m_log);
}
IRCChannel::~IRCChannel()
{
}
NonnullRefPtr<IRCChannel> IRCChannel::create(IRCClient& client, const String& name)
{
return adopt(*new IRCChannel(client, name));
}
void IRCChannel::add_member(const String& name, char prefix)
{
for (auto& member : m_members) {
if (member.name == name) {
member.prefix = prefix;
return;
}
}
m_members.append({ name, prefix });
m_member_model->update();
}
void IRCChannel::remove_member(const String& name)
{
m_members.remove_first_matching([&](auto& member) { return name == member.name; });
}
void IRCChannel::add_message(char prefix, const String& name, const String& text, Color color)
{
log().add_message(prefix, name, text, color);
window().did_add_message(name, text);
}
void IRCChannel::add_message(const String& text, Color color)
{
log().add_message(text, color);
window().did_add_message();
}
void IRCChannel::say(const String& text)
{
m_client.send_privmsg(m_name, text);
add_message(' ', m_client.nickname(), text);
}
void IRCChannel::handle_join(const String& nick, const String& hostmask)
{
if (nick == m_client.nickname()) {
m_open = true;
return;
}
add_member(nick, (char)0);
m_member_model->update();
if (m_client.show_join_part_messages())
add_message(String::formatted("*** {} [{}] has joined {}", nick, hostmask, m_name), Color::MidGreen);
}
void IRCChannel::handle_part(const String& nick, const String& hostmask)
{
if (nick == m_client.nickname()) {
m_open = false;
m_members.clear();
m_client.did_part_from_channel({}, *this);
} else {
remove_member(nick);
}
m_member_model->update();
if (m_client.show_join_part_messages())
add_message(String::formatted("*** {} [{}] has parted from {}", nick, hostmask, m_name), Color::MidGreen);
}
void IRCChannel::handle_quit(const String& nick, const String& hostmask, const String& message)
{
if (nick == m_client.nickname()) {
m_open = false;
m_members.clear();
m_client.did_part_from_channel({}, *this);
} else {
remove_member(nick);
}
m_member_model->update();
add_message(String::formatted("*** {} [{}] has quit ({})", nick, hostmask, message), Color::MidGreen);
}
void IRCChannel::handle_topic(const String& nick, const String& topic)
{
if (nick.is_null())
add_message(String::formatted("*** Topic is \"{}\"", topic), Color::MidBlue);
else
add_message(String::formatted("*** {} set topic to \"{}\"", nick, topic), Color::MidBlue);
}
void IRCChannel::notify_nick_changed(const String& old_nick, const String& new_nick)
{
for (auto& member : m_members) {
if (member.name == old_nick) {
member.name = new_nick;
m_member_model->update();
if (m_client.show_nick_change_messages())
add_message(String::formatted("~ {} changed nickname to {}", old_nick, new_nick), Color::MidMagenta);
return;
}
}
}

View file

@ -0,0 +1,94 @@
/*
* 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 "IRCLogBuffer.h"
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <AK/Vector.h>
class IRCClient;
class IRCChannelMemberListModel;
class IRCWindow;
class IRCChannel : public RefCounted<IRCChannel> {
public:
static NonnullRefPtr<IRCChannel> create(IRCClient&, const String&);
~IRCChannel();
bool is_open() const { return m_open; }
void set_open(bool b) { m_open = b; }
String name() const { return m_name; }
void add_member(const String& name, char prefix);
void remove_member(const String& name);
void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
void add_message(const String& text, Color = Color::Black);
void say(const String&);
const IRCLogBuffer& log() const { return *m_log; }
IRCLogBuffer& log() { return *m_log; }
IRCChannelMemberListModel* member_model() { return m_member_model.ptr(); }
const IRCChannelMemberListModel* member_model() const { return m_member_model.ptr(); }
int member_count() const { return m_members.size(); }
String member_at(int i) { return m_members[i].name; }
void handle_join(const String& nick, const String& hostmask);
void handle_part(const String& nick, const String& hostmask);
void handle_quit(const String& nick, const String& hostmask, const String& message);
void handle_topic(const String& nick, const String& topic);
IRCWindow& window() { return *m_window; }
const IRCWindow& window() const { return *m_window; }
String topic() const { return m_topic; }
void notify_nick_changed(const String& old_nick, const String& new_nick);
private:
IRCChannel(IRCClient&, const String&);
IRCClient& m_client;
String m_name;
String m_topic;
struct Member {
String name;
char prefix { 0 };
};
Vector<Member> m_members;
bool m_open { false };
NonnullRefPtr<IRCLogBuffer> m_log;
NonnullRefPtr<IRCChannelMemberListModel> m_member_model;
IRCWindow* m_window { nullptr };
};

View file

@ -0,0 +1,81 @@
/*
* 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 "IRCChannelMemberListModel.h"
#include "IRCChannel.h"
#include <stdio.h>
#include <time.h>
IRCChannelMemberListModel::IRCChannelMemberListModel(IRCChannel& channel)
: m_channel(channel)
{
}
IRCChannelMemberListModel::~IRCChannelMemberListModel()
{
}
int IRCChannelMemberListModel::row_count(const GUI::ModelIndex&) const
{
return m_channel.member_count();
}
int IRCChannelMemberListModel::column_count(const GUI::ModelIndex&) const
{
return 1;
}
String IRCChannelMemberListModel::column_name(int column) const
{
switch (column) {
case Column::Name:
return "Name";
}
ASSERT_NOT_REACHED();
}
GUI::Variant IRCChannelMemberListModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
if (role == GUI::ModelRole::TextAlignment)
return Gfx::TextAlignment::CenterLeft;
if (role == GUI::ModelRole::Display) {
switch (index.column()) {
case Column::Name:
return m_channel.member_at(index.row());
}
}
return {};
}
void IRCChannelMemberListModel::update()
{
did_update();
}
String IRCChannelMemberListModel::nick_at(const GUI::ModelIndex& index) const
{
return data(index, GUI::ModelRole::Display).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 <AK/Function.h>
#include <LibGUI/Model.h>
class IRCChannel;
class IRCChannelMemberListModel final : public GUI::Model {
public:
enum Column {
Name
};
static NonnullRefPtr<IRCChannelMemberListModel> create(IRCChannel& channel) { return adopt(*new IRCChannelMemberListModel(channel)); }
virtual ~IRCChannelMemberListModel() override;
virtual int row_count(const GUI::ModelIndex&) const override;
virtual int column_count(const GUI::ModelIndex&) const override;
virtual String column_name(int column) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override;
virtual String nick_at(const GUI::ModelIndex& index) const;
private:
explicit IRCChannelMemberListModel(IRCChannel&);
IRCChannel& m_channel;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,236 @@
/*
* 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 "IRCLogBuffer.h"
#include "IRCWindow.h"
#include <AK/CircularQueue.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/String.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/TCPSocket.h>
class IRCChannel;
class IRCQuery;
class IRCWindowListModel;
class IRCClient final : public Core::Object {
C_OBJECT(IRCClient)
friend class IRCChannel;
friend class IRCQuery;
public:
virtual ~IRCClient() override;
void set_server(const String& hostname, int port = 6667);
bool connect();
String hostname() const { return m_hostname; }
int port() const { return m_port; }
String nickname() const { return m_nickname; }
String ctcp_version_reply() const { return m_ctcp_version_reply; }
String ctcp_userinfo_reply() const { return m_ctcp_userinfo_reply; }
String ctcp_finger_reply() const { return m_ctcp_finger_reply; }
bool show_join_part_messages() const { return m_show_join_part_messages; }
bool show_nick_change_messages() const { return m_show_nick_change_messages; }
bool notify_on_message() const { return m_notify_on_message; }
bool notify_on_mention() const { return m_notify_on_mention; }
void join_channel(const String&);
void part_channel(const String&);
void change_nick(const String&);
static bool is_nick_prefix(char);
static bool is_channel_prefix(char);
String nick_without_prefix(const String& nick);
IRCWindow* current_window() { return aid_get_active_window(); }
const IRCWindow* current_window() const { return aid_get_active_window(); }
Function<void()> on_disconnect;
Function<void()> on_server_message;
Function<void(const String&)> on_nickname_changed;
Function<void(IRCChannel&)> on_part_from_channel;
Function<NonnullRefPtr<IRCWindow>(void*, IRCWindow::Type, const String&)> aid_create_window;
Function<IRCWindow*()> aid_get_active_window;
Function<void()> aid_update_window_list;
void register_subwindow(IRCWindow&);
void unregister_subwindow(IRCWindow&);
IRCWindowListModel* client_window_list_model() { return m_client_window_list_model.ptr(); }
const IRCWindowListModel* client_window_list_model() const { return m_client_window_list_model.ptr(); }
int window_count() const { return m_windows.size(); }
const IRCWindow& window_at(int index) const { return *m_windows.at(index); }
IRCWindow& window_at(int index) { return *m_windows.at(index); }
size_t window_index(const IRCWindow& window) const
{
for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows[i] == &window)
return i;
}
ASSERT_NOT_REACHED();
}
void did_part_from_channel(Badge<IRCChannel>, IRCChannel&);
void handle_user_input_in_channel(const String& channel_name, const String&);
void handle_user_input_in_query(const String& query_name, const String&);
void handle_user_input_in_server(const String&);
void handle_list_channels_action();
void handle_whois_action(const String& nick);
void handle_ctcp_user_action(const String& nick, const String& message);
void handle_open_query_action(const String&);
void handle_close_query_action(const String&);
void handle_join_action(const String& channel_name);
void handle_part_action(const String& channel_name);
void handle_cycle_channel_action(const String& channel_name);
void handle_change_nick_action(const String& nick);
void handle_change_topic_action(const String& channel_name, const String&);
void handle_invite_user_action(const String& channel_name, const String& nick);
void handle_banlist_action(const String& channel_name);
void handle_voice_user_action(const String& channel_name, const String& nick);
void handle_devoice_user_action(const String& channel_name, const String& nick);
void handle_hop_user_action(const String& channel_name, const String& nick);
void handle_dehop_user_action(const String& channel_name, const String& nick);
void handle_op_user_action(const String& channel_name, const String& nick);
void handle_deop_user_action(const String& channel_name, const String& nick);
void handle_kick_user_action(const String& channel_name, const String& nick, const String&);
IRCQuery* query_with_name(const String&);
IRCQuery& ensure_query(const String& name);
IRCChannel& ensure_channel(const String& name);
void add_server_message(const String&, Color = Color::Black);
private:
IRCClient(String server, int port);
struct Message {
String prefix;
String command;
Vector<String> arguments;
};
enum class PrivmsgOrNotice {
Privmsg,
Notice,
};
void receive_from_server();
void send(const String&);
void send_user();
void send_nick();
void send_pong(const String& server);
void send_privmsg(const String& target, const String&);
void send_notice(const String& target, const String&);
void send_topic(const String& channel_name, const String&);
void send_invite(const String& channel_name, const String& nick);
void send_banlist(const String& channel_name);
void send_voice_user(const String& channel_name, const String& nick);
void send_devoice_user(const String& channel_name, const String& nick);
void send_hop_user(const String& channel_name, const String& nick);
void send_dehop_user(const String& channel_name, const String& nick);
void send_op_user(const String& channel_name, const String& nick);
void send_deop_user(const String& channel_name, const String& nick);
void send_kick(const String& channel_name, const String& nick, const String&);
void send_list();
void send_whois(const String&);
void process_line(const String&);
void handle_join(const Message&);
void handle_part(const Message&);
void handle_quit(const Message&);
void handle_ping(const Message&);
void handle_topic(const Message&);
void handle_rpl_welcome(const Message&);
void handle_rpl_topic(const Message&);
void handle_rpl_whoisuser(const Message&);
void handle_rpl_whoisserver(const Message&);
void handle_rpl_whoisoperator(const Message&);
void handle_rpl_whoisidle(const Message&);
void handle_rpl_endofwho(const Message&);
void handle_rpl_endofwhois(const Message&);
void handle_rpl_endofwhowas(const Message&);
void handle_rpl_endofmotd(const Message&);
void handle_rpl_whoischannels(const Message&);
void handle_rpl_topicwhotime(const Message&);
void handle_rpl_endofnames(const Message&);
void handle_rpl_endofbanlist(const Message&);
void handle_rpl_namreply(const Message&);
void handle_rpl_banlist(const Message&);
void handle_err_nosuchnick(const Message&);
void handle_err_unknowncommand(const Message&);
void handle_err_nicknameinuse(const Message&);
void handle_privmsg_or_notice(const Message&, PrivmsgOrNotice);
void handle_nick(const Message&);
void handle(const Message&);
void handle_user_command(const String&);
void handle_ctcp_request(const StringView& peer, const StringView& payload);
void handle_ctcp_response(const StringView& peer, const StringView& payload);
void send_ctcp_request(const StringView& peer, const StringView& payload);
void send_ctcp_response(const StringView& peer, const StringView& payload);
void on_socket_connected();
String m_hostname;
int m_port { 6667 };
RefPtr<Core::TCPSocket> m_socket;
String m_nickname;
RefPtr<Core::Notifier> m_notifier;
HashMap<String, RefPtr<IRCChannel>, CaseInsensitiveStringTraits> m_channels;
HashMap<String, RefPtr<IRCQuery>, CaseInsensitiveStringTraits> m_queries;
bool m_show_join_part_messages { 1 };
bool m_show_nick_change_messages { 1 };
bool m_notify_on_message { 1 };
bool m_notify_on_mention { 1 };
String m_ctcp_version_reply;
String m_ctcp_userinfo_reply;
String m_ctcp_finger_reply;
Vector<IRCWindow*> m_windows;
IRCWindow* m_server_subwindow { nullptr };
NonnullRefPtr<IRCWindowListModel> m_client_window_list_model;
NonnullRefPtr<IRCLogBuffer> m_log;
NonnullRefPtr<Core::ConfigFile> m_config;
};

View file

@ -0,0 +1,98 @@
/*
* 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 "IRCLogBuffer.h"
#include <AK/StringBuilder.h>
#include <LibWeb/DOM/DocumentFragment.h>
#include <LibWeb/DOM/DocumentType.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <time.h>
NonnullRefPtr<IRCLogBuffer> IRCLogBuffer::create()
{
return adopt(*new IRCLogBuffer);
}
IRCLogBuffer::IRCLogBuffer()
{
m_document = Web::DOM::Document::create();
m_document->append_child(adopt(*new Web::DOM::DocumentType(document())));
auto html_element = m_document->create_element("html");
m_document->append_child(html_element);
auto head_element = m_document->create_element("head");
html_element->append_child(head_element);
auto style_element = m_document->create_element("style");
style_element->append_child(adopt(*new Web::DOM::Text(document(), "div { font-family: Csilla; font-weight: lighter; }")));
head_element->append_child(style_element);
auto body_element = m_document->create_element("body");
html_element->append_child(body_element);
m_container_element = body_element;
}
IRCLogBuffer::~IRCLogBuffer()
{
}
static String timestamp_string()
{
auto now = time(nullptr);
auto* tm = localtime(&now);
return String::formatted("{:02}:{:02}:{:02} ", tm->tm_hour, tm->tm_min, tm->tm_sec);
}
void IRCLogBuffer::add_message(char prefix, const String& name, const String& text, Color color)
{
auto nick_string = String::formatted("<{}{}> ", prefix ? prefix : ' ', name.characters());
auto html = String::formatted(
"<span>{}</span>"
"<b>{}</b>"
"<span>{}</span>",
timestamp_string(),
escape_html_entities(nick_string),
escape_html_entities(text));
auto wrapper = m_document->create_element(Web::HTML::TagNames::div);
wrapper->set_attribute(Web::HTML::AttributeNames::style, String::formatted("color: {}", color.to_string()));
wrapper->set_inner_html(html);
m_container_element->append_child(wrapper);
m_document->force_layout();
}
void IRCLogBuffer::add_message(const String& text, Color color)
{
auto html = String::formatted(
"<span>{}</span>"
"<span>{}</span>",
timestamp_string(),
escape_html_entities(text));
auto wrapper = m_document->create_element(Web::HTML::TagNames::div);
wrapper->set_attribute(Web::HTML::AttributeNames::style, String::formatted("color: {}", color.to_string()));
wrapper->set_inner_html(html);
m_container_element->append_child(wrapper);
m_document->force_layout();
}

View file

@ -0,0 +1,58 @@
/*
* 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/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <LibGfx/Color.h>
#include <LibWeb/DOM/Document.h>
class IRCLogBuffer : public RefCounted<IRCLogBuffer> {
public:
static NonnullRefPtr<IRCLogBuffer> create();
~IRCLogBuffer();
struct Message {
time_t timestamp { 0 };
char prefix { 0 };
String sender;
String text;
Color color { Color::Black };
};
void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
void add_message(const String& text, Color = Color::Black);
const Web::DOM::Document& document() const { return *m_document; }
Web::DOM::Document& document() { return *m_document; }
private:
IRCLogBuffer();
RefPtr<Web::DOM::Document> m_document;
RefPtr<Web::DOM::Element> m_container_element;
};

View file

@ -0,0 +1,65 @@
/*
* 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 "IRCQuery.h"
#include "IRCClient.h"
#include <stdio.h>
IRCQuery::IRCQuery(IRCClient& client, const String& name)
: m_client(client)
, m_name(name)
, m_log(IRCLogBuffer::create())
{
m_window = m_client->aid_create_window(this, IRCWindow::Query, m_name);
m_window->set_log_buffer(*m_log);
}
IRCQuery::~IRCQuery()
{
}
NonnullRefPtr<IRCQuery> IRCQuery::create(IRCClient& client, const String& name)
{
return adopt(*new IRCQuery(client, name));
}
void IRCQuery::add_message(char prefix, const String& name, const String& text, Color color)
{
log().add_message(prefix, name, text, color);
window().did_add_message(name, text);
}
void IRCQuery::add_message(const String& text, Color color)
{
log().add_message(text, color);
window().did_add_message();
}
void IRCQuery::say(const String& text)
{
m_client->send_privmsg(m_name, text);
add_message(' ', m_client->nickname(), text);
}

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 "IRCLogBuffer.h"
#include <AK/CircularQueue.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <AK/Vector.h>
class IRCClient;
class IRCWindow;
class IRCQuery : public RefCounted<IRCQuery> {
public:
static NonnullRefPtr<IRCQuery> create(IRCClient&, const String& name);
~IRCQuery();
String name() const { return m_name; }
void add_message(char prefix, const String& name, const String& text, Color = Color::Black);
void add_message(const String& text, Color = Color::Black);
const IRCLogBuffer& log() const { return *m_log; }
IRCLogBuffer& log() { return *m_log; }
void say(const String&);
IRCWindow& window() { return *m_window; }
const IRCWindow& window() const { return *m_window; }
private:
IRCQuery(IRCClient&, const String& name);
NonnullRefPtr<IRCClient> m_client;
String m_name;
RefPtr<IRCWindow> m_window;
NonnullRefPtr<IRCLogBuffer> m_log;
};

View file

@ -0,0 +1,278 @@
/*
* 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 "IRCWindow.h"
#include "IRCChannel.h"
#include "IRCChannelMemberListModel.h"
#include "IRCClient.h"
#include <AK/StringBuilder.h>
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Notification.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
#include <LibWeb/InProcessWebView.h>
IRCWindow::IRCWindow(IRCClient& client, void* owner, Type type, const String& name)
: m_client(client)
, m_owner(owner)
, m_type(type)
, m_name(name)
{
set_layout<GUI::VerticalBoxLayout>();
// Make a container for the log buffer view + (optional) member list.
auto& container = add<GUI::HorizontalSplitter>();
m_page_view = container.add<Web::InProcessWebView>();
if (m_type == Channel) {
auto& member_view = container.add<GUI::TableView>();
member_view.set_column_headers_visible(false);
member_view.set_fixed_width(100);
member_view.set_alternating_row_colors(false);
member_view.set_model(channel().member_model());
member_view.set_activates_on_selection(true);
member_view.on_activation = [&](auto& index) {
if (!index.is_valid())
return;
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_open_query_action(m_client->nick_without_prefix(nick.characters()));
};
member_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (!index.is_valid())
return;
m_context_menu = GUI::Menu::construct();
m_context_menu->add_action(GUI::Action::create("Open query", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-open-query.png"), [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_open_query_action(m_client->nick_without_prefix(nick.characters()));
}));
m_context_menu->add_action(GUI::Action::create("Whois", Gfx::Bitmap::load_from_file("/res/icons/16x16/irc-whois.png"), [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_whois_action(m_client->nick_without_prefix(nick.characters()));
}));
auto& context_control_menu = m_context_menu->add_submenu("Control");
context_control_menu.add_action(GUI::Action::create("Voice", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_voice_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_action(GUI::Action::create("DeVoice", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_devoice_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_action(GUI::Action::create("Hop", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_hop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_action(GUI::Action::create("DeHop", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_dehop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_action(GUI::Action::create("Op", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_op_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_action(GUI::Action::create("DeOp", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_deop_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()));
}));
context_control_menu.add_separator();
context_control_menu.add_action(GUI::Action::create("Kick", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
if (IRCClient::is_nick_prefix(nick[0]))
nick = nick.substring(1, nick.length() - 1);
String value;
if (GUI::InputBox::show(value, window(), "Enter reason:", "Reason") == GUI::InputBox::ExecOK)
m_client->handle_kick_user_action(m_name.characters(), m_client->nick_without_prefix(nick.characters()), value);
}));
auto& context_ctcp_menu = m_context_menu->add_submenu("CTCP");
context_ctcp_menu.add_action(GUI::Action::create("User info", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "USERINFO");
}));
context_ctcp_menu.add_action(GUI::Action::create("Finger", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "FINGER");
}));
context_ctcp_menu.add_action(GUI::Action::create("Time", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "TIME");
}));
context_ctcp_menu.add_action(GUI::Action::create("Version", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "VERSION");
}));
context_ctcp_menu.add_action(GUI::Action::create("Client info", [&](const GUI::Action&) {
auto nick = channel().member_model()->nick_at(member_view.selection().first());
if (nick.is_empty())
return;
m_client->handle_ctcp_user_action(m_client->nick_without_prefix(nick.characters()), "CLIENTINFO");
}));
m_context_menu->popup(event.screen_position());
};
}
m_text_box = add<GUI::TextBox>();
m_text_box->set_fixed_height(19);
m_text_box->on_return_pressed = [this] {
if (m_type == Channel)
m_client->handle_user_input_in_channel(m_name, m_text_box->text());
else if (m_type == Query)
m_client->handle_user_input_in_query(m_name, m_text_box->text());
else if (m_type == Server)
m_client->handle_user_input_in_server(m_text_box->text());
m_text_box->add_current_text_to_history();
m_text_box->clear();
};
m_text_box->set_history_enabled(true);
m_text_box->set_placeholder("Message");
m_client->register_subwindow(*this);
}
IRCWindow::~IRCWindow()
{
m_client->unregister_subwindow(*this);
}
void IRCWindow::set_log_buffer(const IRCLogBuffer& log_buffer)
{
m_log_buffer = &log_buffer;
m_page_view->set_document(const_cast<Web::DOM::Document*>(&log_buffer.document()));
}
bool IRCWindow::is_active() const
{
return m_client->current_window() == this;
}
void IRCWindow::post_notification_if_needed(const String& name, const String& message)
{
if (name.is_null() || message.is_null())
return;
if (is_active() && window()->is_active())
return;
auto notification = GUI::Notification::construct();
if (type() == Type::Channel) {
if (!m_client->notify_on_mention())
return;
if (!message.contains(m_client->nickname()))
return;
StringBuilder builder;
builder.append(name);
builder.append(" in ");
builder.append(m_name);
notification->set_title(builder.to_string());
} else {
if (!m_client->notify_on_message())
return;
notification->set_title(name);
}
notification->set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/app-irc-client.png"));
notification->set_text(message);
notification->show();
}
void IRCWindow::did_add_message(const String& name, const String& message)
{
post_notification_if_needed(name, message);
if (!is_active()) {
++m_unread_count;
m_client->aid_update_window_list();
return;
}
m_page_view->scroll_to_bottom();
}
void IRCWindow::clear_unread_count()
{
if (!m_unread_count)
return;
m_unread_count = 0;
m_client->aid_update_window_list();
}
int IRCWindow::unread_count() const
{
return m_unread_count;
}

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