1/**
2 * \file
3 *
4 * \brief Kate Close Except/Like plugin implementation
5 *
6 * Copyright (C) 2012 Alex Turbov <i.zaufi@gmail.com>
7 *
8 * \date Thu Mar 8 08:13:43 MSK 2012 -- Initial design
9 */
10/*
11 * KateCloseExceptPlugin is free software: you can redistribute it and/or modify it
12 * under the terms of the GNU General Public License as published by the
13 * Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * KateCloseExceptPlugin is distributed in the hope that it will be useful, but
17 * WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19 * See the GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License along
22 * with this program. If not, see <http://www.gnu.org/licenses/>.
23 */
24
25// Project specific includes
26#include "config.h"
27#include "close_except_plugin.h"
28#include "close_confirm_dialog.h"
29
30// Standard includes
31#include <kate/application.h>
32#include <kate/documentmanager.h>
33#include <kate/mainwindow.h>
34#include <KAboutData>
35#include <KActionCollection>
36#include <KDebug>
37#include <KPassivePopup>
38#include <KPluginFactory>
39#include <KPluginLoader>
40#include <KTextEditor/Editor>
41#include <QtCore/QFileInfo>
42
43K_PLUGIN_FACTORY(CloseExceptPluginFactory, registerPlugin<kate::CloseExceptPlugin>();)
44K_EXPORT_PLUGIN(
45 CloseExceptPluginFactory(
46 KAboutData(
47 "katecloseexceptplugin"
48 , "katecloseexceptplugin"
49 , ki18n("Close Except/Like Plugin")
50 , PLUGIN_VERSION
51 , ki18n("Close all documents started from specified path")
52 , KAboutData::License_LGPL_V3
53 )
54 )
55 )
56
57namespace kate {
58//BEGIN CloseExceptPlugin
59CloseExceptPlugin::CloseExceptPlugin(
60 QObject* application
61 , const QList<QVariant>&
62 )
63 : Kate::Plugin(static_cast<Kate::Application*>(application), "katecloseexceptplugin")
64{
65}
66
67Kate::PluginView* CloseExceptPlugin::createView(Kate::MainWindow* parent)
68{
69 return new CloseExceptPluginView(parent, CloseExceptPluginFactory::componentData(), this);
70}
71
72void CloseExceptPlugin::readSessionConfig(KConfigBase* config, const QString& groupPrefix)
73{
74 KConfigGroup scg(config, groupPrefix + "menu");
75 m_show_confirmation_needed = scg.readEntry("ShowConfirmation", true);
76}
77
78void CloseExceptPlugin::writeSessionConfig(KConfigBase* config, const QString& groupPrefix)
79{
80 KConfigGroup scg(config, groupPrefix + "menu");
81 scg.writeEntry("ShowConfirmation", m_show_confirmation_needed);
82 scg.sync();
83}
84//END CloseExceptPlugin
85
86//BEGIN CloseExceptPluginView
87CloseExceptPluginView::CloseExceptPluginView(
88 Kate::MainWindow* mw
89 , const KComponentData& data
90 , CloseExceptPlugin* plugin
91 )
92 : Kate::PluginView(mw)
93 , Kate::XMLGUIClient(data)
94 , m_plugin(plugin)
95 , m_show_confirmation_action(new KToggleAction(i18nc("@action:inmenu", "Show Confirmation"), this))
96 , m_except_menu(new KActionMenu(
97 i18nc("@action:inmenu close docs except the following...", "Close Except")
98 , this
99 ))
100 , m_like_menu(new KActionMenu(
101 i18nc("@action:inmenu close docs like the following...", "Close Like")
102 , this
103 ))
104{
105 actionCollection()->addAction("file_close_except", m_except_menu);
106 actionCollection()->addAction("file_close_like", m_like_menu);
107
108 // Subscribe self to document creation
109 connect(
110 m_plugin->application()->editor()
111 , SIGNAL(documentCreated(KTextEditor::Editor*, KTextEditor::Document*))
112 , this
113 , SLOT(documentCreated(KTextEditor::Editor*, KTextEditor::Document*))
114 );
115 // Configure toggle action and connect it to update state
116 m_show_confirmation_action->setChecked(m_plugin->showConfirmationNeeded());
117 connect(
118 m_show_confirmation_action
119 , SIGNAL(toggled(bool))
120 , m_plugin
121 , SLOT(toggleShowConfirmation(bool))
122 );
123 //
124 connect(
125 mainWindow()
126 , SIGNAL(viewCreated(KTextEditor::View*))
127 , this
128 , SLOT(viewCreated(KTextEditor::View*))
129 );
130 // Fill menu w/ currently opened document masks/groups
131 updateMenu();
132
133 mainWindow()->guiFactory()->addClient(this);
134}
135
136CloseExceptPluginView::~CloseExceptPluginView()
137{
138 mainWindow()->guiFactory()->removeClient(this);
139}
140
141void CloseExceptPluginView::viewCreated(KTextEditor::View* view)
142{
143 connectToDocument(view->document());
144 updateMenu();
145}
146
147void CloseExceptPluginView::documentCreated(KTextEditor::Editor*, KTextEditor::Document* document)
148{
149 connectToDocument(document);
150 updateMenu();
151}
152
153void CloseExceptPluginView::connectToDocument(KTextEditor::Document* document)
154{
155 // Subscribe self to document close and name changes
156 connect(
157 document
158 , SIGNAL(aboutToClose(KTextEditor::Document*))
159 , this
160 , SLOT(updateMenuSlotStub(KTextEditor::Document*))
161 );
162 connect(
163 document
164 , SIGNAL(documentNameChanged(KTextEditor::Document*))
165 , this
166 , SLOT(updateMenuSlotStub(KTextEditor::Document*))
167 );
168 connect(
169 document
170 , SIGNAL(documentUrlChanged(KTextEditor::Document*))
171 , this
172 , SLOT(updateMenuSlotStub(KTextEditor::Document*))
173 );
174}
175
176void CloseExceptPluginView::updateMenuSlotStub(KTextEditor::Document*)
177{
178 updateMenu();
179}
180
181void CloseExceptPluginView::appendActionsFrom(
182 const std::set<QString>& paths
183 , actions_map_type& actions
184 , KActionMenu* menu
185 , QSignalMapper* mapper
186 )
187{
188 Q_FOREACH(const QString& path, paths)
189 {
190 QString action = path.startsWith('*') ? path : path + '*';
191 actions[action] = QPointer<KAction>(new KAction(action, menu));
192 menu->addAction(actions[action]);
193 connect(actions[action], SIGNAL(triggered()), mapper, SLOT(map()));
194 mapper->setMapping(actions[action], action);
195 }
196}
197
198QPointer<QSignalMapper> CloseExceptPluginView::updateMenu(
199 const std::set<QString>& paths
200 , const std::set<QString>& masks
201 , actions_map_type& actions
202 , KActionMenu* menu
203 )
204{
205 // turn menu ON or OFF depending on collected results
206 menu->setEnabled(!paths.empty());
207
208 // Clear previous menus
209 for (actions_map_type::iterator it = actions.begin(), last = actions.end(); it !=last;)
210 {
211 menu->removeAction(*it);
212 actions.erase(it++);
213 }
214 // Form a new one
215 QPointer<QSignalMapper> mapper = QPointer<QSignalMapper>(new QSignalMapper(this));
216 appendActionsFrom(paths, actions, menu, mapper);
217 if (!masks.empty())
218 {
219 if (!paths.empty())
220 menu->addSeparator(); // Add separator between paths and file's ext filters
221 appendActionsFrom(masks, actions, menu, mapper);
222 }
223 // Append 'Show Confirmation' toggle menu item
224 menu->addSeparator(); // Add separator between paths and show confirmation
225 menu->addAction(m_show_confirmation_action);
226 return mapper;
227}
228
229void CloseExceptPluginView::updateMenu()
230{
231 const QList<KTextEditor::Document*>& docs = m_plugin->application()->documentManager()->documents();
232 if (docs.size() < 2)
233 {
234 kDebug() << "No docs r (or the only) opened right now --> disable menu";
235 m_except_menu->setEnabled(false);
236 m_except_menu->addSeparator();
237 m_like_menu->setEnabled(false);
238 m_like_menu->addSeparator();
239 /// \note It seems there is always a document present... it named \em 'Untitled'
240 }
241 else
242 {
243 // Iterate over documents and form a set of candidates
244 typedef std::set<QString> paths_set_type;
245 paths_set_type doc_paths;
246 paths_set_type masks;
247 Q_FOREACH(KTextEditor::Document* document, docs)
248 {
249 const QString& ext = QFileInfo(document->url().path()).completeSuffix();
250 if (!ext.isEmpty())
251 masks.insert("*." + ext);
252 doc_paths.insert(document->url().upUrl().path());
253 }
254 paths_set_type paths = doc_paths;
255 kDebug() << "stage #1: Collected" << paths.size() << "paths and" << masks.size() << "masks";
256 // Add common paths to the collection
257 for (paths_set_type::iterator it = doc_paths.begin(), last = doc_paths.end(); it != last; ++it)
258 {
259 for (
260 KUrl url = *it
261 ; url.hasPath() && url.path() != "/"
262 ; url = url.upUrl()
263 )
264 {
265 paths_set_type::iterator not_it = it;
266 for (++not_it; not_it != last; ++not_it)
267 if (!not_it->startsWith(url.path()))
268 break;
269 if (not_it == last)
270 {
271 paths.insert(url.path());
272 break;
273 }
274 }
275 }
276 kDebug() << "stage #2: Collected" << paths.size() << "paths and" << masks.size() << "masks";
277 //
278 m_except_mapper = updateMenu(paths, masks, m_except_actions, m_except_menu);
279 m_like_mapper = updateMenu(paths, masks, m_like_actions, m_like_menu);
280 connect(m_except_mapper, SIGNAL(mapped(const QString&)), this, SLOT(closeExcept(const QString&)));
281 connect(m_like_mapper, SIGNAL(mapped(const QString&)), this, SLOT(closeLike(const QString&)));
282 }
283}
284
285void CloseExceptPluginView::close(const QString& item, const bool close_if_match)
286{
287 assert(
288 "Parameter seems invalid! Is smth has changed in the code?"
289 && !item.isEmpty() && (item[0] == '*' || item[item.size() - 1] == '*')
290 );
291
292 const bool is_path = item[0] != '*';
293 const QString mask = is_path ? item.left(item.size() - 1) : item;
294 kDebug() << "Going to close items [" << close_if_match << "/" << is_path << "]: " << mask;
295
296 QList<KTextEditor::Document*> docs2close;
297 const QList<KTextEditor::Document*>& docs = m_plugin->application()->documentManager()->documents();
298 Q_FOREACH(KTextEditor::Document* document, docs)
299 {
300 const QString& path = document->url().upUrl().path();
301 /// \note Take a dot in account, so \c *.c would not match for \c blah.kcfgc
302 const QString& ext = '.' + QFileInfo(document->url().fileName()).completeSuffix();
303 const bool match = (!is_path && mask.endsWith(ext))
304 || (is_path && path.startsWith(mask))
305 ;
306 if (match == close_if_match)
307 {
308 kDebug() << "*** Will close: " << document->url();
309 docs2close.push_back(document);
310 }
311 }
312 if (docs2close.isEmpty())
313 {
314 KPassivePopup::message(
315 i18nc("@title:window", "Error")
316 , i18nc("@info:tooltip", "No files to close ...")
317 , qobject_cast<QWidget*>(this)
318 );
319 return;
320 }
321 // Show confirmation dialog if needed
322 const bool removeNeeded = !m_plugin->showConfirmationNeeded()
323 || CloseConfirmDialog(docs2close, m_show_confirmation_action, qobject_cast<QWidget*>(this)).exec();
324 if (removeNeeded)
325 {
326 if (docs2close.isEmpty())
327 {
328 KPassivePopup::message(
329 i18nc("@title:window", "Error")
330 , i18nc("@info:tooltip", "No files to close ...")
331 , qobject_cast<QWidget*>(this)
332 );
333 }
334 else
335 {
336 // Close 'em all!
337 m_plugin->application()->documentManager()->closeDocumentList(docs2close);
338 updateMenu();
339 KPassivePopup::message(
340 i18nc("@title:window", "Done")
341 , i18np("%1 file closed", "%1 files closed", docs2close.size())
342 , qobject_cast<QWidget*>(this)
343 );
344 }
345 }
346}
347//END CloseExceptPluginView
348} // namespace kate
349