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