File indexing completed on 2024-04-28 05:48:19

0001 /**
0002  * \file
0003  *
0004  * \brief Kate Close Except/Like plugin implementation
0005  *
0006  * SPDX-FileCopyrightText: 2012 Alex Turbov <i.zaufi@gmail.com>
0007  *
0008  * \date Thu Mar  8 08:13:43 MSK 2012 -- Initial design
0009  */
0010 /*
0011  * KateCloseExceptPlugin is free software: you can redistribute it and/or modify it
0012  * under the terms of the GNU General Public License as published by the
0013  * Free Software Foundation, either version 3 of the License, or
0014  * (at your option) any later version.
0015  *
0016  * KateCloseExceptPlugin is distributed in the hope that it will be useful, but
0017  * WITHOUT ANY WARRANTY; without even the implied warranty of
0018  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
0019  * See the GNU General Public License for more details.
0020  *
0021  * You should have received a copy of the GNU General Public License along
0022  * with this program.  If not, see <http://www.gnu.org/licenses/>.
0023  */
0024 
0025 // Project specific includes
0026 #include "close_except_plugin.h"
0027 #include "close_confirm_dialog.h"
0028 
0029 // Standard includes
0030 #include <KAboutData>
0031 #include <KActionCollection>
0032 #include <KLocalizedString>
0033 #include <KPluginFactory>
0034 #include <KTextEditor/Application>
0035 #include <KTextEditor/MainWindow>
0036 #include <KXMLGUIFactory>
0037 #include <QFileInfo>
0038 #include <QUrl>
0039 #include <kio/global.h>
0040 
0041 K_PLUGIN_FACTORY_WITH_JSON(CloseExceptPluginFactory, "katecloseexceptplugin.json", registerPlugin<kate::CloseExceptPlugin>();)
0042 
0043 namespace kate
0044 {
0045 // BEGIN CloseExceptPlugin
0046 CloseExceptPlugin::CloseExceptPlugin(QObject *application, const QVariantList &)
0047     : KTextEditor::Plugin(application)
0048 {
0049 }
0050 
0051 QObject *CloseExceptPlugin::createView(KTextEditor::MainWindow *parent)
0052 {
0053     return new CloseExceptPluginView(parent, this);
0054 }
0055 
0056 void CloseExceptPlugin::readSessionConfig(const KConfigGroup &config)
0057 {
0058     const KConfigGroup scg(&config, QStringLiteral("menu"));
0059     m_show_confirmation_needed = scg.readEntry(QStringLiteral("ShowConfirmation"), true);
0060 }
0061 
0062 void CloseExceptPlugin::writeSessionConfig(KConfigGroup &config)
0063 {
0064     KConfigGroup scg(&config, QStringLiteral("menu"));
0065     scg.writeEntry(QStringLiteral("ShowConfirmation"), m_show_confirmation_needed);
0066     scg.sync();
0067 }
0068 // END CloseExceptPlugin
0069 
0070 // BEGIN CloseExceptPluginView
0071 CloseExceptPluginView::CloseExceptPluginView(KTextEditor::MainWindow *mw, CloseExceptPlugin *plugin)
0072     : QObject(mw)
0073     , m_plugin(plugin)
0074     , m_show_confirmation_action(new KToggleAction(i18nc("@action:inmenu", "Show Confirmation"), this))
0075     , m_except_menu(new KActionMenu(i18nc("@action:inmenu close docs except the following...", "Close Except"), this))
0076     , m_like_menu(new KActionMenu(i18nc("@action:inmenu close docs like the following...", "Close Like"), this))
0077     , m_mainWindow(mw)
0078 {
0079     KXMLGUIClient::setComponentName(QStringLiteral("katecloseexceptplugin"), i18n("Close Except/Like Plugin"));
0080     setXMLFile(QStringLiteral("ui.rc"));
0081 
0082     actionCollection()->addAction(QStringLiteral("file_close_except"), m_except_menu);
0083     actionCollection()->addAction(QStringLiteral("file_close_like"), m_like_menu);
0084 
0085     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::documentCreated, this, &CloseExceptPluginView::documentCreated);
0086     // Configure toggle action and connect it to update state
0087     m_show_confirmation_action->setChecked(m_plugin->showConfirmationNeeded());
0088     connect(m_show_confirmation_action.data(), &KToggleAction::toggled, m_plugin, &CloseExceptPlugin::toggleShowConfirmation);
0089     //
0090     connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &CloseExceptPluginView::viewCreated);
0091     // Fill menu w/ currently opened document masks/groups
0092     updateMenu();
0093 
0094     m_mainWindow->guiFactory()->addClient(this);
0095 }
0096 
0097 CloseExceptPluginView::~CloseExceptPluginView()
0098 {
0099     m_mainWindow->guiFactory()->removeClient(this);
0100 }
0101 
0102 void CloseExceptPluginView::viewCreated(KTextEditor::View *view)
0103 {
0104     connectToDocument(view->document());
0105     updateMenu();
0106 }
0107 
0108 void CloseExceptPluginView::documentCreated(KTextEditor::Editor *, KTextEditor::Document *document)
0109 {
0110     connectToDocument(document);
0111     updateMenu();
0112 }
0113 
0114 void CloseExceptPluginView::connectToDocument(KTextEditor::Document *document)
0115 {
0116     // Subscribe self to document close and name changes
0117     connect(document, &KTextEditor::Document::aboutToClose, this, &CloseExceptPluginView::updateMenuSlotStub);
0118     connect(document, &KTextEditor::Document::documentNameChanged, this, &CloseExceptPluginView::updateMenuSlotStub);
0119     connect(document, &KTextEditor::Document::documentUrlChanged, this, &CloseExceptPluginView::updateMenuSlotStub);
0120 }
0121 
0122 void CloseExceptPluginView::updateMenuSlotStub(KTextEditor::Document *)
0123 {
0124     updateMenu();
0125 }
0126 
0127 void CloseExceptPluginView::appendActionsFrom(const std::set<QUrl> &paths, actions_map_type &actions, KActionMenu *menu, CloseFunction closeFunction)
0128 {
0129     for (const QUrl &path : paths) {
0130         QString action = path.path() + QLatin1Char('*');
0131         actions[action] = QPointer<QAction>(new QAction(action, menu));
0132         menu->addAction(actions[action]);
0133         connect(actions[action].data(), &QAction::triggered, this, [this, closeFunction, action]() {
0134             (this->*closeFunction)(action);
0135         });
0136     }
0137 }
0138 
0139 void CloseExceptPluginView::appendActionsFrom(const std::set<QString> &masks, actions_map_type &actions, KActionMenu *menu, CloseFunction closeFunction)
0140 {
0141     for (const QString &mask : masks) {
0142         QString action = mask.startsWith(QLatin1Char('*')) ? mask : mask + QLatin1Char('*');
0143         actions[action] = QPointer<QAction>(new QAction(action, menu));
0144         menu->addAction(actions[action]);
0145         connect(actions[action].data(), &QAction::triggered, this, [this, closeFunction, action]() {
0146             (this->*closeFunction)(action);
0147         });
0148     }
0149 }
0150 
0151 void CloseExceptPluginView::updateMenu(const std::set<QUrl> &paths,
0152                                        const std::set<QString> &masks,
0153                                        actions_map_type &actions,
0154                                        KActionMenu *menu,
0155                                        CloseFunction closeFunction)
0156 {
0157     // turn menu ON or OFF depending on collected results
0158     menu->setEnabled(!paths.empty());
0159 
0160     // Clear previous menus
0161     for (const auto &[_, action] : actions) {
0162         menu->removeAction(action);
0163     }
0164     actions.clear();
0165 
0166     // Form a new one
0167     appendActionsFrom(paths, actions, menu, closeFunction);
0168     if (!masks.empty()) {
0169         if (!paths.empty()) {
0170             menu->addSeparator(); // Add separator between paths and file's ext filters
0171         }
0172         appendActionsFrom(masks, actions, menu, closeFunction);
0173     }
0174     // Append 'Show Confirmation' toggle menu item
0175     menu->addSeparator(); // Add separator between paths and show confirmation
0176     menu->addAction(m_show_confirmation_action);
0177 }
0178 
0179 void CloseExceptPluginView::updateMenu()
0180 {
0181     const QList<KTextEditor::Document *> &docs = KTextEditor::Editor::instance()->application()->documents();
0182     if (docs.size() < 2) {
0183         // qDebug() << "No docs r (or the only) opened right now --> disable menu";
0184         m_except_menu->setEnabled(false);
0185         m_except_menu->addSeparator();
0186         m_like_menu->setEnabled(false);
0187         m_like_menu->addSeparator();
0188         /// \note It seems there is always a document present... it named \em 'Untitled'
0189     } else {
0190         // Iterate over documents and form a set of candidates
0191         typedef std::set<QUrl> paths_set_type;
0192         typedef std::set<QString> paths_set_type_masks;
0193         paths_set_type doc_paths;
0194         paths_set_type_masks masks;
0195         for (KTextEditor::Document *document : docs) {
0196             const QString &ext = QFileInfo(document->url().path()).completeSuffix();
0197             if (!ext.isEmpty()) {
0198                 masks.insert(QStringLiteral("*.") + ext);
0199             }
0200             doc_paths.insert(KIO::upUrl(document->url()));
0201         }
0202         paths_set_type paths = doc_paths;
0203         // qDebug() << "stage #1: Collected" << paths.size() << "paths and" << masks.size() << "masks";
0204         // Add common paths to the collection
0205         for (paths_set_type::iterator it = doc_paths.begin(), last = doc_paths.end(); it != last; ++it) {
0206             for (QUrl url = *it; (!url.path().isEmpty()) && url.path() != QLatin1String("/"); url = KIO::upUrl(url)) {
0207                 paths_set_type::iterator not_it = it;
0208                 for (++not_it; not_it != last; ++not_it) {
0209                     if (!not_it->path().startsWith(url.path())) {
0210                         break;
0211                     }
0212                 }
0213                 if (not_it == last) {
0214                     paths.insert(url);
0215                     break;
0216                 }
0217             }
0218         }
0219         // qDebug() << "stage #2: Collected" << paths.size() << "paths and" << masks.size() << "masks";
0220         //
0221         updateMenu(paths, masks, m_except_actions, m_except_menu, &CloseExceptPluginView::closeExcept);
0222         updateMenu(paths, masks, m_like_actions, m_like_menu, &CloseExceptPluginView::closeLike);
0223     }
0224 }
0225 
0226 void CloseExceptPluginView::close(const QString &item, const bool close_if_match)
0227 {
0228     QChar asterisk = QLatin1Char('*');
0229     assert("Parameter seems invalid! Is smth has changed in the code?" && !item.isEmpty() && (item[0] == asterisk || item[item.size() - 1] == asterisk));
0230 
0231     const bool is_path = item[0] != asterisk;
0232     const QString mask = is_path ? item.left(item.size() - 1) : item;
0233     // qDebug() << "Going to close items [" << close_if_match << "/" << is_path << "]: " << mask;
0234 
0235     QList<KTextEditor::Document *> docs2close;
0236     const QList<KTextEditor::Document *> &docs = KTextEditor::Editor::instance()->application()->documents();
0237     for (KTextEditor::Document *document : docs) {
0238         const QString &path = KIO::upUrl(document->url()).path();
0239         /// \note Take a dot in account, so \c *.c would not match for \c blah.kcfgc
0240         const QString &ext = QLatin1Char('.') + QFileInfo(document->url().fileName()).completeSuffix();
0241         const bool match = (!is_path && mask.endsWith(ext)) || (is_path && path.startsWith(mask));
0242         if (match == close_if_match) {
0243             // qDebug() << "*** Will close: " << document->url();
0244             docs2close.push_back(document);
0245         }
0246     }
0247     if (docs2close.isEmpty()) {
0248         displayMessage(i18nc("@title:window", "Error"), i18nc("@info:tooltip", "No files to close ..."), KTextEditor::Message::Error);
0249         return;
0250     }
0251     // Show confirmation dialog if needed
0252     const bool removeNeeded =
0253         !m_plugin->showConfirmationNeeded() || CloseConfirmDialog(docs2close, m_show_confirmation_action, qobject_cast<QWidget *>(this)).exec();
0254     if (removeNeeded) {
0255         if (docs2close.isEmpty()) {
0256             displayMessage(i18nc("@title:window", "Error"), i18nc("@info:tooltip", "No files to close ..."), KTextEditor::Message::Error);
0257         } else {
0258             // Close 'em all!
0259             KTextEditor::Editor::instance()->application()->closeDocuments(docs2close);
0260             updateMenu();
0261             displayMessage(i18nc("@title:window", "Done"), i18np("%1 file closed", "%1 files closed", docs2close.size()), KTextEditor::Message::Positive);
0262         }
0263     }
0264 }
0265 
0266 void CloseExceptPluginView::displayMessage(const QString &title, const QString &msg, KTextEditor::Message::MessageType level)
0267 {
0268     KTextEditor::View *kv = m_mainWindow->activeView();
0269     if (!kv) {
0270         return;
0271     }
0272 
0273     delete m_infoMessage;
0274     m_infoMessage = new KTextEditor::Message(xi18nc("@info", "<title>%1</title><nl/>%2", title, msg), level);
0275     m_infoMessage->setWordWrap(true);
0276     m_infoMessage->setPosition(KTextEditor::Message::TopInView);
0277     m_infoMessage->setAutoHide(5000);
0278     m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
0279     m_infoMessage->setView(kv);
0280     kv->document()->postMessage(m_infoMessage);
0281 }
0282 
0283 // END CloseExceptPluginView
0284 } // namespace kate
0285 
0286 #include "close_except_plugin.moc"
0287 #include "moc_close_except_plugin.cpp"
0288 
0289 // kate: hl C++11/Qt4;