Public Access
Storage: view pictures, PNG, JPEG, BMP and GIF (#45)
Enter on a picture shows it: shrunk to fit the screen, or at its own size with Enter again and the arrows to move. Dithered to the screen's 256 colours; a colour the screen has exactly is left alone, so screenshots are shown as they are. The picture is decoded once, straight into the screen's buffer, and kept there (App::retainsContent): no copy in memory. Decoding runs on the storage task, so the keys keep working and a 12 megapixel photograph appears as it comes instead of tripping the watchdog. PNG, BMP and GIF are read by decoders of our own, host-tested against files made by Pillow; the PNG one needs 32 KB where the display library's needed 44 KB in one block, which the device often doesn't have. JPEG uses the library's TJpgDec. Also corrects two sentences that still gave 16 KB as the editing limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -162,11 +162,15 @@ void FileViewer::open(const std::string& path, uint32_t size) {
|
||||
file_ = std::make_shared<fs::File>();
|
||||
std::string name = files::baseName(path);
|
||||
files::FileKind kind = files::kindOf(name);
|
||||
if (kind == files::FileKind::Unknown) { // what do its first bytes look like?
|
||||
files::ImageKind picture = files::imageKindOfName(name);
|
||||
if (kind == files::FileKind::Unknown && picture == files::ImageKind::None) { // what do its first bytes look like?
|
||||
uint8_t head[256];
|
||||
size_t n = readAt(0, head, sizeof head);
|
||||
picture = files::imageKindOfBytes(head, n);
|
||||
kind = files::looksLikeText(head, n) ? files::FileKind::Text : files::FileKind::Unknown;
|
||||
}
|
||||
std::string notShown;
|
||||
if (picture != files::ImageKind::None) notShown = image_.open(path, size);
|
||||
switch (kind) {
|
||||
case files::FileKind::Text: base_ = Mode::Text; break;
|
||||
case files::FileKind::Gpx: base_ = Mode::Gpx; break;
|
||||
@@ -174,11 +178,13 @@ void FileViewer::open(const std::string& path, uint32_t size) {
|
||||
case files::FileKind::Ota: base_ = Mode::Ota; break;
|
||||
default: base_ = Mode::Hex; break;
|
||||
}
|
||||
if (picture != files::ImageKind::None && notShown.empty()) base_ = Mode::Image;
|
||||
pager_.reset(new files::TextPager([this](uint32_t offset, uint8_t* into, size_t len) { return readAt(offset, into, len); }, size_,
|
||||
kCols, kRows));
|
||||
if (files::opensAtEnd(name)) pager_->toEnd();
|
||||
hexTop_ = 0;
|
||||
show(base_);
|
||||
if (!notShown.empty()) say(notShown); // a picture that can't be shown: its bytes, and why
|
||||
if (base_ == Mode::Gpx || base_ == Mode::Pcap || base_ == Mode::Ota) startScan(base_);
|
||||
}
|
||||
|
||||
@@ -203,6 +209,7 @@ void FileViewer::close() {
|
||||
});
|
||||
}
|
||||
file_.reset();
|
||||
image_.close();
|
||||
pager_.reset();
|
||||
confirm_.reset();
|
||||
message_.clear();
|
||||
@@ -214,11 +221,13 @@ void FileViewer::close() {
|
||||
|
||||
void FileViewer::show(Mode mode) {
|
||||
mode_ = mode;
|
||||
if (mode == Mode::Image) image_.lost(); // another view was drawn where it was
|
||||
stale_ = true;
|
||||
rowsFrom_ = -1;
|
||||
}
|
||||
|
||||
void FileViewer::say(const std::string& text) {
|
||||
if (mode_ == Mode::Image) return image_.say(text);
|
||||
message_ = text;
|
||||
messageMs_ = millis();
|
||||
}
|
||||
@@ -275,6 +284,7 @@ void FileViewer::help(std::vector<KeyHelp>& out) const {
|
||||
case Mode::Packet: keys::add(out, keys::kViewerPacket); break;
|
||||
case Mode::Gpx: keys::add(out, keys::kViewerGpx); break;
|
||||
case Mode::Ota: keys::add(out, keys::kViewerOta); break;
|
||||
case Mode::Image: image_.help(out); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +309,7 @@ bool FileViewer::onKey(const KeyEvent& e) {
|
||||
else show(base_ == Mode::Hex || base_ == Mode::Gpx ? Mode::Text : Mode::Hex);
|
||||
return true;
|
||||
}
|
||||
if (mode_ == Mode::Image) return image_.onKey(e), true;
|
||||
uint32_t ch = e.key == Key::Char ? e.ch : 0;
|
||||
switch (mode_) {
|
||||
case Mode::Text:
|
||||
@@ -332,7 +343,8 @@ bool FileViewer::onKey(const KeyEvent& e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileViewer::update(uint32_t) {
|
||||
bool FileViewer::update(uint32_t nowMs) {
|
||||
if (mode_ == Mode::Image) return image_.update(nowMs);
|
||||
if (!message_.empty() && millis() - messageMs_ >= 4000) {
|
||||
message_.clear();
|
||||
return true;
|
||||
@@ -389,6 +401,7 @@ void FileViewer::refresh() {
|
||||
}
|
||||
|
||||
void FileViewer::draw(Canvas& c) {
|
||||
if (mode_ == Mode::Image) return image_.draw(c);
|
||||
const auto& area = theme::kContent;
|
||||
refresh();
|
||||
c.setTextDatum(top_left);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "apps/image_pane.h"
|
||||
#include "dialog_model.h"
|
||||
#include "key_help.h"
|
||||
#include "key_event.h"
|
||||
@@ -23,7 +24,7 @@ namespace roro {
|
||||
// genuine. Tab switches to the text or the hex of the same file. Nothing here changes a file.
|
||||
class FileViewer {
|
||||
public:
|
||||
FileViewer(StorageService& storage, UpdateService& update) : storage_(storage), update_(update) {}
|
||||
FileViewer(StorageService& storage, UpdateService& update) : storage_(storage), update_(update), image_(storage) {}
|
||||
|
||||
void open(const std::string& path, uint32_t size);
|
||||
void close();
|
||||
@@ -37,9 +38,12 @@ class FileViewer {
|
||||
const std::string& path() const { return path_; }
|
||||
uint32_t size() const { return size_; }
|
||||
void say(const std::string& text);
|
||||
// A picture is drawn once and kept on the screen (App::retainsContent).
|
||||
bool retains() const { return mode_ == Mode::Image; }
|
||||
void lost() { image_.lost(); }
|
||||
|
||||
private:
|
||||
enum class Mode { Text, Hex, Gpx, Pcap, Packet, Ota };
|
||||
enum class Mode { Text, Hex, Gpx, Pcap, Packet, Ota, Image };
|
||||
static constexpr int kRows = 8;
|
||||
static constexpr int kCols = 38;
|
||||
struct Scan; // what a storage job reads through a whole file for: shared with that job
|
||||
@@ -54,6 +58,7 @@ class FileViewer {
|
||||
|
||||
StorageService& storage_;
|
||||
UpdateService& update_;
|
||||
ImagePane image_;
|
||||
std::string path_;
|
||||
uint32_t size_ = 0;
|
||||
Mode mode_ = Mode::Text, base_ = Mode::Text;
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
#include "image_pane.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include <lgfx/utility/lgfx_tjpgd.h>
|
||||
|
||||
#include "app_keys.h"
|
||||
#include "file_names.h"
|
||||
#include "platform/console.h"
|
||||
#include "png_reader.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kNoteMs = 3000;
|
||||
constexpr uint32_t kPushMs = 250; // how often the screen shows how far the decoding is
|
||||
constexpr int kNoteHeight = 11;
|
||||
constexpr size_t kJpegPool = 3900; // what the library gives its own JPEG decoder
|
||||
} // namespace
|
||||
|
||||
struct ImagePane::Job {
|
||||
std::string path, why;
|
||||
files::ImageInfo info;
|
||||
files::ImageFrame frame;
|
||||
files::ImageMap map;
|
||||
uint32_t size = 0, shotAt = 0, tookMs = 0;
|
||||
uint8_t* screen = nullptr; // the screen's buffer: one byte a pixel, RRRGGGBB
|
||||
int stride = 0;
|
||||
std::atomic<bool> stop{false}, done{false};
|
||||
bool enough = false; // the rest of the file is under the view: the decoder is told to stop
|
||||
File* file = nullptr;
|
||||
uint32_t sinceRest = 0;
|
||||
|
||||
void put(int sx, int sy, uint8_t r, uint8_t g, uint8_t b) {
|
||||
int tx, ty;
|
||||
if (map.at(sx, sy, tx, ty)) screen[ty * stride + tx] = files::rgb332Dithered(r, g, b, tx, ty);
|
||||
}
|
||||
// A long decoding must leave the processor to others now and then (the idle task is watched).
|
||||
void rest(uint32_t bytes) {
|
||||
sinceRest += bytes;
|
||||
if (sinceRest < 16 * 1024) return;
|
||||
sinceRest = 0;
|
||||
vTaskDelay(1);
|
||||
}
|
||||
size_t readAt(uint32_t at, uint8_t* into, size_t len) {
|
||||
if (stop || !file->seek(at)) return 0;
|
||||
int n = file->read(into, len);
|
||||
rest(static_cast<uint32_t>(len));
|
||||
return n > 0 ? static_cast<size_t>(n) : 0;
|
||||
}
|
||||
void run();
|
||||
};
|
||||
|
||||
namespace {
|
||||
// The library's JPEG decoder reads the file from its start on; a null buffer means "skip".
|
||||
// Nothing more to read is how a decoding is told to stop.
|
||||
uint32_t readNext(void* job, uint8_t* into, uint32_t len) {
|
||||
auto& j = *static_cast<ImagePane::Job*>(job);
|
||||
if (j.stop || j.enough) return 0;
|
||||
File& f = *j.file;
|
||||
j.rest(len);
|
||||
if (!into) return f.seek(f.position() + len) ? len : 0;
|
||||
int n = f.read(into, len);
|
||||
return n > 0 ? static_cast<uint32_t>(n) : 0;
|
||||
}
|
||||
|
||||
// A block of a JPEG: its pixels row by row, three bytes each.
|
||||
uint32_t jpegBlock(void* job, void* bitmap, JRECT* rect) {
|
||||
auto& j = *static_cast<ImagePane::Job*>(job);
|
||||
const uint8_t* p = static_cast<const uint8_t*>(bitmap);
|
||||
if (j.map.below(static_cast<int>(rect->top))) return j.enough = true, 0;
|
||||
for (uint32_t y = rect->top; y <= rect->bottom; y++)
|
||||
for (uint32_t x = rect->left; x <= rect->right; x++, p += 3) j.put(static_cast<int>(x), static_cast<int>(y), p[0], p[1], p[2]);
|
||||
return j.stop ? 0 : 1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// On the storage task.
|
||||
void ImagePane::Job::run() {
|
||||
uint32_t started = millis();
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
if (!f) {
|
||||
why = "The card refused to open it";
|
||||
done = true;
|
||||
return;
|
||||
}
|
||||
file = &f;
|
||||
files::ImageRead read = [this](uint32_t at, uint8_t* into, size_t len) { return readAt(at, into, len); };
|
||||
files::ImagePixels pixels = [this](int x, int y, int count, const uint8_t* rgb) {
|
||||
for (int i = 0; i < count; i++, rgb += 3) put(x + i, y, rgb[0], rgb[1], rgb[2]);
|
||||
};
|
||||
switch (info.kind) {
|
||||
case files::ImageKind::Png:
|
||||
if (shotAt) { // one of ours: each byte is already a colour of the screen
|
||||
std::unique_ptr<uint8_t[]> row(new (std::nothrow) uint8_t[info.width]);
|
||||
if (!row) {
|
||||
why = "Not enough memory";
|
||||
break;
|
||||
}
|
||||
for (int y = 0; y < info.height && why.empty() && !stop; y++) {
|
||||
if (!map.rowUsed(y)) continue;
|
||||
size_t w = static_cast<size_t>(info.width);
|
||||
if (readAt(shotAt + static_cast<uint32_t>(y) * (info.width + 1), row.get(), w) != w) why = "The card refused to read it";
|
||||
int tx, ty;
|
||||
for (int x = 0; x < info.width; x++)
|
||||
if (map.at(x, y, tx, ty)) screen[ty * stride + tx] = row[x];
|
||||
}
|
||||
break;
|
||||
}
|
||||
why = files::readPng(read, size, pixels, [this](int y) { return map.rowUsed(y); }, [this](int y) { return map.below(y); });
|
||||
break;
|
||||
case files::ImageKind::Jpeg: {
|
||||
std::unique_ptr<lgfxJdec> jpeg(new (std::nothrow) lgfxJdec);
|
||||
std::unique_ptr<uint8_t[]> pool(new (std::nothrow) uint8_t[kJpegPool]);
|
||||
if (!jpeg || !pool) {
|
||||
why = "Not enough memory";
|
||||
break;
|
||||
}
|
||||
int shrink = frame.jpegShrink();
|
||||
map = frame.map(shrink);
|
||||
JRESULT r = lgfx_jd_prepare(jpeg.get(), readNext, pool.get(), kJpegPool, this);
|
||||
if (r == JDR_OK) r = lgfx_jd_decomp(jpeg.get(), jpegBlock, static_cast<uint_fast8_t>(shrink));
|
||||
if (r == JDR_FMT3) why = "This kind of JPEG can't be shown";
|
||||
else if (r == JDR_MEM1 || r == JDR_MEM2) why = "This JPEG is too complex to show";
|
||||
else if (r != JDR_OK && !enough) why = "This JPEG is damaged";
|
||||
break;
|
||||
}
|
||||
case files::ImageKind::Bmp:
|
||||
why = files::readBmp(read, size, pixels, [this](int y) { return map.rowUsed(y); });
|
||||
break;
|
||||
case files::ImageKind::Gif: why = files::readGif(read, size, pixels); break;
|
||||
default: why = "Not a picture this can show"; break;
|
||||
}
|
||||
f.close();
|
||||
file = nullptr;
|
||||
tookMs = millis() - started;
|
||||
if (stop) why.clear();
|
||||
else
|
||||
console.printf("image: %s, %d x %d %s, %s in %u ms\n", path.c_str(), info.width, info.height, files::imageKindName(info.kind),
|
||||
why.empty() ? (frame.actual() ? "its own size" : "fitted") : why.c_str(), (unsigned)tookMs);
|
||||
done = true;
|
||||
}
|
||||
|
||||
std::string ImagePane::open(const std::string& path, uint32_t size) {
|
||||
close();
|
||||
std::string why;
|
||||
files::ImageInfo info;
|
||||
uint32_t shotAt = 0;
|
||||
bool ran = storage_.runAndWait([&]() {
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
if (!f) {
|
||||
why = "The card refused to open it";
|
||||
return;
|
||||
}
|
||||
files::ImageRead read = [&f](uint32_t at, uint8_t* into, size_t len) -> size_t {
|
||||
if (!f.seek(at)) return 0;
|
||||
int n = f.read(into, len);
|
||||
return n > 0 ? static_cast<size_t>(n) : 0;
|
||||
};
|
||||
why = files::imageInfo(read, size, info);
|
||||
if (why.empty() && info.kind == files::ImageKind::Png) shotAt = files::screenshotPixelsAt(read, size, info.width, info.height);
|
||||
f.close();
|
||||
});
|
||||
if (!ran) why = "No SD card";
|
||||
if (!why.empty()) return why;
|
||||
path_ = path;
|
||||
size_ = size;
|
||||
info_ = info;
|
||||
shotAt_ = shotAt;
|
||||
const auto& area = theme::kContent;
|
||||
frame_ = files::ImageFrame(info.width, info.height, area.x, area.y, area.w, area.h);
|
||||
phase_ = Phase::Wanted;
|
||||
told_ = noteDrawn_ = false;
|
||||
problem_.clear();
|
||||
note_.clear();
|
||||
return "";
|
||||
}
|
||||
|
||||
void ImagePane::cancel() {
|
||||
if (job_ && !job_->done) {
|
||||
job_->stop = true;
|
||||
storage_.runAndWait([]() {}); // behind the decoding in the queue: back when it has ended
|
||||
}
|
||||
job_.reset();
|
||||
}
|
||||
|
||||
void ImagePane::close() {
|
||||
cancel();
|
||||
path_.clear();
|
||||
problem_.clear();
|
||||
note_.clear();
|
||||
info_ = files::ImageInfo();
|
||||
phase_ = Phase::Wanted;
|
||||
std::vector<uint8_t>().swap(under_);
|
||||
}
|
||||
|
||||
void ImagePane::lost() {
|
||||
cancel();
|
||||
phase_ = Phase::Wanted;
|
||||
noteDrawn_ = false;
|
||||
}
|
||||
|
||||
std::string ImagePane::describe() const {
|
||||
std::string s = std::to_string(info_.width) + " x " + std::to_string(info_.height) + " " + files::imageKindName(info_.kind);
|
||||
if (frame_.bigger()) s += frame_.actual() ? ", its own size" : ", at " + std::to_string(frame_.percent()) + " %";
|
||||
return s;
|
||||
}
|
||||
|
||||
void ImagePane::say(const std::string& text) {
|
||||
hideNote(canvas_);
|
||||
note_ = text;
|
||||
noteMs_ = millis();
|
||||
}
|
||||
|
||||
void ImagePane::help(std::vector<KeyHelp>& out) const { keys::add(out, keys::kViewerImage); }
|
||||
|
||||
bool ImagePane::onKey(const KeyEvent& e) {
|
||||
if (path_.empty()) return false;
|
||||
bool changed = false;
|
||||
switch (e.key) {
|
||||
case Key::Select:
|
||||
if (!frame_.bigger()) return true;
|
||||
cancel(); // before the frame it is drawing into changes
|
||||
frame_.toggle();
|
||||
told_ = false;
|
||||
changed = true;
|
||||
break;
|
||||
case Key::Up:
|
||||
case Key::Down:
|
||||
case Key::Left:
|
||||
case Key::Right: {
|
||||
if (!frame_.actual()) return true;
|
||||
cancel();
|
||||
int dx = e.key == Key::Left ? -1 : e.key == Key::Right ? 1 : 0, dy = e.key == Key::Up ? -1 : e.key == Key::Down ? 1 : 0;
|
||||
changed = frame_.pan(dx, dy);
|
||||
if (!changed && phase_ == Phase::Decoding) changed = true; // it was stopped: start it again
|
||||
break;
|
||||
}
|
||||
case Key::Char:
|
||||
if (e.ch != 'i' && e.ch != 'I') return false;
|
||||
if (phase_ == Phase::Shown) say(describe());
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
if (changed) {
|
||||
phase_ = Phase::Wanted;
|
||||
note_.clear();
|
||||
noteDrawn_ = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The strip the note was written over goes back as it was: no decoding for that.
|
||||
void ImagePane::hideNote(Canvas* c) {
|
||||
if (noteDrawn_ && c && phase_ == Phase::Shown && under_.size() == static_cast<size_t>(c->width()) * kNoteHeight) {
|
||||
const auto& area = theme::kContent;
|
||||
uint8_t* screen = static_cast<uint8_t*>(c->getBuffer());
|
||||
std::memcpy(screen + (area.y + area.h - kNoteHeight) * c->width(), under_.data(), under_.size());
|
||||
}
|
||||
noteDrawn_ = false;
|
||||
note_.clear();
|
||||
}
|
||||
|
||||
bool ImagePane::update(uint32_t) {
|
||||
if (path_.empty()) return false;
|
||||
uint32_t now = millis();
|
||||
if (phase_ == Phase::Wanted) return true;
|
||||
if (phase_ == Phase::Decoding) {
|
||||
if (job_ && job_->done) return true;
|
||||
if (now - pushedMs_ < kPushMs) return false;
|
||||
pushedMs_ = now;
|
||||
return true; // what has arrived so far
|
||||
}
|
||||
if (!note_.empty() && now - noteMs_ >= kNoteMs) {
|
||||
hideNote(canvas_);
|
||||
return true;
|
||||
}
|
||||
return !note_.empty() && !noteDrawn_;
|
||||
}
|
||||
|
||||
void ImagePane::start(Canvas& c) {
|
||||
cancel();
|
||||
job_ = std::make_shared<Job>();
|
||||
job_->path = path_;
|
||||
job_->info = info_;
|
||||
job_->frame = frame_;
|
||||
job_->map = frame_.map();
|
||||
job_->size = size_;
|
||||
job_->shotAt = shotAt_;
|
||||
job_->screen = static_cast<uint8_t*>(c.getBuffer());
|
||||
job_->stride = c.width();
|
||||
auto job = job_;
|
||||
storage_.runJob([job]() { job->run(); });
|
||||
phase_ = Phase::Decoding;
|
||||
pushedMs_ = millis();
|
||||
}
|
||||
|
||||
void ImagePane::draw(Canvas& c) {
|
||||
if (path_.empty()) return;
|
||||
canvas_ = &c;
|
||||
const auto& area = theme::kContent;
|
||||
if (phase_ == Phase::Wanted) {
|
||||
c.fillRect(area.x, area.y, area.w, area.h, theme::kBackground);
|
||||
noteDrawn_ = false;
|
||||
problem_.clear();
|
||||
start(c);
|
||||
return;
|
||||
}
|
||||
if (phase_ == Phase::Decoding) {
|
||||
if (!job_ || !job_->done) return; // the picture is arriving in the buffer by itself
|
||||
problem_ = job_->why;
|
||||
job_.reset();
|
||||
phase_ = Phase::Shown;
|
||||
if (!problem_.empty()) {
|
||||
c.fillRect(area.x, area.y, area.w, area.h, theme::kBackground);
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.setTextDatum(middle_center);
|
||||
c.drawString(problem_.c_str(), area.x + area.w / 2, area.y + area.h / 2);
|
||||
c.setTextDatum(top_left);
|
||||
} else if (!told_) {
|
||||
told_ = true;
|
||||
note_ = describe();
|
||||
noteMs_ = millis();
|
||||
}
|
||||
}
|
||||
if (!note_.empty() && !noteDrawn_) {
|
||||
int top = area.y + area.h - kNoteHeight;
|
||||
uint8_t* screen = static_cast<uint8_t*>(c.getBuffer());
|
||||
under_.assign(screen + top * c.width(), screen + (top + kNoteHeight) * c.width());
|
||||
c.fillRect(area.x, top, area.w, kNoteHeight, theme::kBackground);
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kText);
|
||||
c.setTextDatum(top_left);
|
||||
c.drawString(note_.c_str(), area.x + 4, top + 2);
|
||||
noteDrawn_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "image_file.h"
|
||||
#include "key_event.h"
|
||||
#include "key_help.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "ui/canvas.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// A picture from the card, for the Storage App's viewer (issue #45, F1 Q233-Q242): PNG, JPEG, BMP
|
||||
// and the first picture of a GIF (only JPEG needs a decoder that isn't ours). It is decoded once, straight into the screen's own buffer, and
|
||||
// left there (App::retainsContent): there is no copy of it in memory. It is decoded again only
|
||||
// when something else was drawn over it, or when it is zoomed or moved.
|
||||
//
|
||||
// The decoding runs on the storage task while the main loop goes on: a photograph takes seconds,
|
||||
// and the picture appears as it comes. Anything that needs the screen back stops it first.
|
||||
class ImagePane {
|
||||
public:
|
||||
explicit ImagePane(StorageService& storage) : storage_(storage) {}
|
||||
|
||||
std::string open(const std::string& path, uint32_t size); // "" or why it can't be shown
|
||||
void close();
|
||||
bool onKey(const KeyEvent& e); // true: the key was the picture's
|
||||
void help(std::vector<KeyHelp>& out) const;
|
||||
bool update(uint32_t nowMs); // true: draw again
|
||||
void draw(Canvas& c);
|
||||
void lost(); // the screen no longer holds it
|
||||
void say(const std::string& text);
|
||||
|
||||
struct Job; // one decoding: shared with the storage task, which may outlive the view of it
|
||||
|
||||
private:
|
||||
enum class Phase { Wanted, Decoding, Shown };
|
||||
|
||||
std::string describe() const;
|
||||
void start(Canvas& c);
|
||||
void cancel(); // returns once the storage task has let go of the screen
|
||||
void hideNote(Canvas* c);
|
||||
|
||||
StorageService& storage_;
|
||||
std::string path_, problem_, note_;
|
||||
uint32_t size_ = 0, noteMs_ = 0, shotAt_ = 0, pushedMs_ = 0;
|
||||
files::ImageInfo info_;
|
||||
files::ImageFrame frame_;
|
||||
Phase phase_ = Phase::Wanted;
|
||||
std::shared_ptr<Job> job_;
|
||||
Canvas* canvas_ = nullptr;
|
||||
bool told_ = false, noteDrawn_ = false;
|
||||
std::vector<uint8_t> under_; // what the note was written over
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -34,6 +34,8 @@ class StorageApp : public App {
|
||||
bool textEntryActive() const override { return view_ == View::Name || view_ == View::Editor; }
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
bool retainsContent() const override { return view_ == View::Viewer && viewer_.retains(); }
|
||||
void contentLost() override { viewer_.lost(); }
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user