From 2415611450ec25c2497d1663e53c8997e4b1b364 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 11 Aug 2017 19:06:29 +1000 Subject: [PATCH] remove sidebar in browser use a pop-up menu instead, which saves a lot of screen real estate and should be more intuitive for new users also: - add options to manage note types and clear unused tags in the relevant submenus - shuffle a few shortcut keys - remove the old favourites code - saving and removing now done via the menu - individual card templates now searchable --- aqt/browser.py | 374 ++++++++++++++++++++---------------------- designer/browser.ui | 388 +++++++++++++++++++++----------------------- 2 files changed, 360 insertions(+), 402 deletions(-) diff --git a/aqt/browser.py b/aqt/browser.py index 61f507d1b..03bd8a057 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -17,7 +17,8 @@ from anki.utils import fmtTimeSpan, ids2str, stripHTMLMedia, htmlToTextLine, isW isMac, isLin from aqt.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter, \ saveHeader, restoreHeader, saveState, restoreState, applyStyles, getTag, \ - showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA + showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA, \ + getOnlyText from anki.hooks import runHook, addHook, remHook from aqt.webview import AnkiWebView from anki.consts import * @@ -367,15 +368,12 @@ class Browser(QMainWindow): self.form.setupUi(self) restoreGeom(self, "editor", 0) restoreState(self, "editor") - restoreSplitter(self.form.splitter_2, "editor2") restoreSplitter(self.form.splitter, "editor3") - self.form.splitter_2.setChildrenCollapsible(False) self.form.splitter.setChildrenCollapsible(False) self.card = None self.setupColumns() self.setupTable() self.setupMenus() - self.setupTree() self.setupHeaders() self.setupHooks() self.setupEditor() @@ -390,6 +388,8 @@ class Browser(QMainWindow): f.previewButton.clicked.connect(self.onTogglePreview) f.previewButton.setToolTip(_("Preview Selected Card (%s)") % shortcut(_("Ctrl+Shift+P"))) + + f.filter.clicked.connect(self.onFilterButton) # edit f.actionUndo.triggered.connect(self.mw.onUndo) f.actionInvertSelection.triggered.connect(self.invertSelection) @@ -400,7 +400,6 @@ class Browser(QMainWindow): f.actionAdd.triggered.connect(self.mw.onAddCard) f.actionAdd_Tags.triggered.connect(lambda: self.addTags()) f.actionRemove_Tags.triggered.connect(lambda: self.deleteTags()) - f.actionClear_Unused_Tags.triggered.connect(self.clearUnusedTags) f.actionChangeModel.triggered.connect(self.onChangeModel) f.actionFindDuplicates.triggered.connect(self.onFindDupes) f.actionFindReplace.triggered.connect(self.onFindReplace) @@ -419,7 +418,7 @@ class Browser(QMainWindow): f.actionLastCard.triggered.connect(self.onLastCard) f.actionFind.triggered.connect(self.onFind) f.actionNote.triggered.connect(self.onNote) - f.actionTags.triggered.connect(self.onTags) + f.actionTags.triggered.connect(self.onFilterButton) f.actionCardList.triggered.connect(self.onCardList) # help f.actionGuide.triggered.connect(self.onHelp) @@ -465,7 +464,6 @@ class Browser(QMainWindow): def _closeEventCleanup(self): self._cancelPreviewTimer() self.editor.setNote(None) - saveSplitter(self.form.splitter_2, "editor2") saveSplitter(self.form.splitter, "editor3") saveGeom(self, "editor") saveState(self, "editor") @@ -484,10 +482,6 @@ class Browser(QMainWindow): "Show answer on RET or register answer." if evt.key() == Qt.Key_Escape: self.close() - elif self.mw.app.focusWidget() == self.form.tree: - if evt.key() in (Qt.Key_Return, Qt.Key_Enter): - item = self.form.tree.currentItem() - self.onTreeClick(item, 0) def setupColumns(self): self.columns = [ @@ -513,7 +507,6 @@ class Browser(QMainWindow): ###################################################################### def setupSearch(self): - self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self)) self.form.searchButton.clicked.connect(self.onSearchActivated) self.form.searchEdit.lineEdit().returnPressed.connect(self.onSearchActivated) self.form.searchEdit.setCompleter(None) @@ -742,40 +735,23 @@ by clicking on one on the left.""")) # Filter tree ###################################################################### - class CallbackItem(QTreeWidgetItem): - def __init__(self, root, name, onclick, oncollapse=None, expanded=False): - QTreeWidgetItem.__init__(self, root, [name]) - self.setExpanded(expanded) - self.onclick = onclick - self.oncollapse = oncollapse + def onFilterButton(self): + m = QMenu() - def setupTree(self): - self.form.tree.itemClicked.connect(self.onTreeClick) - p = QPalette() - p.setColor(QPalette.Base, QColor("#d6dde0")) - self.form.tree.setPalette(p) - self.buildTree() - self.form.tree.itemExpanded.connect(lambda item: self.onTreeCollapse(item)) - self.form.tree.itemCollapsed.connect(lambda item: self.onTreeCollapse(item)) + self._addCommonFilters(m) + m.addSeparator() - def buildTree(self): - self.form.tree.clear() - root = self.form.tree - self._systemTagTree(root) - self._favTree(root) - self._decksTree(root) - self._modelTree(root) - self._userTagTree(root) - self.form.tree.setIndentation(15) + self._addTodayFilters(m) + self._addCardStateFilters(m) + m.addSeparator() - def onTreeClick(self, item, col): - if getattr(item, 'onclick', None): - item.onclick() + self._addDeckFilters(m) + self._addNoteTypeFilters(m) + self._addTagFilters(m) + self._addSavedSearches(m) + + m.exec_(self.form.filter.mapToGlobal(QPoint(0,0))) - def onTreeCollapse(self, item): - if getattr(item, 'oncollapse', None): - item.oncollapse() - def setFilter(self, *args): if len(args) == 1: txt = args[0] @@ -805,71 +781,172 @@ by clicking on one on the left.""")) self.form.searchEdit.lineEdit().setText(txt) self.onSearchActivated() - def _systemTagTree(self, root): - tags = ( - (_("Whole Collection"), "ankibw", ""), - (_("Current Deck"), "deck16", "deck:current"), - (_("Added Today"), "view-pim-calendar.png", "added:1"), - (_("Studied Today"), "view-pim-calendar.png", "rated:1"), - (_("Again Today"), "view-pim-calendar.png", "rated:1:1"), - (_("New"), "plus16.png", "is:new"), - (_("Learning"), "stock_new_template_red.png", "is:learn"), - (_("Review"), "clock16.png", "is:review"), - (_("Due"), "clock16.png", "is:due"), - (_("Marked"), "star16.png", "tag:marked"), - (_("Suspended"), "media-playback-pause.png", "is:suspended"), - (_("Leech"), "emblem-important.png", "tag:leech")) - for name, icon, cmd in tags: - item = self.CallbackItem( - root, name, lambda c=cmd: self.setFilter(c)) - item.setIcon(0, QIcon(":/icons/" + icon)) - return root + def _addSimpleFilters(self, m, items): + for row in items: + if row is None: + m.addSeparator() + else: + label, search = row + a = m.addAction(label) + a.triggered.connect(lambda *, f=search: self.setFilter(f)) - def _favTree(self, root): - saved = self.col.conf.get('savedFilters', []) - if not saved: - # Don't add favourites to tree if none saved - return - root = self.CallbackItem(root, _("My Searches"), None) - root.setExpanded(True) - root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png")) - for name, filt in sorted(saved.items()): - item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s)) - item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png")) - - def _userTagTree(self, root): - for t in sorted(self.col.tags.all()): + def _addCommonFilters(self, m): + items = ( + (_("Whole Collection"), ""), + (_("Current Deck"), "deck:current"), + None, + (_("Marked"), "tag:marked"), + (_("Leech"), "tag:leech")) + self._addSimpleFilters(m, items) + + def _addTodayFilters(self, m): + m = m.addMenu(_("Today")) + items = ( + (_("Added Today"), "added:1"), + (_("Studied Today"), "rated:1"), + (_("Again Today"), "rated:1:1")) + self._addSimpleFilters(m, items) + + def _addCardStateFilters(self, m): + m = m.addMenu(_("Card State")) + items = ( + (_("New"), "is:new"), + (_("Learning"), "is:learn"), + (_("Review"), "is:review"), + (_("Due"), "is:due"), + None, + (_("Suspended"), "is:suspended"), + (_("Buried"), "is:buried")) + self._addSimpleFilters(m, items) + + _tagsMenuSize = 30 + + def _addTagFilters(self, m): + m = m.addMenu(_("Tags")) + + a = m.addAction(_("Clear Unused")) + a.triggered.connect(self.clearUnusedTags) + m.addSeparator() + + tags = sorted(self.col.tags.all(), key=lambda s: s.lower()) + + if len(tags) < self._tagsMenuSize: + self._addTagFilterBlock(m, tags) + else: + # split the list into a more manageable size + chunks = [] + while tags: + chunk = tags[:self._tagsMenuSize] + chunks.append(chunk) + del tags[:self._tagsMenuSize] + # use separate menu for each chunk + for chunk in chunks: + name = chunk[0]+"..." + child = m.addMenu(name) + self._addTagFilterBlock(child, chunk) + + def _addTagFilterBlock(self, m, tags): + for t in tags: if t.lower() == "marked" or t.lower() == "leech": continue - item = self.CallbackItem( - root, t, lambda t=t: self.setFilter("tag", t)) - item.setIcon(0, QIcon(":/icons/anki-tag.png")) + a = m.addAction(t) + a.triggered.connect(lambda *, tag=t: self.setFilter("tag", tag)) - def _decksTree(self, root): - grps = self.col.sched.deckDueTree() - def fillGroups(root, grps, head=""): - for g in grps: - item = self.CallbackItem( - root, g[0], - lambda g=g: self.setFilter("deck", head+g[0]), - lambda g=g: self.mw.col.decks.collapseBrowser(g[1]), - not self.mw.col.decks.get(g[1]).get('browserCollapsed', False)) - item.setIcon(0, QIcon(":/icons/deck16.png")) - newhead = head + g[0]+"::" - fillGroups(item, g[5], newhead) - fillGroups(root, grps) + def _addDeckFilters(self, m): + def addDecks(parent, decks): + for head, did, rev, lrn, new, children in decks: + name = self.mw.col.decks.get(did)['name'] + shortname = name.split("::")[-1] + if children: + newparent = parent.addMenu(shortname) + a = newparent.addAction(_("Filter")) + a.triggered.connect( + lambda *, name=name: self.setFilter("deck", name)) + newparent.addSeparator() + addDecks(newparent, children) + else: + a = parent.addAction(shortname) + a.triggered.connect( + lambda *, name=name: self.setFilter("deck", name)) - def _modelTree(self, root): - for m in sorted(self.col.models.all(), key=itemgetter("name")): - mitem = self.CallbackItem( - root, m['name'], lambda m=m: self.setFilter("mid", str(m['id']))) - mitem.setIcon(0, QIcon(":/icons/product_design.png")) - # for t in m['tmpls']: - # titem = self.CallbackItem( - # t['name'], lambda m=m, t=t: self.setFilter( - # "model", m['name'], "card", t['name'])) - # titem.setIcon(0, QIcon(":/icons/stock_new_template.png")) - # mitem.addChild(titem) + # fixme: could rewrite to avoid calculating due # in the future + alldecks = self.col.sched.deckDueTree() + root = m.addMenu(_("Decks")) + + addDecks(root, alldecks) + + def _addNoteTypeFilters(self, m): + m = m.addMenu(_("Note Types")) + a = m.addAction(_("Manage...")) + a.triggered.connect(self.mw.onNoteTypes) + m.addSeparator() + for nt in sorted(self.col.models.all(), key=itemgetter("name")): + # no sub menu if it's a single template + if len(nt['tmpls']) == 1: + a = m.addAction(nt['name']) + a.triggered.connect(lambda *, nt=nt: self.setFilter("mid", str(nt['id']))) + else: + subm = m.addMenu(nt['name']) + a = subm.addAction(_("All Card Types")) + a.triggered.connect(lambda *, nt=nt: self.setFilter("mid", str(nt['id']))) + + # add templates + subm.addSeparator() + for c, tmpl in enumerate(nt['tmpls']): + a = subm.addAction(_("%(n)d: %(name)s") % dict( + n=c+1, name=tmpl['name'])) + a.triggered.connect(lambda *, nt=nt, c=c: self.setFilter( + "mid", str(nt['id']), "card", str(c+1) + )) + + # Favourites + ###################################################################### + + def _addSavedSearches(self, m): + # make sure exists + if "savedFilters" not in self.col.conf: + self.col.conf['savedFilters'] = {} + + m.addSeparator() + + if self._currentFilterIsSaved(): + a = m.addAction(_("Remove Current Filter...")) + a.triggered.connect(self._onRemoveFilter) + else: + a = m.addAction(_("Save Current Filter...")) + a.triggered.connect(self._onSaveFilter) + + saved = self.col.conf['savedFilters'] + if not saved: + return + + m.addSeparator() + for name, filt in sorted(saved.items()): + a = m.addAction(name) + a.triggered.connect(lambda *, f=filt: self.setFilter(f)) + + def _onSaveFilter(self): + name = getOnlyText(_("Please give your filter a name:")) + if not name: + return + filt = self.form.searchEdit.lineEdit().text() + self.col.conf['savedFilters'][name] = filt + self.col.setMod() + + def _onRemoveFilter(self): + name = self._currentFilterIsSaved() + if not askUser(_("Remove %s from your saved searches?") % name): + return + del self.col.conf['savedFilters'][name] + self.col.setMod() + + # returns name if found + def _currentFilterIsSaved(self): + filt = self.form.searchEdit.lineEdit().text() + for k,v in self.col.conf['savedFilters'].items(): + if filt == v: + return k + return None # Info ###################################################################### @@ -1261,7 +1338,6 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, def _clearUnusedTags(self): self.col.tags.registerNotes() - self.buildTree() # Suspending and marking ###################################################################### @@ -1385,16 +1461,12 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, addHook("reset", self.onReset) addHook("editTimer", self.refreshCurrentCard) addHook("editFocusLost", self.refreshCurrentCardFilter) - for t in "newTag", "newModel", "newDeck": - addHook(t, self.buildTree) def teardownHooks(self): remHook("reset", self.onReset) remHook("editTimer", self.refreshCurrentCard) remHook("editFocusLost", self.refreshCurrentCardFilter) remHook("undoState", self.onUndoState) - for t in "newTag", "newModel", "newDeck": - remHook(t, self.buildTree) def onUndoState(self, on): self.form.actionUndo.setEnabled(on) @@ -1590,9 +1662,6 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, self.editor.web.setFocus() self.editor.web.eval("focusField(0);") - def onTags(self): - self.form.tree.setFocus() - def onCardList(self): self.form.tableView.setFocus() @@ -1774,88 +1843,3 @@ Are you sure you want to continue?""")): def onHelp(self): openHelp("browsermisc") -# Favourites button -###################################################################### -class FavouritesLineEdit(QLineEdit): - buttonClicked = pyqtSignal(bool) - - def __init__(self, mw, browser, parent=None): - super().__init__(parent) - self.mw = mw - self.browser = browser - # add conf if missing - if 'savedFilters' not in self.mw.col.conf: - self.mw.col.conf['savedFilters'] = {} - self.button = QToolButton(self) - self.button.setStyleSheet('border: 0px;') - self.button.setCursor(Qt.ArrowCursor) - self.button.clicked.connect(self.buttonClicked.emit) - self.setIcon(':/icons/emblem-favorite-off.png') - # flag to raise save or delete dialog on button click - self.doSave = True - # name of current saved filter (if query matches) - self.name = None - self.buttonClicked.connect(self.onClicked) - self.textChanged.connect(self.updateButton) - - def resizeEvent(self, event): - buttonSize = self.button.sizeHint() - frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) - self.button.move(self.rect().right() - frameWidth - buttonSize.width(), - (self.rect().bottom() - buttonSize.height() + 1) / 2) - self.setTextMargins(0, 0, buttonSize.width() * 1.5, 0) - super().resizeEvent(event) - - def setIcon(self, path): - self.button.setIcon(QIcon(path)) - - def setText(self, txt): - super().setText(txt) - self.updateButton() - - def updateButton(self, reset=True): - # If search text is a saved query, switch to the delete button. - # Otherwise show save button. - txt = str(self.text()).strip() - for key, value in list(self.mw.col.conf['savedFilters'].items()): - if txt == value: - self.doSave = False - self.name = key - self.setIcon(QIcon(":/icons/emblem-favorite.png")) - return - self.doSave = True - self.setIcon(QIcon(":/icons/emblem-favorite-off.png")) - - def onClicked(self): - if self.doSave: - self.saveClicked() - else: - self.deleteClicked() - - def saveClicked(self): - txt = str(self.text()).strip() - dlg = QInputDialog(self) - dlg.setInputMode(QInputDialog.TextInput) - dlg.setLabelText(_("The current search terms will be added as a new " - "item in the sidebar.\n" - "Search name:")) - dlg.setWindowTitle(_("Save search")) - ok = dlg.exec_() - name = dlg.textValue() - if ok: - self.mw.col.conf['savedFilters'][name] = txt - self.mw.col.setMod() - - self.updateButton() - self.browser.buildTree() - - def deleteClicked(self): - msg = _('Remove "%s" from your saved searches?') % self.name - ok = QMessageBox.question(self, _('Remove search'), - msg, QMessageBox.Yes, QMessageBox.No) - - if ok == QMessageBox.Yes: - self.mw.col.conf['savedFilters'].pop(self.name, None) - self.mw.col.setMod() - self.updateButton() - self.browser.buildTree() diff --git a/designer/browser.ui b/designer/browser.ui index e2ef2b047..2f803d200 100644 --- a/designer/browser.ui +++ b/designer/browser.ui @@ -7,7 +7,7 @@ 0 0 750 - 400 + 493 @@ -26,210 +26,193 @@ - 0 + 3 - 0 + 3 - - - Qt::Horizontal + + + + 4 + 0 + - + + Qt::Vertical + + - - 1 - 0 + + 3 + 1 - - QFrame::NoFrame - - - false - - - - 1 + + + 0 - + + 0 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 9 + 0 + + + + true + + + QComboBox::NoInsert + + + + + + + Search + + + + + + + Preview + + + Ctrl+Shift+P + + + true + + + + + + + Filter... + + + + + + + + + + 9 + 1 + + + + + 0 + 150 + + + + Qt::ActionsContextMenu + + + QFrame::NoFrame + + + QFrame::Plain + + + Qt::ScrollBarAsNeeded + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + QAbstractItemView::SelectRows + + + false + + + false + + + 20 + + + true + + + + - - - - 4 - 0 - - - - Qt::Vertical - - - - - 3 - 1 - + + + + 0 - - - 0 - - - 0 - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 9 - 0 - - - - true - - - QComboBox::NoInsert - - - - - - - Search - - - - - - - Preview - - - Ctrl+Shift+P - - - true - - - - - - - - - - 9 - 1 - - - - - 0 - 150 - - - - Qt::ActionsContextMenu - - - QFrame::NoFrame - - - QFrame::Plain - - - Qt::ScrollBarAsNeeded - - - QAbstractItemView::NoEditTriggers - - - false - - - true - - - QAbstractItemView::SelectRows - - - false - - - false - - - 20 - - - true - - - - - - - - - 0 - - - 0 - - - 1 - - - 0 - - - 0 - - - - - 0 - - - - - - 0 - 1 - - - - - 50 - 200 - - - - - - - - + + 0 + + + 1 + + + 0 + + + 0 + + + + + 0 + + + + + + 0 + 1 + + + + + 50 + 200 + + + + + + + @@ -297,7 +280,6 @@ - @@ -324,7 +306,7 @@ Select &All - Ctrl+A + Ctrl+Alt+A @@ -353,7 +335,7 @@ N&ote - Ctrl+Shift+F + Ctrl+Shift+N @@ -392,9 +374,6 @@ Select &Notes - - Ctrl+Shift+N - @@ -411,10 +390,10 @@ - Fil&ters + Fil&ter - Ctrl+Shift+R + Ctrl+Shift+F @@ -486,11 +465,6 @@ Ctrl+Shift+D - - - Clear Unused Tags - - Toggle Suspend