diff --git a/addons/tabswitcher/CMakeLists.txt b/addons/tabswitcher/CMakeLists.txt --- a/addons/tabswitcher/CMakeLists.txt +++ b/addons/tabswitcher/CMakeLists.txt @@ -5,6 +5,7 @@ set(tabswitcherplugin_PART_SRCS tabswitcher.cpp + tabswitcherfilesmodel.cpp tabswitchertreeview.cpp ) @@ -23,3 +24,8 @@ ) install(TARGETS tabswitcherplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) + +############# unit tests ################ +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() diff --git a/addons/tabswitcher/autotests/CMakeLists.txt b/addons/tabswitcher/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/addons/tabswitcher/autotests/CMakeLists.txt @@ -0,0 +1,11 @@ +include(ECMMarkAsTest) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +set(Src tabswitchertest.cpp ../tabswitcherfilesmodel.cpp) +add_executable(tabswitcher_test ${Src}) +add_test(plugin-tabswitcher_test tabswitcher) +target_link_libraries(tabswitcher_test kdeinit_kate Qt5::Test) +ecm_mark_as_test(tabswitcher_test) diff --git a/addons/tabswitcher/autotests/tabswitchertest.h b/addons/tabswitcher/autotests/tabswitchertest.h new file mode 100644 --- /dev/null +++ b/addons/tabswitcher/autotests/tabswitchertest.h @@ -0,0 +1,36 @@ +/* This file is part of the KDE project + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +class KateTabSwitcherTest : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +private Q_SLOTS: + void testLongestCommonPrefix(); +}; + diff --git a/addons/tabswitcher/autotests/tabswitchertest.cpp b/addons/tabswitcher/autotests/tabswitchertest.cpp new file mode 100644 --- /dev/null +++ b/addons/tabswitcher/autotests/tabswitchertest.cpp @@ -0,0 +1,64 @@ +/* This file is part of the KDE project + * + * Copyright (C) 2018 Gregor Mi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "tabswitchertest.h" +#include "tabswitcherfilesmodel.h" + +#include + +QTEST_MAIN(KateTabSwitcherTest) + +void KateTabSwitcherTest::initTestCase() +{ +} + +void KateTabSwitcherTest::cleanupTestCase() +{ +} + +void KateTabSwitcherTest::testLongestCommonPrefix() +{ + // standard case + std::vector strs; + strs.push_back(QLatin1String("/home/user1/a")); + strs.push_back(QLatin1String("/home/user1/bc")); + QCOMPARE(detail::longestCommonPrefix(strs), QLatin1String("/home/user1/")); + + // empty string at the end of the list + strs.clear(); + strs.push_back(QLatin1String("/home/a")); + strs.push_back(QLatin1String("/home/b")); + strs.push_back(QLatin1String("")); + QCOMPARE(detail::longestCommonPrefix(strs), QLatin1String("")); + + // empty string not only at the end of the list + strs.clear(); + strs.push_back(QLatin1String("")); + strs.push_back(QLatin1String("/home/a")); + strs.push_back(QLatin1String("/home/b")); + strs.push_back(QLatin1String("")); + QCOMPARE(detail::longestCommonPrefix(strs), QLatin1String("")); + + // a prefix with length 1 + strs.clear(); + strs.push_back(QLatin1String("/home/a")); + strs.push_back(QLatin1String("/etc/b")); + QCOMPARE(detail::longestCommonPrefix(strs), QLatin1String("/")); +} diff --git a/addons/tabswitcher/tabswitcher.h b/addons/tabswitcher/tabswitcher.h --- a/addons/tabswitcher/tabswitcher.h +++ b/addons/tabswitcher/tabswitcher.h @@ -34,6 +34,9 @@ class TabSwitcherTreeView; class QStandardItemModel; class QModelIndex; +namespace detail { + class TabswitcherFilesModel; +} class TabSwitcherPlugin : public KTextEditor::Plugin { @@ -136,7 +139,7 @@ private: TabSwitcherPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; - QStandardItemModel * m_model; + detail::TabswitcherFilesModel * m_model; QSet m_documents; TabSwitcherTreeView * m_treeView; }; diff --git a/addons/tabswitcher/tabswitcher.cpp b/addons/tabswitcher/tabswitcher.cpp --- a/addons/tabswitcher/tabswitcher.cpp +++ b/addons/tabswitcher/tabswitcher.cpp @@ -20,6 +20,7 @@ #include "tabswitcher.h" #include "tabswitchertreeview.h" +#include "tabswitcherfilesmodel.h" #include #include @@ -56,7 +57,7 @@ // register this view m_plugin->m_views.append(this); - m_model = new QStandardItemModel(this); + m_model = new detail::TabswitcherFilesModel(this); m_treeView = new TabSwitcherTreeView(); m_treeView->setModel(m_model); @@ -140,7 +141,10 @@ m_documents.insert(document); // add to model - auto item = new QStandardItem(iconForDocument(document), document->documentName()); + auto item = new detail::FilenameListItem( + iconForDocument(document), + document->documentName(), + document->url().toLocalFile()); item->setData(QVariant::fromValue(document)); m_model->insertRow(0, item); @@ -181,7 +185,8 @@ for (int i = 0; i < rowCount; ++i) { auto doc = m_model->item(i)->data().value(); if (doc == document) { - m_model->item(i)->setText(document->documentName()); + m_model->updateItem(m_model->item(i), document->documentName(), document->url().toLocalFile()); + //m_model->item(i)->setText(document->documentName()); break; } } @@ -244,7 +249,8 @@ // max size to be only 1/2th of the central widget size const int rowHeight = m_treeView->sizeHintForRow(0); const int frameWidth = m_treeView->frameWidth(); - const QSize viewSize(std::min(m_treeView->sizeHintForColumn(0) + 2 * frameWidth + m_treeView->verticalScrollBar()->width(), viewMaxSize.width()), + //const QSize viewSize(std::min(m_treeView->sizeHintForColumn(0) + 2 * frameWidth + m_treeView->verticalScrollBar()->width(), viewMaxSize.width()), // ORIG line, sizeHintForColumn was QListView but is protected for QTreeView so we introduced sizeHintWidth() + const QSize viewSize(std::min(m_treeView->sizeHintWidth() + 2 * frameWidth + m_treeView->verticalScrollBar()->width(), viewMaxSize.width()), std::min(std::max(rowHeight * m_model->rowCount() + 2 * frameWidth, rowHeight * 6 ), viewMaxSize.height())); // Position should be central over the editor area, so map to global from diff --git a/addons/tabswitcher/tabswitcherfilesmodel.h b/addons/tabswitcher/tabswitcherfilesmodel.h new file mode 100644 --- /dev/null +++ b/addons/tabswitcher/tabswitcherfilesmodel.h @@ -0,0 +1,85 @@ +/* This file is part of the KDE project + + Copyright (C) 2018 Gregor Mi + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include +#include +#include + +namespace detail { + +/** + * The implementation is close to QStandardItem but not all aspects are supported. + * Probably it would be better not to derive from QStandardItem. + */ +struct FilenameListItem : public QStandardItem +{ + FilenameListItem(const QIcon & icon, const QString & documentName, const QString & fullPath) + { + this->icon = icon; + this->documentName = documentName; + this->fullPath = fullPath; + } + + QIcon icon; + QString documentName; + QString fullPath; + /** + * calculated from documentName and fullPath + */ + QString displayPathPrefix; +}; +using FilenameList = std::vector; + +class TabswitcherFilesModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit TabswitcherFilesModel(QObject *parent = nullptr); + virtual ~TabswitcherFilesModel() = default; + TabswitcherFilesModel(const FilenameList & data); + bool insertRow(int row, FilenameListItem const * const item); + bool removeRow(int row); + int rowCount() const; + /** + * NOTE: The returned pointer will become invalid as soon as the underlying vector changes. + */ + FilenameListItem * item(int row) const; + /* + * Use this method to update an item. + * NOTE: This could be improved if we allow KTextEditor::Document to go into this interface. + * Then we could search and update by KTextEditor::Document. + */ + void updateItem(FilenameListItem * item, QString const & documentName, QString const & fullPath); + +protected: + int columnCount(const QModelIndex & parent) const override; + int rowCount(const QModelIndex & parent) const override; + QVariant data(const QModelIndex & index, int role) const override; + +private: + FilenameList data_; +}; + +QString longestCommonPrefix(std::vector& strs); + +} diff --git a/addons/tabswitcher/tabswitcherfilesmodel.cpp b/addons/tabswitcher/tabswitcherfilesmodel.cpp new file mode 100644 --- /dev/null +++ b/addons/tabswitcher/tabswitcherfilesmodel.cpp @@ -0,0 +1,177 @@ +/* This file is part of the KDE project + + Copyright (C) 2018 Gregor Mi + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "tabswitcherfilesmodel.h" + +#include + +#include + +namespace detail { + /** + * adpated from https://helloacm.com/c-coding-exercise-longest-common-prefix/ + * see also http://www.cplusplus.com/forum/beginner/83540/ + * Note that if strs contains the empty string, the result will be "" + */ + QString longestCommonPrefix(std::vector& strs) { + int n = INT_MAX; + if (strs.size() <= 0) { + return QString(); + } + if (strs.size() == 1) { + return strs[0]; + } + // get the min length + for (size_t i = 0; i < strs.size(); i++) { + n = strs[i].length() < n ? strs[i].length() : n; + } + for (int c = 0; c < n; c++) { // check each character + for (size_t i = 1; i < strs.size(); i++) { + if (strs[i][c] != strs[i - 1][c]) { // we find a mis-match + return QStringRef(&strs[0], 0, c).toString(); + } + } + } + // prefix is n-length + return QStringRef(&strs[0], 0, n).toString(); + } + + void post_process(FilenameList & data) + { + std::vector paths; + std::for_each(data.begin(), data.end(), + [&paths](FilenameListItem & item) { paths.push_back(item.fullPath); }); + + // Removes empty strings because Documents without file have no path and we would + // otherwise in this case always get "" + paths.erase( // erase-remove idiom, see https://en.cppreference.com/w/cpp/algorithm/remove + std::remove_if(paths.begin(), paths.end(), [](QString s) { + return s.isEmpty(); }), + paths.end() + ); + QString prefix = longestCommonPrefix(paths); + int prefix_length = prefix.length(); + if (prefix_length == 1) { // if there is only the "/" at the beginning, then keep it + prefix_length = 0; + } + + std::for_each(data.begin(), data.end(), + [prefix_length](FilenameListItem & item) { + // cut prefix (left side) and cut document name (plus slash) on the right side + int len = item.fullPath.length() - prefix_length - item.documentName.length() - 1; + if (len > 0) { // only assign in case item.fullPath is not empty + // "PREFIXPATH/REMAININGPATH/DOCUMENTNAME" --> "REMAININGPATH" + item.displayPathPrefix = QStringRef(&item.fullPath, prefix_length, len).toString(); + } + }); + + } +} + +detail::TabswitcherFilesModel::TabswitcherFilesModel(QObject *parent) : QAbstractTableModel(parent) +{ +} + +detail::TabswitcherFilesModel::TabswitcherFilesModel(const FilenameList & data) +{ + data_ = data; + post_process(data_); +} + +bool detail::TabswitcherFilesModel::insertRow(int row, FilenameListItem const * const item) +{ + beginInsertRows(QModelIndex(), row, row + 1); + data_.insert(data_.begin() + row, *item); + post_process(data_); + endInsertRows(); + return true; +} + +bool detail::TabswitcherFilesModel::removeRow(int row) +{ + if (data_.begin() + row == data_.end()) { + return false; + } + + beginRemoveRows(QModelIndex(), row, row + 1); + data_.erase(data_.begin() + row); + post_process(data_); + endRemoveRows(); + return true; +} + +int detail::TabswitcherFilesModel::rowCount() const +{ + return data_.size(); +} + +detail::FilenameListItem * detail::TabswitcherFilesModel::item(int row) const +{ + return const_cast(&data_[row]); +} + +void detail::TabswitcherFilesModel::updateItem(FilenameListItem * item, QString const & documentName, QString const & fullPath) +{ + item->documentName = documentName; + item->fullPath = fullPath; + post_process(data_); +} + +int detail::TabswitcherFilesModel::columnCount(const QModelIndex & parent) const +{ + Q_UNUSED(parent); + return 2; +} + +int detail::TabswitcherFilesModel::rowCount(const QModelIndex & parent) const +{ + Q_UNUSED(parent); + return data_.size(); +} + +QVariant detail::TabswitcherFilesModel::data(const QModelIndex & index, int role) const +{ + if (role == Qt::DisplayRole) { + const auto & row = data_[index.row()]; + if (index.column() == 0) + return row.displayPathPrefix; + else + return row.documentName; + } else if (role == Qt::DecorationRole) { + if (index.column() == 1) { + const auto & row = data_[index.row()]; + return row.icon; + } + } else if (role == Qt::ToolTipRole) { + const auto & row = data_[index.row()]; + return row.fullPath; + } else if (role == Qt::TextAlignmentRole) { + if (index.column() == 0) + return Qt::AlignRight + Qt::AlignVCenter; + else + return Qt::AlignVCenter; + } else if (role == Qt::ForegroundRole) { + if (index.column() == 0) + return QBrush(Qt::darkGray); + else + return QVariant(); + } + return QVariant(); +} diff --git a/addons/tabswitcher/tabswitchertreeview.h b/addons/tabswitcher/tabswitchertreeview.h --- a/addons/tabswitcher/tabswitchertreeview.h +++ b/addons/tabswitcher/tabswitchertreeview.h @@ -21,18 +21,29 @@ #ifndef KTEXTEDITOR_TABSWITCHER_TREEVIEW_H #define KTEXTEDITOR_TABSWITCHER_TREEVIEW_H -#include +#include -class TabSwitcherTreeView : public QListView +/** + * TODO: see screenshots https://phabricator.kde.org/D16054: + * some paths are truncated on the right side. Why? + */ +class TabSwitcherTreeView : public QTreeView { Q_OBJECT public: /** - * Default constructor. + * Default constructor */ TabSwitcherTreeView(); + int sizeHintWidth() const; + + /** + * todo: see inside + */ + void resizeColumnsToContents(); + Q_SIGNALS: /** * This signal is emitted whenever use activates an item through @@ -52,6 +63,11 @@ * Reimplemented for tracking the ESCAPE key. */ void keyPressEvent(QKeyEvent * event) override; + + /** + * Reimplemented for adjusting the column widths (todo: does not work yet) + */ + void showEvent(QShowEvent *event) override; }; #endif // KTEXTEDITOR_TABSWITCHER_TREEVIEW_H diff --git a/addons/tabswitcher/tabswitchertreeview.cpp b/addons/tabswitcher/tabswitchertreeview.cpp --- a/addons/tabswitcher/tabswitchertreeview.cpp +++ b/addons/tabswitcher/tabswitchertreeview.cpp @@ -22,16 +22,33 @@ #include "tabswitcher.h" #include +#include TabSwitcherTreeView::TabSwitcherTreeView() - : QListView() + : QTreeView() { setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::SingleSelection); - setUniformItemSizes(true); + //setUniformItemSizes(true); setTextElideMode(Qt::ElideMiddle); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setHeaderHidden(true); + setRootIsDecorated(false); +} + +int TabSwitcherTreeView::sizeHintWidth() const +{ + return sizeHintForColumn(0) + sizeHintForColumn(1); +} + +void TabSwitcherTreeView::resizeColumnsToContents() +{ + // TODO: does not work properly (or at all?) + qDebug() << "resizeColumnsToContents()"; + resizeColumnToContents(0); + resizeColumnToContents(1); } void TabSwitcherTreeView::keyReleaseEvent(QKeyEvent * event) @@ -41,7 +58,7 @@ event->accept(); hide(); } else { - QListView::keyReleaseEvent(event); + QTreeView::keyReleaseEvent(event); } } @@ -51,6 +68,12 @@ event->accept(); hide(); } else { - QListView::keyPressEvent(event); + QTreeView::keyPressEvent(event); } } + +void TabSwitcherTreeView::showEvent(QShowEvent* event) +{ + resizeColumnsToContents(); + QTreeView::showEvent(event); +}