Presenter: Support multiple frames per slide

This commit is contained in:
Kyle Lanmon 2023-03-20 22:56:55 -05:00 committed by Andreas Kling
parent 7c312980b0
commit fcda397136
8 changed files with 84 additions and 37 deletions

View file

@ -11,8 +11,10 @@
"slides": [
{
"title": "Introduction",
"frame_count": 2,
"objects": [
{
"frame": 0,
"type": "text",
"text": "Welcome to Presenter!",
@ -25,6 +27,7 @@
"text-alignment": "Center"
},
{
"frame": 1,
"type": "text",
"text": "This program is very cool. It supports:\n - Scaling properly to the window\n - Text\n - Switching between slides\n - that's all for now lol",
@ -40,8 +43,10 @@
},
{
"title": "The Second Slide",
"frame_count": 2,
"objects": [
{
"frame": 0,
"type": "text",
"text": "CatDog likes this program!",
@ -53,6 +58,7 @@
"text-alignment": "Center"
},
{
"frame": 1,
"type": "image",
"rect": [50, 50, 200, 100],

View file

@ -41,14 +41,20 @@ StringView Presentation::author() const
return "Unknown Author"sv;
}
bool Presentation::has_a_next_frame() const
bool Presentation::has_next_frame() const
{
return m_current_slide < u32(m_slides.size() > 1 ? m_slides.size() - 1 : 0);
if (m_slides.is_empty())
return false;
if (m_current_slide.value() < m_slides.size() - 1)
return true;
return m_current_frame_in_slide < m_slides[m_current_slide.value()].frame_count() - 1;
}
bool Presentation::has_a_previous_frame() const
bool Presentation::has_previous_frame() const
{
return m_current_slide > 0u;
if (m_current_slide > 0u)
return true;
return m_current_frame_in_slide > 0u;
}
void Presentation::next_frame()
@ -65,7 +71,7 @@ void Presentation::previous_frame()
m_current_frame_in_slide.sub(1);
if (m_current_frame_in_slide.has_overflow()) {
m_current_slide.saturating_sub(1);
m_current_frame_in_slide = m_current_slide == 0u ? 0 : current_slide().frame_count() - 1;
m_current_frame_in_slide = current_slide().frame_count() - 1;
}
}
@ -108,13 +114,15 @@ ErrorOr<NonnullOwnPtr<Presentation>> Presentation::load_from_file(StringView fil
auto presentation = Presentation::construct(size, metadata);
auto const& slides = maybe_slides.value();
unsigned i = 0;
for (auto const& maybe_slide : slides.values()) {
if (!maybe_slide.is_object())
return Error::from_string_view("Slides must be objects"sv);
auto const& slide_object = maybe_slide.as_object();
auto slide = TRY(Slide::parse_slide(slide_object));
auto slide = TRY(Slide::parse_slide(slide_object, i));
presentation->append_slide(move(slide));
i++;
}
return presentation;
@ -163,9 +171,8 @@ ErrorOr<DeprecatedString> Presentation::render()
for (size_t i = 0; i < m_slides.size(); ++i) {
HTMLElement slide_div;
slide_div.tag_name = "div"sv;
TRY(slide_div.style.try_set("display"sv, "none"sv));
TRY(slide_div.attributes.try_set("id"sv, DeprecatedString::formatted("slide{}", i)));
TRY(slide_div.attributes.try_set("class"sv, "slide"));
TRY(slide_div.attributes.try_set("class"sv, "slide hidden"sv));
auto& slide = m_slides[i];
TRY(slide_div.children.try_append(TRY(slide.render(*this))));
main_element.children.append(move(slide_div));
@ -181,18 +188,29 @@ ErrorOr<DeprecatedString> Presentation::render()
width: 100%;
height: 100%;
}
.hidden {
display: none;
}
</style><script>
function goto(slideIndex, frameIndex) {
// FIXME: Honor the frameIndex.
let slide;
for (slide of document.getElementsByClassName("slide")) {
slide.style.display = "none";
for (const slide of document.getElementsByClassName("slide")) {
slide.classList.add("hidden");
}
for (const frame of document.getElementsByClassName("frame")) {
frame.classList.add("hidden");
}
const slide = document.getElementById(`slide${slideIndex}`);
if (slide) slide.classList.remove("hidden");
for (let i = 0; i <= frameIndex; i++) {
for (const frame of document.getElementsByClassName(`slide${slideIndex}-frame${i}`)) {
if (frame) frame.classList.remove("hidden");
}
}
if (slide = document.getElementById(`slide${slideIndex}`))
slide.style.display = "block";
}
window.onload = function() { goto(0, 0) }
</script><body>
</script></head><body>
)"sv));
TRY(main_element.serialize(builder));
TRY(builder.try_append("</body></html>"sv));

View file

@ -32,8 +32,8 @@ public:
unsigned current_slide_number() const { return m_current_slide.value(); }
unsigned current_frame_in_slide_number() const { return m_current_frame_in_slide.value(); }
bool has_a_next_frame() const;
bool has_a_previous_frame() const;
bool has_next_frame() const;
bool has_previous_frame() const;
void next_frame();
void previous_frame();
void go_to_first_slide();

View file

@ -128,8 +128,8 @@ void PresenterWidget::update_web_view()
void PresenterWidget::update_slides_actions()
{
if (m_current_presentation) {
m_next_slide_action->set_enabled(m_current_presentation->has_a_next_frame());
m_previous_slide_action->set_enabled(m_current_presentation->has_a_previous_frame());
m_next_slide_action->set_enabled(m_current_presentation->has_next_frame());
m_previous_slide_action->set_enabled(m_current_presentation->has_previous_frame());
m_full_screen_action->set_enabled(true);
m_present_from_first_slide_action->set_enabled(true);
} else {

View file

@ -9,17 +9,18 @@
#include "Presentation.h"
#include <AK/JsonObject.h>
Slide::Slide(Vector<NonnullRefPtr<SlideObject>> slide_objects, DeprecatedString title)
: m_slide_objects(move(slide_objects))
Slide::Slide(unsigned frame_count, Vector<NonnullRefPtr<SlideObject>> slide_objects, DeprecatedString title)
: m_frame_count(move(frame_count))
, m_slide_objects(move(slide_objects))
, m_title(move(title))
{
}
ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json)
ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json, unsigned slide_index)
{
// FIXME: Use the text with the "title" role for a title, if there is no title given.
auto title = slide_json.get_deprecated_string("title"sv).value_or("Untitled slide");
auto frame_count = slide_json.get_u32("frame_count"sv).value_or(1);
auto maybe_slide_objects = slide_json.get_array("objects"sv);
if (!maybe_slide_objects.has_value())
return Error::from_string_view("Slide objects must be an array"sv);
@ -31,11 +32,11 @@ ErrorOr<Slide> Slide::parse_slide(JsonObject const& slide_json)
return Error::from_string_view("Slides must be objects"sv);
auto const& slide_object_json = maybe_slide_object_json.as_object();
auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json));
auto slide_object = TRY(SlideObject::parse_slide_object(slide_object_json, slide_index));
slide_objects.append(move(slide_object));
}
return Slide { move(slide_objects), title };
return Slide { frame_count, move(slide_objects), title };
}
ErrorOr<HTMLElement> Slide::render(Presentation const& presentation) const

View file

@ -14,17 +14,17 @@
// A single slide of a presentation.
class Slide final {
public:
static ErrorOr<Slide> parse_slide(JsonObject const& slide_json);
static ErrorOr<Slide> parse_slide(JsonObject const& slide_json, unsigned slide_index);
// FIXME: shouldn't be hard-coded to 1.
unsigned frame_count() const { return 1; }
unsigned frame_count() const { return m_frame_count; }
StringView title() const { return m_title; }
ErrorOr<HTMLElement> render(Presentation const&) const;
private:
Slide(Vector<NonnullRefPtr<SlideObject>> slide_objects, DeprecatedString title);
Slide(unsigned frame_count, Vector<NonnullRefPtr<SlideObject>> slide_objects, DeprecatedString title);
unsigned m_frame_count;
Vector<NonnullRefPtr<SlideObject>> m_slide_objects;
DeprecatedString m_title;
};

View file

@ -17,8 +17,9 @@ static DeprecatedString to_css_length(float design_value, Presentation const& pr
return DeprecatedString::formatted("{}vw", length_in_vw);
}
ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject const& slide_object_json)
ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject const& slide_object_json, unsigned slide_index)
{
auto frame = slide_object_json.get_u32("frame"sv).value_or(0);
auto maybe_type = slide_object_json.get_deprecated_string("type"sv);
if (!maybe_type.has_value())
return Error::from_string_view("Slide object must have a type"sv);
@ -26,9 +27,9 @@ ErrorOr<NonnullRefPtr<SlideObject>> SlideObject::parse_slide_object(JsonObject c
auto type = maybe_type.value();
RefPtr<SlideObject> object;
if (type == "text"sv)
object = TRY(try_make_ref_counted<Text>());
object = TRY(try_make_ref_counted<Text>(Index { slide_index, frame }));
else if (type == "image"sv)
object = TRY(try_make_ref_counted<Image>());
object = TRY(try_make_ref_counted<Image>(Index { slide_index, frame }));
else
return Error::from_string_view("Unsupported slide object type"sv);
@ -97,6 +98,7 @@ ErrorOr<HTMLElement> Text::render(Presentation const& presentation) const
{
HTMLElement div;
div.tag_name = "div"sv;
TRY(div.attributes.try_set("class"sv, DeprecatedString::formatted("frame slide{}-frame{}", m_slide_index, m_frame_index)));
div.style.set("color"sv, m_color.to_deprecated_string());
div.style.set("font-family"sv, DeprecatedString::formatted("'{}'", m_font_family));
div.style.set("font-size"sv, to_css_length(m_font_size_in_pt * 1.33333333f, presentation));
@ -125,6 +127,7 @@ ErrorOr<HTMLElement> Image::render(Presentation const& presentation) const
HTMLElement image_wrapper;
image_wrapper.tag_name = "div"sv;
TRY(image_wrapper.attributes.try_set("class"sv, DeprecatedString::formatted("frame slide{}-frame{}", m_slide_index, m_frame_index)));
image_wrapper.children.append(move(img));
image_wrapper.style.set("position"sv, "absolute"sv);
image_wrapper.style.set("left"sv, to_css_length(m_rect.left(), presentation));

View file

@ -21,19 +21,29 @@ struct HTMLElement {
ErrorOr<void> serialize(StringBuilder&) const;
};
struct Index {
unsigned slide;
unsigned frame;
};
// Anything that can be on a slide.
class SlideObject : public RefCounted<SlideObject> {
public:
virtual ~SlideObject() = default;
static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json);
static ErrorOr<NonnullRefPtr<SlideObject>> parse_slide_object(JsonObject const& slide_object_json, unsigned slide_index);
virtual ErrorOr<HTMLElement> render(Presentation const&) const = 0;
protected:
SlideObject() = default;
SlideObject(Index index)
: m_frame_index(index.frame)
, m_slide_index(index.slide)
{
}
virtual void set_property(StringView name, JsonValue);
unsigned m_frame_index;
unsigned m_slide_index;
HashMap<DeprecatedString, JsonValue> m_properties;
Gfx::IntRect m_rect;
};
@ -44,7 +54,10 @@ public:
virtual ~GraphicsObject() = default;
protected:
GraphicsObject() = default;
GraphicsObject(Index index)
: SlideObject(index)
{
}
virtual void set_property(StringView name, JsonValue) override;
// FIXME: Change the default color based on the color scheme
@ -53,7 +66,10 @@ protected:
class Text final : public GraphicsObject {
public:
Text() = default;
Text(Index index)
: GraphicsObject(index)
{
}
virtual ~Text() = default;
private:
@ -69,7 +85,10 @@ private:
class Image final : public SlideObject {
public:
Image() = default;
Image(Index index)
: SlideObject(index)
{
}
virtual ~Image() = default;
private: