1 /*
2   This file is part of Lokalize
3 
4   SPDX-FileCopyrightText: 2007-2009 Nick Shaforostoff <shafff@ukr.net>
5   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
6 
7   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
8 */
9 
10 #include "cataloglistview.h"
11 
12 #include "lokalize_debug.h"
13 
14 #include "catalogmodel.h"
15 #include "catalog.h"
16 #include "project.h"
17 #include "prefs.h"
18 #include "headerviewmenu.h"
19 
20 #include <QLineEdit>
21 #include <QTime>
22 #include <QTreeView>
23 #include <QHeaderView>
24 #include <QModelIndex>
25 #include <QToolButton>
26 #include <QVBoxLayout>
27 #include <QAction>
28 #include <QMenu>
29 #include <QShortcut>
30 #include <QKeyEvent>
31 
32 #include <KLocalizedString>
33 #include <KConfigGroup>
34 
35 class CatalogTreeView: public QTreeView
36 {
37 public:
CatalogTreeView(QWidget * parent)38     CatalogTreeView(QWidget * parent)
39         : QTreeView(parent) {}
~CatalogTreeView()40     ~CatalogTreeView() {}
41 
42 protected:
keyReleaseEvent(QKeyEvent * e)43     void keyReleaseEvent(QKeyEvent *e) override
44     {
45         if (e->key() == Qt::Key_Return && currentIndex().isValid()) {
46             Q_EMIT clicked(currentIndex());
47             e->accept();
48         } else {
49             QTreeView::keyReleaseEvent(e);
50         }
51     }
52 
53 };
54 
55 
CatalogView(QWidget * parent,Catalog * catalog)56 CatalogView::CatalogView(QWidget* parent, Catalog* catalog)
57     : QDockWidget(i18nc("@title:window aka Message Tree", "Translation Units"), parent)
58     , m_browser(new CatalogTreeView(this))
59     , m_lineEdit(new QLineEdit(this))
60     , m_model(new CatalogTreeModel(this, catalog))
61     , m_proxyModel(new CatalogTreeFilterModel(this))
62 {
63     setObjectName(QStringLiteral("catalogTreeView"));
64 
65     QWidget* w = new QWidget(this);
66     QVBoxLayout* layout = new QVBoxLayout(w);
67     layout->setContentsMargins(0, 0, 0, 0);
68     QHBoxLayout* l = new QHBoxLayout;
69     l->setContentsMargins(0, 0, 0, 0);
70     l->setSpacing(0);
71     layout->addLayout(l);
72 
73     m_lineEdit->setClearButtonEnabled(true);
74     m_lineEdit->setPlaceholderText(i18n("Quick search..."));
75     m_lineEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions"));
76     connect(m_lineEdit, &QLineEdit::textChanged, this, &CatalogView::setFilterRegExp, Qt::QueuedConnection);
77     // QShortcut* ctrlEsc=new QShortcut(QKeySequence(Qt::META+Qt::Key_Escape),this,SLOT(reset()),0,Qt::WidgetWithChildrenShortcut);
78     QShortcut* esc = new QShortcut(QKeySequence(Qt::Key_Escape), this, nullptr, nullptr, Qt::WidgetWithChildrenShortcut);
79     connect(esc, &QShortcut::activated, this, &CatalogView::escaped);
80 
81 
82     QToolButton* btn = new QToolButton(w);
83     btn->setPopupMode(QToolButton::InstantPopup);
84     btn->setText(i18n("options"));
85     //btn->setArrowType(Qt::DownArrow);
86     btn->setMenu(new QMenu(this));
87     m_filterOptionsMenu = btn->menu();
88     connect(m_filterOptionsMenu, &QMenu::aboutToShow, this, &CatalogView::fillFilterOptionsMenu);
89     connect(m_filterOptionsMenu, &QMenu::triggered, this, &CatalogView::filterOptionToggled);
90 
91     l->addWidget(m_lineEdit);
92     l->addWidget(btn);
93     layout->addWidget(m_browser);
94 
95 
96     setTabOrder(m_lineEdit, btn);
97     setTabOrder(btn, m_browser);
98     setFocusProxy(m_lineEdit);
99 
100     setWidget(w);
101 
102     connect(m_browser, &CatalogTreeView::clicked, this, &CatalogView::slotItemActivated);
103     m_browser->setRootIsDecorated(false);
104     m_browser->setAllColumnsShowFocus(true);
105     m_browser->setAlternatingRowColors(true);
106     m_browser->viewport()->setBackgroundRole(QPalette::Window);
107 #ifdef Q_OS_DARWIN
108     QPalette p;
109     p.setColor(QPalette::AlternateBase, p.color(QPalette::Window).darker(110));
110     p.setColor(QPalette::Highlight, p.color(QPalette::Window).darker(150));
111     m_browser->setPalette(p);
112 #endif
113 
114     m_proxyModel->setSourceModel(m_model);
115     m_browser->setModel(m_proxyModel);
116     m_browser->setColumnWidth(0, m_browser->columnWidth(0) / 3);
117     m_browser->setSortingEnabled(true);
118     m_browser->sortByColumn(0, Qt::AscendingOrder);
119     m_browser->setWordWrap(false);
120     m_browser->setUniformRowHeights(true);
121     m_browser->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
122 
123     new HeaderViewMenuHandler(m_browser->header());
124     m_browser->header()->restoreState(readUiState("CatalogTreeViewState"));
125 }
126 
~CatalogView()127 CatalogView::~CatalogView()
128 {
129     writeUiState("CatalogTreeViewState", m_browser->header()->saveState());
130 }
131 
setFocus()132 void CatalogView::setFocus()
133 {
134     QDockWidget::setFocus();
135     m_lineEdit->selectAll();
136 }
137 
slotNewEntryDisplayed(const DocPosition & pos)138 void CatalogView::slotNewEntryDisplayed(const DocPosition& pos)
139 {
140     QModelIndex item = m_proxyModel->mapFromSource(m_model->index(pos.entry, 0));
141     m_browser->setCurrentIndex(item);
142     m_browser->scrollTo(item/*,QAbstractItemView::PositionAtCenter*/);
143     m_lastKnownDocPosition = pos.entry;
144 }
145 
setFilterRegExp()146 void CatalogView::setFilterRegExp()
147 {
148     QString expr = m_lineEdit->text();
149     if (m_proxyModel->filterRegExp().pattern() != expr)
150         m_proxyModel->setFilterRegExp(m_proxyModel->filterOptions()&CatalogTreeFilterModel::IgnoreAccel ? expr.remove(Project::instance()->accel()) : expr);
151     refreshCurrentIndex();
152 }
153 
refreshCurrentIndex()154 void CatalogView::refreshCurrentIndex()
155 {
156     QModelIndex newPositionOfSelectedItem = m_proxyModel->mapFromSource(m_model->index(m_lastKnownDocPosition, 0));
157     m_browser->setCurrentIndex(newPositionOfSelectedItem);
158     m_browser->scrollTo(newPositionOfSelectedItem);
159 }
160 
slotItemActivated(const QModelIndex & idx)161 void CatalogView::slotItemActivated(const QModelIndex& idx)
162 {
163     Q_EMIT gotoEntry(DocPosition(m_proxyModel->mapToSource(idx).row()), 0);
164 }
165 
filterOptionToggled(QAction * action)166 void CatalogView::filterOptionToggled(QAction* action)
167 {
168     if (action->data().isNull())
169         return;
170 
171     int opt = action->data().toInt();
172     if (opt > 0)
173         m_proxyModel->setFilterOptions(m_proxyModel->filterOptions()^opt);
174     else {
175         if (opt != -1) opt = -opt - 2;
176         m_proxyModel->setFilterKeyColumn(opt);
177     }
178     m_filterOptionsMenu->clear();
179     refreshCurrentIndex();
180 }
fillFilterOptionsMenu()181 void CatalogView::fillFilterOptionsMenu()
182 {
183     m_filterOptionsMenu->clear();
184 
185     if (m_proxyModel->individualRejectFilterEnabled())
186         m_filterOptionsMenu->addAction(i18n("Reset individual filter"), this, SLOT(setEntriesFilteredOut()));
187 
188 
189     bool extStates = m_model->catalog()->capabilities()&ExtendedStates;
190 
191     const char* const basicTitles[] = {
192         I18N_NOOP("Case insensitive"),
193         I18N_NOOP("Ignore accelerator marks"),
194         I18N_NOOP("Ready"),
195         I18N_NOOP("Non-ready"),
196         I18N_NOOP("Non-empty"),
197         I18N_NOOP("Empty"),
198         I18N_NOOP("Changed since file open"),
199         I18N_NOOP("Unchanged since file open"),
200         I18N_NOOP("Same in sync file"),
201         I18N_NOOP("Different in sync file"),
202         I18N_NOOP("Not in sync file"),
203         I18N_NOOP("Plural"),
204         I18N_NOOP("Non-plural"),
205     };
206     const char* const* extTitles = Catalog::states();
207     const char* const* alltitles[2] = {basicTitles, extTitles};
208 
209     QMenu* basicMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Basic"));
210     QMenu* extMenu = extStates ? m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "States")) : nullptr;
211     QMenu* allmenus[2] = {basicMenu, extMenu};
212     QMenu* columnsMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Searchable column"));
213 
214     QActionGroup* columnsMenuGroup = new QActionGroup(columnsMenu);
215     QAction* txt;
216     txt = m_filterOptionsMenu->addAction(i18nc("@title:inmenu", "Resort and refilter on content change"), m_proxyModel, &CatalogTreeFilterModel::setDynamicSortFilter);
217     txt->setCheckable(true);
218     txt->setChecked(m_proxyModel->dynamicSortFilter());
219 
220     for (int i = 0; (1 << i) < CatalogTreeFilterModel::MaxOption; ++i) {
221         bool ext = (1 << i) >= CatalogTreeFilterModel::New;
222         if (!extStates && ext) break;
223         txt = allmenus[ext]->addAction(i18n(alltitles[ext][i - ext * FIRSTSTATEPOSITION]));
224         txt->setData(1 << i);
225         txt->setCheckable(true);
226         txt->setChecked(m_proxyModel->filterOptions() & (1 << i));
227         if ((1 << i) == CatalogTreeFilterModel::IgnoreAccel)
228             basicMenu->addSeparator();
229     }
230     if (!extStates)
231         m_filterOptionsMenu->addSeparator();
232     for (int i = -1; i < CatalogTreeModel::DisplayedColumnCount - 1; ++i) {
233         txt = columnsMenu->addAction((i == -1) ? i18nc("@item:inmenu all columns", "All") :
234                                      m_model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString());
235         txt->setData(-i - 2);
236         txt->setCheckable(true);
237         txt->setChecked(m_proxyModel->filterKeyColumn() == i);
238         txt->setActionGroup(columnsMenuGroup);
239     }
240     refreshCurrentIndex();
241 }
242 
reset()243 void CatalogView::reset()
244 {
245     m_proxyModel->setFilterKeyColumn(-1);
246     m_proxyModel->setFilterOptions(CatalogTreeFilterModel::AllStates);
247     m_lineEdit->clear();
248     refreshCurrentIndex();
249     //Q_EMIT gotoEntry(DocPosition(m_proxyModel->mapToSource(m_browser->currentIndex()).row()),0);
250     slotItemActivated(m_browser->currentIndex());
251 }
252 
setMergeCatalogPointer(MergeCatalog * pointer)253 void CatalogView::setMergeCatalogPointer(MergeCatalog* pointer)
254 {
255     m_proxyModel->setMergeCatalogPointer(pointer);
256 }
257 
siblingEntryNumber(int step)258 int CatalogView::siblingEntryNumber(int step)
259 {
260     QModelIndex item = m_browser->currentIndex();
261     int lastRow = m_proxyModel->rowCount() - 1;
262     if (!item.isValid()) {
263         if (lastRow == -1)
264             return -1;
265         item = m_proxyModel->index((step == 1) ? 0 : lastRow, 0);
266         m_browser->setCurrentIndex(item);
267     } else {
268         if (item.row() == ((step == -1) ? 0 : lastRow))
269             return -1;
270         item = item.sibling(item.row() + step, 0);
271     }
272     return m_proxyModel->mapToSource(item).row();
273 }
274 
nextEntryNumber()275 int CatalogView::nextEntryNumber()
276 {
277     return siblingEntryNumber(1);
278 }
279 
prevEntryNumber()280 int CatalogView::prevEntryNumber()
281 {
282     return siblingEntryNumber(-1);
283 }
284 
edgeEntry(CatalogTreeFilterModel * m_proxyModel,int row)285 static int edgeEntry(CatalogTreeFilterModel* m_proxyModel, int row)
286 {
287     if (!m_proxyModel->rowCount())
288         return -1;
289 
290     return m_proxyModel->mapToSource(m_proxyModel->index(row, 0)).row();
291 }
292 
firstEntryNumber()293 int CatalogView::firstEntryNumber()
294 {
295     return edgeEntry(m_proxyModel, 0);
296 }
297 
lastEntryNumber()298 int CatalogView::lastEntryNumber()
299 {
300     return edgeEntry(m_proxyModel, m_proxyModel->rowCount() - 1);
301 }
302 
303 
setEntryFilteredOut(int entry,bool filteredOut)304 void CatalogView::setEntryFilteredOut(int entry, bool filteredOut)
305 {
306     m_proxyModel->setEntryFilteredOut(entry, filteredOut);
307     refreshCurrentIndex();
308 }
309 
setEntriesFilteredOut(bool filteredOut)310 void CatalogView::setEntriesFilteredOut(bool filteredOut)
311 {
312     show();
313     m_proxyModel->setEntriesFilteredOut(filteredOut);
314     refreshCurrentIndex();
315 }
316 
317