From e9c14a763c99ef5456eae5ac386804a5352ec507 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 20:50:09 +0200 Subject: [PATCH 01/12] Refactor card_due_str() --- rslib/src/browser_table.rs | 56 ++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index e699c8257..cedd1a458 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -155,6 +155,41 @@ fn card_render_required(columns: &[Column]) -> bool { .any(|c| matches!(c, Column::Question | Column::Answer)) } +impl Card { + fn is_new_type_or_queue(&self) -> bool { + self.queue == CardQueue::New || self.ctype == CardType::New + } + + fn is_filtered_deck(&self) -> bool { + self.original_deck_id != DeckId(0) + } + + /// Returns true if the card can not be due as it's buried or suspended. + fn is_undue_queue(&self) -> bool { + (self.queue as i8) < 0 + } + + /// Returns true of the card has a due date in terms of days. + fn is_due_in_days(&self) -> bool { + matches!(self.queue, CardQueue::DayLearn | CardQueue::Review) + || (self.ctype == CardType::Review && self.is_undue_queue()) + } + + /// Returns the card's due date as a timestamp if it has one. + fn due_time(&self, timing: &SchedTimingToday) -> Option { + if self.queue == CardQueue::Learn { + Some(TimestampSecs(self.due as i64)) + } else if self.is_due_in_days() { + Some( + TimestampSecs::now() + .adding_secs(((self.due - timing.days_elapsed as i32) * 86400) as i64), + ) + } else { + None + } + } +} + impl Collection { pub fn browser_row_for_id(&mut self, id: i64) -> Result { if self.get_bool(BoolKey::BrowserTableShowNotesMode) { @@ -297,25 +332,16 @@ impl<'a> CardRowContext<'a> { } fn card_due_str(&mut self) -> String { - let due = if self.card.original_deck_id != DeckId(0) { + let due = if self.card.is_filtered_deck() { self.tr.browsing_filtered() - } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New { + } else if self.card.is_new_type_or_queue() { self.tr.statistics_due_for_new_card(self.card.due) + } else if let Some(time) = self.card.due_time(&self.timing) { + time.date_string().into() } else { - let date = if self.card.queue == CardQueue::Learn { - TimestampSecs(self.card.due as i64) - } else if self.card.queue == CardQueue::DayLearn - || self.card.queue == CardQueue::Review - || (self.card.ctype == CardType::Review && (self.card.queue as i8) < 0) - { - TimestampSecs::now() - .adding_secs(((self.card.due - self.timing.days_elapsed as i32) * 86400) as i64) - } else { - return "".into(); - }; - date.date_string().into() + return "".into(); }; - if (self.card.queue as i8) < 0 { + if self.card.is_undue_queue() { format!("({})", due) } else { due.into() From 1ad91a531287d925e30442d5e2c64444e8e8256c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 21:39:15 +0200 Subject: [PATCH 02/12] Add note due column --- qt/aqt/table.py | 1 + rslib/backend.proto | 1 + rslib/src/backend/search/browser_table.rs | 1 + rslib/src/backend/search/mod.rs | 1 + rslib/src/browser_table.rs | 57 +++++++++++++++-------- rslib/src/config/mod.rs | 1 + rslib/src/search/mod.rs | 10 ++-- rslib/src/search/note_due_order.sql | 16 +++++++ 8 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 rslib/src/search/note_due_order.sql diff --git a/qt/aqt/table.py b/qt/aqt/table.py index f7141298c..665a13d41 100644 --- a/qt/aqt/table.py +++ b/qt/aqt/table.py @@ -717,6 +717,7 @@ class NoteState(ItemState): ("note", tr.browsing_note()), ("noteCards", tr.editing_cards()), ("noteCrt", tr.browsing_created()), + ("noteDue", tr.statistics_due_date()), ("noteEase", tr.browsing_average_ease()), ("noteFld", tr.browsing_sort_field()), ("noteLapses", tr.scheduling_lapses()), diff --git a/rslib/backend.proto b/rslib/backend.proto index c8fd113f4..dbc10b2b1 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -811,6 +811,7 @@ message SortOrder { enum Kind { NOTE_CARDS = 0; NOTE_CREATION = 1; + NOTE_DUE = 17; NOTE_EASE = 2; NOTE_FIELD = 3; NOTE_LAPSES = 4; diff --git a/rslib/src/backend/search/browser_table.rs b/rslib/src/backend/search/browser_table.rs index 768de1dad..4538a208e 100644 --- a/rslib/src/backend/search/browser_table.rs +++ b/rslib/src/backend/search/browser_table.rs @@ -24,6 +24,7 @@ impl From for browser_table::Column { "template" => browser_table::Column::CardTemplate, "noteCards" => browser_table::Column::NoteCards, "noteCrt" => browser_table::Column::NoteCreation, + "noteDue" => browser_table::Column::NoteDue, "noteEase" => browser_table::Column::NoteEase, "noteFld" => browser_table::Column::NoteField, "noteLapses" => browser_table::Column::NoteLapses, diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs index e5acfb948..ea3aeead7 100644 --- a/rslib/src/backend/search/mod.rs +++ b/rslib/src/backend/search/mod.rs @@ -109,6 +109,7 @@ impl From for SortKind { match kind { SortKindProto::NoteCards => SortKind::NoteCards, SortKindProto::NoteCreation => SortKind::NoteCreation, + SortKindProto::NoteDue => SortKind::NoteDue, SortKindProto::NoteEase => SortKind::NoteEase, SortKindProto::NoteLapses => SortKind::NoteLapses, SortKindProto::NoteMod => SortKind::NoteMod, diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index cedd1a458..9f83258d7 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -24,26 +24,27 @@ use crate::{ #[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone, Copy)] #[repr(u8)] pub enum Column { - Custom = 0, - Question = 1, - Answer = 2, - CardDeck = 3, - CardDue = 4, - CardEase = 5, - CardLapses = 6, - CardInterval = 7, - CardMod = 8, - CardReps = 9, - CardTemplate = 10, - NoteCards = 11, - NoteCreation = 12, - NoteEase = 13, - NoteField = 14, - NoteLapses = 15, - NoteMod = 16, - NoteReps = 17, - NoteTags = 18, - Notetype = 19, + Custom, + Question, + Answer, + CardDeck, + CardDue, + CardEase, + CardLapses, + CardInterval, + CardMod, + CardReps, + CardTemplate, + NoteCards, + NoteCreation, + NoteDue, + NoteEase, + NoteField, + NoteLapses, + NoteMod, + NoteReps, + NoteTags, + Notetype, } #[derive(Debug, PartialEq)] @@ -147,6 +148,7 @@ struct NoteRowContext<'a> { notetype: Arc, cards: Vec, tr: &'a I18n, + timing: SchedTimingToday, } fn card_render_required(columns: &[Column]) -> bool { @@ -453,12 +455,14 @@ impl<'a> NoteRowContext<'a> { .get_notetype(note.notetype_id)? .ok_or(AnkiError::NotFound)?; let cards = col.storage.all_cards_of_note(note.id)?; + let timing = col.timing_today()?; Ok(NoteRowContext { note, notetype, cards, tr: &col.tr, + timing, }) } @@ -475,6 +479,18 @@ impl<'a> NoteRowContext<'a> { format!("{}%", ease / 10) } } + + /// Returns the due date of the next due card that is not in a filtered deck, new, suspended or + /// buried or the empty string if there is no such card. + fn note_due_str(&self) -> String { + self.cards + .iter() + .filter(|c| !(c.is_filtered_deck() || c.is_new_type_or_queue() || c.is_undue_queue())) + .filter_map(|c| c.due_time(&self.timing)) + .min() + .map(|time| time.date_string()) + .unwrap_or_else(|| "".into()) + } } impl RowContext for NoteRowContext<'_> { @@ -482,6 +498,7 @@ impl RowContext for NoteRowContext<'_> { Ok(match column { Column::NoteCards => self.cards.len().to_string(), Column::NoteCreation => self.note_creation_str(), + Column::NoteDue => self.note_due_str(), Column::NoteEase => self.note_ease_str(), Column::NoteField => self.note_field_str(), Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::().to_string(), diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs index 3b39ca46a..4598ee184 100644 --- a/rslib/src/config/mod.rs +++ b/rslib/src/config/mod.rs @@ -271,6 +271,7 @@ pub enum SortKind { NoteCards, #[serde(rename = "noteCrt")] NoteCreation, + NoteDue, NoteEase, NoteLapses, NoteMod, diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 05619cf45..7c1300232 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -90,6 +90,7 @@ impl SortKind { match self { SortKind::NoteCards | SortKind::NoteCreation + | SortKind::NoteDue | SortKind::NoteEase | SortKind::NoteField | SortKind::NoteLapses @@ -252,9 +253,11 @@ fn card_order_from_sortkind(kind: SortKind) -> Cow<'static, str> { fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> { match kind { - SortKind::NoteCards | SortKind::NoteEase | SortKind::NoteLapses | SortKind::NoteReps => { - "(select pos from sort_order where nid = n.id) asc".into() - } + SortKind::NoteCards + | SortKind::NoteDue + | SortKind::NoteEase + | SortKind::NoteLapses + | SortKind::NoteReps => "(select pos from sort_order where nid = n.id) asc".into(), SortKind::NoteCreation => "n.id asc".into(), SortKind::NoteField => "n.sfld collate nocase asc".into(), SortKind::NoteMod => "n.mod asc".into(), @@ -270,6 +273,7 @@ fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> { CardDeck => include_str!("deck_order.sql"), CardTemplate => include_str!("template_order.sql"), NoteCards => include_str!("note_cards_order.sql"), + NoteDue => include_str!("note_due_order.sql"), NoteEase => include_str!("note_ease_order.sql"), NoteLapses => include_str!("note_lapses_order.sql"), NoteReps => include_str!("note_reps_order.sql"), diff --git a/rslib/src/search/note_due_order.sql b/rslib/src/search/note_due_order.sql new file mode 100644 index 000000000..1648295f4 --- /dev/null +++ b/rslib/src/search/note_due_order.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS sort_order; +CREATE TEMPORARY TABLE sort_order ( + pos integer PRIMARY KEY, + nid integer NOT NULL UNIQUE +); +INSERT INTO sort_order (nid) +SELECT nid +FROM cards +WHERE ( + odid = 0 + AND type != 0 + AND queue > 0 + ) +GROUP BY nid +ORDER BY MIN(type), + MIN(due); \ No newline at end of file From f530c6d85208a2a880c3b5fed0ee9d70db57a60f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 21:40:35 +0200 Subject: [PATCH 03/12] Fix comment typo --- rslib/src/browser_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 9f83258d7..3c3a1c594 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -171,7 +171,7 @@ impl Card { (self.queue as i8) < 0 } - /// Returns true of the card has a due date in terms of days. + /// Returns true if the card has a due date in terms of days. fn is_due_in_days(&self) -> bool { matches!(self.queue, CardQueue::DayLearn | CardQueue::Review) || (self.ctype == CardType::Review && self.is_undue_queue()) From d7da1c15782b8c152be6bcd7ec9fadc7179569fe Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 22:06:58 +0200 Subject: [PATCH 04/12] Fix previewer not changing card --- qt/aqt/browser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 5084b9213..c1b23400f 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -408,7 +408,7 @@ class Browser(QMainWindow): @ensure_editor_saved def onRowChanged( - self, current: Optional[QItemSelection], previous: Optional[QItemSelection] + self, _current: Optional[QItemSelection], _previous: Optional[QItemSelection] ) -> None: """Update current note and hide/show editor. """ if self._closeEventHasCleanedUp: @@ -428,7 +428,7 @@ class Browser(QMainWindow): self.editor.card = card else: self.editor.set_note(None) - self._renderPreview() + self._renderPreview() self._update_flags_menu() gui_hooks.browser_did_change_row(self) From 31155f2dcdf05f0da52dbad434d5a944fb694728 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 23:44:16 +0200 Subject: [PATCH 05/12] Refactor note_ease_str() --- rslib/src/browser_table.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index 3c3a1c594..a8d1bf5c5 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -466,17 +466,18 @@ impl<'a> NoteRowContext<'a> { }) } + /// Returns the average ease of the non-new cards or a hint if there aren't any. fn note_ease_str(&self) -> String { - let cards = self + let eases: Vec = self .cards .iter() .filter(|c| c.ctype != CardType::New) - .collect::>(); - if cards.is_empty() { + .map(|c| c.ease_factor) + .collect(); + if eases.is_empty() { self.tr.browsing_new().into() } else { - let ease = cards.iter().map(|c| c.ease_factor).sum::() / cards.len() as u16; - format!("{}%", ease / 10) + format!("{}%", eases.iter().sum::() / eases.len() as u16 / 10) } } From 8779fb5edebef03a80db1790c00254ead9c0ee2e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 30 Mar 2021 23:44:35 +0200 Subject: [PATCH 06/12] Add note interval column --- ftl/core/browsing.ftl | 1 + qt/aqt/table.py | 1 + rslib/backend.proto | 33 ++++++++++++----------- rslib/src/backend/search/browser_table.rs | 1 + rslib/src/backend/search/mod.rs | 1 + rslib/src/browser_table.rs | 22 +++++++++++++++ rslib/src/config/mod.rs | 2 ++ rslib/src/search/mod.rs | 3 +++ rslib/src/search/note_interval_order.sql | 11 ++++++++ 9 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 rslib/src/search/note_interval_order.sql diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index e07435e9c..24091e4f8 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -8,6 +8,7 @@ browsing-answer = Answer browsing-any-cards-mapped-to-nothing-will = Any cards mapped to nothing will be deleted. If a note has no remaining cards, it will be lost. Are you sure you want to continue? browsing-any-flag = Any Flag browsing-average-ease = Average Ease +browsing-average-interval = Average Interval browsing-browser-appearance = Browser Appearance browsing-browser-options = Browser Options browsing-buried = Buried diff --git a/qt/aqt/table.py b/qt/aqt/table.py index 665a13d41..2e41d37e9 100644 --- a/qt/aqt/table.py +++ b/qt/aqt/table.py @@ -720,6 +720,7 @@ class NoteState(ItemState): ("noteDue", tr.statistics_due_date()), ("noteEase", tr.browsing_average_ease()), ("noteFld", tr.browsing_sort_field()), + ("noteIvl", tr.browsing_average_interval()), ("noteLapses", tr.scheduling_lapses()), ("noteMod", tr.search_note_modified()), ("noteReps", tr.scheduling_reviews()), diff --git a/rslib/backend.proto b/rslib/backend.proto index dbc10b2b1..2ca96464b 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -811,22 +811,23 @@ message SortOrder { enum Kind { NOTE_CARDS = 0; NOTE_CREATION = 1; - NOTE_DUE = 17; - NOTE_EASE = 2; - NOTE_FIELD = 3; - NOTE_LAPSES = 4; - NOTE_MOD = 5; - NOTE_REPS = 6; - NOTE_TAGS = 7; - NOTETYPE = 8; - CARD_MOD = 9; - CARD_REPS = 10; - CARD_DUE = 11; - CARD_EASE = 12; - CARD_LAPSES = 13; - CARD_INTERVAL = 14; - CARD_DECK = 15; - CARD_TEMPLATE = 16; + NOTE_DUE = 2; + NOTE_EASE = 3; + NOTE_FIELD = 4; + NOTE_INTERVAL = 5; + NOTE_LAPSES = 6; + NOTE_MOD = 7; + NOTE_REPS = 8; + NOTE_TAGS = 9; + NOTETYPE = 10; + CARD_MOD = 11; + CARD_REPS = 12; + CARD_DUE = 13; + CARD_EASE = 14; + CARD_LAPSES = 15; + CARD_INTERVAL = 16; + CARD_DECK = 17; + CARD_TEMPLATE = 18; } Kind kind = 1; bool reverse = 2; diff --git a/rslib/src/backend/search/browser_table.rs b/rslib/src/backend/search/browser_table.rs index 4538a208e..cb405769a 100644 --- a/rslib/src/backend/search/browser_table.rs +++ b/rslib/src/backend/search/browser_table.rs @@ -27,6 +27,7 @@ impl From for browser_table::Column { "noteDue" => browser_table::Column::NoteDue, "noteEase" => browser_table::Column::NoteEase, "noteFld" => browser_table::Column::NoteField, + "noteIvl" => browser_table::Column::NoteInterval, "noteLapses" => browser_table::Column::NoteLapses, "noteMod" => browser_table::Column::NoteMod, "noteReps" => browser_table::Column::NoteReps, diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs index ea3aeead7..5aaf25780 100644 --- a/rslib/src/backend/search/mod.rs +++ b/rslib/src/backend/search/mod.rs @@ -111,6 +111,7 @@ impl From for SortKind { SortKindProto::NoteCreation => SortKind::NoteCreation, SortKindProto::NoteDue => SortKind::NoteDue, SortKindProto::NoteEase => SortKind::NoteEase, + SortKindProto::NoteInterval => SortKind::NoteInterval, SortKindProto::NoteLapses => SortKind::NoteLapses, SortKindProto::NoteMod => SortKind::NoteMod, SortKindProto::NoteField => SortKind::NoteField, diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index a8d1bf5c5..ba0150772 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -40,6 +40,7 @@ pub enum Column { NoteDue, NoteEase, NoteField, + NoteInterval, NoteLapses, NoteMod, NoteReps, @@ -492,6 +493,26 @@ impl<'a> NoteRowContext<'a> { .map(|time| time.date_string()) .unwrap_or_else(|| "".into()) } + + /// Returns the average interval of the review and relearn cards or the empty string if there + /// aren't any. + fn note_interval_str(&self) -> String { + let intervals: Vec = self + .cards + .iter() + .filter(|c| matches!(c.ctype, CardType::Review | CardType::Relearn)) + .map(|c| c.interval) + .collect(); + if intervals.is_empty() { + "".into() + } else { + time_span( + (intervals.iter().sum::() * 86400 / (intervals.len() as u32)) as f32, + self.tr, + false, + ) + } + } } impl RowContext for NoteRowContext<'_> { @@ -502,6 +523,7 @@ impl RowContext for NoteRowContext<'_> { Column::NoteDue => self.note_due_str(), Column::NoteEase => self.note_ease_str(), Column::NoteField => self.note_field_str(), + Column::NoteInterval => self.note_interval_str(), Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::().to_string(), Column::NoteMod => self.note.mtime.date_string(), Column::NoteReps => self.cards.iter().map(|c| c.reps).sum::().to_string(), diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs index 4598ee184..a076934f3 100644 --- a/rslib/src/config/mod.rs +++ b/rslib/src/config/mod.rs @@ -273,6 +273,8 @@ pub enum SortKind { NoteCreation, NoteDue, NoteEase, + #[serde(rename = "noteIvl")] + NoteInterval, NoteLapses, NoteMod, #[serde(rename = "noteFld")] diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 7c1300232..d3fa0a6ec 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -93,6 +93,7 @@ impl SortKind { | SortKind::NoteDue | SortKind::NoteEase | SortKind::NoteField + | SortKind::NoteInterval | SortKind::NoteLapses | SortKind::NoteMod | SortKind::NoteReps @@ -256,6 +257,7 @@ fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> { SortKind::NoteCards | SortKind::NoteDue | SortKind::NoteEase + | SortKind::NoteInterval | SortKind::NoteLapses | SortKind::NoteReps => "(select pos from sort_order where nid = n.id) asc".into(), SortKind::NoteCreation => "n.id asc".into(), @@ -275,6 +277,7 @@ fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> { NoteCards => include_str!("note_cards_order.sql"), NoteDue => include_str!("note_due_order.sql"), NoteEase => include_str!("note_ease_order.sql"), + NoteInterval => include_str!("note_interval_order.sql"), NoteLapses => include_str!("note_lapses_order.sql"), NoteReps => include_str!("note_reps_order.sql"), Notetype => include_str!("notetype_order.sql"), diff --git a/rslib/src/search/note_interval_order.sql b/rslib/src/search/note_interval_order.sql new file mode 100644 index 000000000..375bf10bf --- /dev/null +++ b/rslib/src/search/note_interval_order.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS sort_order; +CREATE TEMPORARY TABLE sort_order ( + pos integer PRIMARY KEY, + nid integer NOT NULL UNIQUE +); +INSERT INTO sort_order (nid) +SELECT nid +FROM cards +WHERE type IN (2, 3) +GROUP BY nid +ORDER BY AVG(ivl); \ No newline at end of file From 5e151cdc42319e28bd7ac6a9a9c3cecc4e6d129b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 00:02:10 +0200 Subject: [PATCH 07/12] Pass Column by value --- rslib/src/browser_table.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index ba0150772..bbfb31b6d 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -79,13 +79,13 @@ pub struct Font { } trait RowContext { - fn get_cell_text(&mut self, column: &Column) -> Result; + fn get_cell_text(&mut self, column: Column) -> Result; fn get_row_color(&self) -> Color; fn get_row_font(&self) -> Result; fn note(&self) -> &Note; fn notetype(&self) -> &Notetype; - fn get_cell(&mut self, column: &Column) -> Result { + fn get_cell(&mut self, column: Column) -> Result { Ok(Cell { text: self.get_cell_text(column)?, is_rtl: self.get_is_rtl(column), @@ -103,7 +103,7 @@ trait RowContext { html_to_text_line(&self.note().fields()[index]).into() } - fn get_is_rtl(&self, column: &Column) -> bool { + fn get_is_rtl(&self, column: Column) -> bool { match column { Column::NoteField => { let index = self.notetype().config.sort_field_idx as usize; @@ -117,7 +117,7 @@ trait RowContext { Ok(Row { cells: columns .iter() - .map(|column| self.get_cell(column)) + .map(|&column| self.get_cell(column)) .collect::>()?, color: self.get_row_color(), font: self.get_row_font()?, @@ -389,7 +389,7 @@ impl<'a> CardRowContext<'a> { } impl RowContext for CardRowContext<'_> { - fn get_cell_text(&mut self, column: &Column) -> Result { + fn get_cell_text(&mut self, column: Column) -> Result { Ok(match column { Column::Question => self.question_str(), Column::Answer => self.answer_str(), @@ -516,7 +516,7 @@ impl<'a> NoteRowContext<'a> { } impl RowContext for NoteRowContext<'_> { - fn get_cell_text(&mut self, column: &Column) -> Result { + fn get_cell_text(&mut self, column: Column) -> Result { Ok(match column { Column::NoteCards => self.cards.len().to_string(), Column::NoteCreation => self.note_creation_str(), From 99e28068f9fbd1d3b6312e5f2a045cb34c42b33f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 08:56:54 +0200 Subject: [PATCH 08/12] Refactor get_row_color() --- rslib/src/browser_table.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index bbfb31b6d..c1fb265cc 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -193,6 +193,14 @@ impl Card { } } +impl Note { + fn is_marked(&self) -> bool { + self.tags + .iter() + .any(|tag| tag.eq_ignore_ascii_case("marked")) + } +} + impl Collection { pub fn browser_row_for_id(&mut self, id: i64) -> Result { if self.get_bool(BoolKey::BrowserTableShowNotesMode) { @@ -417,12 +425,7 @@ impl RowContext for CardRowContext<'_> { 3 => Color::FlagGreen, 4 => Color::FlagBlue, _ => { - if self - .note - .tags - .iter() - .any(|tag| tag.eq_ignore_ascii_case("marked")) - { + if self.note.is_marked() { Color::Marked } else if self.card.queue == CardQueue::Suspended { Color::Suspended @@ -534,12 +537,7 @@ impl RowContext for NoteRowContext<'_> { } fn get_row_color(&self) -> Color { - if self - .note - .tags - .iter() - .any(|tag| tag.eq_ignore_ascii_case("marked")) - { + if self.note.is_marked() { Color::Marked } else { Color::Default From e5d68df52704e808fd2fa1577cb6be4e846f22c5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 09:16:25 +0200 Subject: [PATCH 09/12] Change switch knob colour --- qt/aqt/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index 83afdcd99..ef70a2cd0 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -91,7 +91,7 @@ class Switch(QAbstractButton): ) def _paint_knob(self, painter: QPainter) -> None: - painter.setBrush(QBrush(theme_manager.qcolor(colors.HIGHLIGHT_BG))) + painter.setBrush(QBrush(theme_manager.qcolor(colors.LINK))) painter.drawEllipse(self._current_knob_rectangle()) def _paint_label(self, painter: QPainter) -> None: From c8146c13c1f50a77d1dda39d28c9647117bf19dd Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 10:05:44 +0200 Subject: [PATCH 10/12] Make toggle actions checkable --- qt/aqt/browser.py | 37 +++++++++++++++++++++---------------- qt/aqt/forms/browser.ui | 6 ++++++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index c1b23400f..0055f75b7 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -167,9 +167,7 @@ class Browser(QMainWindow): lambda: self.remove_tags_from_selected_notes(), ) qconnect(f.actionClear_Unused_Tags.triggered, self.clear_unused_tags) - qconnect( - f.actionToggle_Mark.triggered, lambda: self.toggle_mark_of_selected_notes() - ) + qconnect(f.actionToggle_Mark.triggered, self.toggle_mark_of_selected_notes) qconnect(f.actionChangeModel.triggered, self.onChangeModel) qconnect(f.actionFindDuplicates.triggered, self.onFindDupes) qconnect(f.actionFindReplace.triggered, self.onFindReplace) @@ -429,9 +427,14 @@ class Browser(QMainWindow): else: self.editor.set_note(None) self._renderPreview() - self._update_flags_menu() + self._update_context_actions() gui_hooks.browser_did_change_row(self) + def _update_context_actions(self) -> None: + self._update_flags_menu() + self._update_toggle_mark_action() + self._update_toggle_suspend_action() + @ensure_editor_saved def on_table_state_changed(self, checked: bool) -> None: self.mw.progress.start() @@ -725,15 +728,14 @@ where id in %s""" # Suspending ###################################################################### - def current_card_is_suspended(self) -> bool: - return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) + def _update_toggle_suspend_action(self) -> None: + is_suspended = bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) + self.form.actionToggle_Suspend.setChecked(is_suspended) - @ensure_editor_saved_on_trigger - def suspend_selected_cards(self) -> None: - want_suspend = not self.current_card_is_suspended() + @ensure_editor_saved + def suspend_selected_cards(self, checked: bool) -> None: cids = self.selected_cards() - - if want_suspend: + if checked: suspend_cards(mw=self.mw, card_ids=cids) else: unsuspend_cards(mw=self.mw, card_ids=cids) @@ -776,12 +778,15 @@ where id in %s""" qtMenuShortcutWorkaround(self.form.menuFlag) - def toggle_mark_of_selected_notes(self) -> None: - have_mark = bool(self.card and self.card.note().has_tag(MARKED_TAG)) - if have_mark: - self.remove_tags_from_selected_notes(tags=MARKED_TAG) - else: + def toggle_mark_of_selected_notes(self, checked: bool) -> None: + if checked: self.add_tags_to_selected_notes(tags=MARKED_TAG) + else: + self.remove_tags_from_selected_notes(tags=MARKED_TAG) + + def _update_toggle_mark_action(self) -> None: + is_marked = bool(self.card and self.card.note().has_tag(MARKED_TAG)) + self.form.actionToggle_Mark.setChecked(is_marked) # Scheduling ###################################################################### diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index 76217999f..f7cfde752 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -467,6 +467,9 @@ + + true + browsing_toggle_suspend @@ -561,6 +564,9 @@ + + true + browsing_toggle_mark From fd4b5dc695dc2b0db27327ee27f61bd87860465e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 12:13:09 +0200 Subject: [PATCH 11/12] Delay switch animation If the switch triggers heavy GUI action, like a lot of paint()s, the concurrently running switch animation may look choppy. A small timer makes these events execute first and the animation will run smoothly afterwards when the event queue has been drained. --- qt/aqt/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index ef70a2cd0..2f8816ee8 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -108,7 +108,8 @@ class Switch(QAbstractButton): animation.setDuration(100) animation.setStartValue(self.start_position) animation.setEndValue(self.end_position) - animation.start() + # make triggered events execute first so the animation runs smoothly afterwards + QTimer.singleShot(50, animation.start) def enterEvent(self, event: QEvent) -> None: self.setCursor(Qt.PointingHandCursor) From 52b66dc985e48b07594398c77ee1f90a5ae9fe9b Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 31 Mar 2021 18:53:36 +0200 Subject: [PATCH 12/12] Add shortcut and tooltip to switch --- ftl/core/browsing.ftl | 1 + qt/aqt/browser.py | 2 ++ qt/aqt/forms/browser.ui | 10 ++++++++++ qt/aqt/switch.py | 23 ++++++++++++++++------- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 24091e4f8..89755266c 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -101,6 +101,7 @@ browsing-suspended = Suspended browsing-tag-duplicates = Tag Duplicates browsing-tag-rename-warning-empty = You can't rename a tag that has no notes. browsing-target-field = Target field: +browsing-toggle-cards-notes-mode = Toggle Cards/Notes Mode browsing-toggle-mark = Toggle Mark browsing-toggle-suspend = Toggle Suspend browsing-treat-input-as-regular-expression = Treat input as regular expression diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 0055f75b7..62562c77a 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -376,6 +376,8 @@ class Browser(QMainWindow): self.table.set_view(self.form.tableView) switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial()) switch.setChecked(self.table.is_notes_mode()) + switch.setToolTip(tr.browsing_toggle_cards_notes_mode()) + qconnect(self.form.action_toggle_mode.triggered, switch.toggle) qconnect(switch.toggled, self.on_table_state_changed) self.form.gridLayout.addWidget(switch, 0, 0) diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui index f7cfde752..810724b33 100644 --- a/qt/aqt/forms/browser.ui +++ b/qt/aqt/forms/browser.ui @@ -218,6 +218,8 @@ + + @@ -603,6 +605,14 @@ qt_accel_forget + + + browsing_toggle_cards_notes_mode + + + Ctrl+M + + diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index 2f8816ee8..cd6590dac 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -9,7 +9,9 @@ from aqt.theme import theme_manager class Switch(QAbstractButton): """A horizontal slider to toggle between two states which can be denoted by short strings. - The left state is the default and corresponds to isChecked=False. + + The left state is the default and corresponds to isChecked()=False. + The suppoorted slots are toggle(), for an animated transition, and setChecked(). """ _margin: int = 2 @@ -104,13 +106,20 @@ class Switch(QAbstractButton): def mouseReleaseEvent(self, event: QMouseEvent) -> None: super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton: - animation = QPropertyAnimation(self, b"position", self) - animation.setDuration(100) - animation.setStartValue(self.start_position) - animation.setEndValue(self.end_position) - # make triggered events execute first so the animation runs smoothly afterwards - QTimer.singleShot(50, animation.start) + self._animate_toggle() def enterEvent(self, event: QEvent) -> None: self.setCursor(Qt.PointingHandCursor) super().enterEvent(event) + + def toggle(self) -> None: + super().toggle() + self._animate_toggle() + + def _animate_toggle(self) -> None: + animation = QPropertyAnimation(self, b"position", self) + animation.setDuration(100) + animation.setStartValue(self.start_position) + animation.setEndValue(self.end_position) + # make triggered events execute first so the animation runs smoothly afterwards + QTimer.singleShot(50, animation.start)