diff --git a/addons/tabswitcher/CMakeLists.txt b/addons/tabswitcher/CMakeLists.txt index 44a2c5837..65734ce81 100644 --- a/addons/tabswitcher/CMakeLists.txt +++ b/addons/tabswitcher/CMakeLists.txt @@ -1,31 +1,32 @@ project(tabswitcherplugin) add_definitions(-DTRANSLATION_DOMAIN=\"tabswitcherplugin\") include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) set(tabswitcherplugin_PART_SRCS tabswitcher.cpp tabswitcherfilesmodel.cpp tabswitchertreeview.cpp ) # resource for ui file and stuff qt5_add_resources(tabswitcherplugin_PART_SRCS plugin.qrc) add_library (tabswitcherplugin MODULE ${tabswitcherplugin_PART_SRCS}) kcoreaddons_desktop_to_json (tabswitcherplugin tabswitcherplugin.desktop) target_link_libraries(tabswitcherplugin KF5::TextEditor KF5::IconThemes KF5::I18n KF5::Service ) install(TARGETS tabswitcherplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) ############# unit tests ################ if (BUILD_TESTING) add_subdirectory(autotests) + add_subdirectory(tests) endif() diff --git a/addons/tabswitcher/tabswitcherfilesmodel.cpp b/addons/tabswitcher/tabswitcherfilesmodel.cpp index bb4957c8b..4de00405b 100644 --- a/addons/tabswitcher/tabswitcherfilesmodel.cpp +++ b/addons/tabswitcher/tabswitcherfilesmodel.cpp @@ -1,177 +1,190 @@ /* 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 #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 const & 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) { + // Note that item.documentName can contain additional characters - e.g. "README.md (2)" - + // so we cannot use that and have to parse the base filename by other means: + QFileInfo fileinfo(item.fullPath); + QString basename = fileinfo.fileName(); // e.g. "archive.tar.gz" // 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; + int len = item.fullPath.length() - prefix_length - basename.length() - 1; if (len > 0) { // only assign in case item.fullPath is not empty - // "PREFIXPATH/REMAININGPATH/DOCUMENTNAME" --> "REMAININGPATH" + // "PREFIXPATH/REMAININGPATH/BASENAME" --> "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; } +void detail::TabswitcherFilesModel::clear() +{ + if (data_.size() > 0) { + beginRemoveRows(QModelIndex(), 0, data_.size() - 1); + data_.clear(); + endRemoveRows(); + } +} + 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/tabswitcherfilesmodel.h b/addons/tabswitcher/tabswitcherfilesmodel.h index ae525efd8..25c0744f9 100644 --- a/addons/tabswitcher/tabswitcherfilesmodel.h +++ b/addons/tabswitcher/tabswitcherfilesmodel.h @@ -1,85 +1,89 @@ /* 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); + /** + * Clears all data from the model + */ + void clear(); 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 const & strs); } diff --git a/addons/tabswitcher/tests/CMakeLists.txt b/addons/tabswitcher/tests/CMakeLists.txt new file mode 100644 index 000000000..f54d7234a --- /dev/null +++ b/addons/tabswitcher/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.0.0) +project(tstestapp) + +# Find includes in corresponding build directories +set(CMAKE_INCLUDE_CURRENT_DIR ON) +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) + +# Find the QtWidgets library +find_package(Qt5Widgets CONFIG REQUIRED) + +set(SRC + main.cpp + tstestapp.cpp + ../tabswitcherfilesmodel.cpp +) + +add_executable(tstestapp ${SRC}) +target_link_libraries(tstestapp Qt5::Widgets) + diff --git a/addons/tabswitcher/tests/main.cpp b/addons/tabswitcher/tests/main.cpp new file mode 100644 index 000000000..415be34ee --- /dev/null +++ b/addons/tabswitcher/tests/main.cpp @@ -0,0 +1,11 @@ +#include "tstestapp.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + TsTestApp w; + w.show(); + + return app.exec(); +} diff --git a/addons/tabswitcher/tests/tstestapp.cpp b/addons/tabswitcher/tests/tstestapp.cpp new file mode 100644 index 000000000..2028a4bdb --- /dev/null +++ b/addons/tabswitcher/tests/tstestapp.cpp @@ -0,0 +1,112 @@ +#include "tstestapp.h" +#include "../tabswitcherfilesmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// https://www.fluentcpp.com/2017/09/22/make-pimpl-using-unique_ptr/ +class TsTestApp::Impl +{ +public: + void insert_1_item() + { + auto icon = QIcon::fromTheme(QLatin1String("blurimage")); + model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("abc.d"), QLatin1String("/home/user2/folder1/abc.d"))); + treeview1->resizeColumnToContents(0); + } + + void remove_1_item() + { + model.removeRow(0); + treeview1->resizeColumnToContents(0); + } + + void set_items_cutoff_bug() + { + model.clear(); + auto icon = QIcon::fromTheme(QLatin1String("document-export")); + +#define INS__(a, b) model.insertRow(model.rowCount(), new detail::FilenameListItem(icon, QLatin1String(a), QLatin1String(b))); + + INS__("multimedia-system.log", "/home/gregor/logs/notifications/multimedia-system.log") + INS__("servicemenueditor", "/home/gregor/dev/src/kservicemenueditor-0.2a/servicemenueditor") + INS__("kdesrc-build", "/home/gregor/kde/src/kdesrc-build/kdesrc-build") + INS__("README.md (3)", "/home/gregor/node_modules/autolinker/README.md") + INS__("package.json (3)", "/home/gregor/node_modules/autolinker/package.json") + INS__("LICENSE (3)", "/home/gregor/node_modules/autolinker/LICENSE") + INS__("package.json (2)", "/home/gregor/node_modules/asynckit/package.json") + + treeview1->resizeColumnToContents(0); + } + +public: + detail::TabswitcherFilesModel model; + QTreeView* treeview1; +}; + +TsTestApp::TsTestApp(QWidget *parent) : + QMainWindow(parent), + impl_(new TsTestApp::Impl) +{ + setGeometry(0, 0, 1024, 800); + setCentralWidget(new QWidget(this)); + auto l = new QVBoxLayout(); + centralWidget()->setLayout(l); + + auto hl = new QHBoxLayout(); + l->addLayout(hl); + + auto buttonInsert1 = new QPushButton(QLatin1String("Ins 1 item"), this); + connect(buttonInsert1, &QPushButton::clicked, this, [=] { impl_->insert_1_item(); }); + hl->addWidget(buttonInsert1); + + auto buttonRemove1 = new QPushButton(QLatin1String("Del 1 item"), this); + connect(buttonRemove1, &QPushButton::clicked, this, [=] { impl_->remove_1_item(); }); + hl->addWidget(buttonRemove1); + + auto buttonSetTestSet1 = new QPushButton(QLatin1String("set_items_cutoff_bug"), this); + connect(buttonSetTestSet1, &QPushButton::clicked, this, [=] { impl_->set_items_cutoff_bug(); }); + hl->addWidget(buttonSetTestSet1); + + impl_->treeview1 = new QTreeView(this); + l->addWidget(impl_->treeview1); + impl_->treeview1->setHeaderHidden(true); + impl_->treeview1->setRootIsDecorated(false); + + auto icon = QIcon::fromTheme(QLatin1String("edit-undo")); + impl_->model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("file1.h"), QLatin1String("/home/gm/projects/proj1/src/file1.h"))); + impl_->model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("file2.cpp"), QLatin1String("/home/gm/projects/proj1/src/file2.cpp"))); + impl_->model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("file3.py"), QLatin1String("/home/gm/dev/file3.py"))); + impl_->model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("file3kjaskdfkljasdfklj089asdfkjklasdjf90asdfsdfkj.py"), QLatin1String("/home/gm/dev/file3kjaskdfkljasdfklj089asdfkjklasdjf90asdfsdfkj.py"))); + impl_->model.insertRow(0, new detail::FilenameListItem(icon, QLatin1String("file3.py"), QLatin1String("/home/gm/dev/proj2/asldfkjasdfk/asdlfkjasd;faf/;ajsdkfgjaskdfgasdf/file3.py"))); + //impl_->insert_a_item(); + //impl_->remove_a_item(); + + impl_->model.rowCount(); + impl_->model.item(0); + impl_->model.index(0, 0); + + impl_->treeview1->setModel(&impl_->model); + impl_->treeview1->resizeColumnToContents(0); + impl_->treeview1->resizeColumnToContents(1); + + auto listview1 = new QListView(this); + l->addWidget(listview1); + listview1->setModel(&impl_->model); + + auto treeview2 = new QTreeView(this); + l->addWidget(treeview2); +} + +TsTestApp::~TsTestApp() +{ +} diff --git a/addons/tabswitcher/tests/tstestapp.h b/addons/tabswitcher/tests/tstestapp.h new file mode 100644 index 000000000..605f3ef56 --- /dev/null +++ b/addons/tabswitcher/tests/tstestapp.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class TsTestApp : public QMainWindow +{ + Q_OBJECT + +public: + explicit TsTestApp(QWidget *parent = nullptr); + ~TsTestApp(); + +private: + class Impl; + std::unique_ptr impl_; +};