diff --git a/kiwix-desktop.pro b/kiwix-desktop.pro index 9fb0c123..8d370af0 100644 --- a/kiwix-desktop.pro +++ b/kiwix-desktop.pro @@ -91,6 +91,7 @@ SOURCES += \ src/fullscreenwindow.cpp \ src/fullscreennotification.cpp \ src/zimview.cpp \ + src/multizimbutton.cpp \ HEADERS += \ src/choiceitem.h \ @@ -143,6 +144,7 @@ HEADERS += \ src/zimview.h \ src/portutils.h \ src/css_constants.h \ + src/multizimbutton.h \ FORMS += \ src/choiceitem.ui \ diff --git a/resources/css/style.css b/resources/css/style.css index ee91655e..e3b08c85 100644 --- a/resources/css/style.css +++ b/resources/css/style.css @@ -75,6 +75,52 @@ SearchBar > QToolButton:hover { border-radius: 3px; } +MultiZimButton QListWidget { + border: 0px; + outline: 0px; + padding: 5px 0px; /* XXX: duplicated in css_constants.h */ + background-color: white; +} + +MultiZimButton QListWidget::item { + padding: 0px 5px; /* XXX: duplicated in css_constants.h */ + border: 1px solid transparent; /* XXX: duplicated in css_constants.h */ + background-color: white; +} + +MultiZimButton QListWidget::item:hover, +MultiZimButton QListWidget::item:selected:active { + border: 1px solid #3366CC; + background-color: #D9E9FF; +} + +MultiZimButton QScrollBar { + width: 5px; /* XXX: duplicated in css_constants.h */ + border: none; + outline: none; +} + +MultiZimButton QScrollBar::handle { + background-color: grey; +} + +ZimItemWidget * { + background-color: transparent; +} + +ZimItemWidget QLabel { + font-size: 16px; + line-height: 24px; /* XXX: duplicated in css_constants.h */ +} + +ZimItemWidget QRadioButton::indicator { + image: none; +} + +ZimItemWidget QRadioButton::indicator:checked { + image: url(:/icons/tick.svg); +} + TopWidget QToolButton:pressed, TopWidget QToolButton::hover { border: 1px solid #3366CC; diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 9dfc1d69..b884f129 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -172,5 +172,6 @@ "portable-disabled-tooltip": "Function disabled in portable mode", "scroll-next-tab": "Scroll to next tab", "scroll-previous-tab": "Scroll to previous tab", - "kiwix-search": "Kiwix search" + "kiwix-search": "Kiwix search", + "search-options": "Search Options" } diff --git a/resources/i18n/qqq.json b/resources/i18n/qqq.json index 55846a1b..2fdc8c7b 100644 --- a/resources/i18n/qqq.json +++ b/resources/i18n/qqq.json @@ -180,5 +180,6 @@ "portable-disabled-tooltip": "Tooltip used to explain disabled components in the portable version.", "scroll-next-tab": "Represents the action of scrolling to the next tab of the current tab which toward the end of the tab bar.", "scroll-previous-tab": "Represents the action of scrolling to the previous tab of the current tab which toward the start of the tab bar.", - "kiwix-search": "Title text for a list of search results, which notes to the user those are from Kiwix's Search Engine" + "kiwix-search": "Title text for a list of search results, which notes to the user those are from Kiwix's Search Engine", + "search-options": "The term for a collection of additional search filtering options to help users narrow down search results." } diff --git a/src/css_constants.h b/src/css_constants.h index dba10b0b..337b0f63 100644 --- a/src/css_constants.h +++ b/src/css_constants.h @@ -28,6 +28,26 @@ namespace SearchBar{ const int border = 1; } +namespace MultiZimButton { +namespace QListWidget { +namespace item { + const int paddingHorizontal = 5; + const int border = 1; +} + const int paddingVertical = 5; +} + +namespace QScrollBar { + const int width = 5; +} +} + +namespace ZimItemWidget { +namespace QLabel { + const int lineHeight = 24; +} +} + namespace TopWidget { namespace QToolButton { namespace backButton { diff --git a/src/kiwixapp.cpp b/src/kiwixapp.cpp index f9be1da4..ed29b526 100644 --- a/src/kiwixapp.cpp +++ b/src/kiwixapp.cpp @@ -438,6 +438,8 @@ void KiwixApp::createActions() CREATE_ACTION_SHORTCUT(ToggleTOCAction, gt("table-of-content"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_1)); HIDE_ACTION(ToggleTOCAction); + CREATE_ACTION_ICON_SHORTCUT(OpenMultiZimAction, "filter", gt("search-options"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_L)); + CREATE_ACTION_ONOFF_ICON_SHORTCUT(ToggleReadingListAction, "reading-list-active", "reading-list", gt("reading-list"), QKeySequence(Qt::CTRL | Qt::Key_B)); CREATE_ACTION(ExportReadingListAction, gt("export-reading-list")); diff --git a/src/kiwixapp.h b/src/kiwixapp.h index fa422fc2..d8524c02 100644 --- a/src/kiwixapp.h +++ b/src/kiwixapp.h @@ -39,6 +39,7 @@ class KiwixApp : public QtSingleApplication BrowseLibraryAction, OpenFileAction, OpenRecentAction, + OpenMultiZimAction, SavePageAsAction, SearchArticleAction, SearchLibraryAction, diff --git a/src/mainmenu.cpp b/src/mainmenu.cpp index 07666716..1d550914 100644 --- a/src/mainmenu.cpp +++ b/src/mainmenu.cpp @@ -35,6 +35,7 @@ MainMenu::MainMenu(QWidget *parent) : m_editMenu.ADD_ACTION(SearchLibraryAction); m_editMenu.ADD_ACTION(FindInPageAction); m_editMenu.ADD_ACTION(ToggleAddBookmarkAction); + m_editMenu.ADD_ACTION(OpenMultiZimAction); addMenu(&m_editMenu); m_viewMenu.setTitle(gt("view")); diff --git a/src/multizimbutton.cpp b/src/multizimbutton.cpp new file mode 100644 index 00000000..25b5379b --- /dev/null +++ b/src/multizimbutton.cpp @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include +#include "kiwixapp.h" +#include "multizimbutton.h" +#include "css_constants.h" + +QString getElidedText(const QFont& font, int length, const QString& text); + +MultiZimButton::MultiZimButton(QWidget *parent) : + QToolButton(parent), + mp_buttonList(new QListWidget), + mp_radioButtonGroup(new QButtonGroup(this)) +{ + setMenu(new QMenu(this)); + setPopupMode(QToolButton::InstantPopup); + setDefaultAction(KiwixApp::instance()->getAction(KiwixApp::OpenMultiZimAction)); + connect(this, &QToolButton::triggered, this, &MultiZimButton::showMenu); + + const auto popupAction = new QWidgetAction(menu()); + popupAction->setDefaultWidget(mp_buttonList); + menu()->addAction(popupAction); + + connect(mp_buttonList, &QListWidget::currentRowChanged, this, [=](int row){ + if (const auto widget = getZimWidget(row)) + widget->getRadioButton()->setChecked(true); + }); +} + +void MultiZimButton::updateDisplay() +{ + mp_buttonList->clear(); + for (const auto& button : mp_radioButtonGroup->buttons()) + mp_radioButtonGroup->removeButton(button); + + const auto library = KiwixApp::instance()->getLibrary(); + const auto view = KiwixApp::instance()->getTabWidget()->currentWebView(); + QListWidgetItem* currentItem = nullptr; + QIcon currentIcon; + const int paddingTopBot = CSS::MultiZimButton::QListWidget::paddingVertical * 2; + const int itemHeight = paddingTopBot + CSS::ZimItemWidget::QLabel::lineHeight; + for (const auto& bookId : library->getBookIds()) + { + try + { + library->getArchive(bookId); + } catch (...) { continue; } + + const QString bookTitle = QString::fromStdString(library->getBookById(bookId).getTitle()); + const QIcon zimIcon = library->getBookIcon(bookId); + + const auto item = new QListWidgetItem(); + item->setData(Qt::UserRole, bookId); + item->setData(Qt::DisplayRole, bookTitle); + item->setSizeHint(QSize(0, itemHeight)); + + if (view && view->zimId() == bookId) + { + currentItem = item; + currentIcon = zimIcon; + continue; + } + + mp_buttonList->addItem(item); + setItemZimWidget(item, bookTitle, zimIcon); + } + + mp_buttonList->sortItems(); + if (currentItem) + { + mp_buttonList->insertItem(0, currentItem); + + const auto title = currentItem->data(Qt::DisplayRole).toString(); + setItemZimWidget(currentItem, "*" + title, currentIcon); + } + + /* Display should not be used other than for sorting. */ + for (int i = 0; i < mp_buttonList->count(); i++) + mp_buttonList->item(i)->setData(Qt::DisplayRole, QVariant()); + + setDisabled(mp_buttonList->model()->rowCount() == 0); + + mp_buttonList->scrollToTop(); + mp_buttonList->setCurrentRow(0); + + /* We set a maximum display height for list. Respect padding. */ + const int listHeight = itemHeight * std::min(7, mp_buttonList->count()); + mp_buttonList->setFixedHeight(listHeight + paddingTopBot); + mp_buttonList->setFixedWidth(menu()->width()); +} + +QStringList MultiZimButton::getZimIds() const +{ + QStringList idList; + for (int row = 0; row < mp_buttonList->count(); row++) + { + const auto widget = getZimWidget(row); + if (widget && widget->getRadioButton()->isChecked()) + idList.append(mp_buttonList->item(row)->data(Qt::UserRole).toString()); + } + return idList; +} + +ZimItemWidget *MultiZimButton::getZimWidget(int row) const +{ + const auto widget = mp_buttonList->itemWidget(mp_buttonList->item(row)); + return qobject_cast(widget); +} + +void MultiZimButton::setItemZimWidget(QListWidgetItem *item, + const QString &title, const QIcon &icon) +{ + const auto zimWidget = new ZimItemWidget(title, icon); + mp_radioButtonGroup->addButton(zimWidget->getRadioButton()); + mp_buttonList->setItemWidget(item, zimWidget); +} + +ZimItemWidget::ZimItemWidget(QString text, QIcon icon, QWidget *parent) : + QWidget(parent), + textLabel(new QLabel(this)), + iconLabel(new QLabel(this)), + radioBt(new QRadioButton(this)) +{ + setLayout(new QHBoxLayout); + + const int paddingHorizontal = CSS::MultiZimButton::QListWidget::item::paddingHorizontal; + layout()->setSpacing(paddingHorizontal); + layout()->setContentsMargins(0, 0, 0, 0); + + const int iconWidth = CSS::ZimItemWidget::QLabel::lineHeight; + const QSize iconSize = QSize(iconWidth, iconWidth); + iconLabel->setPixmap(icon.pixmap(iconSize)); + + /* Align text on same side irregardless of text direction. */ + const bool needAlignReverse = KiwixApp::isRightToLeft() == text.isRightToLeft(); + const auto align = needAlignReverse ? Qt::AlignLeft : Qt::AlignRight; + textLabel->setAlignment({Qt::AlignVCenter | align}); + + /* Need to align checkmark with select all button. Avoid scroller from + changing checkmark position by always leaving out space on scroller's + side. Do this by align items to the other side and reducing the total + length. textLabel is the only expandable element here. + + We set textLabel width to make sure the entire length always leave out + a fixed amount of white space for scroller. + */ + layout()->setAlignment({Qt::AlignVCenter | Qt::AlignLeading}); + const auto menu = KiwixApp::instance()->getSearchBar().getMultiZimButton().menu(); + const int iconAndCheckerWidth = iconWidth * 2; + const int totalSpacing = paddingHorizontal * 4; + + /* Add an extra border to counteract item border on one side */ + const int border = CSS::MultiZimButton::QListWidget::item::border; + const int scrollerWidth = CSS::MultiZimButton::QScrollBar::width; + const int contentWidthExcludeText = iconAndCheckerWidth + totalSpacing + scrollerWidth + border; + const int labelWidth = menu->width() - contentWidthExcludeText; + textLabel->setFixedWidth(labelWidth); + + QString elidedText = getElidedText(textLabel->font(), labelWidth, text); + textLabel->setText(elidedText == text ? text : elidedText.trimmed() + "(...)"); + + layout()->addWidget(iconLabel); + layout()->addWidget(textLabel); + layout()->addWidget(radioBt); +} diff --git a/src/multizimbutton.h b/src/multizimbutton.h new file mode 100644 index 00000000..0f20ffeb --- /dev/null +++ b/src/multizimbutton.h @@ -0,0 +1,44 @@ +#ifndef MULTIZIMBUTTON_H +#define MULTIZIMBUTTON_H + +#include + +class QListWidget; +class QButtonGroup; +class QListWidgetItem; +class QRadioButton; +class QLabel; + +class ZimItemWidget : public QWidget { + Q_OBJECT + +public: + ZimItemWidget(QString text, QIcon icon, QWidget *parent = nullptr); + + QRadioButton* getRadioButton() const { return radioBt; } + +private: + QLabel* textLabel; + QLabel* iconLabel; + QRadioButton* radioBt; +}; + +class MultiZimButton : public QToolButton { + Q_OBJECT + +public: + explicit MultiZimButton(QWidget *parent = nullptr); + +public slots: + void updateDisplay(); + QStringList getZimIds() const; + +private: + QListWidget* mp_buttonList; + QButtonGroup* mp_radioButtonGroup; + + ZimItemWidget* getZimWidget(int row) const; + void setItemZimWidget(QListWidgetItem* item, const QString& title, const QIcon& icon); +}; + +#endif // MULTIZIMBUTTON_H diff --git a/src/searchbar.cpp b/src/searchbar.cpp index 253dcc88..b3da94c6 100644 --- a/src/searchbar.cpp +++ b/src/searchbar.cpp @@ -204,7 +204,8 @@ void SearchBarLineEdit::focusInEvent( QFocusEvent* event) } if (event->reason() == Qt::ActiveWindowFocusReason || event->reason() == Qt::MouseFocusReason || - event->reason() == Qt::ShortcutFocusReason) { + event->reason() == Qt::ShortcutFocusReason || + event->reason() == Qt::PopupFocusReason) { connect(&m_completer, QOverload::of(&QCompleter::activated), this, &QLineEdit::setText,Qt::UniqueConnection); @@ -236,8 +237,8 @@ void SearchBarLineEdit::updateCompletion() { mp_typingTimer->stop(); clearSuggestions(); - WebView* current = KiwixApp::instance()->getTabWidget()->currentWebView(); - if (!current || current->url().isEmpty() || m_searchbarInput.isEmpty()) { + const auto& multiZim = KiwixApp::instance()->getSearchBar().getMultiZimButton(); + if (multiZim.getZimIds().isEmpty() || m_searchbarInput.isEmpty()) { hideSuggestions(); return; } @@ -310,7 +311,9 @@ void SearchBarLineEdit::openCompletion(const QModelIndex &index) if (index.isValid()) { const QUrl url = index.data(Qt::UserRole).toUrl(); - QTimer::singleShot(0, [=](){KiwixApp::instance()->openUrl(url, false);}); + const auto app = KiwixApp::instance(); + const bool newTab = app->getTabWidget()->currentWebView() == nullptr; + QTimer::singleShot(0, [=](){KiwixApp::instance()->openUrl(url, newTab);}); } } @@ -414,7 +417,8 @@ QRect SearchBarLineEdit::getCompleterRect() const SearchBar::SearchBar(QWidget *parent) : QToolBar(parent), m_searchBarLineEdit(this), - m_bookmarkButton(this) + m_bookmarkButton(this), + m_multiZimButton(this) { QLabel* searchIconLabel = new QLabel; searchIconLabel->setObjectName("searchIcon"); @@ -425,9 +429,15 @@ SearchBar::SearchBar(QWidget *parent) : addWidget(searchIconLabel); addWidget(&m_searchBarLineEdit); addWidget(&m_bookmarkButton); + addWidget(&m_multiZimButton); connect(this, &SearchBar::currentTitleChanged, &m_searchBarLineEdit, &SearchBarLineEdit::on_currentTitleChanged); connect(this, &SearchBar::currentTitleChanged, &m_bookmarkButton, &BookmarkButton::update_display); + connect(KiwixApp::instance()->getContentManager(), + &ContentManager::booksChanged, &m_multiZimButton, + &MultiZimButton::updateDisplay); + connect(this, &SearchBar::currentTitleChanged, &m_multiZimButton, + &MultiZimButton::updateDisplay); } diff --git a/src/searchbar.h b/src/searchbar.h index 8b6b4e0b..1a9f6546 100644 --- a/src/searchbar.h +++ b/src/searchbar.h @@ -11,6 +11,7 @@ #include #include #include "suggestionlistmodel.h" +#include "multizimbutton.h" class QTreeView; @@ -78,6 +79,7 @@ class SearchBar : public QToolBar { public: SearchBar(QWidget *parent = nullptr); SearchBarLineEdit& getLineEdit() { return m_searchBarLineEdit; }; + MultiZimButton& getMultiZimButton() { return m_multiZimButton; }; signals: void currentTitleChanged(const QString &title); @@ -85,5 +87,6 @@ class SearchBar : public QToolBar { private: SearchBarLineEdit m_searchBarLineEdit; BookmarkButton m_bookmarkButton; + MultiZimButton m_multiZimButton; }; #endif // SEARCHBAR_H diff --git a/src/suggestionlistdelegate.cpp b/src/suggestionlistdelegate.cpp index 954f7782..8ff8fbd2 100644 --- a/src/suggestionlistdelegate.cpp +++ b/src/suggestionlistdelegate.cpp @@ -44,6 +44,26 @@ void SuggestionListDelegate::paintIcon(QPainter *p, p->drawPixmap(pixmapRect, pixmap); } +/** + * @brief Get the elided text using font that can fit inside the length when + * appended with the custom elide text "(...)". + * + * @param font + * @param textRect + * @param text + * @return QString the elided text without any marker. + */ +QString getElidedText(const QFont& font, int length, const QString& text) +{ + const QFontMetrics metrics(font); + const int elideMarkerLength = metrics.tightBoundingRect("(...)").width(); + const int textLength = length - elideMarkerLength; + QString elidedText = metrics.elidedText(text, Qt::ElideRight, textLength); + if (elidedText != text) + return elidedText.chopped(1); + return text; +} + void SuggestionListDelegate::paintText(QPainter *p, const QStyleOptionViewItem &opt, const QModelIndex &index) const @@ -70,15 +90,9 @@ void SuggestionListDelegate::paintText(QPainter *p, const QString text = index.data(Qt::DisplayRole).toString(); /* Custom text elide. */ - const QFontMetrics metrics = opt.fontMetrics; - const int elideMarkerLength = metrics.tightBoundingRect("(...)").width(); - const int textLength = textRect.width() - elideMarkerLength; - QString elidedText = metrics.elidedText(text, Qt::ElideRight, textLength); + QString elidedText = getElidedText(opt.font, textRect.width(), text); if (elidedText != text) { - /* Remove built-in elide marker */ - elidedText.chop(1); - /* drawText's Align direction determines text direction */ const bool textDirFlipped = KiwixApp::isRightToLeft() != text.isRightToLeft(); elidedText = textDirFlipped ? "(...)" + elidedText.trimmed() diff --git a/src/suggestionlistworker.cpp b/src/suggestionlistworker.cpp index 9230ef0a..5f328b04 100644 --- a/src/suggestionlistworker.cpp +++ b/src/suggestionlistworker.cpp @@ -14,13 +14,13 @@ void SuggestionListWorker::run() { QList suggestionList; - WebView *current = KiwixApp::instance()->getTabWidget()->currentWebView(); - if(!current) - return; - auto qurl = current->url(); - auto currentZimId = qurl.host().split(".")[0]; + const auto app = KiwixApp::instance(); + const auto selectedIdList = app->getSearchBar().getMultiZimButton().getZimIds(); + + /* TODO: re-implement this after introducing the actual Multi-Zim. */ + const auto currentZimId = selectedIdList[0]; try { - auto archive = KiwixApp::instance()->getLibrary()->getArchive(currentZimId); + const auto archive = app->getLibrary()->getArchive(currentZimId); QUrl url; url.setScheme("zim"); url.setHost(currentZimId + ".zim");