class PresentationController

Manages the mutable state of a presentation: current slide, clock, and listeners.

Wraps an immutable class Presently::Presentation and provides navigation, timing, and listener notification. Multiple views (display, presenter) can register as listeners to receive updates.

Definitions

def initialize(presentation, state: nil, recordings: nil)

Initialize a new controller for the given presentation.

Signature

parameter presentation Presentation

The presentation to control.

parameter state State | Nil

The state persistence object. If provided, state is saved on changes and restored on initialization.

parameter recordings Recordings | Nil

The narration recording store, when recording is enabled.

Implementation

def initialize(presentation, state: nil, recordings: nil)
	@presentation = presentation
	@recordings = recordings
	@current_index = 0
	@clock = Clock.new
	@listeners = []
	
	state&.restore(self)
	@state = state
end

attr :presentation

Signature

attribute Presentation

The underlying presentation data.

attr :current_index

Signature

attribute Integer

The index of the current slide.

attr :clock

Signature

attribute Clock

The presentation timer.

def templates

The template resolver, delegated to the presentation.

Signature

returns Templates

The templates instance.

Implementation

def templates
	@presentation.templates
end

def slides

The ordered list of slides, delegated to the presentation.

Signature

returns Array(Slide)

The slides.

Implementation

def slides
	@presentation.slides
end

def current_slide

The currently displayed slide.

Signature

returns Slide | Nil

The current slide, or nil if no slides are loaded.

Implementation

def current_slide
	@presentation.slides[@current_index]
end

def recording_available?(slide)

Whether the given slide has a narration recording.

Signature

parameter slide Slide

The slide to check.

returns Boolean

Implementation

def recording_available?(slide)
	@recordings&.exist?(slide) || false
end

def next_slide

The slide following the current one.

Signature

returns Slide | Nil

The next slide, or nil if on the last slide.

Implementation

def next_slide
	@presentation.slides[@current_index + 1]
end

def previous_slide

The slide preceding the current one.

Signature

returns Slide | Nil

The previous slide, or nil if on the first slide.

Implementation

def previous_slide
	@presentation.slides[@current_index - 1] if @current_index > 0
end

def slide_count

The total number of slides.

Signature

returns Integer

The slide count.

Implementation

def slide_count
	@presentation.slide_count
end

def total_duration

The total expected duration of the presentation.

Signature

returns Numeric

The total duration in seconds.

Implementation

def total_duration
	@presentation.total_duration
end

def slide_progress

The progress through the current slide's allocated time. Slides with durations of zero or less are complete once their expected start time is reached.

Signature

returns Float

A value between 0.0 and 1.0.

Implementation

def slide_progress
	return 0.0 unless @clock.started?
	
	slide = current_slide
	return 0.0 unless slide
	
	time_into_slide = @clock.elapsed - @presentation.expected_time_at(@current_index)
	if slide.duration <= 0
		time_into_slide.negative? ? 0.0 : 1.0
	else
		(time_into_slide / slide.duration).clamp(0.0, 1.0)
	end
end

def reset_timer!

Reset a paused timer to begin the current slide again. All slides reset to their expected start time. A timer: start slide becomes ready to start when advancing; other slides remain paused.

Implementation

def reset_timer!
	return unless @clock.paused? && current_slide
	
	@clock.reset!(@presentation.expected_time_at(@current_index), started: current_slide.timer != "start")
	
	notify_listeners!
end

def pacing

The current pacing status relative to the slide timing.

Signature

returns Symbol | Nil

One of :on_time, :ahead, or :behind, or nil when no presentation time is allocated.

Implementation

def pacing
	return unless total_duration.positive?
	return :on_time unless @clock.started?
	
	elapsed = @clock.elapsed
	slide_start = @presentation.expected_time_at(@current_index)
	slide_end = @presentation.expected_time_at(@current_index + 1)
	
	if elapsed > slide_end
		:behind
	elsif elapsed < slide_start
		:ahead
	else
		:on_time
	end
end

def time_remaining

The estimated time remaining in the presentation.

Signature

returns Numeric

The remaining time in seconds.

Implementation

def time_remaining
	expected_remaining = @presentation.expected_time_at(slide_count) - @clock.elapsed
	
	[expected_remaining, 0].max
end

def go_to(index, timer: false)

Navigate to a specific slide by index. Ignores out-of-bounds indices. Notifies listeners on change.

Signature

parameter index Integer

The slide index to navigate to.

parameter timer Boolean

Whether to apply the outgoing slide's timer action before notifying listeners.

returns Boolean

Whether the destination index was valid.

Implementation

def go_to(index, timer: false)
	return false if index < 0 || index >= slide_count
	
	advance_timer! if timer
	@current_index = index
	notify_listeners!
	return true
end

def advance!(timer: true)

Advance to the next slide, applying the current slide's timer action. Timer actions only run when there is a next slide.

Signature

parameter timer Boolean

Whether to apply the outgoing slide's timer action.

returns Boolean

Whether the presentation advanced to the next slide.

Implementation

def advance!(timer: true)
	go_to(@current_index + 1, timer: timer)
end

def retreat!

Go back to the previous slide.

Signature

returns Boolean

Whether the presentation moved to the previous slide.

Implementation

def retreat!
	go_to(@current_index - 1)
end

def add_listener(listener)

Register a listener to be notified when the slide changes. The listener must respond to #slide_changed!.

Signature

parameter listener Object

The listener to add.

Implementation

def add_listener(listener)
	@listeners << listener
end

def remove_listener(listener)

Remove a previously registered listener.

Signature

parameter listener Object

The listener to remove.

Implementation

def remove_listener(listener)
	@listeners.delete(listener)
end

def reload!

Reload slides from disk and notify listeners.

Implementation

def reload!
	@presentation = @presentation.reload
	notify_listeners!
end

def save_state!

Persist the current state to disk.

Implementation

def save_state!
	@state&.save(self)
end

def advance_timer!

Apply the outgoing slide's timer action without resetting elapsed time.

Implementation

def advance_timer!
	case current_slide.timer
	when "start"
		@clock.start! unless @clock.started?
	when "pause"
		@clock.pause!
	when "resume"
		@clock.resume! if @clock.started?
	end
end

def notify_listeners!

Notify all registered listeners that the slide has changed, and persist state.

Implementation

def notify_listeners!
	@state&.save(self)
	
	@listeners.each do |listener|
		listener.slide_changed!
	rescue => error
		Console.warn(self, "Listener notification failed", exception: error)
	end
end