Merge pull request #1108 from RumovZ/more-columns

Even more browser fixes and features
This commit is contained in:
Damien Elmes 2021-04-01 15:59:06 +10:00 committed by GitHub
commit 8449bbe469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 245 additions and 101 deletions

View File

@ -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
@ -100,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

View File

@ -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)
@ -378,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)
@ -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,10 +428,15 @@ class Browser(QMainWindow):
self.editor.card = card
else:
self.editor.set_note(None)
self._renderPreview()
self._update_flags_menu()
self._renderPreview()
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 +730,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 +780,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
######################################################################

View File

@ -218,6 +218,8 @@
</property>
<addaction name="actionUndo"/>
<addaction name="separator"/>
<addaction name="action_toggle_mode"/>
<addaction name="separator"/>
<addaction name="actionSelectAll"/>
<addaction name="actionSelectNotes"/>
<addaction name="actionInvertSelection"/>
@ -467,6 +469,9 @@
</property>
</action>
<action name="actionToggle_Suspend">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>browsing_toggle_suspend</string>
</property>
@ -561,6 +566,9 @@
</property>
</action>
<action name="actionToggle_Mark">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>browsing_toggle_mark</string>
</property>
@ -597,6 +605,14 @@
<string>qt_accel_forget</string>
</property>
</action>
<action name="action_toggle_mode">
<property name="text">
<string>browsing_toggle_cards_notes_mode</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+M</string>
</property>
</action>
</widget>
<resources>
<include location="icons.qrc"/>

View File

@ -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
@ -91,7 +93,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:
@ -104,12 +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)
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)

View File

@ -717,8 +717,10 @@ 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()),
("noteIvl", tr.browsing_average_interval()),
("noteLapses", tr.scheduling_lapses()),
("noteMod", tr.search_note_modified()),
("noteReps", tr.scheduling_reviews()),

View File

@ -811,21 +811,23 @@ message SortOrder {
enum Kind {
NOTE_CARDS = 0;
NOTE_CREATION = 1;
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;

View File

@ -24,8 +24,10 @@ impl From<String> 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,
"noteIvl" => browser_table::Column::NoteInterval,
"noteLapses" => browser_table::Column::NoteLapses,
"noteMod" => browser_table::Column::NoteMod,
"noteReps" => browser_table::Column::NoteReps,

View File

@ -109,7 +109,9 @@ impl From<SortKindProto> for SortKind {
match kind {
SortKindProto::NoteCards => SortKind::NoteCards,
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,

View File

@ -24,26 +24,28 @@ 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,
NoteInterval,
NoteLapses,
NoteMod,
NoteReps,
NoteTags,
Notetype,
}
#[derive(Debug, PartialEq)]
@ -77,13 +79,13 @@ pub struct Font {
}
trait RowContext {
fn get_cell_text(&mut self, column: &Column) -> Result<String>;
fn get_cell_text(&mut self, column: Column) -> Result<String>;
fn get_row_color(&self) -> Color;
fn get_row_font(&self) -> Result<Font>;
fn note(&self) -> &Note;
fn notetype(&self) -> &Notetype;
fn get_cell(&mut self, column: &Column) -> Result<Cell> {
fn get_cell(&mut self, column: Column) -> Result<Cell> {
Ok(Cell {
text: self.get_cell_text(column)?,
is_rtl: self.get_is_rtl(column),
@ -101,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;
@ -115,7 +117,7 @@ trait RowContext {
Ok(Row {
cells: columns
.iter()
.map(|column| self.get_cell(column))
.map(|&column| self.get_cell(column))
.collect::<Result<_>>()?,
color: self.get_row_color(),
font: self.get_row_font()?,
@ -147,6 +149,7 @@ struct NoteRowContext<'a> {
notetype: Arc<Notetype>,
cards: Vec<Card>,
tr: &'a I18n,
timing: SchedTimingToday,
}
fn card_render_required(columns: &[Column]) -> bool {
@ -155,6 +158,49 @@ 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 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())
}
/// Returns the card's due date as a timestamp if it has one.
fn due_time(&self, timing: &SchedTimingToday) -> Option<TimestampSecs> {
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 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<Row> {
if self.get_bool(BoolKey::BrowserTableShowNotesMode) {
@ -297,25 +343,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()
@ -360,7 +397,7 @@ impl<'a> CardRowContext<'a> {
}
impl RowContext for CardRowContext<'_> {
fn get_cell_text(&mut self, column: &Column) -> Result<String> {
fn get_cell_text(&mut self, column: Column) -> Result<String> {
Ok(match column {
Column::Question => self.question_str(),
Column::Answer => self.answer_str(),
@ -388,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
@ -427,37 +459,74 @@ 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,
})
}
/// 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<u16> = self
.cards
.iter()
.filter(|c| c.ctype != CardType::New)
.collect::<Vec<&Card>>();
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::<u16>() / cards.len() as u16;
format!("{}%", ease / 10)
format!("{}%", eases.iter().sum::<u16>() / eases.len() as u16 / 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())
}
/// 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<u32> = 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::<u32>() * 86400 / (intervals.len() as u32)) as f32,
self.tr,
false,
)
}
}
}
impl RowContext for NoteRowContext<'_> {
fn get_cell_text(&mut self, column: &Column) -> Result<String> {
fn get_cell_text(&mut self, column: Column) -> Result<String> {
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::NoteInterval => self.note_interval_str(),
Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::<u32>().to_string(),
Column::NoteMod => self.note.mtime.date_string(),
Column::NoteReps => self.cards.iter().map(|c| c.reps).sum::<u32>().to_string(),
@ -468,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

View File

@ -271,7 +271,10 @@ pub enum SortKind {
NoteCards,
#[serde(rename = "noteCrt")]
NoteCreation,
NoteDue,
NoteEase,
#[serde(rename = "noteIvl")]
NoteInterval,
NoteLapses,
NoteMod,
#[serde(rename = "noteFld")]

View File

@ -90,8 +90,10 @@ impl SortKind {
match self {
SortKind::NoteCards
| SortKind::NoteCreation
| SortKind::NoteDue
| SortKind::NoteEase
| SortKind::NoteField
| SortKind::NoteInterval
| SortKind::NoteLapses
| SortKind::NoteMod
| SortKind::NoteReps
@ -252,9 +254,12 @@ 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::NoteInterval
| 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,7 +275,9 @@ 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"),
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"),

View File

@ -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);

View File

@ -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);