1 #include "mainwindow.h"
2 
3 #include <QCloseEvent>
4 #include <QLayout>
5 #include <QSettings>
6 #include <QToolBar>
7 
8 namespace NeovimQt {
9 
MainWindow(NeovimConnector * c,ShellOptions opts,QWidget * parent)10 MainWindow::MainWindow(NeovimConnector *c, ShellOptions opts, QWidget *parent)
11 :QMainWindow(parent), m_nvim(0), m_errorWidget(0), m_shell(0),
12 	m_delayedShow(DelayedShow::Disabled), m_tabline(0), m_tabline_bar(0),
13 	m_shell_options(opts), m_neovim_requested_close(false)
14 {
15 	m_errorWidget = new ErrorWidget();
16 	m_stack.addWidget(m_errorWidget);
17 	connect(m_errorWidget, &ErrorWidget::reconnectNeovim,
18 			this, &MainWindow::reconnectNeovim);
19 	setCentralWidget(&m_stack);
20 
21 	init(c);
22 }
23 
init(NeovimConnector * c)24 void MainWindow::init(NeovimConnector *c)
25 {
26 	if (m_shell) {
27 		m_shell->deleteLater();
28 		m_stack.removeWidget(m_shell);
29 	}
30 	if (m_nvim) {
31 		m_nvim->deleteLater();
32 	}
33 
34 	m_tabline_bar = addToolBar("tabline");
35 	m_tabline_bar->setObjectName("tabline");
36 	m_tabline_bar->setAllowedAreas(Qt::TopToolBarArea);
37 	m_tabline_bar->setMovable(false);
38 	m_tabline_bar->setFloatable(false);
39 	// Avoid margins around the tabbar
40 	m_tabline_bar->layout()->setContentsMargins(0, 0, 0, 0);
41 
42 	m_tabline = new QTabBar(m_tabline_bar);
43 	m_tabline->setDrawBase(false);
44 	m_tabline->setExpanding(false);
45 	m_tabline->setDocumentMode(true);
46 	m_tabline->setFocusPolicy(Qt::NoFocus);
47 	connect(m_tabline, &QTabBar::currentChanged,
48 			this, &MainWindow::changeTab);
49 
50 	m_tabline_bar->addWidget(m_tabline);
51 	m_tabline_bar->setVisible(m_shell_options.enable_ext_tabline);
52 
53 	// Context menu and actions for right-click
54 	m_contextMenu = new QMenu();
55 	m_actCut = new QAction(QIcon::fromTheme("edit-cut"), QString("Cut"), nullptr /*parent*/);
56 	m_actCopy = new QAction(QIcon::fromTheme("edit-copy"), QString("Copy"), nullptr /*parent*/);
57 	m_actPaste = new QAction(QIcon::fromTheme("edit-paste"), QString("Paste"), nullptr /*parent*/);
58 	m_actSelectAll = new QAction(QIcon::fromTheme("edit-select-all"), QString("Select All"),
59 		nullptr /*parent*/);
60 	m_contextMenu->addAction(m_actCut);
61 	m_contextMenu->addAction(m_actCopy);
62 	m_contextMenu->addAction(m_actPaste);
63 	m_contextMenu->addSeparator();
64 	m_contextMenu->addAction(m_actSelectAll);
65 
66 	m_nvim = c;
67 
68 	m_tree = new TreeView(c);
69 	m_shell = new Shell(c, m_shell_options);
70 
71 	// GuiScrollBar
72 	m_scrollbar = new ScrollBar{ m_nvim };
73 
74 	// ShellWidget + GuiScrollBar Layout
75 	// QSplitter does not allow layouts directly: QWidget { HLayout { ShellWidget, QScrollBar } }
76 	QWidget* shellScrollable{ new QWidget() };
77 	QHBoxLayout* layout{ new QHBoxLayout() };
78 	layout->setSpacing(0);
79 	layout->setMargin(0);
80 	layout->addWidget(m_shell);
81 	layout->addWidget(m_scrollbar);
82 	shellScrollable->setLayout(layout);
83 
84 	m_window = new QSplitter();
85 	m_window->addWidget(m_tree);
86 	m_tree->hide();
87 	m_window->addWidget(shellScrollable);
88 
89 	m_stack.insertWidget(1, m_window);
90 	m_stack.setCurrentIndex(1);
91 
92 	connect(m_shell, SIGNAL(neovimAttached(bool)),
93 			this, SLOT(neovimAttachmentChanged(bool)));
94 	connect(m_shell, SIGNAL(neovimTitleChanged(const QString &)),
95 			this, SLOT(neovimSetTitle(const QString &)));
96 	connect(m_shell, &Shell::neovimResized,
97 			this, &MainWindow::neovimWidgetResized);
98 	connect(m_shell, &Shell::neovimMaximized,
99 			this, &MainWindow::neovimMaximized);
100 	connect(m_shell, &Shell::neovimSuspend,
101 			this, &MainWindow::neovimSuspend);
102 	connect(m_shell, &Shell::neovimFullScreen,
103 			this, &MainWindow::neovimFullScreen);
104 	connect(m_shell, &Shell::neovimGuiCloseRequest,
105 			this, &MainWindow::neovimGuiCloseRequest);
106 	connect(m_nvim, &NeovimConnector::processExited,
107 			this, &MainWindow::neovimExited);
108 	connect(m_nvim, &NeovimConnector::error,
109 			this, &MainWindow::neovimError);
110 	connect(m_shell, &Shell::neovimIsUnsupported,
111 			this, &MainWindow::neovimIsUnsupported);
112 	connect(m_shell, &Shell::neovimExtTablineSet,
113 			this, &MainWindow::extTablineSet);
114 	connect(m_shell, &Shell::neovimTablineUpdate,
115 			this, &MainWindow::neovimTablineUpdate);
116 	connect(m_shell, &Shell::neovimShowtablineSet,
117 			this, &MainWindow::neovimShowtablineSet);
118 	connect(m_shell, &Shell::neovimShowContextMenu,
119 			this, &MainWindow::neovimShowContextMenu);
120 	connect(m_actCut, &QAction::triggered,
121 			this, &MainWindow::neovimSendCut);
122 	connect(m_actCopy, &QAction::triggered,
123 			this, &MainWindow::neovimSendCopy);
124 	connect(m_actPaste, &QAction::triggered,
125 			this, &MainWindow::neovimSendPaste);
126 	connect(m_actSelectAll, &QAction::triggered,
127 			this, &MainWindow::neovimSendSelectAll);
128 	m_shell->setFocus(Qt::OtherFocusReason);
129 
130 	if (m_nvim->errorCause()) {
131 		neovimError(m_nvim->errorCause());
132 	}
133 }
134 
neovimAttached() const135 bool MainWindow::neovimAttached() const
136 {
137 	return (m_shell != NULL && m_shell->neovimAttached());
138 }
139 
140 /** The Neovim process has exited */
neovimExited(int status)141 void MainWindow::neovimExited(int status)
142 {
143 	showIfDelayed();
144 
145 	if (m_nvim->errorCause() != NeovimConnector::NoError) {
146 		m_errorWidget->setText(m_nvim->errorString());
147 		m_errorWidget->showReconnect(m_nvim->canReconnect());
148 		m_stack.setCurrentIndex(0);
149 	} else if (status != 0) {
150 		m_errorWidget->setText(QString("Neovim exited with status code (%1)").arg(status));
151 		m_errorWidget->showReconnect(m_nvim->canReconnect());
152 		m_stack.setCurrentIndex(0);
153 	} else {
154 		close();
155 	}
156 }
neovimError(NeovimConnector::NeovimError err)157 void MainWindow::neovimError(NeovimConnector::NeovimError err)
158 {
159 	showIfDelayed();
160 
161 	switch(err) {
162 	case NeovimConnector::FailedToStart:
163 		m_errorWidget->setText("Unable to start nvim: " + m_nvim->errorString());
164 		break;
165 	default:
166 		m_errorWidget->setText(m_nvim->errorString());
167 	}
168 	m_errorWidget->showReconnect(m_nvim->canReconnect());
169 	m_stack.setCurrentIndex(0);
170 }
neovimIsUnsupported()171 void MainWindow::neovimIsUnsupported()
172 {
173 	showIfDelayed();
174 	m_errorWidget->setText(QString("Cannot connect to this Neovim, required API version 1, found [%1-%2]")
175 			.arg(m_nvim->apiCompatibility())
176 			.arg(m_nvim->apiLevel()));
177 	m_errorWidget->showReconnect(m_nvim->canReconnect());
178 	m_stack.setCurrentIndex(0);
179 }
180 
neovimSetTitle(const QString & title)181 void MainWindow::neovimSetTitle(const QString &title)
182 {
183 	this->setWindowTitle(title);
184 }
185 
neovimWidgetResized()186 void MainWindow::neovimWidgetResized()
187 {
188 	m_shell->resizeNeovim(m_shell->size());
189 }
190 
neovimMaximized(bool set)191 void MainWindow::neovimMaximized(bool set)
192 {
193 	if (set) {
194 		setWindowState(windowState() | Qt::WindowMaximized);
195 	} else {
196 		setWindowState(windowState() & ~Qt::WindowMaximized);
197 	}
198 }
199 
neovimSuspend()200 void MainWindow::neovimSuspend()
201 {
202 	qDebug() << "Minimizing window";
203 	setWindowState(windowState() | Qt::WindowMinimized);
204 }
205 
neovimFullScreen(bool set)206 void MainWindow::neovimFullScreen(bool set)
207 {
208 	if (set) {
209 		setWindowState(windowState() | Qt::WindowFullScreen);
210 	} else {
211 		setWindowState(windowState() & ~Qt::WindowFullScreen);
212 	}
213 }
214 
neovimGuiCloseRequest()215 void MainWindow::neovimGuiCloseRequest()
216 {
217 	m_neovim_requested_close = true;
218 	QMainWindow::close();
219 	m_neovim_requested_close = false;
220 }
221 
reconnectNeovim()222 void MainWindow::reconnectNeovim()
223 {
224 	if (m_nvim->canReconnect()) {
225 		init(m_nvim->reconnect());
226 	}
227 	m_stack.setCurrentIndex(1);
228 }
229 
closeEvent(QCloseEvent * ev)230 void MainWindow::closeEvent(QCloseEvent *ev)
231 {
232 	// Do not save window geometry in '--fullscreen' mode. If saved, all
233 	// subsequent Neovim-Qt sessions would default to fullscreen mode.
234 	if (!isFullScreen()) {
235 		saveWindowGeometry();
236 	}
237 
238 	if (m_neovim_requested_close) {
239 		// If this was requested by nvim, shutdown
240 		QWidget::closeEvent(ev);
241 	} else if (m_shell->close()) {
242 		// otherwise only if the Neovim shell closes too
243 		QWidget::closeEvent(ev);
244 	} else {
245 		ev->ignore();
246 	}
247 }
changeEvent(QEvent * ev)248 void MainWindow::changeEvent( QEvent *ev)
249 {
250 	if (ev->type() == QEvent::WindowStateChange && isWindow()) {
251 		m_shell->updateGuiWindowState(windowState());
252 	}
253 	QWidget::changeEvent(ev);
254 }
255 
256 /// Call show() after a 1s delay or when Neovim attachment
257 /// is complete, whichever comes first
delayedShow(DelayedShow type)258 void MainWindow::delayedShow(DelayedShow type)
259 {
260 	m_delayedShow = type;
261 	if (m_nvim->errorCause() != NeovimConnector::NoError) {
262 		showIfDelayed();
263 		return;
264 	}
265 
266 	if (type != DelayedShow::Disabled) {
267 		QTimer *t = new QTimer(this);
268 		t->setSingleShot(true);
269 		t->setInterval(1000);
270 		connect(m_shell, &Shell::neovimResized, this, &MainWindow::showIfDelayed);
271 		connect(t, &QTimer::timeout, this, &MainWindow::showIfDelayed);
272 		t->start();
273 	}
274 }
275 
showIfDelayed()276 void MainWindow::showIfDelayed()
277 {
278 	if (!isVisible()) {
279 		if (m_delayedShow == DelayedShow::Normal) {
280 			show();
281 		} else if (m_delayedShow == DelayedShow::Maximized) {
282 			showMaximized();
283 		} else if (m_delayedShow == DelayedShow::FullScreen) {
284 			showFullScreen();
285 		}
286 	}
287 	m_delayedShow = DelayedShow::Disabled;
288 }
289 
neovimAttachmentChanged(bool attached)290 void MainWindow::neovimAttachmentChanged(bool attached)
291 {
292 	emit neovimAttached(attached);
293 	if (attached) {
294 		if (isWindow() && m_shell != NULL) {
295 			m_shell->updateGuiWindowState(windowState());
296 		}
297 	} else {
298 		m_tabline->deleteLater();
299 		m_tabline_bar->deleteLater();
300 	}
301 }
302 
shell()303 Shell* MainWindow::shell()
304 {
305 	return m_shell;
306 }
307 
extTablineSet(bool val)308 void MainWindow::extTablineSet(bool val)
309 {
310 	bool old = m_shell_options.enable_ext_tabline;
311 	m_shell_options.enable_ext_tabline = val;
312 	// redraw if state changed
313 	if (old != m_shell_options.enable_ext_tabline) {
314 		if (!val) m_tabline_bar->setVisible(false);
315 		m_nvim->api0()->vim_command("silent! redraw!");
316 	}
317 }
318 
neovimShowtablineSet(int val)319 void MainWindow::neovimShowtablineSet(int val)
320 {
321 	m_shell_options.nvim_show_tabline = val;
322 }
323 
neovimTablineUpdate(int64_t curtab,QList<Tab> tabs)324 void MainWindow::neovimTablineUpdate(int64_t curtab, QList<Tab> tabs)
325 {
326 	if (!m_shell_options.enable_ext_tabline) {
327 		return;
328 	}
329 
330 	// remove extra tabs
331 	for (int index=tabs.size(); index<m_tabline->count(); index++) {
332 		m_tabline->removeTab(index);
333 	}
334 
335 	for (int index=0; index<tabs.size(); index++) {
336 		// Escape & in tab name otherwise it will be interpreted as
337 		// a keyboard shortcut (#357) - escaping is done using &&
338 		QString text = tabs[index].name;
339 		text.replace("&", "&&");
340 
341 		if (m_tabline->count() <= index) {
342 			m_tabline->addTab(text);
343 		} else {
344 			m_tabline->setTabText(index, text);
345 		}
346 
347 		m_tabline->setTabToolTip(index, text);
348 		m_tabline->setTabData(index, QVariant::fromValue(tabs[index].tab));
349 
350 		if (curtab == tabs[index].tab) {
351 			m_tabline->setCurrentIndex(index);
352 		}
353 	}
354 
355 	// hide/show the tabline toolbar
356 	if (m_shell_options.nvim_show_tabline==0) {
357 		m_tabline_bar->setVisible(false);
358 	} else if (m_shell_options.nvim_show_tabline==2) {
359 		m_tabline_bar->setVisible(true);
360 	} else {
361 		m_tabline_bar->setVisible(tabs.size() > 1);
362 	}
363 
364 	Q_ASSERT(tabs.size() == m_tabline->count());
365 }
366 
neovimShowContextMenu()367 void MainWindow::neovimShowContextMenu()
368 {
369 	m_contextMenu->popup(QCursor::pos());
370 }
371 
neovimSendCut()372 void MainWindow::neovimSendCut()
373 {
374 	m_nvim->api0()->vim_command_output(R"(normal! "+x)");
375 }
376 
377 void MainWindow::neovimSendCopy()
378 {
379 	m_nvim->api0()->vim_command(R"(normal! "+y)");
380 }
381 
neovimSendPaste()382 void MainWindow::neovimSendPaste()
383 {
384 	m_nvim->api0()->vim_command(R"(normal! "+gP)");
385 }
386 
387 void MainWindow::neovimSendSelectAll()
388 {
389 	m_nvim->api0()->vim_command("normal! ggVG");
390 }
391 
392 void MainWindow::changeTab(int index)
393 {
394 	if (!m_shell_options.enable_ext_tabline) {
395 		return;
396 	}
397 
398 	if (m_nvim->api2() == NULL) {
399 		return;
400 	}
401 
402 	int64_t tab = m_tabline->tabData(index).toInt();
403 	m_nvim->api2()->nvim_set_current_tabpage(tab);
404 }
405 
406 void MainWindow::saveWindowGeometry()
407 {
408 	QSettings settings{ "nvim-qt", "window-geometry" };
409 	settings.setValue("window_geometry", saveGeometry());
410 	settings.setValue("window_state", saveState());
411 }
412 
413 void MainWindow::restoreWindowGeometry()
414 {
415 	QSettings settings{ "nvim-qt", "window-geometry" };
416 	restoreGeometry(settings.value("window_geometry").toByteArray());
417 	restoreState(settings.value("window_state").toByteArray());
418 }
419 
420 }  // namespace NeovimQt
421