* Rename "Database Tags" to "Searches and Tags" * Separate searching for all entries and resetting the search * Support selecting multiple tags to search against * Fix using escaped quotes in search terms * Make tag searching more precise * Support `is:expired-#` to search for entries expiring within # days. Exclude recycled entries from expired search. * Don't list tags from entries that are recycled * Force hide tag auto-completion menu when tag editing widget is hidden. On rare occasions the focus out signal is not called when the tag view is hidden (entry edit is closed), this resolves that problem. * Remove spaces from before and after tags to prevent seemingly duplicate tags from being created. * Also fix some awkward signal/slot dances that were setup over time with the entry view and preview widget. Allow changing tags for multiple entries through context menu * Closes #8277 - show context menu with currently available tags in database and checks those that are set on one or more selected entries. When a tag is selected it is either set or unset on all entries depending on its checked state. * Add ability to save searches and recall them from the "Searches and Tags" view * Add ability to remove a tag from all entries from the "Searches and Tags" view * Cleanup tag handling and widgets
506 lines
19 KiB
C++
506 lines
19 KiB
C++
/*
|
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
* version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "EntryPreviewWidget.h"
|
|
#include "ui_EntryPreviewWidget.h"
|
|
|
|
#include "gui/Clipboard.h"
|
|
#include "gui/Font.h"
|
|
#include "gui/Icons.h"
|
|
#include "totp/totp.h"
|
|
#if defined(WITH_XC_KEESHARE)
|
|
#include "keeshare/KeeShare.h"
|
|
#include "keeshare/KeeShareSettings.h"
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
constexpr int GeneralTabIndex = 0;
|
|
}
|
|
|
|
EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_ui(new Ui::EntryPreviewWidget())
|
|
, m_locked(false)
|
|
, m_currentEntry(nullptr)
|
|
, m_currentGroup(nullptr)
|
|
, m_selectedTabEntry(0)
|
|
, m_selectedTabGroup(0)
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
// Entry
|
|
m_ui->entryTotpButton->setIcon(icons()->icon("chronometer"));
|
|
m_ui->entryCloseButton->setIcon(icons()->icon("dialog-close"));
|
|
m_ui->togglePasswordButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
m_ui->toggleEntryNotesButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
m_ui->toggleGroupNotesButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
|
|
m_ui->entryAttachmentsWidget->setReadOnly(true);
|
|
m_ui->entryAttachmentsWidget->setButtonsVisible(false);
|
|
|
|
// Match background of read-only text edit fields with the window
|
|
m_ui->entryPasswordLabel->setBackgroundRole(QPalette::Window);
|
|
m_ui->entryUsernameLabel->setBackgroundRole(QPalette::Window);
|
|
m_ui->entryNotesTextEdit->setBackgroundRole(QPalette::Window);
|
|
m_ui->groupNotesTextEdit->setBackgroundRole(QPalette::Window);
|
|
// Align notes text with label text
|
|
m_ui->entryNotesTextEdit->document()->setDocumentMargin(0);
|
|
m_ui->groupNotesTextEdit->document()->setDocumentMargin(0);
|
|
|
|
connect(m_ui->entryUrlLabel, SIGNAL(linkActivated(QString)), SLOT(openEntryUrl()));
|
|
|
|
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpLabel, SLOT(setVisible(bool)));
|
|
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpProgress, SLOT(setVisible(bool)));
|
|
connect(m_ui->entryCloseButton, SIGNAL(clicked()), SLOT(hide()));
|
|
connect(m_ui->togglePasswordButton, SIGNAL(clicked(bool)), SLOT(setPasswordVisible(bool)));
|
|
connect(m_ui->toggleEntryNotesButton, SIGNAL(clicked(bool)), SLOT(setEntryNotesVisible(bool)));
|
|
connect(m_ui->toggleGroupNotesButton, SIGNAL(clicked(bool)), SLOT(setGroupNotesVisible(bool)));
|
|
connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
|
// Prevent the url from being focused after clicked to allow the Copy Password button to work properly
|
|
connect(m_ui->entryUrlLabel, &QLabel::linkActivated, this, [this] { m_ui->entryTabWidget->setFocus(); });
|
|
connect(&m_totpTimer, SIGNAL(timeout()), SLOT(updateTotpLabel()));
|
|
|
|
connect(m_ui->entryAttributesTable, &QTableWidget::itemDoubleClicked, this, [](QTableWidgetItem* item) {
|
|
auto userData = item->data(Qt::UserRole);
|
|
if (userData.isValid()) {
|
|
clipboard()->setText(userData.toString());
|
|
}
|
|
});
|
|
|
|
connect(config(), &Config::changed, this, [this](Config::ConfigKey key) {
|
|
if (key == Config::GUI_HidePreviewPanel) {
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
}
|
|
});
|
|
|
|
// Group
|
|
m_ui->groupCloseButton->setIcon(icons()->icon("dialog-close"));
|
|
connect(m_ui->groupCloseButton, SIGNAL(clicked()), SLOT(hide()));
|
|
connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
|
|
|
setFocusProxy(m_ui->entryTabWidget);
|
|
|
|
#if !defined(WITH_XC_KEESHARE)
|
|
removeTab(m_ui->groupTabWidget, m_ui->groupShareTab);
|
|
#endif
|
|
}
|
|
|
|
EntryPreviewWidget::~EntryPreviewWidget()
|
|
{
|
|
}
|
|
|
|
void EntryPreviewWidget::clear()
|
|
{
|
|
hide();
|
|
m_currentEntry = nullptr;
|
|
m_currentGroup = nullptr;
|
|
m_ui->entryAttachmentsWidget->unlinkAttachments();
|
|
}
|
|
|
|
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
|
|
{
|
|
disconnect(m_currentEntry);
|
|
disconnect(m_currentGroup);
|
|
|
|
m_currentEntry = selectedEntry;
|
|
m_currentGroup = nullptr;
|
|
|
|
if (!selectedEntry) {
|
|
hide();
|
|
return;
|
|
}
|
|
|
|
connect(selectedEntry, &Entry::modified, this, &EntryPreviewWidget::refresh);
|
|
refresh();
|
|
}
|
|
|
|
void EntryPreviewWidget::setGroup(Group* selectedGroup)
|
|
{
|
|
disconnect(m_currentEntry);
|
|
disconnect(m_currentGroup);
|
|
|
|
m_currentEntry = nullptr;
|
|
m_currentGroup = selectedGroup;
|
|
|
|
if (!selectedGroup) {
|
|
hide();
|
|
return;
|
|
}
|
|
|
|
connect(m_currentGroup, &Group::modified, this, &EntryPreviewWidget::refresh);
|
|
refresh();
|
|
}
|
|
|
|
void EntryPreviewWidget::refresh()
|
|
{
|
|
if (m_currentEntry) {
|
|
updateEntryHeaderLine();
|
|
updateEntryTotp();
|
|
updateEntryGeneralTab();
|
|
updateEntryAdvancedTab();
|
|
updateEntryAutotypeTab();
|
|
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
|
const int tabIndex =
|
|
m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
|
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
|
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
|
} else if (m_currentGroup) {
|
|
updateGroupHeaderLine();
|
|
updateGroupGeneralTab();
|
|
|
|
#if defined(WITH_XC_KEESHARE)
|
|
updateGroupSharingTab();
|
|
#endif
|
|
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
|
const int tabIndex =
|
|
m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
|
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
|
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
|
} else {
|
|
hide();
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
|
{
|
|
m_locked = mode == DatabaseWidget::Mode::LockedMode;
|
|
if (m_locked) {
|
|
return;
|
|
}
|
|
|
|
if (mode == DatabaseWidget::Mode::ViewMode) {
|
|
if (m_currentGroup && m_ui->stackedWidget->currentWidget() == m_ui->pageGroup) {
|
|
setGroup(m_currentGroup);
|
|
} else if (m_currentEntry) {
|
|
setEntry(m_currentEntry);
|
|
} else {
|
|
hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryHeaderLine()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title());
|
|
m_ui->entryTitleLabel->setRawText(hierarchy(m_currentEntry->group(), title));
|
|
m_ui->entryIcon->setPixmap(Icons::entryIconPixmap(m_currentEntry, IconSize::Large));
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryTotp()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
const bool hasTotp = m_currentEntry->hasTotp();
|
|
m_ui->entryTotpButton->setVisible(hasTotp);
|
|
m_ui->entryTotpLabel->hide();
|
|
m_ui->entryTotpProgress->hide();
|
|
m_ui->entryTotpButton->setChecked(false);
|
|
|
|
if (hasTotp) {
|
|
m_totpTimer.start(1000);
|
|
m_ui->entryTotpProgress->setMaximum(m_currentEntry->totpSettings()->step);
|
|
updateTotpLabel();
|
|
} else {
|
|
m_ui->entryTotpLabel->clear();
|
|
m_totpTimer.stop();
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::setPasswordVisible(bool state)
|
|
{
|
|
const QString password = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password());
|
|
if (state) {
|
|
m_ui->entryPasswordLabel->setText(password);
|
|
m_ui->entryPasswordLabel->setCursorPosition(0);
|
|
m_ui->entryPasswordLabel->setFont(Font::fixedFont());
|
|
} else if (password.isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) {
|
|
m_ui->entryPasswordLabel->setText("");
|
|
} else {
|
|
m_ui->entryPasswordLabel->setText(QString("\u25cf").repeated(6));
|
|
}
|
|
m_ui->togglePasswordButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setEntryNotesVisible(bool state)
|
|
{
|
|
setNotesVisible(m_ui->entryNotesTextEdit, m_currentEntry->notes(), state);
|
|
m_ui->toggleEntryNotesButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setGroupNotesVisible(bool state)
|
|
{
|
|
setNotesVisible(m_ui->groupNotesTextEdit, m_currentGroup->notes(), state);
|
|
m_ui->toggleGroupNotesButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setNotesVisible(QTextEdit* notesWidget, const QString& notes, bool state)
|
|
{
|
|
if (state) {
|
|
notesWidget->setPlainText(notes);
|
|
notesWidget->moveCursor(QTextCursor::Start);
|
|
notesWidget->ensureCursorVisible();
|
|
} else {
|
|
if (!notes.isEmpty()) {
|
|
notesWidget->setPlainText(QString("\u25cf").repeated(6));
|
|
} else {
|
|
notesWidget->setPlainText("");
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryGeneralTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
m_ui->entryUsernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username()));
|
|
m_ui->entryUsernameLabel->setCursorPosition(0);
|
|
|
|
if (config()->get(Config::Security_HidePasswordPreviewPanel).toBool()) {
|
|
// Hide password
|
|
setPasswordVisible(false);
|
|
// Show the password toggle button if there are dots in the label
|
|
m_ui->togglePasswordButton->setVisible(!m_ui->entryPasswordLabel->text().isEmpty());
|
|
m_ui->togglePasswordButton->setChecked(false);
|
|
} else {
|
|
// Show password
|
|
setPasswordVisible(true);
|
|
m_ui->togglePasswordButton->setVisible(false);
|
|
}
|
|
|
|
auto hasNotes = !m_currentEntry->notes().isEmpty();
|
|
auto hideNotes = config()->get(Config::Security_HideNotes).toBool();
|
|
|
|
setEntryNotesVisible(hasNotes && !hideNotes);
|
|
m_ui->toggleEntryNotesButton->setVisible(hasNotes && hideNotes
|
|
&& !m_ui->entryNotesTextEdit->toPlainText().isEmpty());
|
|
m_ui->toggleEntryNotesButton->setChecked(false);
|
|
|
|
if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
|
|
m_ui->entryNotesTextEdit->setFont(Font::fixedFont());
|
|
} else {
|
|
m_ui->entryNotesTextEdit->setFont(Font::defaultFont());
|
|
}
|
|
|
|
m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl());
|
|
const QString url = m_currentEntry->url();
|
|
if (!url.isEmpty()) {
|
|
// URL is well formed and can be opened in a browser
|
|
m_ui->entryUrlLabel->setUrl(m_currentEntry->resolveMultiplePlaceholders(url));
|
|
m_ui->entryUrlLabel->setCursor(Qt::PointingHandCursor);
|
|
m_ui->entryUrlLabel->setOpenExternalLinks(false);
|
|
} else {
|
|
m_ui->entryUrlLabel->setUrl({});
|
|
m_ui->entryUrlLabel->setCursor(Qt::ArrowCursor);
|
|
}
|
|
|
|
const TimeInfo entryTime = m_currentEntry->timeInfo();
|
|
const QString expires =
|
|
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
|
m_ui->entryExpirationLabel->setText(expires);
|
|
m_ui->entryTagsList->tags(m_currentEntry->tagList());
|
|
m_ui->entryTagsList->setReadOnly(true);
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryAdvancedTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
m_ui->entryAttributesTable->clear();
|
|
|
|
const EntryAttributes* attributes = m_currentEntry->attributes();
|
|
const QStringList customAttributes = attributes->customKeys();
|
|
const bool hasAttributes = !customAttributes.isEmpty();
|
|
const bool hasAttachments = !m_currentEntry->attachments()->isEmpty();
|
|
m_ui->entryAttributesTable->setRowCount(customAttributes.size());
|
|
m_ui->entryAttributesTable->setColumnCount(3);
|
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAdvancedTab, hasAttributes || hasAttachments);
|
|
if (hasAttributes) {
|
|
auto i = 0;
|
|
QFont font;
|
|
font.setBold(true);
|
|
for (const QString& key : customAttributes) {
|
|
m_ui->entryAttributesTable->setItem(i, 0, new QTableWidgetItem(key));
|
|
m_ui->entryAttributesTable->item(i, 0)->setFont(font);
|
|
m_ui->entryAttributesTable->item(i, 0)->setTextAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
|
|
if (attributes->isProtected(key)) {
|
|
// only show the reveal button on protected attributes
|
|
auto button = new QToolButton();
|
|
button->setCheckable(true);
|
|
button->setChecked(false);
|
|
button->setIcon(icons()->onOffIcon("password-show", false));
|
|
button->setProperty("row", i);
|
|
button->setIconSize({12, 12});
|
|
connect(button, &QToolButton::clicked, this, [this](bool state) {
|
|
auto btn = qobject_cast<QToolButton*>(sender());
|
|
btn->setIcon(icons()->onOffIcon("password-show", state));
|
|
auto item = m_ui->entryAttributesTable->item(btn->property("row").toInt(), 2);
|
|
if (state) {
|
|
item->setText(item->data(Qt::UserRole).toString());
|
|
} else {
|
|
item->setText(QString("\u25cf").repeated(6));
|
|
}
|
|
// Maintain button height while showing contents of cell
|
|
auto size = btn->size();
|
|
m_ui->entryAttributesTable->resizeRowToContents(item->row());
|
|
btn->setFixedSize(size);
|
|
});
|
|
|
|
m_ui->entryAttributesTable->setCellWidget(i, 1, button);
|
|
m_ui->entryAttributesTable->setItem(i, 2, new QTableWidgetItem(QString("\u25cf").repeated(6)));
|
|
} else {
|
|
m_ui->entryAttributesTable->setItem(i, 2, new QTableWidgetItem(attributes->value(key)));
|
|
}
|
|
|
|
m_ui->entryAttributesTable->item(i, 2)->setData(Qt::UserRole, attributes->value(key));
|
|
m_ui->entryAttributesTable->item(i, 2)->setToolTip(tr("Double click to copy value"));
|
|
m_ui->entryAttributesTable->item(i, 2)->setTextAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
|
|
++i;
|
|
}
|
|
}
|
|
|
|
m_ui->entryAttributesTable->horizontalHeader()->setStretchLastSection(true);
|
|
m_ui->entryAttributesTable->resizeColumnsToContents();
|
|
m_ui->entryAttributesTable->resizeRowsToContents();
|
|
m_ui->entryAttachmentsWidget->linkAttachments(m_currentEntry->attachments());
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryAutotypeTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
m_ui->entrySequenceLabel->setText(m_currentEntry->effectiveAutoTypeSequence());
|
|
m_ui->entryAutotypeTree->clear();
|
|
QList<QTreeWidgetItem*> items;
|
|
const AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations();
|
|
const auto associations = autotypeAssociations->getAll();
|
|
for (const auto& assoc : associations) {
|
|
const QString sequence =
|
|
assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence() : assoc.sequence;
|
|
items.append(new QTreeWidgetItem(m_ui->entryAutotypeTree, {assoc.window, sequence}));
|
|
}
|
|
|
|
m_ui->entryAutotypeTree->addTopLevelItems(items);
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, m_currentEntry->autoTypeEnabled());
|
|
}
|
|
|
|
void EntryPreviewWidget::updateGroupHeaderLine()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
m_ui->groupTitleLabel->setRawText(hierarchy(m_currentGroup, {}));
|
|
m_ui->groupIcon->setPixmap(Icons::groupIconPixmap(m_currentGroup, IconSize::Large));
|
|
}
|
|
|
|
void EntryPreviewWidget::updateGroupGeneralTab()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
const QString searchingText = m_currentGroup->resolveSearchingEnabled() ? tr("Enabled") : tr("Disabled");
|
|
m_ui->groupSearchingLabel->setText(searchingText);
|
|
|
|
const QString autotypeText = m_currentGroup->resolveAutoTypeEnabled() ? tr("Enabled") : tr("Disabled");
|
|
m_ui->groupAutotypeLabel->setText(autotypeText);
|
|
|
|
const TimeInfo groupTime = m_currentGroup->timeInfo();
|
|
const QString expiresText =
|
|
groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
|
m_ui->groupExpirationLabel->setText(expiresText);
|
|
|
|
if (config()->get(Config::Security_HideNotes).toBool()) {
|
|
setGroupNotesVisible(false);
|
|
m_ui->toggleGroupNotesButton->setVisible(!m_ui->groupNotesTextEdit->toPlainText().isEmpty());
|
|
m_ui->toggleGroupNotesButton->setChecked(false);
|
|
} else {
|
|
setGroupNotesVisible(true);
|
|
m_ui->toggleGroupNotesButton->setVisible(false);
|
|
}
|
|
|
|
if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
|
|
m_ui->groupNotesTextEdit->setFont(Font::fixedFont());
|
|
} else {
|
|
m_ui->groupNotesTextEdit->setFont(Font::defaultFont());
|
|
}
|
|
}
|
|
|
|
#if defined(WITH_XC_KEESHARE)
|
|
void EntryPreviewWidget::updateGroupSharingTab()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
setTabEnabled(m_ui->groupTabWidget, m_ui->groupShareTab, KeeShare::isShared(m_currentGroup));
|
|
auto reference = KeeShare::referenceOf(m_currentGroup);
|
|
m_ui->groupShareTypeLabel->setText(KeeShare::referenceTypeLabel(reference));
|
|
m_ui->groupSharePathLabel->setText(reference.path);
|
|
}
|
|
#endif
|
|
|
|
void EntryPreviewWidget::updateTotpLabel()
|
|
{
|
|
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
|
|
auto totpCode = m_currentEntry->totp();
|
|
totpCode.insert(totpCode.size() / 2, " ");
|
|
m_ui->entryTotpLabel->setText(totpCode);
|
|
|
|
auto step = m_currentEntry->totpSettings()->step;
|
|
auto timeleft = step - (Clock::currentSecondsSinceEpoch() % step);
|
|
m_ui->entryTotpProgress->setValue(timeleft);
|
|
m_ui->entryTotpProgress->update();
|
|
} else {
|
|
m_ui->entryTotpLabel->clear();
|
|
m_totpTimer.stop();
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateTabIndexes()
|
|
{
|
|
m_selectedTabEntry = m_ui->entryTabWidget->currentIndex();
|
|
m_selectedTabGroup = m_ui->groupTabWidget->currentIndex();
|
|
}
|
|
|
|
void EntryPreviewWidget::openEntryUrl()
|
|
{
|
|
if (m_currentEntry) {
|
|
emit entryUrlActivated(m_currentEntry);
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::removeTab(QTabWidget* tabWidget, QWidget* widget)
|
|
{
|
|
const int tabIndex = tabWidget->indexOf(widget);
|
|
Q_ASSERT(tabIndex != -1);
|
|
tabWidget->removeTab(tabIndex);
|
|
}
|
|
|
|
void EntryPreviewWidget::setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled)
|
|
{
|
|
const int tabIndex = tabWidget->indexOf(widget);
|
|
Q_ASSERT(tabIndex != -1);
|
|
tabWidget->setTabEnabled(tabIndex, enabled);
|
|
}
|
|
|
|
QString EntryPreviewWidget::hierarchy(const Group* group, const QString& title)
|
|
{
|
|
QString groupList = QString("%1").arg(group->hierarchy().join(" / "));
|
|
return title.isEmpty() ? groupList : QString("%1 / %2").arg(groupList, title);
|
|
}
|