mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 17:12:43 +00:00 
			
		
		
		
	LibWeb: Make Frame point weakly to Page
This patch makes Page weakable and allows page-less frames to exist. Page is single-owner, and Frame is multiple-owner, so it's not sound for Frame to assume its containing Page will stick around for its own entire lifetime. Fixes #3976.
This commit is contained in:
		
							parent
							
								
									e445ff670d
								
							
						
					
					
						commit
						81add73955
					
				
					 12 changed files with 87 additions and 56 deletions
				
			
		|  | @ -172,7 +172,8 @@ Color IdentifierStyleValue::to_color(const DOM::Document& document) const | ||||||
|     if (id() == CSS::ValueID::VendorSpecificLink) |     if (id() == CSS::ValueID::VendorSpecificLink) | ||||||
|         return document.link_color(); |         return document.link_color(); | ||||||
| 
 | 
 | ||||||
|     auto palette = document.frame()->page().palette(); |     ASSERT(document.page()); | ||||||
|  |     auto palette = document.page()->palette(); | ||||||
|     switch (id()) { |     switch (id()) { | ||||||
|     case CSS::ValueID::VendorSpecificPaletteDesktopBackground: |     case CSS::ValueID::VendorSpecificPaletteDesktopBackground: | ||||||
|         return palette.color(ColorRole::DesktopBackground); |         return palette.color(ColorRole::DesktopBackground); | ||||||
|  |  | ||||||
|  | @ -343,8 +343,10 @@ void Document::layout() | ||||||
|     m_layout_root->layout(); |     m_layout_root->layout(); | ||||||
|     m_layout_root->set_needs_display(); |     m_layout_root->set_needs_display(); | ||||||
| 
 | 
 | ||||||
|     if (frame()->is_main_frame()) |     if (frame()->is_main_frame()) { | ||||||
|         frame()->page().client().page_did_layout(); |         if (auto* page = this->page()) | ||||||
|  |             page->client().page_did_layout(); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Document::update_style() | void Document::update_style() | ||||||
|  | @ -446,27 +448,27 @@ Color Document::link_color() const | ||||||
| { | { | ||||||
|     if (m_link_color.has_value()) |     if (m_link_color.has_value()) | ||||||
|         return m_link_color.value(); |         return m_link_color.value(); | ||||||
|     if (!frame()) |     if (!page()) | ||||||
|         return Color::Blue; |         return Color::Blue; | ||||||
|     return frame()->page().palette().link(); |     return page()->palette().link(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Color Document::active_link_color() const | Color Document::active_link_color() const | ||||||
| { | { | ||||||
|     if (m_active_link_color.has_value()) |     if (m_active_link_color.has_value()) | ||||||
|         return m_active_link_color.value(); |         return m_active_link_color.value(); | ||||||
|     if (!frame()) |     if (!page()) | ||||||
|         return Color::Red; |         return Color::Red; | ||||||
|     return frame()->page().palette().active_link(); |     return page()->palette().active_link(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Color Document::visited_link_color() const | Color Document::visited_link_color() const | ||||||
| { | { | ||||||
|     if (m_visited_link_color.has_value()) |     if (m_visited_link_color.has_value()) | ||||||
|         return m_visited_link_color.value(); |         return m_visited_link_color.value(); | ||||||
|     if (!frame()) |     if (!page()) | ||||||
|         return Color::Magenta; |         return Color::Magenta; | ||||||
|     return frame()->page().palette().visited_link(); |     return page()->palette().visited_link(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static JS::VM& main_thread_vm() | static JS::VM& main_thread_vm() | ||||||
|  | @ -596,4 +598,14 @@ void Document::set_ready_state(const String& ready_state) | ||||||
|     dispatch_event(Event::create("readystatechange")); |     dispatch_event(Event::create("readystatechange")); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Page* Document::page() | ||||||
|  | { | ||||||
|  |     return m_frame ? m_frame->page() : nullptr; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Page* Document::page() const | ||||||
|  | { | ||||||
|  |     return m_frame ? m_frame->page() : nullptr; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -99,6 +99,9 @@ public: | ||||||
|     Frame* frame() { return m_frame.ptr(); } |     Frame* frame() { return m_frame.ptr(); } | ||||||
|     const Frame* frame() const { return m_frame.ptr(); } |     const Frame* frame() const { return m_frame.ptr(); } | ||||||
| 
 | 
 | ||||||
|  |     Page* page(); | ||||||
|  |     const Page* page() const; | ||||||
|  | 
 | ||||||
|     Color background_color(const Gfx::Palette&) const; |     Color background_color(const Gfx::Palette&) const; | ||||||
|     RefPtr<Gfx::Bitmap> background_image() const; |     RefPtr<Gfx::Bitmap> background_image() const; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -61,9 +61,8 @@ void Window::set_wrapper(Badge<Bindings::WindowObject>, Bindings::WindowObject& | ||||||
| 
 | 
 | ||||||
| void Window::alert(const String& message) | void Window::alert(const String& message) | ||||||
| { | { | ||||||
|     if (!m_document.frame()) |     if (auto* page = m_document.page()) | ||||||
|         return; |         page->client().page_did_request_alert(message); | ||||||
|     m_document.frame()->page().client().page_did_request_alert(message); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool Window::confirm(const String& message) | bool Window::confirm(const String& message) | ||||||
|  |  | ||||||
|  | @ -105,7 +105,8 @@ void HTMLFormElement::submit(RefPtr<HTMLInputElement> submitter) | ||||||
|         request.set_body(body); |         request.set_body(body); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     document().frame()->page().load(request); |     if (auto* page = document().page()) | ||||||
|  |         page->load(request); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -62,9 +62,9 @@ void HTMLInputElement::did_click_button(Badge<LayoutButton>) | ||||||
| 
 | 
 | ||||||
| RefPtr<LayoutNode> HTMLInputElement::create_layout_node(const CSS::StyleProperties* parent_style) | RefPtr<LayoutNode> HTMLInputElement::create_layout_node(const CSS::StyleProperties* parent_style) | ||||||
| { | { | ||||||
|     ASSERT(document().frame()); |     ASSERT(document().page()); | ||||||
|     auto& frame = *document().frame(); |     auto& page = *document().page(); | ||||||
|     auto& page_view = const_cast<InProcessWebView&>(static_cast<const InProcessWebView&>(frame.page().client())); |     auto& page_view = const_cast<InProcessWebView&>(static_cast<const InProcessWebView&>(page.client())); | ||||||
| 
 | 
 | ||||||
|     if (type() == "hidden") |     if (type() == "hidden") | ||||||
|         return nullptr; |         return nullptr; | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ void LayoutWidget::did_set_rect() | ||||||
| void LayoutWidget::update_widget() | void LayoutWidget::update_widget() | ||||||
| { | { | ||||||
|     auto adjusted_widget_position = absolute_rect().location().to_type<int>(); |     auto adjusted_widget_position = absolute_rect().location().to_type<int>(); | ||||||
|     auto& page_view = static_cast<const InProcessWebView&>(frame().page().client()); |     auto& page_view = static_cast<const InProcessWebView&>(frame().page()->client()); | ||||||
|     adjusted_widget_position.move_by(-page_view.horizontal_scrollbar().value(), -page_view.vertical_scrollbar().value()); |     adjusted_widget_position.move_by(-page_view.horizontal_scrollbar().value(), -page_view.vertical_scrollbar().value()); | ||||||
|     widget().move_to(adjusted_widget_position); |     widget().move_to(adjusted_widget_position); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -160,8 +160,10 @@ bool FrameLoader::load(const LoadRequest& request, Type type) | ||||||
| 
 | 
 | ||||||
|     set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, request)); |     set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, request)); | ||||||
| 
 | 
 | ||||||
|     if (type == Type::Navigation) |     if (type == Type::Navigation) { | ||||||
|         frame().page().client().page_did_start_loading(url); |         if (auto* page = frame().page()) | ||||||
|  |             page->client().page_did_start_loading(url); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if (type == Type::IFrame) |     if (type == Type::IFrame) | ||||||
|         return true; |         return true; | ||||||
|  | @ -184,7 +186,8 @@ bool FrameLoader::load(const LoadRequest& request, Type type) | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 dbg() << "Decoded favicon, " << bitmap->size(); |                 dbg() << "Decoded favicon, " << bitmap->size(); | ||||||
|                 frame().page().client().page_did_change_favicon(*bitmap); |                 if (auto* page = frame().page()) | ||||||
|  |                     page->client().page_did_change_favicon(*bitmap); | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -125,7 +125,6 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     NonnullRefPtr document = *m_frame.document(); |     NonnullRefPtr document = *m_frame.document(); | ||||||
|     auto& page_client = m_frame.page().client(); |  | ||||||
| 
 | 
 | ||||||
|     auto result = layout_root()->hit_test(position, HitTestType::Exact); |     auto result = layout_root()->hit_test(position, HitTestType::Exact); | ||||||
|     if (!result.layout_node) |     if (!result.layout_node) | ||||||
|  | @ -148,7 +147,8 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     m_frame.page().set_focused_frame({}, m_frame); |     if (auto* page = m_frame.page()) | ||||||
|  |         page->set_focused_frame({}, m_frame); | ||||||
| 
 | 
 | ||||||
|     auto offset = compute_mouse_event_offset(position, *result.layout_node); |     auto offset = compute_mouse_event_offset(position, *result.layout_node); | ||||||
|     node->dispatch_event(UIEvents::MouseEvent::create("mousedown", offset.x(), offset.y())); |     node->dispatch_event(UIEvents::MouseEvent::create("mousedown", offset.x(), offset.y())); | ||||||
|  | @ -158,7 +158,8 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt | ||||||
|     if (button == GUI::MouseButton::Right && is<HTML::HTMLImageElement>(*node)) { |     if (button == GUI::MouseButton::Right && is<HTML::HTMLImageElement>(*node)) { | ||||||
|         auto& image_element = downcast<HTML::HTMLImageElement>(*node); |         auto& image_element = downcast<HTML::HTMLImageElement>(*node); | ||||||
|         auto image_url = image_element.document().complete_url(image_element.src()); |         auto image_url = image_element.document().complete_url(image_element.src()); | ||||||
|         page_client.page_did_request_image_context_menu(m_frame.to_main_frame_position(position), image_url, "", modifiers, image_element.bitmap()); |         if (auto* page = m_frame.page()) | ||||||
|  |             page->client().page_did_request_image_context_menu(m_frame.to_main_frame_position(position), image_url, "", modifiers, image_element.bitmap()); | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -176,16 +177,19 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt | ||||||
|                 m_frame.scroll_to_anchor(anchor); |                 m_frame.scroll_to_anchor(anchor); | ||||||
|             } else { |             } else { | ||||||
|                 if (m_frame.is_main_frame()) { |                 if (m_frame.is_main_frame()) { | ||||||
|                     page_client.page_did_click_link(url, link->target(), modifiers); |                     if (auto* page = m_frame.page()) | ||||||
|  |                         page->client().page_did_click_link(url, link->target(), modifiers); | ||||||
|                 } else { |                 } else { | ||||||
|                     // FIXME: Handle different targets!
 |                     // FIXME: Handle different targets!
 | ||||||
|                     m_frame.loader().load(url, FrameLoader::Type::Navigation); |                     m_frame.loader().load(url, FrameLoader::Type::Navigation); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } else if (button == GUI::MouseButton::Right) { |         } else if (button == GUI::MouseButton::Right) { | ||||||
|             page_client.page_did_request_link_context_menu(m_frame.to_main_frame_position(position), url, link->target(), modifiers); |             if (auto* page = m_frame.page()) | ||||||
|  |                 page->client().page_did_request_link_context_menu(m_frame.to_main_frame_position(position), url, link->target(), modifiers); | ||||||
|         } else if (button == GUI::MouseButton::Middle) { |         } else if (button == GUI::MouseButton::Middle) { | ||||||
|             page_client.page_did_middle_click_link(url, link->target(), modifiers); |             if (auto* page = m_frame.page()) | ||||||
|  |                 page->client().page_did_middle_click_link(url, link->target(), modifiers); | ||||||
|         } |         } | ||||||
|     } else { |     } else { | ||||||
|         if (button == GUI::MouseButton::Left) { |         if (button == GUI::MouseButton::Left) { | ||||||
|  | @ -197,7 +201,8 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt | ||||||
|                 m_in_mouse_selection = true; |                 m_in_mouse_selection = true; | ||||||
|             } |             } | ||||||
|         } else if (button == GUI::MouseButton::Right) { |         } else if (button == GUI::MouseButton::Right) { | ||||||
|             page_client.page_did_request_context_menu(m_frame.to_main_frame_position(position)); |             if (auto* page = m_frame.page()) | ||||||
|  |                 page->client().page_did_request_context_menu(m_frame.to_main_frame_position(position)); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
|  | @ -214,7 +219,6 @@ bool EventHandler::handle_mousemove(const Gfx::IntPoint& position, unsigned butt | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     auto& document = *m_frame.document(); |     auto& document = *m_frame.document(); | ||||||
|     auto& page_client = m_frame.page().client(); |  | ||||||
| 
 | 
 | ||||||
|     bool hovered_node_changed = false; |     bool hovered_node_changed = false; | ||||||
|     bool is_hovering_link = false; |     bool is_hovering_link = false; | ||||||
|  | @ -227,7 +231,8 @@ bool EventHandler::handle_mousemove(const Gfx::IntPoint& position, unsigned butt | ||||||
|             document.set_hovered_node(result.layout_node->node()); |             document.set_hovered_node(result.layout_node->node()); | ||||||
|             result.layout_node->handle_mousemove({}, position, buttons, modifiers); |             result.layout_node->handle_mousemove({}, position, buttons, modifiers); | ||||||
|             // FIXME: It feels a bit aggressive to always update the cursor like this.
 |             // FIXME: It feels a bit aggressive to always update the cursor like this.
 | ||||||
|             page_client.page_did_request_cursor_change(Gfx::StandardCursor::None); |             if (auto* page = m_frame.page()) | ||||||
|  |                 page->client().page_did_request_cursor_change(Gfx::StandardCursor::None); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -262,28 +267,31 @@ bool EventHandler::handle_mousemove(const Gfx::IntPoint& position, unsigned butt | ||||||
|                 layout_root()->set_selection_end({ hit.layout_node, hit.index_in_node }); |                 layout_root()->set_selection_end({ hit.layout_node, hit.index_in_node }); | ||||||
|             } |             } | ||||||
|             dump_selection("MouseMove"); |             dump_selection("MouseMove"); | ||||||
|             page_client.page_did_change_selection(); |             if (auto* page = m_frame.page()) | ||||||
|  |                 page->client().page_did_change_selection(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (auto* page = m_frame.page()) { | ||||||
|         if (is_hovering_link) |         if (is_hovering_link) | ||||||
|         page_client.page_did_request_cursor_change(Gfx::StandardCursor::Hand); |             page->client().page_did_request_cursor_change(Gfx::StandardCursor::Hand); | ||||||
|         else if (is_hovering_text) |         else if (is_hovering_text) | ||||||
|         page_client.page_did_request_cursor_change(Gfx::StandardCursor::IBeam); |             page->client().page_did_request_cursor_change(Gfx::StandardCursor::IBeam); | ||||||
|         else |         else | ||||||
|         page_client.page_did_request_cursor_change(Gfx::StandardCursor::None); |             page->client().page_did_request_cursor_change(Gfx::StandardCursor::None); | ||||||
| 
 | 
 | ||||||
|         if (hovered_node_changed) { |         if (hovered_node_changed) { | ||||||
|             RefPtr<HTML::HTMLElement> hovered_html_element = document.hovered_node() ? document.hovered_node()->enclosing_html_element() : nullptr; |             RefPtr<HTML::HTMLElement> hovered_html_element = document.hovered_node() ? document.hovered_node()->enclosing_html_element() : nullptr; | ||||||
|             if (hovered_html_element && !hovered_html_element->title().is_null()) { |             if (hovered_html_element && !hovered_html_element->title().is_null()) { | ||||||
|             page_client.page_did_enter_tooltip_area(m_frame.to_main_frame_position(position), hovered_html_element->title()); |                 page->client().page_did_enter_tooltip_area(m_frame.to_main_frame_position(position), hovered_html_element->title()); | ||||||
|             } else { |             } else { | ||||||
|             page_client.page_did_leave_tooltip_area(); |                 page->client().page_did_leave_tooltip_area(); | ||||||
|             } |             } | ||||||
|             if (is_hovering_link) |             if (is_hovering_link) | ||||||
|             page_client.page_did_hover_link(document.complete_url(hovered_link_element->href())); |                 page->client().page_did_hover_link(document.complete_url(hovered_link_element->href())); | ||||||
|             else |             else | ||||||
|             page_client.page_did_unhover_link(); |                 page->client().page_did_unhover_link(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
| namespace Web { | namespace Web { | ||||||
| 
 | 
 | ||||||
| Frame::Frame(DOM::Element& host_element, Frame& main_frame) | Frame::Frame(DOM::Element& host_element, Frame& main_frame) | ||||||
|     : m_page(main_frame.page()) |     : m_page(*main_frame.page()) | ||||||
|     , m_main_frame(main_frame) |     , m_main_frame(main_frame) | ||||||
|     , m_loader(*this) |     , m_loader(*this) | ||||||
|     , m_event_handler({}, *this) |     , m_event_handler({}, *this) | ||||||
|  | @ -72,7 +72,7 @@ void Frame::setup() | ||||||
| 
 | 
 | ||||||
| bool Frame::is_focused_frame() const | bool Frame::is_focused_frame() const | ||||||
| { | { | ||||||
|     return this == &page().focused_frame(); |     return m_page && &m_page->focused_frame() == this; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Frame::set_document(DOM::Document* document) | void Frame::set_document(DOM::Document* document) | ||||||
|  | @ -89,10 +89,12 @@ void Frame::set_document(DOM::Document* document) | ||||||
| 
 | 
 | ||||||
|     if (m_document) { |     if (m_document) { | ||||||
|         m_document->attach_to_frame({}, *this); |         m_document->attach_to_frame({}, *this); | ||||||
|         page().client().page_did_change_title(m_document->title()); |         if (m_page) | ||||||
|  |             m_page->client().page_did_change_title(m_document->title()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     page().client().page_did_set_document_in_main_frame(m_document); |     if (m_page) | ||||||
|  |         m_page->client().page_did_set_document_in_main_frame(m_document); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Frame::set_size(const Gfx::IntSize& size) | void Frame::set_size(const Gfx::IntSize& size) | ||||||
|  | @ -120,7 +122,8 @@ void Frame::set_needs_display(const Gfx::IntRect& rect) | ||||||
|         return; |         return; | ||||||
| 
 | 
 | ||||||
|     if (is_main_frame()) { |     if (is_main_frame()) { | ||||||
|         page().client().page_did_invalidate(to_main_frame_rect(rect)); |         if (m_page) | ||||||
|  |             m_page->client().page_did_invalidate(to_main_frame_rect(rect)); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -168,7 +171,8 @@ void Frame::scroll_to_anchor(const String& fragment) | ||||||
|         float_rect.move_by(-padding_box.left, -padding_box.top); |         float_rect.move_by(-padding_box.left, -padding_box.top); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     page().client().page_did_request_scroll_into_view(enclosing_int_rect(float_rect)); |     if (m_page) | ||||||
|  |         m_page->client().page_did_request_scroll_into_view(enclosing_int_rect(float_rect)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Gfx::IntRect Frame::to_main_frame_rect(const Gfx::IntRect& a_rect) | Gfx::IntRect Frame::to_main_frame_rect(const Gfx::IntRect& a_rect) | ||||||
|  |  | ||||||
|  | @ -55,8 +55,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     void set_document(DOM::Document*); |     void set_document(DOM::Document*); | ||||||
| 
 | 
 | ||||||
|     Page& page() { return m_page; } |     Page* page() { return m_page; } | ||||||
|     const Page& page() const { return m_page; } |     const Page* page() const { return m_page; } | ||||||
| 
 | 
 | ||||||
|     const Gfx::IntSize& size() const { return m_size; } |     const Gfx::IntSize& size() const { return m_size; } | ||||||
|     void set_size(const Gfx::IntSize&); |     void set_size(const Gfx::IntSize&); | ||||||
|  | @ -100,7 +100,7 @@ private: | ||||||
| 
 | 
 | ||||||
|     void setup(); |     void setup(); | ||||||
| 
 | 
 | ||||||
|     Page& m_page; |     WeakPtr<Page> m_page; | ||||||
|     Frame& m_main_frame; |     Frame& m_main_frame; | ||||||
| 
 | 
 | ||||||
|     FrameLoader m_loader; |     FrameLoader m_loader; | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ namespace Web { | ||||||
| 
 | 
 | ||||||
| class PageClient; | class PageClient; | ||||||
| 
 | 
 | ||||||
| class Page { | class Page : public Weakable<Page> { | ||||||
|     AK_MAKE_NONCOPYABLE(Page); |     AK_MAKE_NONCOPYABLE(Page); | ||||||
|     AK_MAKE_NONMOVABLE(Page); |     AK_MAKE_NONMOVABLE(Page); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Andreas Kling
						Andreas Kling