1 // Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>.
2 // See the file "COPYING" for licence details.
17 #include <gdkmm/pixbuf.h>
18 #include <gtkmm/main.h>
19 #include <gtkmm/window.h>
21 #include <nsGUIEvent.h>
22 #include <nsIBoxObject.h>
23 #include <nsIContent.h>
24 #include <nsIDocShell.h>
25 #include <nsIDOMAbstractView.h>
26 #include <nsIDOMDocumentEvent.h>
27 #include <nsIDOMDocumentView.h>
28 #include <nsIDOMElement.h>
29 #include <nsIDOMEventTarget.h>
30 #include <nsIDOMHTMLDocument.h>
31 #include <nsIDOMMouseEvent.h>
32 #include <nsIDOMNSDocument.h>
33 #include <nsIDOMWindow.h>
34 #include <nsIEventStateManager.h>
35 #include <nsIInterfaceRequestorUtils.h>
36 #include <nsIURI.h> // required before nsILink.h
38 #include <nsIPresContext.h>
39 #include <nsIPresShell.h>
40 #include <nsIWebBrowser.h>
43 #include "browserwidget.hpp"
44 #include "childiterator.hpp"
46 #include "framebuffer.hpp"
47 #include "linkiterator.hpp"
48 #include "pixbufs.hpp"
49 #include "stylesheets.hpp"
51 #include "xpcom_support.hpp"
53 using xpcom_support::check;
59 int left, top; // inclusive
60 int right, bottom; // exclusive
62 rectangle operator|=(const rectangle & other)
66 // use current extents unchanged
75 // find rectangle enclosing both extents
76 left = std::min(left, other.left);
77 top = std::min(top, other.top);
78 right = std::max(right, other.right);
79 bottom = std::max(bottom, other.bottom);
85 rectangle operator&=(const rectangle & other)
87 // find rectangle enclosed in both extents
88 left = std::max(left, other.left);
89 top = std::max(top, other.top);
90 right = std::max(left, std::min(right, other.right));
91 bottom = std::max(top, std::min(bottom, other.bottom));
97 return left == right || bottom == top;
101 rectangle get_elem_rect(nsIDOMNSDocument * ns_doc,
102 nsIDOMElement * elem)
106 nsCOMPtr<nsIBoxObject> box;
107 check(ns_doc->GetBoxObjectFor(elem, getter_AddRefs(box)));
109 check(box->GetScreenX(&result.left));
110 check(box->GetScreenY(&result.top));
111 check(box->GetWidth(&width));
112 check(box->GetHeight(&height));
113 result.right = result.left + width;
114 result.bottom = result.top + height;
116 for (ChildIterator it = ChildIterator(elem), end; it != end; ++it)
118 nsCOMPtr<nsIDOMNode> child_node(*it);
120 if (check(child_node->GetNodeType(&child_type)),
121 child_type == nsIDOMNode::ELEMENT_NODE)
123 nsCOMPtr<nsIDOMElement> child_elem(
124 do_QueryInterface(child_node));
125 result |= get_elem_rect(ns_doc, child_elem);
132 class WebDvdWindow : public Gtk::Window
135 WebDvdWindow(int width, int height);
136 void add_page(const std::string & uri);
139 void add_video(const std::string & uri);
140 void load_next_page();
141 void on_net_state_change(const char * uri, gint flags, guint status);
142 void save_screenshot();
143 void process_links(nsIPresShell * pres_shell,
144 nsIPresContext * pres_context,
145 nsIDOMWindow * dom_window);
146 void generate_dvdauthor_file();
148 enum ResourceType { page_resource, video_resource };
149 typedef std::pair<ResourceType, int> ResourceEntry;
151 BrowserWidget browser_widget_;
152 nsCOMPtr<nsIStyleSheet> stylesheet_;
153 std::queue<std::string> page_queue_;
154 std::map<std::string, ResourceEntry> resource_map_;
155 std::vector<std::vector<std::string> > page_links_;
156 std::vector<std::string> video_paths_;
158 int pending_req_count_;
160 std::auto_ptr<link_state> link_state_;
163 WebDvdWindow::WebDvdWindow(int width, int height)
164 : width_(width), height_(height),
165 stylesheet_(load_css("file://" WEBDVD_LIB_DIR "/webdvd.css")),
167 pending_req_count_(0)
169 set_default_size(width, height);
170 add(browser_widget_);
171 browser_widget_.show();
172 browser_widget_.signal_net_state().connect(
173 SigC::slot(*this, &WebDvdWindow::on_net_state_change));
176 void WebDvdWindow::add_page(const std::string & uri)
178 if (resource_map_.insert(
179 std::make_pair(uri, ResourceEntry(page_resource, 0)))
182 page_queue_.push(uri);
188 void WebDvdWindow::add_video(const std::string & uri)
190 if (resource_map_.insert(
191 std::make_pair(uri, ResourceEntry(video_resource,
192 video_paths_.size() + 1)))
195 // FIXME: Should accept some slightly different URI prefixes
196 // (e.g. file://localhost/) and decode any URI-escaped
197 // characters in the path.
198 assert(uri.compare(0, 8, "file:///") == 0);
199 video_paths_.push_back(uri.substr(7));
203 void WebDvdWindow::load_next_page()
207 assert(!page_queue_.empty());
208 const std::string & uri = page_queue_.front();
209 std::cout << "loading " << uri << std::endl;
211 std::size_t page_count = page_links_.size();
212 resource_map_[uri].second = ++page_count;
213 page_links_.resize(page_count);
214 browser_widget_.load_uri(uri);
217 void WebDvdWindow::on_net_state_change(const char * uri,
218 gint flags, guint status)
224 } action = process_nothing;
226 if (flags & GTK_MOZ_EMBED_FLAG_IS_REQUEST)
228 if (flags & GTK_MOZ_EMBED_FLAG_START)
229 ++pending_req_count_;
230 if (flags & GTK_MOZ_EMBED_FLAG_STOP)
232 assert(pending_req_count_ != 0);
233 --pending_req_count_;
235 if (pending_req_count_ == 0 && link_state_.get())
236 action = process_current_link;
239 if (flags & GTK_MOZ_EMBED_FLAG_STOP
240 && flags & GTK_MOZ_EMBED_FLAG_IS_WINDOW)
241 action = process_new_page;
243 if (action != process_nothing)
245 assert(loading_ && !page_queue_.empty());
246 assert(pending_req_count_ == 0);
252 nsCOMPtr<nsIWebBrowser> browser(
253 browser_widget_.get_browser());
254 nsCOMPtr<nsIDocShell> doc_shell(do_GetInterface(browser));
256 nsCOMPtr<nsIPresShell> pres_shell;
257 check(doc_shell->GetPresShell(getter_AddRefs(pres_shell)));
258 nsCOMPtr<nsIPresContext> pres_context;
259 check(doc_shell->GetPresContext(
260 getter_AddRefs(pres_context)));
261 nsCOMPtr<nsIDOMWindow> dom_window;
262 check(browser->GetContentDOMWindow(
263 getter_AddRefs(dom_window)));
265 if (action == process_new_page)
267 apply_style_sheet(stylesheet_, pres_shell);
270 process_links(pres_shell, pres_context, dom_window);
271 if (!link_state_.get())
274 if (page_queue_.empty())
276 generate_dvdauthor_file();
283 catch (std::exception & e)
285 std::cerr << "Fatal error";
286 if (!page_queue_.empty())
287 std::cerr << " while processing <" << page_queue_.front()
289 std::cerr << ": " << e.what() << "\n";
295 void WebDvdWindow::save_screenshot()
298 std::sprintf(filename, "page_%06d_back.png", page_links_.size());
299 Glib::RefPtr<Gdk::Window> window(get_window());
301 window->process_updates(true);
302 std::cout << "saving " << filename << std::endl;
303 Gdk::Pixbuf::create(Glib::RefPtr<Gdk::Drawable>(window),
304 window->get_colormap(),
305 0, 0, 0, 0, width_, height_)
306 ->save(filename, "png");
309 struct WebDvdWindow::link_state
311 Glib::RefPtr<Gdk::Pixbuf> diff_pixbuf;
313 std::ofstream spumux_file;
316 LinkIterator links_it, links_end;
320 Glib::RefPtr<Gdk::Pixbuf> norm_pixbuf;
323 void WebDvdWindow::process_links(nsIPresShell * pres_shell,
324 nsIPresContext * pres_context,
325 nsIDOMWindow * dom_window)
327 Glib::RefPtr<Gdk::Window> window(get_window());
330 nsCOMPtr<nsIDOMDocument> basic_doc;
331 check(dom_window->GetDocument(getter_AddRefs(basic_doc)));
332 nsCOMPtr<nsIDOMNSDocument> ns_doc(do_QueryInterface(basic_doc));
334 nsCOMPtr<nsIEventStateManager> event_state_man(
335 pres_context->EventStateManager()); // does not AddRef
336 assert(event_state_man);
337 nsCOMPtr<nsIDOMDocumentEvent> event_factory(
338 do_QueryInterface(basic_doc));
339 assert(event_factory);
340 nsCOMPtr<nsIDOMDocumentView> doc_view(do_QueryInterface(basic_doc));
342 nsCOMPtr<nsIDOMAbstractView> view;
343 check(doc_view->GetDefaultView(getter_AddRefs(view)));
345 // Set up or recover our iteration state.
346 std::auto_ptr<link_state> state(link_state_);
349 state.reset(new link_state);
351 state->diff_pixbuf = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB,
353 8, // bits_per_sample
356 char spumux_filename[20];
357 std::sprintf(spumux_filename,
358 "page_%06d.spumux", page_links_.size());
359 state->spumux_file.open(spumux_filename);
360 state->spumux_file <<
363 " <spu force='yes' start='00:00:00.00'\n"
364 " highlight='page_" << std::setfill('0')
365 << std::setw(6) << page_links_.size()
367 " select='page_" << std::setfill('0')
368 << std::setw(6) << page_links_.size()
372 state->links_it = LinkIterator(basic_doc);
373 state->link_changing = false;
376 rectangle window_rect = { 0, 0, width_, height_ };
378 for (/* no initialisation */;
379 state->links_it != state->links_end;
382 nsCOMPtr<nsIDOMNode> node(*state->links_it);
384 // Find the link URI.
385 nsCOMPtr<nsILink> link(do_QueryInterface(node));
387 nsCOMPtr<nsIURI> uri;
388 check(link->GetHrefURI(getter_AddRefs(uri)));
389 std::string uri_string;
391 nsCString uri_ns_string;
392 check(uri->GetSpec(uri_ns_string));
393 uri_string.assign(uri_ns_string.BeginReading(),
394 uri_ns_string.EndReading());
396 std::string uri_sans_fragment(uri_string, 0, uri_string.find('#'));
398 // Is this a new link?
399 if (!state->link_changing)
401 // Find a rectangle enclosing the link and clip it to the
403 nsCOMPtr<nsIDOMElement> elem(do_QueryInterface(node));
405 state->link_rect = get_elem_rect(ns_doc, elem);
406 state->link_rect &= window_rect;
408 if (state->link_rect.empty())
410 std::cerr << "Ignoring invisible link to "
411 << uri_string << "\n";
417 if (state->link_num >= dvd::menu_buttons_max)
419 if (state->link_num == dvd::menu_buttons_max)
420 std::cerr << "No more than " << dvd::menu_buttons_max
421 << " buttons can be placed on a page\n";
422 std::cerr << "Ignoring link to " << uri_string << "\n";
426 // Check whether this is a link to a video or a page then
427 // add it to the known resources if not already seen.
429 check(uri->GetPath(path));
430 // FIXME: This is a bit of a hack. Perhaps we could decide
431 // later based on the MIME type determined by Mozilla?
432 if (path.Length() > 4
433 && std::strcmp(path.EndReading() - 4, ".vob") == 0)
436 check(uri->SchemeIs("file", &is_file));
439 std::cerr << "Links to video must use the file:"
443 add_video(uri_sans_fragment);
447 add_page(uri_sans_fragment);
450 nsCOMPtr<nsIContent> content(do_QueryInterface(node));
452 nsCOMPtr<nsIDOMEventTarget> event_target(
453 do_QueryInterface(node));
454 assert(event_target);
456 state->norm_pixbuf = Gdk::Pixbuf::create(
457 Glib::RefPtr<Gdk::Drawable>(window),
458 window->get_colormap(),
459 state->link_rect.left,
460 state->link_rect.top,
463 state->link_rect.right - state->link_rect.left,
464 state->link_rect.bottom - state->link_rect.top);
466 nsCOMPtr<nsIDOMEvent> event;
467 check(event_factory->CreateEvent(
468 NS_ConvertASCIItoUTF16("MouseEvents"),
469 getter_AddRefs(event)));
470 nsCOMPtr<nsIDOMMouseEvent> mouse_event(
471 do_QueryInterface(event));
473 check(mouse_event->InitMouseEvent(
474 NS_ConvertASCIItoUTF16("mouseover"),
478 0, // detail: mouse click count
479 state->link_rect.left, // screenX
480 state->link_rect.top, // screenY
481 state->link_rect.left, // clientX
482 state->link_rect.top, // clientY
483 false, false, false, false, // qualifiers
484 0, // button: left (or primary)
485 0)); // related target
487 check(event_target->DispatchEvent(mouse_event,
489 check(event_state_man->SetContentState(content,
490 NS_EVENT_STATE_HOVER));
492 pres_shell->FlushPendingNotifications(true);
494 // We may have to exit and wait for image loading
495 // to complete, at which point we will be called
497 if (pending_req_count_ > 0)
499 state->link_changing = true;
505 window->process_updates(true);
507 Glib::RefPtr<Gdk::Pixbuf> changed_pixbuf(
509 Glib::RefPtr<Gdk::Drawable>(window),
510 window->get_colormap(),
511 state->link_rect.left,
512 state->link_rect.top,
515 state->link_rect.right - state->link_rect.left,
516 state->link_rect.bottom - state->link_rect.top));
521 state->link_rect.left,
522 state->link_rect.top,
523 state->link_rect.right - state->link_rect.left,
524 state->link_rect.bottom - state->link_rect.top);
526 state->spumux_file <<
527 " <button x0='" << state->link_rect.left << "'"
528 " y0='" << state->link_rect.top << "'"
529 " x1='" << state->link_rect.right - 1 << "'"
530 " y1='" << state->link_rect.bottom - 1 << "'/>\n";
532 // Add to the page's links, ignoring any fragment (for now).
533 page_links_.back().push_back(uri_sans_fragment);
536 quantise_rgba_pixbuf(state->diff_pixbuf, dvd::button_n_colours);
539 std::sprintf(filename, "page_%06d_links.png", page_links_.size());
540 std::cout << "saving " << filename << std::endl;
541 state->diff_pixbuf->save(filename, "png");
543 state->spumux_file <<
549 void generate_page_dispatch(std::ostream &, int indent,
550 int first_page, int last_page);
552 void WebDvdWindow::generate_dvdauthor_file()
554 std::ofstream file("webdvd.dvdauthor");
556 // We generate code that uses registers in the following way:
558 // g0: link destination (when jumping to menu 1), then scratch
559 // g1: current location
560 // g2-g11: location history (g2 = most recent)
561 // g12: location that last linked to a video
563 // All locations are divided into two bitfields: the least
564 // significant 10 bits are a page/menu number and the most
565 // significant 6 bits are a link/button number. This is
566 // chosen for compatibility with the encoding of the s8
567 // (button) register.
569 static const int link_mult = dvd::reg_s8_button_mult;
570 static const int page_mask = link_mult - 1;
571 static const int link_mask = (1 << dvd::reg_bits) - link_mult;
578 for (std::size_t page_num = 1;
579 page_num <= page_links_.size();
582 std::vector<std::string> & page_links =
583 page_links_[page_num - 1];
587 // This is the first page (root menu) which needs to
588 // include initialisation and dispatch code.
591 " <pgc entry='title'>\n"
593 // Has the location been set yet?
596 // Initialise the current location to first link on
598 " g1 = " << 1 * link_mult + 1 << ";\n"
602 // Has the user selected a link?
605 // First update the history.
606 // Does link go to the last page in the history?
607 " if (((g0 ^ g2) & " << page_mask
609 // It does; we treat this as going back and pop the old
610 // location off the history stack into the current
611 // location. Clear the free stack slot.
613 " g1 = g2; g2 = g3; g3 = g4; g4 = g5;\n"
614 " g5 = g6; g6 = g7; g7 = g8; g8 = g9;\n"
615 " g9 = g10; g10 = g11; g11 = 0;\n"
618 // Link goes to some other page, so push current
619 // location onto the history stack and set the current
620 // location to be exactly the target location.
622 " g11 = g10; g10 = g9; g9 = g8; g8 = g7;\n"
623 " g7 = g6; g6 = g5; g5 = g4; g4 = g3;\n"
624 " g3 = g2; g2 = g1; g1 = g0;\n"
627 // Find the target page number.
628 " g0 = g1 & " << page_mask << ";\n";
629 // There seems to be no way to perform a computed jump,
630 // so we generate all possible jumps and a binary search
631 // to select the correct one.
632 generate_page_dispatch(file, 12, 1, page_links_.size());
636 else // page_num != 1
644 // Clear link indicator and highlight the
645 // appropriate link/button.
646 " g0 = 0; s8 = g1 & " << link_mask << ";\n"
649 << std::setfill('0') << std::setw(6) << page_num
652 for (std::size_t link_num = 1;
653 link_num <= page_links.size();
658 // Update current location.
659 " g1 = " << link_num * link_mult + page_num << ";";
661 // Jump to appropriate resource.
662 const ResourceEntry & resource_loc =
663 resource_map_[page_links[link_num - 1]];
664 if (resource_loc.first == page_resource)
666 " g0 = " << 1 * link_mult + resource_loc.second << ";"
668 else if (resource_loc.first == video_resource)
669 file << " jump title " << resource_loc.second << ";";
671 file << " </button>\n";
681 // Generate a titleset for each video. This appears to make
682 // jumping to titles a whole lot simpler.
683 for (std::size_t video_num = 1;
684 video_num <= video_paths_.size();
689 // Generate a dummy menu so that the menu button on the
690 // remote control will work.
692 " <pgc entry='root'>\n"
693 " <pre> jump vmgm menu; </pre>\n"
698 // Record calling page/menu.
699 " <pre> g12 = g1; </pre>\n"
700 // FIXME: Should XML-escape the path
701 " <vob file='" << video_paths_[video_num - 1]
703 // If page/menu location has not been changed during the
704 // video, change the location to be the following
705 // link/button when returning to it. In any case,
706 // return to a page/menu.
707 " <post> if (g1 eq g12) g1 = g1 + " << link_mult
708 << "; call menu; </post>\n"
718 void generate_page_dispatch(std::ostream & file, int indent,
719 int first_page, int last_page)
721 if (first_page == 1 && last_page == 1)
723 // The dispatch code is *on* page 1 so we must not dispatch to
724 // page 1 since that would cause an infinite loop. This case
725 // should be unreachable if there is more than one page due
726 // to the following case.
728 else if (first_page == 1 && last_page == 2)
730 // dvdauthor doesn't allow empty blocks or null statements so
731 // when selecting between pages 1 and 2 we don't use an "else"
732 // part. We must use braces so that a following "else" will
733 // match the right "if".
734 file << std::setw(indent) << "" << "{\n"
735 << std::setw(indent) << "" << "if (g0 eq 2)\n"
736 << std::setw(indent + 2) << "" << "jump menu 2;\n"
737 << std::setw(indent) << "" << "}\n";
739 else if (first_page == last_page)
741 file << std::setw(indent) << ""
742 << "jump menu " << first_page << ";\n";
746 int middle = (first_page + last_page) / 2;
747 file << std::setw(indent) << "" << "if (g0 le " << middle << ")\n";
748 generate_page_dispatch(file, indent + 2, first_page, middle);
749 file << std::setw(indent) << "" << "else\n";
750 generate_page_dispatch(file, indent + 2, middle + 1, last_page);
756 int main(int argc, char ** argv)
759 int width = video::pal_oscan_width, height = video::pal_oscan_height;
760 for (int i = 1; i < argc - 1; ++i)
761 if (std::strcmp(argv[i], "-geometry") == 0)
763 std::sscanf(argv[i + 1], "%dx%d", &width, &height);
766 // A depth of 24 results in 8 bits each for RGB components, which
767 // translates into "enough" bits for YUV components.
768 const int depth = 24;
772 // Spawn Xvfb and set env variables so that Xlib will use it
773 FrameBuffer fb(width, height, depth);
774 setenv("XAUTHORITY", fb.get_x_authority().c_str(), true);
775 setenv("DISPLAY", fb.get_x_display().c_str(), true);
777 // Initialise Gtk and Mozilla
778 Gtk::Main kit(argc, argv);
779 BrowserWidget::init();
781 WebDvdWindow window(width, height);
782 for (int argi = 1; argi < argc; ++argi)
783 window.add_page(argv[argi]);
785 window.add_page("about:");
786 Gtk::Main::run(window);
788 catch (std::exception & e)
790 std::cerr << "Fatal error: " << e.what() << "\n";