diff --git a/Applications/Browser/DownloadWidget.cpp b/Applications/Browser/DownloadWidget.cpp new file mode 100644 index 0000000000..71b618656d --- /dev/null +++ b/Applications/Browser/DownloadWidget.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2020, Andreas Kling + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(url.to_string()); + ASSERT(m_download); + m_download->on_progress = [this](Optional total_size, u32 downloaded_size) { + did_progress(total_size.value(), downloaded_size); + }; + m_download->on_finish = [this](bool success, const ByteBuffer& payload, RefPtr payload_storage, const HashMap& response_headers) { + did_finish(success, payload, payload_storage, response_headers); + }; + + set_fill_with_background_color(true); + auto& layout = set_layout(); + layout.set_margins({ 4, 4, 4, 4 }); + + auto& animation_container = add(); + animation_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + animation_container.set_preferred_size(0, 32); + auto& animation_layout = animation_container.set_layout(); + auto& browser_icon_label = animation_container.add(); + browser_icon_label.set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/app-browser.png")); + browser_icon_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + browser_icon_label.set_preferred_size(32, 32); + animation_layout.add_spacer(); + auto& folder_icon_label = animation_container.add(); + folder_icon_label.set_icon(Gfx::Bitmap::load_from_file("/res/icons/32x32/filetype-folder.png")); + folder_icon_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + folder_icon_label.set_preferred_size(32, 32); + + auto& source_label = add(String::format("From: %s", url.to_string().characters())); + source_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + source_label.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + source_label.set_preferred_size(0, 16); + + m_progress_bar = add(); + m_progress_bar->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + m_progress_bar->set_preferred_size(0, 20); + + m_progress_label = add(); + m_progress_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); + m_progress_label->set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + m_progress_label->set_preferred_size(0, 16); + + auto& destination_label = add(String::format("To: %s", m_destination_path.characters())); + destination_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + destination_label.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + destination_label.set_preferred_size(0, 16); + + auto& button_container = add(); + auto& button_container_layout = button_container.set_layout(); + button_container_layout.add_spacer(); + m_cancel_button = button_container.add("Cancel"); + m_cancel_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + m_cancel_button->set_preferred_size(100, 22); + m_cancel_button->on_click = [this] { + bool success = m_download->stop(); + ASSERT(success); + window()->close(); + }; + + m_close_button = button_container.add("OK"); + m_close_button->set_enabled(false); + m_close_button->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + m_close_button->set_preferred_size(100, 22); + m_close_button->on_click = [this] { + window()->close(); + }; +} + +DownloadWidget::~DownloadWidget() +{ +} + +void DownloadWidget::did_progress(Optional total_size, u32 downloaded_size) +{ + m_progress_bar->set_min(0); + if (total_size.has_value()) + 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.appendf(" in %d 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.appendf("%d%%", 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, const ByteBuffer& payload, RefPtr payload_storage, const HashMap& response_headers) +{ + (void)payload; + (void)payload_storage; + (void)response_headers; + dbg() << "did_finish, success=" << success; + m_cancel_button->set_enabled(false); + m_close_button->set_enabled(true); + + if (!success) { + GUI::MessageBox::show(String::format("Download failed for some reason"), "Download failed", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); + window()->close(); + return; + } + + auto file_or_error = Core::File::open(m_destination_path, Core::IODevice::WriteOnly); + if (file_or_error.is_error()) { + GUI::MessageBox::show(String::format("Cannot open %s for writing", m_destination_path.characters()), "Download failed", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window()); + window()->close(); + return; + } + + auto& file = *file_or_error.value(); + bool write_success = file.write(payload.data(), payload.size()); + ASSERT(write_success); +} + +} diff --git a/Applications/Browser/DownloadWidget.h b/Applications/Browser/DownloadWidget.h new file mode 100644 index 0000000000..9c632bbfc6 --- /dev/null +++ b/Applications/Browser/DownloadWidget.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, Andreas Kling + * 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 +#include +#include +#include +#include + +namespace Browser { + +class DownloadWidget final : public GUI::Widget { + C_OBJECT(DownloadWidget); + +public: + virtual ~DownloadWidget() override; + +private: + explicit DownloadWidget(const URL&); + + void did_progress(Optional total_size, u32 downloaded_size); + void did_finish(bool success, const ByteBuffer& payload, RefPtr payload_storage, const HashMap& response_headers); + + URL m_url; + String m_destination_path; + RefPtr m_download; + RefPtr m_progress_bar; + RefPtr m_progress_label; + RefPtr m_cancel_button; + RefPtr m_close_button; + Core::ElapsedTimer m_elapsed_timer; +}; + +} diff --git a/Applications/Browser/Makefile b/Applications/Browser/Makefile index ac0182b6d5..354d07c7cb 100644 --- a/Applications/Browser/Makefile +++ b/Applications/Browser/Makefile @@ -1,5 +1,6 @@ OBJS = \ BookmarksBarWidget.o \ + DownloadWidget.o \ InspectorWidget.o \ Tab.o \ WindowActions.o \ diff --git a/Applications/Browser/Tab.cpp b/Applications/Browser/Tab.cpp index 970386e038..7eafb09f6d 100644 --- a/Applications/Browser/Tab.cpp +++ b/Applications/Browser/Tab.cpp @@ -26,6 +26,7 @@ #include "Tab.h" #include "BookmarksBarWidget.h" +#include "DownloadWidget.h" #include "History.h" #include "InspectorWidget.h" #include "WindowActions.h" @@ -143,6 +144,17 @@ Tab::Tab() m_link_context_menu->add_action(GUI::Action::create("Open in new tab", [this](auto&) { m_html_widget->on_link_click(m_link_context_menu_href, "_blank", 0); })); + m_link_context_menu->add_separator(); + m_link_context_menu->add_action(GUI::Action::create("Download", [this](auto&) { + auto window = GUI::Window::construct(); + window->set_rect(300, 300, 300, 150); + auto url = m_html_widget->document()->complete_url(m_link_context_menu_href); + window->set_title(String::format("0% of %s", url.basename().characters())); + window->set_resizable(false); + window->set_main_widget(url); + window->show(); + (void)window.leak_ref(); + })); m_html_widget->on_link_context_menu_request = [this](auto& href, auto& screen_position) { m_link_context_menu_href = href; @@ -165,10 +177,12 @@ Tab::Tab() on_favicon_change(icon); }; - 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); + 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 = widget.add(); @@ -198,37 +212,41 @@ Tab::Tab() })); auto& inspect_menu = m_menubar->add_menu("Inspect"); - inspect_menu.add_action(GUI::Action::create("View source", { Mod_Ctrl, Key_U }, [this](auto&) { - String filename_to_open; - char tmp_filename[] = "/tmp/view-source.XXXXXX"; - ASSERT(m_html_widget->document()); - if (m_html_widget->document()->url().protocol() == "file") { - filename_to_open = m_html_widget->document()->url().path(); - } else { - int fd = mkstemp(tmp_filename); - ASSERT(fd >= 0); - auto source = m_html_widget->document()->source(); - write(fd, source.characters(), source.length()); - close(fd); - filename_to_open = tmp_filename; - } - if (fork() == 0) { - execl("/bin/TextEditor", "TextEditor", filename_to_open.characters(), nullptr); - ASSERT_NOT_REACHED(); - } - }, this)); - inspect_menu.add_action(GUI::Action::create("Inspect DOM tree", { Mod_None, Key_F12 }, [this](auto&) { - if (!m_dom_inspector_window) { - m_dom_inspector_window = GUI::Window::construct(); - m_dom_inspector_window->set_rect(100, 100, 300, 500); - m_dom_inspector_window->set_title("DOM inspector"); - m_dom_inspector_window->set_main_widget(); - } - auto* inspector_widget = static_cast(m_dom_inspector_window->main_widget()); - inspector_widget->set_document(m_html_widget->document()); - m_dom_inspector_window->show(); - m_dom_inspector_window->move_to_front(); - }, this)); + inspect_menu.add_action(GUI::Action::create( + "View source", { Mod_Ctrl, Key_U }, [this](auto&) { + String filename_to_open; + char tmp_filename[] = "/tmp/view-source.XXXXXX"; + ASSERT(m_html_widget->document()); + if (m_html_widget->document()->url().protocol() == "file") { + filename_to_open = m_html_widget->document()->url().path(); + } else { + int fd = mkstemp(tmp_filename); + ASSERT(fd >= 0); + auto source = m_html_widget->document()->source(); + write(fd, source.characters(), source.length()); + close(fd); + filename_to_open = tmp_filename; + } + if (fork() == 0) { + execl("/bin/TextEditor", "TextEditor", filename_to_open.characters(), nullptr); + ASSERT_NOT_REACHED(); + } + }, + this)); + inspect_menu.add_action(GUI::Action::create( + "Inspect DOM tree", { Mod_None, Key_F12 }, [this](auto&) { + if (!m_dom_inspector_window) { + m_dom_inspector_window = GUI::Window::construct(); + m_dom_inspector_window->set_rect(100, 100, 300, 500); + m_dom_inspector_window->set_title("DOM inspector"); + m_dom_inspector_window->set_main_widget(); + } + auto* inspector_widget = static_cast(m_dom_inspector_window->main_widget()); + inspector_widget->set_document(m_html_widget->document()); + m_dom_inspector_window->show(); + m_dom_inspector_window->move_to_front(); + }, + this)); auto& debug_menu = m_menubar->add_menu("Debug"); debug_menu.add_action(GUI::Action::create(