/// // Copyright (C) 2002 - 2004, Fredrik Arnerup & Rasmus Kaj, See COPYING /// #include "docview.h" #include #include #include #include #include #include "document/loader.h" #include "document/textframe.h" #include "document/imageframe.h" #include "document/rasterframe.h" #include "document/document.h" #include "document/page.h" #include "util/warning.h" #include "util/stringutil.h" #include "util/filesys.h" #include "widget/usererror.h" #include "widget/programs.h" #include "groupmeta.h" #include "pptcore.h" #include "config.h" #include "lengthunits.h" #include "defines.h" #ifdef HAVE_GNOME #include #endif float DocumentView::pt2scr(float pt) const { return int(resolution * zoom_factor * length_units.from_base(pt, "in") + 0.5); } float DocumentView::scr2pt(float scr) const { return length_units.to_base(scr / (resolution * zoom_factor), "in"); } Gdk::Point DocumentView::pt2scr(const Vector& pt) const { double x, y; world_to_window(pt.x, -pt.y, x, y); return Gdk::Point(int(x + 0.5), int(y + 0.5)); } Vector DocumentView::scr2pt(const Gdk::Point& scr) const { Vector w; window_to_world(scr.get_x(), scr.get_y(), w.x, w.y); w.y = -w.y; return w; } //////////////////////////////////// BEGIN JUNK void DocumentView::delete_page() { if(document) { pageview.clear(); // Do this first, so the viewent won't be // listening to the selection_changed_signal // which is emitted after the page has been // deleted. document->delete_page(current_page_num); // Todo: this is an ugly fix to keep the pageview updated set_current_page_num(current_page_num); current_page_num_changed_signal(); // sometimes redundant } } void DocumentView::new_image_frame(std::string filename, float res) { Page *page = get_page(); if(!page) return; get_document()->select_all(false); /// \todo more helpful error messages if(ImageFrame::is_postscript(filename)) page->addObject(new ImageFrame(page, filename)); else { try { page->addObject(new RasterFrame(page, filename, res)); } catch(const std::exception& e){ throw UserError("Could not open \"" + filename + "\"", e); } } } //////////////////////////////////////END JUNK Page *DocumentView::get_page() { if(document) { Page *page = document->get_page(current_page_num); // if page has changed, release memory from old page if(old_page && page != old_page) pageview.clear(); old_page = page; return page; } else return 0; } void DocumentView::act_on_document_change(DocRef document_) { if(!document || document != document_) return; int first = document->get_first_page_num(); int num = document->get_num_of_pages(); int last = first + num - 1; if(current_page_num < first) { current_page_num = first; current_page_num_changed_signal(); } else if(current_page_num > last) { current_page_num = last; current_page_num_changed_signal(); } document_changed_signal(); } void DocumentView::act_on_selection_change(DocRef document_) { if(!document || document != document_) return; update_handles(); } void DocumentView::set_current_page_num(int current) { if(!document) return; int first = document->get_first_page_num(); int num = document->get_num_of_pages(); int last = first + num - 1; if(current < first) current = first; else if(current > last) current = last; if(current != current_page_num) { current_page_num = current; current_page_num_changed_signal(); } if(Page *page = get_page()) { pageview = core.getMeta("page")->create_viewent(*this, *page); } } void DocumentView::update_document_size() { float w = document->get_width(); float h = document->get_height(); set_scroll_region(-margin_size, -h - margin_size, w + margin_size, margin_size); no_pages_frame->property_x1() = 0; no_pages_frame->property_y1() = 0; no_pages_frame->property_x2() = w; no_pages_frame->property_y2() = -h; no_pages_text->property_x() = w / 2; no_pages_text->property_y() = -h / 1.8; } void DocumentView::set_document(DocMeta d) { if(document == d) return; document = d; update_handles(); if(document) { update_document_size(); no_pages_group.show(); current_page_num = document->get_first_page_num(); document->size_changed_signal.connect (sigc::mem_fun(*this, &DocumentView::update_document_size)); Document::changed_signal.connect (sigc::mem_fun(*this, &DocumentView::act_on_document_change)); Document::selection_changed_signal.connect (sigc::mem_fun(*this, &DocumentView::act_on_selection_change)); } else { no_pages_group.hide(); } document_changed_signal(); //? document_set_signal(); if(Page *page = get_page()) { pageview = core.getMeta("page")->create_viewent(*this, *page); } else { pageview.clear(); } } DocumentView::DocumentView(DocMeta d, float zoom_factor) : no_pages_group(*root()), pagent_group(*root()), guide_group(*root()), handle_group(*root()), snap_mode(snap::NONE), zoom_factor(zoom_factor), document(DocRef()), old_page(0) { set_events(Gdk::EXPOSURE_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK | Gdk::POINTER_MOTION_HINT_MASK); Glib::RefPtr colormap = get_default_colormap(); white = Gdk::Color("white"); black = Gdk::Color("black"); gray = Gdk::Color("gray"); red = Gdk::Color("red"); colormap->alloc_color(white); colormap->alloc_color(black); colormap->alloc_color(gray); colormap->alloc_color(red); Glib::RefPtr screen = Gdk::Display::get_default()->get_default_screen(); float xresolution = screen->get_width() / (screen->get_width_mm() / 25.4); float yresolution = screen->get_height() / (screen->get_height_mm() / 25.4); resolution = (xresolution + yresolution) / 2; // average :-) verbose << "Screen resolution: " << resolution << " dpi" << std::endl; set_pixels_per_unit(resolution / 72.0 * zoom_factor); using namespace Gnome::Canvas; no_pages_frame.reset(new Rect(no_pages_group)); no_pages_frame->property_outline_color_gdk() = get_color(Color::frame); no_pages_frame->property_width_pixels() = 1; no_pages_text.reset(new Text(no_pages_group, 0, 0, "No pages")); no_pages_text->property_anchor() = Gtk::ANCHOR_CENTER; no_pages_group.hide(); for(int i = 0; i < 8; i++) { handles[i] = manage(new Rect(handle_group)); handles[i]->property_fill_color_gdk() = get_color(Color::bg); handles[i]->property_outline_color_gdk() = get_color(Color::frame); handles[i]->property_width_pixels() = 1; } set_document(d); reshaping = moving = false; margin_size = 30; // drag'n drop stuff: std::list targets; // targets.push_back(Gtk::TargetEntry("")); targets.push_back(Gtk::TargetEntry("text/uri-list")); targets.push_back(Gtk::TargetEntry("_NETSCAPE_URL")); targets.push_back(Gtk::TargetEntry("text/unicode")); targets.push_back(Gtk::TargetEntry("text/plain")); targets.push_back(Gtk::TargetEntry("UTF8_STRING")); targets.push_back(Gtk::TargetEntry("STRING")); drag_dest_set(targets); // Update handles when we turn pages current_page_num_changed_signal.connect (sigc::mem_fun(*this, &DocumentView::update_handles)); // make sure the scrolling region is the right size (it only seems // to work if the canvas has been realized) if(document) signal_realize().connect (sigc::mem_fun(*this, &DocumentView::update_document_size)); } DocumentView::~DocumentView() { } void DocumentView::set_zoom_factor(float factor) { if(factor > 0) { zoom_factor = factor; set_pixels_per_unit(resolution / 72.0 * zoom_factor); if(document) { zoom_change_signal(zoom_factor); update_handles(); } } } bool DocumentView::in_move_area(int x, int y) { if(Page* page = get_page()) { const Document::Selection selected = get_document()->selected(); // "paper coordinates" const Vector pos = scr2pt(Gdk::Point(x, y)); const float dist = scr2pt(2); for(Document::Selection::const_iterator i = selected.begin(); i != selected.end(); ++i) if(&Page::containing(**i) == page && (*i)->get_box()->isInsideOrClose(pos, dist)) return true; } return false; } Pagent* DocumentView::in_select_area(int x, int y) { if(Page* page = get_page()) { const Vector pos = scr2pt(Gdk::Point(x, y)); const float dist = scr2pt(2); for(Group::ChildVec::const_iterator i = page->pbegin(); i != page->pend(); i++) { if((*i)->get_box()->isInsideOrClose(pos, dist)) return *i; } } return 0; } namespace { typedef std::vector ResizeHandles; ResizeHandles get_resize_handles(const Pagent &p) { ResizeHandles handles; Polygon poly = p.get_box()->get_polygon(); for(Polygon::const_iterator i = poly.begin(); i != poly.end(); i++) { handles.push_back(*i); Polygon::const_iterator j = i; j++; if(j == poly.end()) j = poly.begin(); handles.push_back(Vector((i->x + j->x) / 2, (i->y + j->y) / 2)); } return handles; } } int DocumentView::hover_reshape(int x, int y) { if(!document) return -1; Vector mouse = scr2pt(Gdk::Point(x, y)); Gnome::Canvas::Item *item = get_item_at(mouse.x, -mouse.y); for(int i = 0; i < 8; i++) if(item == handles[i]) return i; return -1; } bool DocumentView::on_button_press_event(GdkEventButton *event) { Page *page = get_page(); if(!page) return true; switch(event->button) { case 1: { const Document::Selection selected = get_document()->selected(); int j = hover_reshape(int(event->x), int(event->y)); if(j != -1 && selected.size() == 1 && !selected.front()->get_lock()) { // maybe start moving/resizing? // reshape begin_reshape(int(event->x), int(event->y), j); } else if(Pagent* select = in_select_area(int(event->x), int(event->y))) { // select / move // is it already selected? if(find(selected.begin(), selected.end(), select) != selected.end()) { if(event->state & Gdk::CONTROL_MASK) { // deselect get_document()->deselect(select); } else begin_move(int(event->x), int(event->y)); } else { // select and begin moving get_document()->select(select, !(event->state & Gdk::CONTROL_MASK)); if(!select->get_lock()) begin_move(int(event->x), int(event->y)); } } else if((event->state & Gdk::CONTROL_MASK) == 0) { // deselect all get_document()->select_all(false); } } break; case 3: { if(event->type == GDK_BUTTON_PRESS) { Pagent* select = in_select_area(int(event->x), int(event->y)); if(select) { std::string filename; std::vector mimetypes; /// \todo move the mime type heuristics elsewhere ImageFrame *i_f; RasterFrame *r_f; TextFrame *t_f; if((i_f = dynamic_cast(select))) { filename = i_f->get_association(); } else if((r_f = dynamic_cast(select))) { filename = r_f->get_association(); } else if((t_f = dynamic_cast(select))) { TextStream *stream = t_f->get_stream(); filename = stream->get_association(); mimetypes.push_back("text/plain"); // always enable text editors } else break; if(filename.empty()) break; #ifdef HAVE_GNOME const char *mtype = gnome_vfs_mime_type_from_name(filename.c_str()); if(mtype) mimetypes.push_back(mtype); #endif if(mimetypes.empty()) mimetypes.push_back("text/plain"); popup_menu.items().clear(); try { for(std::vector::const_iterator type = mimetypes.begin(); type != mimetypes.end(); type++) Programs::populate_file_handler_menu(popup_menu, filename, *type); popup_menu.popup(event->button, event->time); } catch(const std::exception &e) { verbose << "Failed to display popup menu: " << e.what() << std::endl; } } } } break; default: break; } return true; } namespace { // Todo: This method should be removed. Instead, there should exist something // (not related to a specific view, but to the document), that listens to // geometry changes, finds all text streams that have frames that intersects // with the object (before or after the change) and retypesets them. // What is done here is to much typsetting when objects moved / resized by the // view, and no typesetting at all when objects are moved / resized by e.g. // the properties dialog. void refresh_streams(Page *page) { if(page) { Document::StreamVec streams; for(Group::ChildVec::const_iterator i = page->pbegin(); i != page->pend(); i++) { TextFrame *tmp = dynamic_cast(*i); if(tmp) { TextStream *stream = tmp->get_stream(); if(stream && find(streams.begin(), streams.end(), stream) == streams.end()) streams.push_back(stream); } } for(Document::StreamVec::iterator i = streams.begin(); i != streams.end(); i++) { try { (*i)->run_typesetter(); } catch(const BasicFrame::GenPicError &e) { warning << "Failed to refresh stream: " << e.what() << std::endl; } debug << (*i)->get_name() << ", " << (*i)->get_association() << std::endl; } } } } void DocumentView::begin_reshape(int x, int y, int box) { DocRef document = get_document(); if(!get_page() || !document) return; const Document::Selection selected = document->selected(); if(selected.size() != 1) return; const Pagent& obj = *selected.front(); ResizeHandles handles = get_resize_handles(obj); if(box >= int(handles.size())) return; Gdk::Point handle = pt2scr(handles[box]); // pointer is probably not in center of handle offset = IVector(x - handle.get_x(), y - handle.get_y()); old_size = obj.get_inherent_size(); old_matrix = obj.get_matrix(); reshape_box = box; reshaping = true; } void DocumentView::end_reshape(bool revert) { reshaping = false; if(DocRef document = get_document()) { if(revert) { const Document::Selection selected = document->selected(); if(selected.size() == 1) { Pagent& obj = *selected.front(); obj.set_matrix(old_matrix); if(Pagent::Resizable* o = dynamic_cast(&obj)) { o->set_size(old_size.x, old_size.y); } } } refresh_streams(get_page()); } } namespace { // get bounding box of selected pagents void box_of_selected(Vector &ll, Vector &ur, DocRef document) { bool first = true; const Document::Selection &selected = document->selected(); Document::Selection::const_iterator i; for(i = selected.begin(); i != selected.end(); i++) { Boundary b = (*i)->get_box(); Vector b_ur = b->getCorner(Corner::UR); Vector b_ll = b->getCorner(Corner::LL); if(first) { ll = b_ll; ur = b_ur; } else { ll.x = std::min(ll.x, b_ll.x); ll.y = std::min(ll.y, b_ll.y); ur.x = std::max(ur.x, b_ur.x); ur.y = std::max(ur.y, b_ur.y); } first &= false; } } } void DocumentView::begin_move(int x, int y) { if(get_page()) { Vector ur, ll; box_of_selected(ll, ur, get_document()); Gdk::Point ll_scr = pt2scr(ll); offset = IVector(x, y) - IVector(ll_scr.get_x(), ll_scr.get_y()); moving = true; } } void DocumentView::end_move(bool revert) { moving = false; refresh_streams(get_page()); } bool DocumentView::on_key_press_event(GdkEventKey* key) { if(key->keyval == 65307) { // escape if(moving) end_move(true); if(reshaping) end_reshape(true); } return false; } bool DocumentView::on_button_release_event(GdkEventButton *event) { if(moving) end_move(false); if(reshaping) end_reshape(false); return true; } Gdk::CursorType DocumentView::get_cursor(int x, int y) { DocRef document = get_document(); if(!document) return Gdk::LEFT_PTR; if(moving) return Gdk::FLEUR; const Document::Selection selection = document->selected(); Pagent *obj = in_select_area(x, y); if(!reshaping && obj && find(selection.begin(), selection.end(), obj) == selection.end()) return Gdk::TOP_LEFT_ARROW; int j = reshape_box; if(reshaping || ((j = hover_reshape(x, y)) != -1)) switch(j) { case 0: return Gdk::BOTTOM_LEFT_CORNER; case 1: return Gdk::LEFT_SIDE; case 2: return Gdk::TOP_LEFT_CORNER; case 3: return Gdk::TOP_SIDE; case 4: return Gdk::TOP_RIGHT_CORNER; case 5: return Gdk::RIGHT_SIDE; case 6: return Gdk::BOTTOM_RIGHT_CORNER; case 7: return Gdk::BOTTOM_SIDE; default: return Gdk::QUESTION_ARROW; } if(in_move_area(x, y)) return Gdk::FLEUR; if(in_select_area(x, y)) return Gdk::TOP_LEFT_ARROW; return Gdk::LEFT_PTR; } void DocumentView::update_cursor(int x, int y) { get_window()->set_cursor(Gdk::Cursor(get_cursor(x, y))); } namespace { float grid_size_x = 24, grid_size_y = 24; Vector grid_offset(0, 0); float snap_to_grid(float x, float offset, float step) { x -= offset; int n = int(x / step); float low = n * step, high = (n + 1) * step; float dlow = x - low, dhigh = high - x; x = dlow < dhigh ? low : high; x += offset; return x; } } void DocumentView::move_reshape_box(int x, int y) { const Document::Selection selected = document->selected(); if(selected.size() != 1) return; Pagent& obj = *selected.front(); const Matrix &m = obj.get_matrix(); // cursor wasn't necessarily in the center of the handle x -= offset.x; y -= offset.y; Vector p = scr2pt(Gdk::Point(x, y)); // snap to grid if(snap_mode == snap::GRID) { p = Vector(snap_to_grid(p.x, grid_offset.x, grid_size_x), snap_to_grid(p.y, grid_offset.y, grid_size_y)); } // get pos in matrix coordinates Vector pos = m.inv().transform(p); Vector ll(0, 0), ur(obj.get_inherent_size()); // lower left / upper right switch(reshape_box) { case 0: // bottom left ll = pos; break; case 1: // left ll.x = pos.x; break; case 2: // top left ll.x = pos.x; ur.y = pos.y; break; case 3: // top ur.y = pos.y; break; case 4: // top right ur = pos; break; case 5: // right ur.x = pos.x; break; case 6: // bottom right; ur.x = pos.x; ll.y = pos.y; break; case 7: // bottom ll.y = pos.y; break; default: break; } bool negx = ll.x > ur.x, negy = ll.y > ur.y; // change reshape_box if going past zero if(negx) { switch(reshape_box) { case 0: reshape_box = 6; break; case 1: reshape_box = 5; break; case 2: reshape_box = 4; break; case 4: reshape_box = 2; break; case 5: reshape_box = 1; break; case 6: reshape_box = 0; break; default: break; } } if(negy) { switch(reshape_box) { case 0: reshape_box = 2; break; case 2: reshape_box = 0; break; case 3: reshape_box = 7; break; case 4: reshape_box = 6; break; case 6: reshape_box = 4; break; case 7: reshape_box = 3; break; default: break; } } // avoid negative scaling Vector _ll(std::min(ll.x, ur.x), std::min(ll.y, ur.y)); // Vector _ur(std::max(ll.x, ur.x), std::max(ll.y, ur.y)); Vector size = ur - ll; // can't set size to zero float minsize = 1; if(fabs(size.x) < minsize || fabs(size.y) < minsize) { return; } if(Pagent::Resizable* o = dynamic_cast(&obj)) { o->set_size(fabs(size.x), fabs(size.y)); } else { const Vector osize = obj.get_inherent_size(); obj.set_scaling(m.sc_x() * size.x / osize.x, m.sc_y() * size.y / osize.y); } obj.set_matrix(Matrix::translation(_ll) * obj.get_matrix()); } bool DocumentView::on_motion_notify_event(GdkEventMotion *event) { if(!get_page()) return true; // some silliness we have to do or we won't get new motion notify events if(event->is_hint) { Gdk::ModifierType state; int x, y; get_window()->get_pointer(x, y, state); } int x = int(event->x); int y = int(event->y); update_cursor(x, y); Document::Selection selected = get_document()->selected(); Document::Selection::const_iterator i; if(moving || reshaping) { bool locked = false; for(i = selected.begin(); i != selected.end(); i++) { locked |= (*i)->get_lock(); // if one is locked, all are locked } if(moving) { Vector ur, ll; box_of_selected(ll, ur, get_document()); // new ll is at pointer pos minus offset Vector move = scr2pt(Gdk::Point(x - offset.x, y - offset.y)) - ll; if(snap_mode == snap::GRID && !locked) { ll += move; ur += move; Vector ll_s(snap_to_grid(ll.x, grid_offset.x, grid_size_x), snap_to_grid(ll.y, grid_offset.y, grid_size_y)); Vector ur_s(snap_to_grid(ur.x, grid_offset.x, grid_size_x), snap_to_grid(ur.y, grid_offset.y, grid_size_y)); Vector dll = ll_s - ll, dur = ur_s - ur; // the edge closest to a grid line wins Vector d(fabs(dll.x) < fabs(dur.x) ? dll.x : dur.x, fabs(dll.y) < fabs(dur.y) ? dll.y : dur.y); move += d; } if(!locked) { for(i = selected.begin(); i != selected.end(); i++) { Vector t = (*i)->get_matrix().tr(); t += move; (*i)->set_translation(t); } } } else if(reshaping) { for(i = selected.begin(); i != selected.end(); i++) { if(!(*i)->get_lock()) move_reshape_box(x, y); } } } return true; } void DocumentView::on_drag_data_received (const Glib::RefPtr& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time) { /// \todo unicode ??? debug << "DocumentView::on_drop_drag_data_received:" << std::endl << "x = " << x << ", y = " << y << std::endl << selection_data.get_data_type() << " : \"" << selection_data.get_text() << "\"" << std::endl << "\"" << selection_data.get_data_as_string() << "\"" << std::endl << std::endl; if(!get_page()) { context->drag_finish(false, false, time); return; } // split into lines typedef std::vector Strings; Strings strings; std::string data = selection_data.get_data_as_string(); unsigned int start = 0; unsigned int stop; unsigned int length = data.length(); do { unsigned int r = data.find('\r', start); unsigned int n = data.find('\n', start); stop = std::min(std::min(r, n), length); strings.push_back(data.substr(start, stop - start)); start = stop; while(start < length && (data[start] == '\r' || data[start] == '\n')) start++; } while(start < length); bool success = false; for(Strings::const_iterator i = strings.begin(); i != strings.end(); i++) { try { std::string filename = *i; if(filename.find("file://") == 0) filename.replace(0, 7, ""); debug << "\"" << filename << "\"" << std::endl; new_image_frame(filename, false); success = true; // ok if at least one works } catch (const std::exception&) { debug << "failed to open" << std::endl; } } context->drag_finish(success, false, time); } void DocumentView::update_handles() { Pagent *pagent; // Show handles if there is one and only one selected object and it // is on the current page.and it isn't locked if(!document || document->selected().size() != 1 || !get_page()->has_child(pagent = document->selected().front()) || pagent->get_lock()) { handle_group.hide(); return; } handle_group.show(); // Make sure we connect only to the currently selected pagent if(last_selected != pagent) { last_selected = pagent; selected_geometry_connection.disconnect(); selected_props_connection.disconnect(); selected_geometry_connection = pagent->geometry_changed_signal.connect (sigc::mem_fun(*this, &DocumentView::update_handles)); // this is for noticing if the pagent is being locked/unlocked selected_props_connection = pagent->props_changed_signal.connect (sigc::mem_fun(*this, &DocumentView::update_handles)); } ResizeHandles pos = get_resize_handles(*pagent); assert(pos.size() == 8); const float hs = scr2pt(int(config.ReshapeBoxSize.values.front()) / 2); for(int i = 0; i < 8; i++) { handles[i]->property_x1() = pos[i].x - hs; handles[i]->property_x2() = pos[i].x + hs; handles[i]->property_y1() = -(pos[i].y - hs); handles[i]->property_y2() = -(pos[i].y + hs); } } const Gdk::Color& DocumentView::get_color(Color::Id color) const { switch(color) { case Color::bg: return white; case Color::frame: return black; case Color::locked: return gray; case Color::guide: return red; case Color::empty: return gray; default: throw std::runtime_error("Unknown color id get_color"); } }