Merge feature/breadman-full-views: breadman full view coverage + breadpad reminder window
This commit is contained in:
commit
b8a37cbb85
4 changed files with 190 additions and 20 deletions
|
|
@ -506,7 +506,7 @@ fn build_app_window(
|
||||||
let state_c = state.clone();
|
let state_c = state.clone();
|
||||||
let window_c = window.clone();
|
let window_c = window.clone();
|
||||||
new_note_btn.connect_clicked(move |_| {
|
new_note_btn.connect_clicked(move |_| {
|
||||||
show_add_note_window(&window_c, state_c.clone());
|
show_add_note_window(&window_c, state_c.clone(), |_| {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -527,7 +527,7 @@ fn build_app_window(
|
||||||
stack.set_visible_child_name(initial);
|
stack.set_visible_child_name(initial);
|
||||||
|
|
||||||
if let Some(req) = screenshot_req {
|
if let Some(req) = screenshot_req {
|
||||||
screenshot::dispatch(&window, req);
|
screenshot::dispatch(&window, req, state.clone(), new_note_btn.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
window.present();
|
window.present();
|
||||||
|
|
@ -796,7 +796,7 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box {
|
||||||
|
|
||||||
// ── Add note window ───────────────────────────────────────────────────────────
|
// ── Add note window ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) {
|
fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, on_build: impl FnOnce(>k4::Window)) {
|
||||||
let win = gtk4::Window::builder()
|
let win = gtk4::Window::builder()
|
||||||
.title("New Note")
|
.title("New Note")
|
||||||
.transient_for(parent)
|
.transient_for(parent)
|
||||||
|
|
@ -981,6 +981,7 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on_build(&win);
|
||||||
win.present();
|
win.present();
|
||||||
body_entry.grab_focus();
|
body_entry.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,25 @@
|
||||||
//! hand-rolled `mod args` instead of bolting on a second parser that would
|
//! hand-rolled `mod args` instead of bolting on a second parser that would
|
||||||
//! reject its real flags (`--view`, `done`, `upcoming --plain`).
|
//! reject its real flags (`--view`, `done`, `upcoming --plain`).
|
||||||
//!
|
//!
|
||||||
//! `--screenshot <view>` doubles as the view selector — it's passed through
|
//! `--screenshot <view>` doubles as the view selector for every named stack
|
||||||
//! as `initial_view` (the same field `--view` already sets) rather than
|
//! page ("all", "upcoming", "todo", ...) — it's passed through as
|
||||||
|
//! `initial_view` (the same field `--view` already sets) rather than
|
||||||
//! needing a separate mechanism, since breadman already supports opening
|
//! needing a separate mechanism, since breadman already supports opening
|
||||||
//! directly to a named stack page.
|
//! directly to a named stack page.
|
||||||
|
//!
|
||||||
|
//! One view isn't a stack page at all: "editor" opens the per-note editor
|
||||||
|
//! popover (`editor::build_editor_popover`), normally only reachable by
|
||||||
|
//! clicking a real note card's edit button. Screenshot mode calls the same
|
||||||
|
//! builder function directly against the first real note in the store
|
||||||
|
//! (bypassing the button/click-handler entirely — there's no clean way to
|
||||||
|
//! synthesize a click on a button that only ever existed as a local inside
|
||||||
|
//! `build_note_card`, never stored anywhere else), with no-op save/delete/
|
||||||
|
//! error callbacks since nothing here should actually persist a change.
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Extra settle time after `map` for the first frame to actually paint
|
/// Extra settle time after `map` for the first frame to actually paint
|
||||||
|
|
@ -20,6 +32,11 @@ use std::time::Duration;
|
||||||
/// anything has been drawn into it.
|
/// anything has been drawn into it.
|
||||||
const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
/// Delay before popping the editor popover open — same reasoning as every
|
||||||
|
/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat
|
||||||
|
/// to settle first.
|
||||||
|
const PRE_POPUP_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ScreenshotRequest {
|
pub struct ScreenshotRequest {
|
||||||
pub view: String,
|
pub view: String,
|
||||||
|
|
@ -33,12 +50,81 @@ pub struct ScreenshotRequest {
|
||||||
/// it never returns control to the normal note-manager UI.
|
/// it never returns control to the normal note-manager UI.
|
||||||
///
|
///
|
||||||
/// Unlike the other apps' `dispatch`, this doesn't validate `req.view`
|
/// Unlike the other apps' `dispatch`, this doesn't validate `req.view`
|
||||||
/// against a known-views list — an invalid name just falls through to
|
/// against a known-views list for the stack-page case — an invalid name
|
||||||
/// breadman's own `unwrap_or("all")` default (see `build_app_window`),
|
/// just falls through to breadman's own `unwrap_or("all")` default (see
|
||||||
/// same as `--view` already behaves for a normal run.
|
/// `build_app_window`), same as `--view` already behaves for a normal run.
|
||||||
pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) {
|
pub fn dispatch(
|
||||||
|
window: >k4::ApplicationWindow,
|
||||||
|
req: ScreenshotRequest,
|
||||||
|
state: crate::AppState,
|
||||||
|
editor_anchor: gtk4::Button,
|
||||||
|
) {
|
||||||
let output = req.output;
|
let output = req.output;
|
||||||
let (width, height) = (req.width as i32, req.height as i32);
|
let (width, height) = (req.width as i32, req.height as i32);
|
||||||
|
|
||||||
|
if req.view == "new-note" {
|
||||||
|
window.connect_map(move |root| {
|
||||||
|
let output = output.clone();
|
||||||
|
let root = root.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||||
|
crate::show_add_note_window(&root, state, move |dialog| {
|
||||||
|
let output = output.clone();
|
||||||
|
dialog.connect_map(move |_| {
|
||||||
|
let output = output.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||||
|
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.view == "editor" {
|
||||||
|
window.connect_map(move |_| {
|
||||||
|
let output = output.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
let editor_anchor = editor_anchor.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||||
|
let Some(note) = state.notes.borrow().first().cloned() else {
|
||||||
|
eprintln!("breadman: no notes in the store to build the editor view from");
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
let morning = state.cfg.borrow().reminders.default_morning.clone();
|
||||||
|
let store = Arc::new(state.write_store());
|
||||||
|
let popover = crate::editor::build_editor_popover(
|
||||||
|
¬e,
|
||||||
|
store,
|
||||||
|
morning,
|
||||||
|
Rc::new(|_| {}),
|
||||||
|
Rc::new(|| {}),
|
||||||
|
Rc::new(|_| {}),
|
||||||
|
);
|
||||||
|
popover.set_parent(&editor_anchor);
|
||||||
|
// Parenting to the whole window (rather than a small,
|
||||||
|
// concretely-placed widget like the real edit-button call
|
||||||
|
// site does) left the popover positioned above the window
|
||||||
|
// entirely (GTK4's default Popover position is Top) — off
|
||||||
|
// the top of the canvas and clipped out of every capture.
|
||||||
|
// Anchoring to a real button plus an explicit Bottom
|
||||||
|
// position keeps it inside the visible canvas.
|
||||||
|
popover.set_position(gtk4::PositionType::Bottom);
|
||||||
|
popover.set_autohide(false);
|
||||||
|
let output = output.clone();
|
||||||
|
popover.connect_map(move |_| {
|
||||||
|
let output = output.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||||
|
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
popover.popup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.connect_map(move |_| {
|
window.connect_map(move |_| {
|
||||||
let output = output.clone();
|
let output = output.clone();
|
||||||
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,21 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let screenshot_req = args.screenshot_request();
|
let screenshot_req = args.screenshot_request();
|
||||||
|
if let Some(req) = &screenshot_req {
|
||||||
|
if req.view == "reminder" || req.view == "reminder-snooze" {
|
||||||
|
// The real path (`fire <id>`, above) needs a real due note from
|
||||||
|
// the Store. A screenshot doesn't have one to work with — and
|
||||||
|
// shouldn't wait for one — so it builds a throwaway sample
|
||||||
|
// instead, never touching the Store at all.
|
||||||
|
let mut sample = Note::new(
|
||||||
|
"Sample reminder text".into(),
|
||||||
|
NoteType::from_str("reminder"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
sample.time = Some(chrono::Utc::now());
|
||||||
|
return run_reminder_window(sample, &cfg, screenshot_req);
|
||||||
|
}
|
||||||
|
}
|
||||||
run_popup(args.note_type, args.no_classify, cfg, screenshot_req)
|
run_popup(args.note_type, args.no_classify, cfg, screenshot_req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,19 +341,28 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run_reminder_window(note, cfg)
|
run_reminder_window(note, cfg, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_reminder_window(note: breadpad_shared::types::Note, cfg: &Config) -> Result<()> {
|
fn run_reminder_window(
|
||||||
let app = gtk4::Application::builder()
|
note: breadpad_shared::types::Note,
|
||||||
.application_id("com.breadway.breadpad.reminder")
|
cfg: &Config,
|
||||||
.build();
|
screenshot_req: Option<screenshot::ScreenshotRequest>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad.reminder");
|
||||||
|
if screenshot_req.is_some() {
|
||||||
|
// Same reasoning as run_popup's NON_UNIQUE: a screenshot run must
|
||||||
|
// get its own fresh window, never activate a real reminder that
|
||||||
|
// happens to already be showing.
|
||||||
|
builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE);
|
||||||
|
}
|
||||||
|
let app = builder.build();
|
||||||
|
|
||||||
let note = Arc::new(note);
|
let note = Arc::new(note);
|
||||||
let cfg = Arc::new(cfg.clone());
|
let cfg = Arc::new(cfg.clone());
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
build_reminder_window(app, note.clone(), cfg.clone());
|
build_reminder_window(app, note.clone(), cfg.clone(), screenshot_req.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run_with_args::<String>(&[]);
|
app.run_with_args::<String>(&[]);
|
||||||
|
|
@ -381,6 +405,7 @@ fn build_reminder_window(
|
||||||
app: >k4::Application,
|
app: >k4::Application,
|
||||||
note: Arc<breadpad_shared::types::Note>,
|
note: Arc<breadpad_shared::types::Note>,
|
||||||
cfg: Arc<Config>,
|
cfg: Arc<Config>,
|
||||||
|
screenshot_req: Option<screenshot::ScreenshotRequest>,
|
||||||
) {
|
) {
|
||||||
let window = gtk4::ApplicationWindow::builder()
|
let window = gtk4::ApplicationWindow::builder()
|
||||||
.application(app)
|
.application(app)
|
||||||
|
|
@ -557,6 +582,15 @@ fn build_reminder_window(
|
||||||
outer.append(&btn_row);
|
outer.append(&btn_row);
|
||||||
|
|
||||||
window.set_child(Some(&outer));
|
window.set_child(Some(&outer));
|
||||||
|
|
||||||
|
if let Some(req) = screenshot_req {
|
||||||
|
if req.view == "reminder-snooze" {
|
||||||
|
screenshot::capture_with_snooze_open(&window, &req, snooze_popover.clone());
|
||||||
|
} else {
|
||||||
|
screenshot::capture_window(&window, &req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.present();
|
window.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@
|
||||||
//! `--width`/`--height` are just three more fields on that same `Args`
|
//! `--width`/`--height` are just three more fields on that same `Args`
|
||||||
//! struct instead.
|
//! struct instead.
|
||||||
//!
|
//!
|
||||||
//! Only the "popup" view (the compose window from `run_popup`) is wired up
|
//! Three views: "popup" (the compose window from `run_popup`), "reminder"
|
||||||
//! — the reminder window (`run_reminder_window`, reached via `fire <id>`)
|
//! (the alert window from `run_reminder_window`/`build_reminder_window`,
|
||||||
//! needs a real stored `Note` to render, which isn't worth fabricating for
|
//! normally only reachable via a real due note through `fire <id>`, built
|
||||||
//! a screenshot pass.
|
//! here against a fabricated sample `Note` instead — see `main`'s
|
||||||
|
//! `screenshot_req.view == "reminder"` branch, which skips the Store lookup
|
||||||
|
//! entirely), and "reminder-snooze" (the same window with its snooze
|
||||||
|
//! popover open).
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -24,6 +27,11 @@ use std::time::Duration;
|
||||||
/// anything has been drawn into it.
|
/// anything has been drawn into it.
|
||||||
const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
/// Delay before popping the snooze popover open — same reasoning as every
|
||||||
|
/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat
|
||||||
|
/// to settle first.
|
||||||
|
const PRE_POPUP_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ScreenshotRequest {
|
pub struct ScreenshotRequest {
|
||||||
pub view: String,
|
pub view: String,
|
||||||
|
|
@ -48,12 +56,53 @@ pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)");
|
eprintln!("breadpad: unknown screenshot view '{other}' (known: popup, reminder, reminder-snooze)");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same shape as `dispatch`'s "popup" arm, for the reminder window itself
|
||||||
|
/// (view "reminder") — pulled out since `build_reminder_window` calls this
|
||||||
|
/// directly rather than going through `dispatch` (the reminder window is
|
||||||
|
/// built via a completely separate `run_reminder_window` entry point, not
|
||||||
|
/// `run_popup`'s).
|
||||||
|
pub fn capture_window(window: >k4::ApplicationWindow, req: &ScreenshotRequest) {
|
||||||
|
let output = req.output.clone();
|
||||||
|
let (width, height) = (req.width as i32, req.height as i32);
|
||||||
|
window.connect_map(move |_| {
|
||||||
|
let output = output.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||||
|
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View "reminder-snooze": force the snooze popover open shortly after the
|
||||||
|
/// window maps, then capture once *it* maps.
|
||||||
|
pub fn capture_with_snooze_open(
|
||||||
|
window: >k4::ApplicationWindow,
|
||||||
|
req: &ScreenshotRequest,
|
||||||
|
snooze_popover: gtk4::Popover,
|
||||||
|
) {
|
||||||
|
let output = req.output.clone();
|
||||||
|
let (width, height) = (req.width as i32, req.height as i32);
|
||||||
|
let popover_to_open = snooze_popover.clone();
|
||||||
|
window.connect_map(move |_| {
|
||||||
|
popover_to_open.set_autohide(false);
|
||||||
|
let popover_to_open = popover_to_open.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||||
|
popover_to_open.popup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
snooze_popover.connect_map(move |_| {
|
||||||
|
let output = output.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||||
|
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn finish(result: anyhow::Result<()>) {
|
fn finish(result: anyhow::Result<()>) {
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => std::process::exit(0),
|
Ok(()) => std::process::exit(0),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue