Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/quickdeck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ inspector opens over the canvas from an Inspector button in the toolbar
| Editor | The navigator, the canvas and the inspector. Tap to choose (`deck.hit`), a long press adds to the choice, tap a chosen text box again (or double-tap) to type in it, drag to move, a handle to resize or rotate. Insert strips for text, pictures (from images/ or this device), shapes (the engine's gallery), tables and charts. A chosen video or sound plays from its control | `deck.slides`, `deck.slide`, `deck.view`, `deck.hit`, `deck.set_text`, `deck.format_state`, `deck.format`, `deck.insert`, `deck.shape`, `deck.picture`, `deck.table`, `deck.chart`, `deck.layouts`, `deck.slides_op`, `deck.design`, `deck.transition`, `deck.notes`, `deck.fonts`, `deck.shapes`, `deck.media` |
| Sorter | Every slide as a tile in its sections, with its number and transition: choose slides, New slide, Duplicate, Delete, Hide or Show, Move earlier and later, drag a tile to move it, add, rename and remove sections | `deck.slides_op`, `deck.view` |
| New from an outline | A card per slide (a title, then its points, one per line), a theme and a slide size, Make deck. Draft with AI asks the person's own provider for the cards (`model.complete`, fast class); without a provider it says to set one up in Settings → AI providers, and typing the outline works either way | `deck.create`; the host's `model.complete` |
| Present | The slide alone on a dark stage inside the app's window, a small bar (previous, "4 / 12", next, Notes, the time, Exit) and an Exit in the corner; each build step shows its end state, and nothing animates. Notes opens a strip with the slide's notes. A clip set to play by itself starts when its slide shows, a tap on a clip plays or pauses it, and another slide or Exit stops it | `deck.show`, `deck.view {step}`, `deck.slide`, `deck.media` |
| Present | The slide alone on a dark stage that takes the whole screen: the Present tap asks the shell to put the window in full screen (`window.fullscreen`), and where the host can't (card-host, a phone), the stage fills the app's window. A small bar (previous, "4 / 12", next, Notes, the time, Exit) and an Exit in the corner; Esc, the shell's key in full screen, also ends the show, and the window comes back as it was. Each build step shows its end state, and nothing animates; a slide drawn for the smaller window is drawn again for the full screen. Notes opens a strip with the slide's notes. A clip set to play by itself starts when its slide shows, a tap on a clip plays or pauses it, and another slide or Exit stops it | `deck.show`, `deck.view {step}`, `deck.slide`, `deck.media`; the shell's `window.fullscreen` |
| Export | Presentation (.pptx), PDF (slides, notes pages or handouts, 1 to 9 per page) or Images (one PNG a slide); all slides, the current one or a range; hidden slides in or out; a name. The deck's earlier exports are listed and can be removed | `deck.export` |

Undo and Redo use the engine's history; Save writes the native `.deckcraft`
Expand Down
2 changes: 1 addition & 1 deletion apps/quickdeck/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Quick Deck(`os.quickdeck`)是 OctoSense 桌面版的演示文稿应用:演
| 编辑器 | 导航栏、画布和检查器。点按选中(`deck.hit`),长按加入或移出选择,再点一次已选中的文本框(或双击)即可在其中输入,拖动可移动,拖动控制点可调整大小或旋转。插入条用于文本、图片(来自 images/ 或本设备)、形状(引擎的形状库)、表格和图表。选中的视频或音频可用它的控件播放 | `deck.slides`、`deck.slide`、`deck.view`、`deck.hit`、`deck.set_text`、`deck.format_state`、`deck.format`、`deck.insert`、`deck.shape`、`deck.picture`、`deck.table`、`deck.chart`、`deck.layouts`、`deck.slides_op`、`deck.design`、`deck.transition`、`deck.notes`、`deck.fonts`、`deck.shapes`、`deck.media` |
| 浏览视图 | 每张幻灯片以磁贴显示在所属分节中,带编号和切换效果:选择幻灯片、New slide、Duplicate、Delete、Hide 或 Show、前移和后移、拖动磁贴移动,添加、重命名和删除分节 | `deck.slides_op`、`deck.view` |
| 从大纲新建 | 每张幻灯片一张卡片(一个标题,下面每行一个要点)、一个主题和一种幻灯片尺寸,然后 Make deck。Draft with AI 请用户自己的提供方起草卡片(`model.complete`,fast 档);没有提供方时提示到 Settings → AI providers 设置,手动输入大纲始终可用 | `deck.create`;宿主的 `model.complete` |
| 放映 | 幻灯片单独显示在应用窗口内的深色舞台上,下方一个小控制条(上一张、"4 / 12"、下一张、Notes、计时、Exit),角落还有一个 Exit;每个动画步骤显示其最终状态,不播放任何动画。Notes 打开显示本张备注的条带。设为自动播放的媒体剪辑在其幻灯片出现时开始播放,点按剪辑可播放或暂停,切换到其他幻灯片或 Exit 时停止 | `deck.show`、`deck.view {step}`、`deck.slide`、`deck.media` |
| 放映 | 幻灯片单独显示在占满整个屏幕的深色舞台上:点按 Present 时会请 Shell 让窗口进入全屏(`window.fullscreen`);宿主做不到时(card-host、手机),舞台占满应用窗口。下方一个小控制条(上一张、"4 / 12"、下一张、Notes、计时、Exit),角落还有一个 Exit;全屏时 Esc 是 Shell 的按键,也会结束放映,窗口恢复原样。每个动画步骤显示其最终状态,不播放任何动画;为较小窗口渲染的幻灯片会按全屏尺寸重新渲染。Notes 打开显示本张备注的条带。设为自动播放的媒体剪辑在其幻灯片出现时开始播放,点按剪辑可播放或暂停,切换到其他幻灯片或 Exit 时停止 | `deck.show`、`deck.view {step}`、`deck.slide`、`deck.media`;Shell 的 `window.fullscreen` |
| 导出 | Presentation(.pptx)、PDF(幻灯片、备注页或讲义,每页 1 到 9 张)或 Images(每张幻灯片一个 PNG);全部幻灯片、当前幻灯片或一个范围;是否包含隐藏的幻灯片;名称。列出该演示文稿以前的导出,并可删除 | `deck.export` |

Undo 和 Redo 使用引擎的历史记录;Save 写入原生 `.deckcraft` 文件(`deck.undo`、`deck.redo`、`deck.save`、`deck.state`)。关闭有未保存更改的标签时会询问:保存、不保存或取消。
Expand Down
41 changes: 36 additions & 5 deletions apps/quickdeck/bundle/main.splash
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,7 @@ if st == nil {
heartbeat: nil // the interval below, made once
index_dirty: false // library.json waits for the heartbeat
present: nil // the show going on
screen_full: false // the shell has the window in full screen for Present (on_app_fullscreen)
next_key: 1
insp_over: false // a narrow window shows the inspector over the canvas
clip: nil // the clip playing or getting ready (playback, below)
Expand Down Expand Up @@ -3156,14 +3157,21 @@ fn start_present(t){
return
}
// media: each slide's clips (deck.slide); autoplayed: the slide on
// show has started its clip by itself (playback).
st.present = {key: t.key order: order i: 0 step: 0 notes: st.show_notes started: time_now() pics: {} failed: {} full: {} timer: nil done: false
// show has started its clip by itself (playback); px: the width each
// picture was drawn at, so a bigger stage draws it again.
st.present = {key: t.key order: order i: 0 step: 0 notes: st.show_notes started: time_now() pics: {} px: {} failed: {} full: {} timer: nil done: false
media: {} autoplayed: {}}
st.present.timer = start_interval(1, || present_tick())
// The stage shows the slide alone: an earlier note goes.
note("")
show()
pump()
// The Present tap also asks the shell for the whole screen
// (window.fullscreen), within the 5 seconds a press counts for.
// Where it can't (a host with no window service, such as card-host,
// or a refusal), the show fills the window as before, and the answer
// is not shown.
host.request("window.fullscreen", {on: true}, fn(r){})
})
}
fn present_tab(){
Expand All @@ -3190,7 +3198,10 @@ fn present_next(){
if p.step >= optional(item, "steps", 0) && p.i + 1 < p.order.len() { want.push({item: p.order[p.i + 1] step: 0}) }
for i w in want {
let k = present_key(w.item, w.step)
if optional(p.pics, k, nil) == nil && optional(p.failed, k, nil) == nil {
// A picture drawn for a smaller stage (before the window went full
// screen) is drawn again; it shows, stretched, until then.
let stale = optional(p.pics, k, nil) == nil || optional(p.px, k, 0) != stage_px()
if stale && optional(p.failed, k, nil) == nil {
present_render(t, w.item, w.step)
return true
}
Expand Down Expand Up @@ -3231,13 +3242,17 @@ fn present_notes(t, item){
fn present_render(t, item, step){
let p = st.present
let k = present_key(item, step)
let px = stage_px()
busy_on()
engine("view", {doc: t.doc slide: item.slide width: stage_px() step: step}, fn(a){
engine("view", {doc: t.doc slide: item.slide width: px step: step}, fn(a){
st.busy = false
if st.present != p { return }
let src = nil
if a.is_ok { src = read_png(optional(a.data, "path", "")) }
if src != nil { p.pics[k] = src } else { p.failed[k] = true }
if src != nil {
p.pics[k] = src
p.px[k] = px
} else { p.failed[k] = true }
let now = present_item()
if now != nil && present_key(now, p.step) == k { render_present() }
later_pump()
Expand Down Expand Up @@ -3304,6 +3319,21 @@ fn stop_present(){
if t != nil && item != nil { go_slide(t, item.slide) }
pump()
}
// The shell's word that the window's full screen started or ended: when Esc,
// ⌘F or another window ends it, Present ends too. One that starts after
// Present already ended goes straight back.
fn on_app_fullscreen(on){
st.screen_full = on
if on { screen_back() } else { stop_present() }
}
// The full screen Present asked for goes back once Present is over, however
// it ended: Exit, the corner control or the end of the show (show() asks
// after every change).
fn screen_back(){
if !optional(st, "screen_full", false) || st.present != nil { return }
st.screen_full = false
host.request("window.fullscreen", {on: false}, fn(r){})
}

// ---- playback -------------------------------------------------------------------
// One clip plays at a time: in the editor, the chosen clip, from its control
Expand Down Expand Up @@ -4004,6 +4034,7 @@ fn show_regions(){
fn show(){
// Another tab, view or screen stops a clip (playback).
clip_check()
screen_back()
let t = cur_tab()
let presenting = st.present != nil
let deck = !presenting && t != nil && t.kind == "deck"
Expand Down
72 changes: 64 additions & 8 deletions apps/quickdeck/tests/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
shell a deck made from an outline, then edited: text in place, a format,
a chart inserted and its data changed, a layout, a theme, the sorter
(duplicate, hide, a section), notes, undo and redo, save, export
(.pptx, PDF and pictures), Present and its notes, light, then dark
(.pptx, PDF and pictures), Present and its notes (the show takes
the window full screen, window.fullscreen, and gives it back: by
Exit in the light pass, by Esc in the dark one), light, then dark
through the shell's own style menu; in the light pass the
inspector's states follow real changes and their undo: a box's
anchor and autofit, a chart element, a table option, the colour set
Expand All @@ -53,8 +55,9 @@
(.cache/media/<sha256>.<ext>, checked against the clip's own
bytes) and the Video widget plays it where the system has a player
(a Mac: two grabs a second apart differ) or says "Playback isn't
available on this system" (Linux), in the editor and in Present,
light, then dark; a closed deck's clips leave the cache. On a Mac
available on this system" (Linux), in the editor and in Present
(which takes the window full screen, the clip playing on through
it), light, then dark; a closed deck's clips leave the cache. On a Mac
it never plays the sound (the runs open no audio device)

Shell runs hold a lock folder, so one hidden shell runs on this machine at a
Expand Down Expand Up @@ -454,6 +457,25 @@ def wait_until(check, failure, timeout=30.0):
raise AssertionError(failure)


def window_rect(app):
"""The whole window, in points: what a full-screen window covers."""
w, h = app.size()
return [0, 0, w, h]


def same_rect(a, b):
return len(a) == len(b) and all(abs(x - y) < 1.5 for x, y in zip(a, b))


def full_screen(app):
"""Wait until the app covers the whole window: Present's tap asked the
shell for full screen (window.fullscreen). A hidden shell never asks the
host window itself, so the window is the screen here."""
wait_until(lambda: same_rect(app.area(), window_rect(app)),
f"the app did not go full screen: {app.area()} in {window_rect(app)}")
return app.area()


def slide_rect(app):
"""The canvas's slide picture: [x, y, w, h] in points (the largest picture
right of the navigator and left of the inspector)."""
Expand Down Expand Up @@ -927,6 +949,24 @@ def stage_rect(app):
return max(imgs, key=lambda r: r[2] * r[3])


def stage_settled(app, timeout=20.0):
"""Present's slide picture once it stops moving: in a shell the show goes
full screen just after it starts, and the stage grows with the window."""
deadline = time.monotonic() + timeout
last, same = None, 0
while time.monotonic() < deadline:
try:
rect = stage_rect(app)
except AssertionError:
rect = None
same = same + 1 if rect is not None and rect == last else 0
if same >= 3:
return rect
last = rect
time.sleep(0.4)
raise AssertionError(f"Present's slide never settled; last {last}")


def box_on(rect, box):
"""A clip's box (slide points) on screen, in the slide picture `rect`."""
x, y, w, h = rect
Expand Down Expand Up @@ -1141,11 +1181,13 @@ def shell_playback(app, out, dark, home):
app.find("Paused.", "Label", timeout=10)
app.tap("Stop")
still_open(app, "the editor's playback")
# Present: the video plays by itself (a native deck keeps autoplay).
# Present: the video plays by itself (a native deck keeps autoplay), and
# keeps playing as the show takes the window full screen.
app.tap("Present", "Button")
app.find("Exit", "Button", timeout=60)
full_screen(app)
app.images(1, timeout=60)
stage = box_on(stage_rect(app), LAUNCH_FILM)
stage = box_on(stage_settled(app), LAUNCH_FILM)
if LINUX:
wait_until(lambda: stage_note(app) == NO_PLAYER, "the show's clip never said it can't play", timeout=40)
app.grab(out, f"{p}p2-present-unavailable")
Expand Down Expand Up @@ -1189,10 +1231,11 @@ def shell_playback(app, out, dark, home):
app.find("Starts when it's tapped.", "Label", timeout=30)
app.tap("Present", "Button")
app.find("Exit", "Button", timeout=60)
full_screen(app)
app.images(1, timeout=60)
time.sleep(2.0)
assert stage_note(app) is None, f"a .pptx clip started by itself: {stage_note(app)}"
stage = box_on(stage_rect(app), LAUNCH_FILM)
stage = box_on(stage_settled(app), LAUNCH_FILM)
app.click(stage[0] + stage[2] / 2, stage[1] + stage[3] / 2)
if LINUX:
wait_until(lambda: stage_note(app) == NO_PLAYER, "the tapped clip never said it can't play", timeout=40)
Expand Down Expand Up @@ -1336,14 +1379,27 @@ def shell_journey(app, out, dark):
app.grab(out, f"{p}08-export")
app.scroll(-3000)
app.tap("Export", "Button")
# present
# present: the tap takes the whole screen (window.fullscreen); the light
# pass leaves by Exit, the dark one by Esc, the shell's key in full
# screen (a script gets no keys), and the window comes back either way.
go_to(app, 1)
windowed = app.area()
app.tap("Present", "Button")
app.find("Exit", "Button", timeout=60)
full = full_screen(app)
app.images(1, timeout=60)
app.grab(out, f"{p}09-present")
app.click(app.area()[2] / 2, app.area()[3] / 2)
app.tap("Exit", "Button")
if dark:
app.key("Escape")
else:
app.tap("Exit", "Button")
app.find("Present", "Button", timeout=30)
app.gone("Exit")
wait_until(lambda: same_rect(app.area(), windowed),
f"the window did not come back after Present: {app.area()}, was {windowed}")
(out / f"{p}present-full-screen.json").write_text(json.dumps(
{"window": windowed, "full_screen": full, "after": app.area(), "left_by": "Escape" if dark else "Exit"}, indent=1))


def states_follow(app, out):
Expand Down
Loading