1 #include <iostream>
2 #include <QCoreApplication>
3 #include <QFileInfo>
4 #include <QAction>
5 #include <QFileDialog>
6 #include <csignal>
7 #include <cerrno>
8 #include <unistd.h>
9 #include "viewer.h"
10 #include "resourcemanager.h"
11 #include "canvas.h"
12 #include "search.h"
13 #include "config.h"
14 #include "layout/layout.h"
15 #include "beamerwindow.h"
16 #include "toc.h"
17 #include "splitter.h"
18 #include "util.h"
19 
20 using namespace std;
21 
22 
23 int Viewer::sig_fd[2];
24 
Viewer(const QString & file,QWidget * parent)25 Viewer::Viewer(const QString &file, QWidget *parent) :
26 		QWidget(parent),
27 		res(NULL),
28 		splitter(NULL),
29 		toc(NULL),
30 		canvas(NULL),
31 		search_bar(NULL),
32 		layout(NULL),
33 		sig_notifier(NULL),
34 		beamer(NULL),
35 		valid(true) {
36 	res = new ResourceManager(file, this);
37 	if (!res->is_valid()) {
38 		if (CFG::get_instance()->get_most_current_value("Settings/quit_on_init_fail").toBool()) {
39 			valid = false;
40 			return;
41 		}
42 	}
43 
44 	search_bar = new SearchBar(file, this, this);
45 	if (!search_bar->is_valid()) {
46 		if (CFG::get_instance()->get_most_current_value("Settings/quit_on_init_fail").toBool()) {
47 			valid = false;
48 			return;
49 		}
50 	}
51 
52 	// setup signal handling
53 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd) == -1) {
54 		cerr << "socketpair: " << strerror(errno) << endl;
55 		valid = false;
56 		return;
57 	}
58 	sig_notifier = new QSocketNotifier(sig_fd[1], QSocketNotifier::Read, this);
59 	connect(sig_notifier, SIGNAL(activated(int)), this, SLOT(signal_slot()),
60 			Qt::UniqueConnection);
61 
62 	struct sigaction usr;
63 	usr.sa_handler = Viewer::signal_handler;
64 	sigemptyset(&usr.sa_mask);
65 	usr.sa_flags = SA_RESTART;
66 
67 	if (sigaction(SIGUSR1, &usr, 0) > 0) {
68 		cerr << "sigaction: " << strerror(errno) << endl;
69 		valid = false;
70 		return;
71 	}
72 
73 	setup_keys(this);
74 	beamer = new BeamerWindow(this);
75 	setup_keys(beamer);
76 
77 	splitter = new Splitter(this);
78 	toc = new Toc(this, splitter);
79 
80 	canvas = new Canvas(this, splitter); // beamer must already exist
81 	if (!canvas->is_valid()) {
82 		valid = false;
83 		return;
84 	}
85 	res->connect_canvas();
86 	canvas->update_page_overlay(); // set initial position
87 
88 	splitter->addWidget(toc);
89 	splitter->addWidget(canvas);
90 	splitter->setSizes(QList<int>() << (width() / 4) << (width() * 3 / 4)); // TODO config option
91 
92 	toc->hide();
93 
94 	// load config options
95 	CFG *config = CFG::get_instance();
96 	smooth_scroll_delta = config->get_value("Settings/smooth_scroll_delta").toInt();
97 	screen_scroll_factor = config->get_value("Settings/screen_scroll_factor").toFloat();
98 
99 	layout = new QVBoxLayout();
100 	layout->setContentsMargins(0, 0, 0, 0);
101 	layout->setSpacing(0);
102 
103 	// initialize info bar
104 	QIcon::setThemeName(CFG::get_instance()->get_value("Settings/icon_theme").toString());
105 
106 	info_label_icon.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
107 	info_password.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
108 	info_widget.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
109 
110 	info_label_text.setWordWrap(true);
111 	info_label_text.setTextFormat(Qt::PlainText);
112 	info_password.setEchoMode(QLineEdit::Password);
113 
114 	info_layout.addWidget(&info_label_icon);
115 	info_layout.addWidget(&info_label_text);
116 	info_layout.addWidget(&info_password);
117 	info_widget.setLayout(&info_layout);
118 
119 	layout->addWidget(&info_widget);
120 	connect(&info_password, SIGNAL(returnPressed()), this, SLOT(reload()),
121 			Qt::UniqueConnection);
122 
123 	update_info_widget();
124 
125 	presenter_progress.setMinimum(1);
126  	presenter_progress.setMaximum(res->get_page_count());
127  	presenter_progress.setValue(canvas->get_layout()->get_page() + 1);
128 //	presenter_progress.setFormat("%v/%m");
129  	presenter_progress.setTextVisible(true);
130 
131 	layout->addWidget(splitter);
132 	layout->addWidget(&presenter_progress);
133 	layout->addWidget(search_bar);
134 	setLayout(layout);
135 
136 	QFileInfo info(res->get_file());
137 	setWindowTitle(QString::fromUtf8("%1 \u2014 katarakt").arg(info.fileName()));
138 	setMinimumSize(50, 50);
139 	resize(500, 500);
140 
141 	search_bar->hide();
142 	info_password.setFocus(Qt::OtherFocusReason); // only works if shown
143 
144 	// apply start options
145 	if (res->is_valid()) {
146 		canvas->get_layout()->scroll_page(config->get_tmp_value("start_page").toInt(), false);
147 	}
148 	if (CFG::get_instance()->get_tmp_value("fullscreen").toBool()) {
149 		toggle_fullscreen();
150 	}
151 
152 	// enable transparency, but only in the right places
153 	setAttribute(Qt::WA_TranslucentBackground);
154 	presenter_progress.setAutoFillBackground(true);
155 	search_bar->setAutoFillBackground(true);
156 	info_widget.setAutoFillBackground(true);
157 	toc->setAutoFillBackground(true);
158 	// the splitter handle has to be opaque, canvas has to be transparent
159 	// problem: either both are transparent or none is
160 	// these functions don't help, the splitter handle is still transparent (depending on theme)
161 //	splitter->handle(0)->setAttribute(Qt::WA_TranslucentBackground, false);
162 //	splitter->handle(0)->setBackgroundRole(QPalette::Window);
163 //	splitter->handle(0)->setAutoFillBackground(true);
164 }
165 
~Viewer()166 Viewer::~Viewer() {
167 	::close(sig_fd[0]);
168 	::close(sig_fd[1]);
169 	delete beamer;
170 	delete sig_notifier;
171 	delete layout;
172 	delete search_bar;
173 	delete canvas;
174 	delete toc;
175 	delete splitter;
176 	delete res;
177 }
178 
is_valid() const179 bool Viewer::is_valid() const {
180 	return valid;
181 }
182 
reload(bool clamp)183 void Viewer::reload(bool clamp) {
184 #ifdef DEBUG
185 	cerr << "reloading file " << res->get_file().toUtf8().constData() << endl;
186 #endif
187 
188 	res->load(res->get_file(), info_password.text().toLatin1());
189 
190 	search_bar->reset_search(); // TODO restart search if loading the same document?
191 	search_bar->load(res->get_file(), info_password.text().toLatin1());
192 
193 	update_info_widget();
194 
195 	toc->init();
196 	canvas->get_layout()->clear_selection();
197 	canvas->reload(clamp);
198 
199 	canvas->update_page_overlay();
200  	presenter_progress.setMaximum(res->get_page_count());
201 }
202 
open(QString new_file)203 void Viewer::open(QString new_file) {
204 	res->set_file(new_file);
205 	QFileInfo info(new_file);
206 
207 	// different file - clear jumplist
208 	// e.g. in inotify-caused reload it doesn't hurt to keep the old jumplist
209 	// search is always cleared, see reload()
210 	res->clear_jumps();
211 	// TODO reset rotation?
212 	setWindowTitle(QString::fromUtf8("%1 \u2014 katarakt").arg(info.fileName()));
213 	reload();
214 }
215 
open()216 void Viewer::open() {
217 	QString new_file = QFileDialog::getOpenFileName(this, QString::fromUtf8("Open File"), QString(), QString::fromUtf8("PDF Files (*.pdf)"));
218 	if (!new_file.isNull()) {
219 		open(new_file);
220 	}
221 }
222 
save()223 void Viewer::save() {
224 	QString new_file = QFileDialog::getSaveFileName(this, QString::fromUtf8("Save File"), QString(), QString::fromUtf8("PDF Files (*.pdf)"));
225 	if (!new_file.isNull()) {
226 		// file exists? Remove it (qt doesn't overwrite)
227 		QFile f(new_file);
228 		if (f.exists()) {
229 			QFileInfo source_file(res->get_file());
230 			QFileInfo dest_file(f);
231 			// source and destination the same?
232 			if (source_file == dest_file) {
233 #ifdef DEBUG
234 				cerr << "copy: src and dest file are the same" << endl;
235 #endif
236 				return;
237 			}
238 
239 			if (!f.remove()) {
240 #ifdef DEBUG
241 				cerr << "failed to remove the file" << endl;
242 #endif
243 			}
244 		}
245 		if (!QFile::copy(res->get_file(), new_file)) {
246 			// TODO error dialog?
247 #ifdef DEBUG
248 			cerr << "failed to copy the file" << endl;
249 #endif
250 		}
251 	}
252 }
253 
jump_back()254 void Viewer::jump_back() {
255 	int new_page = res->jump_back();
256 	if (new_page == -1) {
257 		return;
258 	}
259 	canvas->get_layout()->scroll_page(new_page, false);
260 }
261 
jump_forward()262 void Viewer::jump_forward() {
263 	int new_page = res->jump_forward();
264 	if (new_page == -1) {
265 		return;
266 	}
267 	canvas->get_layout()->scroll_page(new_page, false);
268 }
269 
mark_jump()270 void Viewer::mark_jump() {
271 	int page = canvas->get_layout()->get_page();
272 	res->store_jump(page);
273 }
274 
275 // general movement
page_up()276 void Viewer::page_up() {
277 	canvas->get_layout()->scroll_page(-1);
278 }
279 
page_down()280 void Viewer::page_down() {
281 	canvas->get_layout()->scroll_page(1);
282 }
283 
top()284 void Viewer::top() {
285 	canvas->get_layout()->scroll_page_jump(-1, false);
286 }
287 
bottom()288 void Viewer::bottom() {
289 	canvas->get_layout()->scroll_page_jump(res->get_page_count(), false);
290 }
291 
half_screen_up()292 void Viewer::half_screen_up() {
293 	if (canvas->get_layout()->supports_smooth_scrolling()) {
294 		canvas->get_layout()->scroll_smooth(0, height() * 0.5f);
295 	} else { // fallback
296 		page_up();
297 	}
298 }
299 
half_screen_down()300 void Viewer::half_screen_down() {
301 	if (canvas->get_layout()->supports_smooth_scrolling()) {
302 		canvas->get_layout()->scroll_smooth(0, -height() * 0.5f);
303 	} else { // fallback
304 		page_down();
305 	}
306 }
307 
screen_up()308 void Viewer::screen_up() {
309 	if (canvas->get_layout()->supports_smooth_scrolling()) {
310 		canvas->get_layout()->scroll_smooth(0, height() * screen_scroll_factor);
311 	} else { // fallback
312 		page_up();
313 	}
314 }
315 
screen_down()316 void Viewer::screen_down() {
317 	if (canvas->get_layout()->supports_smooth_scrolling()) {
318 		canvas->get_layout()->scroll_smooth(0, -height() * screen_scroll_factor);
319 	} else { // fallback
320 		page_down();
321 	}
322 }
323 
smooth_up()324 void Viewer::smooth_up() {
325 	if (canvas->get_layout()->supports_smooth_scrolling()) {
326 		canvas->get_layout()->scroll_smooth(0, smooth_scroll_delta);
327 	} else { // fallback
328 		page_up();
329 	}
330 }
331 
smooth_down()332 void Viewer::smooth_down() {
333 	if (canvas->get_layout()->supports_smooth_scrolling()) {
334 		canvas->get_layout()->scroll_smooth(0, -smooth_scroll_delta);
335 	} else { // fallback
336 		page_down();
337 	}
338 }
339 
smooth_left()340 void Viewer::smooth_left() {
341 	if (canvas->get_layout()->supports_smooth_scrolling()) {
342 		canvas->get_layout()->scroll_smooth(smooth_scroll_delta, 0);
343 	} else { // fallback
344 		page_up();
345 	}
346 }
347 
smooth_right()348 void Viewer::smooth_right() {
349 	if (canvas->get_layout()->supports_smooth_scrolling()) {
350 		canvas->get_layout()->scroll_smooth(-smooth_scroll_delta, 0);
351 	} else { // fallback
352 		page_down();
353 	}
354 }
355 
zoom_in()356 void Viewer::zoom_in() {
357 	canvas->get_layout()->set_zoom(1);
358 }
359 
zoom_out()360 void Viewer::zoom_out() {
361 	canvas->get_layout()->set_zoom(-1);
362 }
363 
reset_zoom()364 void Viewer::reset_zoom() {
365 	canvas->get_layout()->set_zoom(0, false);
366 }
367 
increase_columns()368 void Viewer::increase_columns() {
369 	canvas->get_layout()->set_columns(1);
370 }
371 
decrease_columns()372 void Viewer::decrease_columns() {
373 	canvas->get_layout()->set_columns(-1);
374 }
375 
increase_offset()376 void Viewer::increase_offset() {
377 	canvas->get_layout()->set_offset(1);
378 }
379 
decrease_offset()380 void Viewer::decrease_offset() {
381 	canvas->get_layout()->set_offset(-1);
382 }
383 
quit()384 void Viewer::quit() {
385 	QCoreApplication::exit(0);
386 }
387 
search()388 void Viewer::search() {
389 	search_bar->focus();
390 }
391 
search_backward()392 void Viewer::search_backward() {
393 	search_bar->focus(false);
394 }
395 
next_hit()396 void Viewer::next_hit() {
397 	if (canvas->get_layout()->get_search_visible()) {
398 		canvas->get_layout()->advance_hit();
399 	}
400 }
401 
previous_hit()402 void Viewer::previous_hit() {
403 	if (canvas->get_layout()->get_search_visible()) {
404 		canvas->get_layout()->advance_hit(false);
405 	}
406 }
407 
next_invisible_hit()408 void Viewer::next_invisible_hit() {
409 	if (canvas->get_layout()->get_search_visible()) {
410 		canvas->get_layout()->advance_invisible_hit();
411 	}
412 }
413 
previous_invisible_hit()414 void Viewer::previous_invisible_hit() {
415 	if (canvas->get_layout()->get_search_visible()) {
416 		canvas->get_layout()->advance_invisible_hit(false);
417 	}
418 }
419 
rotate_left()420 void Viewer::rotate_left() {
421 	res->rotate(-1);
422 	canvas->get_layout()->rebuild();
423 	canvas->update();
424 }
425 
rotate_right()426 void Viewer::rotate_right() {
427 	res->rotate(1);
428 	canvas->get_layout()->rebuild();
429 	canvas->update();
430 }
431 
invert_colors()432 void Viewer::invert_colors() {
433 	res->invert_colors();
434 	canvas->update();
435 	beamer->update();
436 }
437 
copy_to_clipboard()438 void Viewer::copy_to_clipboard() {
439 	canvas->get_layout()->copy_selection_text(QClipboard::Clipboard);
440 }
441 
toggle_toc()442 void Viewer::toggle_toc() {
443 	toc->setVisible(!toc->isVisible());
444 	toc->setFocus(Qt::OtherFocusReason); // only works if shown
445 }
446 
freeze_presentation()447 void Viewer::freeze_presentation() {
448 	if (beamer->is_frozen()) {
449 		beamer->freeze(false);
450 		beamer->get_layout()->scroll_page(canvas->get_layout()->get_page(), false);
451 	} else {
452 		beamer->freeze(true);
453 	}
454 }
455 
update_info_widget()456 void Viewer::update_info_widget() {
457 	if (!res->is_valid() || !search_bar->is_valid()) {
458 		QIcon icon;
459 		const QString file = res->get_file();
460 
461 		if (file.isEmpty()) {
462 			icon = QIcon::fromTheme(QString::fromUtf8("dialog-information"));
463 
464 			info_label_text.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
465 			info_label_text.setText(QString::fromUtf8("No file opened."));
466 			info_password.hide();
467 		} else if (!res->is_locked()) {
468 			icon = QIcon::fromTheme(QString::fromUtf8("dialog-error"));
469 
470 			info_label_text.setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
471 			info_label_text.setText(
472 				QString::fromUtf8("Failed to open file '") + file + QString::fromUtf8("'."));
473 
474 			info_password.hide();
475 		} else {
476 			icon = QIcon::fromTheme(QString::fromUtf8("dialog-password"));
477 
478 			info_label_text.setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
479 			info_label_text.setText(QString::fromUtf8("password:"));
480 
481 			info_password.show();
482 			info_password.setFocus(Qt::OtherFocusReason);
483 			info_password.clear();
484 		}
485 		info_label_icon.setPixmap(icon.pixmap(32, 32));
486 		info_widget.show();
487 	} else {
488 		info_widget.hide();
489 	}
490 }
491 
get_res() const492 ResourceManager *Viewer::get_res() const {
493 	return res;
494 }
495 
get_canvas() const496 Canvas *Viewer::get_canvas() const {
497 	return canvas;
498 }
499 
get_search_bar() const500 SearchBar *Viewer::get_search_bar() const {
501 	return search_bar;
502 }
503 
get_beamer() const504 BeamerWindow *Viewer::get_beamer() const {
505 	return beamer;
506 }
507 
layout_updated(int new_page,bool page_changed)508 void Viewer::layout_updated(int new_page, bool page_changed) {
509 	if (page_changed) {
510 		canvas->get_layout()->scroll_page(new_page, false);
511 		if (beamer->isVisible() && !beamer->is_frozen()) {
512 			beamer->get_layout()->scroll_page(new_page, false);
513 		}
514 		// TODO unfold toc tree to show current entry?
515 		canvas->update_page_overlay();
516 		presenter_progress.setValue(new_page + 1);
517 	}
518 	canvas->update();
519 	beamer->update();
520 }
521 
show_progress(bool show)522 void Viewer::show_progress(bool show) {
523 	presenter_progress.setVisible(show);
524 }
525 
signal_slot()526 void Viewer::signal_slot() {
527 	sig_notifier->setEnabled(false);
528 	char tmp;
529 	if (read(sig_fd[1], &tmp, sizeof(char)) < 0) {
530 		cerr << "read: " << strerror(errno) << endl;
531 	}
532 
533 	reload();
534 
535 	sig_notifier->setEnabled(true);
536 }
537 
signal_handler(int)538 void Viewer::signal_handler(int /*unused*/) {
539 	char tmp = '1';
540 	if (write(sig_fd[0], &tmp, sizeof(char)) < 0) {
541 		cerr << "write: " << strerror(errno) << endl;
542 	}
543 }
544 
toggle_fullscreen()545 void Viewer::toggle_fullscreen() {
546 	setWindowState(windowState() ^ Qt::WindowFullScreen);
547 }
548 
close_search()549 void Viewer::close_search() {
550 	canvas->setFocus(Qt::OtherFocusReason);
551 	search_bar->hide();
552 }
553 
setup_keys(QWidget * base)554 void Viewer::setup_keys(QWidget *base) {
555 	add_action(base, "Keys/page_up", SLOT(page_up()), this);
556 	add_action(base, "Keys/page_down", SLOT(page_down()), this);
557 	add_action(base, "Keys/top", SLOT(top()), this);
558 	add_action(base, "Keys/bottom", SLOT(bottom()), this);
559 	add_action(base, "Keys/half_screen_up", SLOT(half_screen_up()), this);
560 	add_action(base, "Keys/half_screen_down", SLOT(half_screen_down()), this);
561 	add_action(base, "Keys/screen_up", SLOT(screen_up()), this);
562 	add_action(base, "Keys/screen_down", SLOT(screen_down()), this);
563 	add_action(base, "Keys/smooth_up", SLOT(smooth_up()), this);
564 	add_action(base, "Keys/smooth_down", SLOT(smooth_down()), this);
565 	add_action(base, "Keys/smooth_left", SLOT(smooth_left()), this);
566 	add_action(base, "Keys/smooth_right", SLOT(smooth_right()), this);
567 	add_action(base, "Keys/next_hit", SLOT(next_hit()), this);
568 	add_action(base, "Keys/previous_hit", SLOT(previous_hit()), this);
569 	add_action(base, "Keys/next_invisible_hit", SLOT(next_invisible_hit()), this);
570 	add_action(base, "Keys/previous_invisible_hit", SLOT(previous_invisible_hit()), this);
571 	add_action(base, "Keys/jump_back", SLOT(jump_back()), this);
572 	add_action(base, "Keys/jump_forward", SLOT(jump_forward()), this);
573 
574 	add_action(base, "Keys/zoom_in", SLOT(zoom_in()), this);
575 	add_action(base, "Keys/zoom_out", SLOT(zoom_out()), this);
576 	add_action(base, "Keys/reset_zoom", SLOT(reset_zoom()), this);
577 	add_action(base, "Keys/increase_columns", SLOT(increase_columns()), this);
578 	add_action(base, "Keys/decrease_columns", SLOT(decrease_columns()), this);
579 	add_action(base, "Keys/increase_offset", SLOT(increase_offset()), this);
580 	add_action(base, "Keys/decrease_offset", SLOT(decrease_offset()), this);
581 	add_action(base, "Keys/rotate_left", SLOT(rotate_left()), this);
582 	add_action(base, "Keys/rotate_right", SLOT(rotate_right()), this);
583 
584 	add_action(base, "Keys/quit", SLOT(quit()), this);
585 	add_action(base, "Keys/search", SLOT(search()), this);
586 	add_action(base, "Keys/search_backward", SLOT(search_backward()), this);
587 	add_action(base, "Keys/close_search", SLOT(close_search()), this);
588 	add_action(base, "Keys/mark_jump", SLOT(mark_jump()), this);
589 	add_action(base, "Keys/invert_colors", SLOT(invert_colors()), this);
590 	add_action(base, "Keys/copy_to_clipboard", SLOT(copy_to_clipboard()), this);
591 	add_action(base, "Keys/toggle_fullscreen", SLOT(toggle_fullscreen()), base);
592 	add_action(base, "Keys/reload", SLOT(reload()), this);
593 	add_action(base, "Keys/open", SLOT(open()), this);
594 	add_action(base, "Keys/save", SLOT(save()), this);
595 	add_action(base, "Keys/toggle_toc", SLOT(toggle_toc()), this);
596 	add_action(base, "Keys/freeze_presentation", SLOT(freeze_presentation()), this);
597 }
598 
599