Commit 1546cbc0 authored by Philipp Erhardt's avatar Philipp Erhardt
Browse files

Implement Table of Contents

Default key binding: F9
parent ccfd7236
......@@ -84,6 +84,8 @@ KEY BINDINGS
process. *katarakt* reloads automatically if the opened file has changed.
*o* ::
Open a different document; shows a file dialog.
*F9* ::
Toggle the table of contents.
*-*, *+*, *=* ::
Adjust zoom level ('grid layout' only).
*z* ::
......
......@@ -3,7 +3,7 @@ TARGET = katarakt
DEPENDPATH += .
INCLUDEPATH += .
CONFIG += qt
QT += network
QT += network xml
DEFINES += "POPPLER_VERSION_MAJOR=`pkg-config --modversion poppler-qt4 | cut -d . -f 1`"
DEFINES += "POPPLER_VERSION_MINOR=`pkg-config --modversion poppler-qt4 | cut -d . -f 2`"
......@@ -13,9 +13,9 @@ QMAKE_CXXFLAGS_DEBUG += -DDEBUG
# Input
HEADERS += src/layout/layout.h src/layout/presentationlayout.h src/layout/gridlayout.h src/layout/presenterlayout.h \
src/viewer.h src/canvas.h src/resourcemanager.h src/grid.h src/search.h src/gotoline.h src/config.h src/download.h src/util.h src/kpage.h src/worker.h src/beamerwindow.h
src/viewer.h src/canvas.h src/resourcemanager.h src/grid.h src/search.h src/gotoline.h src/config.h src/download.h src/util.h src/kpage.h src/worker.h src/beamerwindow.h src/toc.h
SOURCES += src/main.cpp \
src/layout/layout.cpp src/layout/presentationlayout.cpp src/layout/gridlayout.cpp src/layout/presenterlayout.cpp \
src/viewer.cpp src/canvas.cpp src/resourcemanager.cpp src/grid.cpp src/search.cpp src/gotoline.cpp src/config.cpp src/download.cpp src/util.cpp src/kpage.cpp src/worker.cpp src/beamerwindow.cpp
src/viewer.cpp src/canvas.cpp src/resourcemanager.cpp src/grid.cpp src/search.cpp src/gotoline.cpp src/config.cpp src/download.cpp src/util.cpp src/kpage.cpp src/worker.cpp src/beamerwindow.cpp src/toc.cpp
unix:LIBS += -lpoppler-qt4
......@@ -58,3 +58,4 @@ toggle_fullscreen=F
close_search=Esc
reload=R
open=O
toggle_toc=F9
......@@ -17,6 +17,7 @@
#include "gotoline.h"
#include "config.h"
#include "beamerwindow.h"
#include "util.h"
using namespace std;
......@@ -35,7 +36,7 @@ Canvas::Canvas(Viewer *v, QWidget *parent) :
presentation_layout = new PresentationLayout(viewer);
grid_layout = new GridLayout(viewer);
presenter_layout = new PresenterLayout(viewer);
presenter_layout = new PresenterLayout(viewer); // TODO add config string
QString default_layout = config->get_value("default_layout").toString();
if (default_layout == "grid") {
......@@ -87,21 +88,11 @@ void Canvas::reload(bool clamp) {
}
void Canvas::setup_keys(QWidget *base) {
add_action(base, "set_presentation_layout", SLOT(set_presentation_layout()));
add_action(base, "set_grid_layout", SLOT(set_grid_layout()));
add_action(base, "set_presenter_layout", SLOT(set_presenter_layout()));
add_action(base, "toggle_overlay", SLOT(toggle_overlay()));
add_action(base, "focus_goto", SLOT(focus_goto()));
}
void Canvas::add_action(QWidget *base, const char *action, const char *slot) {
QStringListIterator i(CFG::get_instance()->get_keys(action));
while (i.hasNext()) {
QAction *a = new QAction(base);
a->setShortcut(QKeySequence(i.next()));
base->addAction(a);
connect(a, SIGNAL(triggered()), this, slot);
}
add_action(base, "set_presentation_layout", SLOT(set_presentation_layout()), this);
add_action(base, "set_grid_layout", SLOT(set_grid_layout()), this);
add_action(base, "set_presenter_layout", SLOT(set_presenter_layout()), this);
add_action(base, "toggle_overlay", SLOT(toggle_overlay()), this);
add_action(base, "focus_goto", SLOT(focus_goto()), this);
}
Layout *Canvas::get_layout() const {
......
......@@ -56,7 +56,6 @@ private slots:
private:
void setup_keys(QWidget *base);
void add_action(QWidget *base, const char *action, const char *slot);
Viewer *viewer;
Layout *cur_layout;
......
......@@ -78,6 +78,7 @@ CFG::CFG() :
keys["close_search"] = QStringList() << "Esc";
keys["reload"] = QStringList() << "R";
keys["open"] = QStringList() << "O";
keys["toggle_toc"] = QStringList() << "F9";
settings.endGroup();
}
......
......@@ -416,89 +416,100 @@ bool GridLayout::advance_invisible_hit(bool forward) {
QList<QRectF>::const_iterator it = hit_it;
do {
Layout::advance_hit(forward);
r = get_hit_rect();
r = get_target_rect(hit_page, *hit_it);
if (r.x() < 0 || r.y() < 0 ||
r.x() + r.width() >= width ||
r.y() + r.height() >= height) {
break; // TODO always breaks for boxes larger than the viewport
}
} while (it != hit_it);
view_hit(r);
view_rect(r);
return true;
}
void GridLayout::view_hit() {
QRect r = get_hit_rect();
view_hit(r);
bool GridLayout::view_hit() {
QRect r = get_target_rect(hit_page, *hit_it);
return view_rect(r);
}
void GridLayout::view_hit(const QRect &r) {
bool GridLayout::view_rect(const QRect &r) {
bool change = false;
// move view horizontally
if (r.width() <= width * (1 - 2 * search_padding)) {
if (r.x() < width * search_padding) {
scroll_smooth(width * search_padding - r.x(), 0);
change |= scroll_smooth(width * search_padding - r.x(), 0);
} else if (r.x() + r.width() > width * (1 - search_padding)) {
scroll_smooth(width * (1 - search_padding) - r.x() - r.width(), 0);
change |= scroll_smooth(width * (1 - search_padding) - r.x() - r.width(), 0);
}
} else {
int center = (width - r.width()) / 2;
if (center < 0) {
center = 0;
}
scroll_smooth(center - r.x(), 0);
change |= scroll_smooth(center - r.x(), 0);
}
// vertically
if (r.height() <= height * (1 - 2 * search_padding)) {
if (r.y() < height * search_padding) {
scroll_smooth(0, height * search_padding - r.y());
change |= scroll_smooth(0, height * search_padding - r.y());
} else if (r.y() + r.height() > height * (1 - search_padding)) {
scroll_smooth(0, height * (1 - search_padding) - r.y() - r.height());
change |= scroll_smooth(0, height * (1 - search_padding) - r.y() - r.height());
}
} else {
int center = (height - r.height()) / 2;
if (center < 0) {
center = 0;
}
scroll_smooth(0, center - r.y());
change |= scroll_smooth(0, center - r.y());
}
return change;
}
bool GridLayout::view_point(const QPoint &p) {
return scroll_smooth(-p.x(), -p.y());
}
QRect GridLayout::get_hit_rect() {
int hit_page_offset = hit_page + grid->get_offset();
// calculate offsets
int page_width = res->get_page_width(hit_page) * size;
int page_height = ROUND(res->get_page_height(hit_page) * size);
QRect GridLayout::get_target_rect(int target_page, QRectF target_rect) const {
// get rect coordinates relative to the current view
QRectF rot = rotate_rect(target_rect, res->get_page_width(target_page),
res->get_page_height(target_page), res->get_rotation());
QPoint p = get_target_page_distance(target_page);
return transform_rect(rot, size, p.x(), p.y());
}
int center_x = (grid->get_width(hit_page_offset % grid->get_column_count()) * size - page_width) / 2;
int center_y = (grid->get_height(hit_page_offset / grid->get_column_count()) * size - page_height) / 2;
QPoint GridLayout::get_target_page_distance(int target_page) const {
int target_page_offset = target_page + grid->get_offset();
// calculate distances
int page_width = res->get_page_width(target_page) * size;
int page_height = ROUND(res->get_page_height(target_page) * size);
int center_x = (grid->get_width(target_page_offset % grid->get_column_count()) * size - page_width) / 2;
int center_y = (grid->get_height(target_page_offset / grid->get_column_count()) * size - page_height) / 2;
int wpos = off_x;
if (hit_page_offset % grid->get_column_count() > horizontal_page) {
for (int i = horizontal_page; i < hit_page_offset % grid->get_column_count(); i++) {
if (target_page_offset % grid->get_column_count() > horizontal_page) {
for (int i = horizontal_page; i < target_page_offset % grid->get_column_count(); i++) {
wpos += grid->get_width(i) * size + useless_gap;
}
} else {
for (int i = horizontal_page; i > hit_page_offset % grid->get_column_count(); i--) {
for (int i = horizontal_page; i > target_page_offset % grid->get_column_count(); i--) {
wpos -= grid->get_width(i) * size + useless_gap;
}
}
int hpos = off_y;
if (hit_page_offset > page) {
if (target_page_offset > page) {
for (int i = (page + grid->get_offset()) / grid->get_column_count();
i < hit_page_offset / grid->get_column_count(); i++) {
i < target_page_offset / grid->get_column_count(); i++) {
hpos += grid->get_height(i) * size + useless_gap;
}
} else {
for (int i = (page + grid->get_offset()) / grid->get_column_count();
i > hit_page_offset / grid->get_column_count(); i--) {
i > target_page_offset / grid->get_column_count(); i--) {
hpos -= grid->get_height(i) * size + useless_gap;
}
}
// get search rect coordinates relative to the current view
QRectF rot = rotate_rect(*hit_it, res->get_page_width(hit_page),
res->get_page_height(hit_page), res->get_rotation());
return transform_rect(rot, size, wpos + center_x, hpos + center_y);
return QPoint(wpos + center_x, hpos + center_y);
}
pair<int,QPointF> GridLayout::get_page_at(int mx, int my) {
......@@ -582,6 +593,25 @@ bool GridLayout::click_mouse(int mx, int my) {
return false;
}
bool GridLayout::goto_link_destination(Poppler::LinkDestination *link) {
// TODO variable margin?
// TODO use this interface for clicked links
int link_page = link->pageNumber() - 1;
float w = res->get_page_width(link_page);
float h = res->get_page_height(link_page);
const QPointF link_point = rotate_point(QPointF(link->left(), link->top()) * h, w, h, res->get_rotation());
QPoint p = get_target_page_distance(link_page);
if (link->isChangeLeft()) {
p.rx() += link_point.x() * size;
}
if (link->isChangeTop()) {
p.ry() += link_point.y() * size;
}
return view_point(p);
}
bool GridLayout::goto_page_at(int mx, int my) {
pair<int,QPointF> page = get_page_at(mx, my);
......
......@@ -30,6 +30,7 @@ public:
bool advance_invisible_hit(bool forward = true);
bool click_mouse(int mx, int my);
bool goto_link_destination(Poppler::LinkDestination *link);
bool goto_page_at(int mx, int my);
bool page_visible(int p) const;
......@@ -37,9 +38,11 @@ public:
private:
void initialize(int columns, int offset, bool clamp = true);
void set_constants(bool clamp = true);
void view_hit();
void view_hit(const QRect &r);
QRect get_hit_rect();
bool view_hit();
bool view_rect(const QRect &r);
bool view_point(const QPoint &p);
QRect get_target_rect(int target_page, QRectF target_rect) const;
QPoint get_target_page_distance(int target_page) const;
std::pair<int,QPointF> get_page_at(int x, int y);
Grid *grid;
......
......@@ -110,10 +110,10 @@ bool Layout::scroll_page(int new_page, bool relative) {
}
}
void Layout::update_search() {
bool Layout::update_search() {
const map<int,QList<QRectF> *> *hits = viewer->get_search_bar()->get_hits();
if (hits->empty()) {
return;
return false;
}
// find the right page before/after the current one
......@@ -140,6 +140,7 @@ void Layout::update_search() {
--hit_it;
}
view_hit();
return true;
}
void Layout::set_search_visible(bool visible) {
......@@ -185,6 +186,10 @@ bool Layout::click_mouse(int /*mx*/, int /*my*/) {
return false;
}
bool Layout::goto_link_destination(Poppler::LinkDestination *link) {
return scroll_page(link->pageNumber() - 1, false);
}
bool Layout::goto_page_at(int /*mx*/, int /*my*/) {
return false;
}
......
......@@ -9,6 +9,9 @@
class Viewer;
class ResourceManager;
class Grid;
namespace Poppler {
class LinkDestination;
}
class Layout {
......@@ -29,19 +32,20 @@ public:
virtual bool scroll_page(int new_page, bool relative = true);
virtual void render(QPainter *painter) = 0;
virtual void update_search();
virtual bool update_search();
virtual void set_search_visible(bool visible);
virtual bool advance_hit(bool forward = true);
virtual bool advance_invisible_hit(bool forward = true) = 0;
virtual bool click_mouse(int mx, int my);
virtual bool goto_link_destination(Poppler::LinkDestination *link);
virtual bool goto_page_at(int mx, int my);
virtual bool get_search_visible() const;
virtual bool page_visible(int p) const = 0;
protected:
virtual void view_hit() = 0;
virtual bool view_hit() = 0;
Viewer *viewer;
ResourceManager *res;
......
......@@ -160,8 +160,8 @@ bool PresentationLayout::advance_invisible_hit(bool forward) {
return true;
}
void PresentationLayout::view_hit() {
scroll_page(hit_page, false);
bool PresentationLayout::view_hit() {
return scroll_page(hit_page, false);
}
bool PresentationLayout::click_mouse(int mx, int my) {
......
......@@ -23,7 +23,7 @@ public:
private:
int calculate_fit_width(int page) const;
void view_hit();
bool view_hit();
};
#endif
......
......@@ -9,7 +9,7 @@ using namespace std;
PresenterLayout::PresenterLayout(Viewer *v, int page) :
Layout(v, page),
main_ratio(0.67) {
main_ratio(0.67) { // TODO add config option
rebuild();
}
......@@ -235,8 +235,8 @@ bool PresenterLayout::advance_invisible_hit(bool forward) {
return true;
}
void PresenterLayout::view_hit() {
scroll_page(hit_page, false);
bool PresenterLayout::view_hit() {
return scroll_page(hit_page, false);
}
bool PresenterLayout::click_mouse(int mx, int my) {
......
......@@ -23,7 +23,7 @@ public:
protected:
int calculate_fit_width(int page) const;
void view_hit();
bool view_hit();
float main_ratio;
float optimized_ratio;
......
......@@ -332,6 +332,10 @@ int ResourceManager::jump_forward() {
return *cur_jump_pos;
}
Poppler::LinkDestination *ResourceManager::resolve_link_destination(const QString &name) const {
return doc->linkDestination(name);
}
void ResourceManager::inotify_slot() {
#ifdef __linux__
i_notifier->setEnabled(false);
......@@ -446,6 +450,10 @@ const list<Poppler::LinkGoto *> *ResourceManager::get_links(int page) {
return l;
}
QDomDocument *ResourceManager::get_toc() const {
return doc->toc();
}
void ResourceManager::join_threads() {
worker->die = true;
requestSemaphore.release(1);
......
......@@ -18,6 +18,7 @@ class KPage;
class Worker;
class Viewer;
class QSocketNotifier;
class QDomDocument;
class ResourceManager : public QObject {
......@@ -45,6 +46,7 @@ public:
float get_max_aspect() const;
int get_page_count() const;
const std::list<Poppler::LinkGoto *> *get_links(int page);
QDomDocument *get_toc() const;
int get_rotation() const;
void rotate(int value, bool relative = true);
......@@ -60,6 +62,8 @@ public:
int jump_back();
int jump_forward();
Poppler::LinkDestination *resolve_link_destination(const QString &name) const;
public slots:
void inotify_slot();
......
......@@ -76,11 +76,17 @@ void SearchWorker::run() {
has_upper_case ? Poppler::Page::CaseSensitive : Poppler::Page::CaseInsensitive)) {
hits->push_back(QRectF(x, y, x2 - x, y2 - y));
}
#else
#elif POPPLER_VERSION < POPPLER_VERSION_CHECK(0, 31, 0)
// new search interface
QList<QRectF> tmp = p->search(search_term,
has_upper_case ? Poppler::Page::CaseSensitive : Poppler::Page::CaseInsensitive);
hits->swap(tmp);
#else
// even newer interface
QList<QRectF> tmp = p->search(search_term,
has_upper_case ? (Poppler::Page::SearchFlags) 0 : Poppler::Page::IgnoreCase);
// TODO support Poppler::Page::WholeWords
hits->swap(tmp);
#endif
#ifdef DEBUG
if (hits->size() > 0) {
......@@ -260,9 +266,10 @@ void SearchBar::insert_hits(int page, QList<QRectF> *l) {
// only update the layout if the hits should be viewed
if (empty) {
viewer->get_canvas()->get_layout()->update_search();
if (viewer->get_canvas()->get_layout()->update_search()) {
viewer->get_canvas()->update();
}
}
viewer->get_canvas()->update(); // TODO updates too often
}
void SearchBar::clear_hits() {
......@@ -283,9 +290,10 @@ void SearchBar::set_text() {
Canvas *c = viewer->get_canvas();
// do not start the same search again but signal slots
if (term == line->text()) {
c->get_layout()->update_search();
c->setFocus(Qt::OtherFocusReason);
c->update();
if (c->get_layout()->update_search()) {
c->update();
}
return;
}
......
#include <QEvent>
#include <QKeyEvent>
#include <QDomDocument>
#include <QDomNode>
#include <QHeaderView>
#include "toc.h"
#include "viewer.h"
#include "canvas.h"
#include "layout/layout.h"
#include "resourcemanager.h"
#include "util.h"
Q_DECLARE_METATYPE(Poppler::LinkDestination *)
Toc::Toc(Viewer *v, QWidget *parent) :
QTreeWidget(parent), viewer(v) {
setColumnCount(2);
QHeaderView *h = header();
h->setStretchLastSection(false);
h->setResizeMode(0, QHeaderView::Stretch);
h->setResizeMode(1, QHeaderView::ResizeToContents);
h->hide();
setAlternatingRowColors(true);
QDomDocument *contents = viewer->get_res()->get_toc();
if (contents != NULL) {
build(contents, invisibleRootItem());
delete contents;
}
connect(this, SIGNAL(itemActivated(QTreeWidgetItem *, int)), this, SLOT(goto_link(QTreeWidgetItem *, int)), Qt::UniqueConnection);
}
void Toc::goto_link(QTreeWidgetItem *item, int column) {
if (column == -1) {
return;
}
Poppler::LinkDestination *link = item->data(0, Qt::UserRole).value<Poppler::LinkDestination *>();
if (viewer->get_canvas()->get_layout()->goto_link_destination(link)) {
viewer->get_canvas()->update();
}
viewer->get_canvas()->setFocus(Qt::OtherFocusReason);
}
bool Toc::event(QEvent *e) {
if (e->type() == QEvent::ShortcutOverride) {
QKeyEvent *ke = static_cast<QKeyEvent *>(e);
if (ke->key() < Qt::Key_Escape) {
ke->accept();
} else {
switch (ke->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
case Qt::Key_Delete:
case Qt::Key_Home:
case Qt::Key_End:
case Qt::Key_Backspace:
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_Tab:
ke->accept();
default:
break;
}
}
}
return QTreeWidget::event(e);
}
void Toc::build(QDomNode *node, QTreeWidgetItem *parent) {
if (node->isNull() || !node->hasChildNodes()) {
return;
}
QDomNodeList list = node->childNodes();
for (int i = 0; i < list.count(); i++) {
QDomNode n = list.at(i);
QStringList strings;
strings << n.nodeName();
QDomNamedNodeMap attributes = n.attributes();
QDomNode dest = attributes.namedItem("Destination");
Poppler::LinkDestination *link = NULL;
if (!dest.isNull()) {
// strings << dest.nodeValue();
link = new Poppler::LinkDestination(dest.nodeValue());
} else {
dest = attributes.namedItem("DestinationName");
if (!dest.isNull()) {
link = viewer->get_res()->resolve_link_destination(dest.nodeValue());
// if (dest_page >= 0) {
// strings << QString::number(dest_page);
// }
}
}
if (!dest.isNull() && link != NULL) {
strings << QString::number(link->pageNumber());
}
// TODO check "ExternalFileName"
// TODO take "Open" into account?
QTreeWidgetItem *item = new QTreeWidgetItem(parent, strings);
item->setTextAlignment(1, Qt::AlignRight);
item->setData(0, Qt::UserRole, QVariant::fromValue(link));
build(&n, item);
}
}
src/toc.h 0 → 100644