// K-3D
// Copyright (c) 1995-2004, Timothy M. Shead
//
// Contact: tshead@k-3d.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public
// License along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/** \file
		\brief Implements the document_window class, which encapsulates an open K-3D document's UI
		\author Tim Shead (tshead@k-3d.com)
*/

#include "button.h"
#include "cursors.h"
#include "dag_control.h"
#include "dag_window.h"
#include "dnd.h"
#include "document_window.h"
#include "filter_selector.h"
#include "menu_item.h"
#include "toggle_button.h"
#include "viewport_window.h"

#include <k3dsdk/application.h>
#include <k3dsdk/basic_math.h>
#include <k3dsdk/classes.h>
#include <k3dsdk/file_filter.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/imesh_sink.h>
#include <k3dsdk/imesh_source.h>
#include <k3dsdk/igeometry_read_format.h>
#include <k3dsdk/igeometry_write_format.h>
#include <k3dsdk/iplugin_factory.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/iviewport.h>
#include <k3dsdk/iviewport_host.h>
#include <k3dsdk/mesh.h>
#include <k3dsdk/objects.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/property.h>
#include <k3dsdk/objects.h>
#include <k3dsdk/scripting.h>
#include <k3dsdk/selection.h>
#include <k3dsdk/serialization.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/time_source.h>
#include <k3dsdk/user_interface.h>
#include <k3dsdk/viewport.h>

#include <sdpgtk/sdpgtkfileselector.h>
#include <sdpgtk/sdpgtkevents.h>

#include <boost/filesystem/fstream.hpp>

// We have an unfortunate clash with X ...
#ifdef RootWindow
#undef RootWindow
#endif // RootWindow

namespace
{

/////////////////////////////////////////////////////////////////////////////
// insert_factories

/// Inserts plugin factories into the user interface
class insert_factories
{
public:
	insert_factories(sdpGtkCList ListControl) :
		m_list_control(ListControl)
	{
	}

	void operator()(k3d::iplugin_factory* Factory)
	{
		if(Factory->default_category() != "Tools" && Factory->default_category() != "Objects")
			return;
	
		const std::string name = Factory->name() +
			(k3d::iplugin_factory::EXPERIMENTAL == Factory->quality() ? " (Experimental)" : "") +
			(k3d::iplugin_factory::DEPRECATED == Factory->quality() ? " (Deprecated)" : "");

		const char* labels[] = { name.c_str() };
		const int row = m_list_control.Append(labels);
		m_list_control.SetRowData(row, dynamic_cast<k3d::iunknown*>(Factory));

		if(k3d::iplugin_factory::EXPERIMENTAL == Factory->quality())
			{
				GdkColor color;
				color.red = 0x0000;
				color.green = 0x0000;
				color.blue = 0xffff;
				gdk_colormap_alloc_color(gdk_colormap_get_system(), &color, FALSE, TRUE);

				m_list_control.SetForegroundColor(row, &color);
			}
		else if(k3d::iplugin_factory::DEPRECATED == Factory->quality())
			{
				GdkColor color;
				color.red = 0xffff;
				color.green = 0x0000;
				color.blue = 0x0000;
				gdk_colormap_alloc_color(gdk_colormap_get_system(), &color, FALSE, TRUE);

				m_list_control.SetForegroundColor(row, &color);
			}
	}

private:
	sdpGtkCList m_list_control;
};

const std::string control_undostack = "undostack";
const std::string control_animationscrollbar = "animationscrollbar";
const std::string control_animationframe = "animationframe";
const std::string control_focusin = "focusin";
const std::string control_dragdataget = "dragdataget";
const std::string control_pluginsclicked = "pluginsclicked";
const std::string control_highlight_plugin = "highlight_plugin";

} // namespace

namespace k3d
{

/////////////////////////////////////////////////////////////////////////////
// document_window

document_window::document_window(idocument& Document) :
	base(dynamic_cast<icommand_node*>(&Document), "window", new options_window_geometry_store()),
	property_collection(Document.dag()),
	m_document(Document),
	m_dag_control(0),
	m_animation_timer(0),
	m_ignore_animation_timer(false),
	m_playback_mode(init_name("playback_mode") + init_description("Playback mode [enumeration]") + init_value(STOP) + init_document(Document)),
	m_rewind(init_value(false)),
	m_reverse_loop_play(init_value(false)),
	m_reverse_play(init_value(false)),
	m_stop(init_value(true)),
	m_play(init_value(false)),
	m_loop_play(init_value(false)),
	m_fast_forward(init_value(false)),
	m_ignore_playback_change(false)
{
	// Register public properties
	register_property(m_playback_mode);

	// We want to be asked before closing the document ...
	m_document.safe_to_close_signal().connect(SigC::slot(*this, &document_window::safe_to_close_document));

	// We want to be notified when changes are made to the document
	m_document.close_signal().connect(SigC::slot(*this, &document_window::on_document_closed));
	m_document.title_signal().connect(SigC::slot(*this, &document_window::on_document_title_changed));

	// We want to be notified whenever the playback mode changes
	m_playback_mode.changed_signal().connect(SigC::slot(*this, &document_window::on_playback_mode_changed));

	return_if_fail(LoadGTKMLTemplate("document_window.gtkml"));

	if(get_menu_item("file_new_dag_window"))
		get_menu_item("file_new_dag_window")->signal_activate().connect(SigC::slot(*this, &document_window::on_file_new_dag_window));
	if(get_menu_item("file_new_viewport"))
		get_menu_item("file_new_viewport")->signal_activate().connect(SigC::slot(*this, &document_window::on_file_new_viewport));
	if(get_menu_item("file_export"))
		get_menu_item("file_export")->signal_activate().connect(SigC::slot(*this, &document_window::on_file_export));
	if(get_menu_item("file_import"))
		get_menu_item("file_import")->signal_activate().connect(SigC::slot(*this, &document_window::on_file_import));
	if(get_menu_item("file_close"))
		get_menu_item("file_close")->signal_activate().connect(SigC::slot(*this, &document_window::on_file_close));
	if(get_menu_item("edit_undo"))
		get_menu_item("edit_undo")->signal_activate().connect(SigC::slot(*this, &document_window::on_edit_undo));
	if(get_menu_item("edit_redo"))
		get_menu_item("edit_redo")->signal_activate().connect(SigC::slot(*this, &document_window::on_edit_redo));
	if(get_menu_item("tools_play_script"))
		get_menu_item("tools_play_script")->signal_activate().connect(SigC::slot(*this, &document_window::on_tools_play_script));

	if(get_button("undo"))
		get_button("undo")->signal_activate().connect(SigC::slot(*this, &document_window::on_edit_undo));
	if(get_button("redo"))
		get_button("redo")->signal_activate().connect(SigC::slot(*this, &document_window::on_edit_redo));

	if(get_button("create_object"))
		get_button("create_object")->signal_activate().connect(SigC::slot(*this, &document_window::OnCreatePlugin));

	if(get_toggle_button("rewind"))
		get_toggle_button("rewind")->attach(toggle_button::proxy(m_rewind), 0, "");
	if(get_toggle_button("loopreverseplay"))
		get_toggle_button("loopreverseplay")->attach(toggle_button::proxy(m_reverse_loop_play), 0, "");
	if(get_toggle_button("reverseplay"))
		get_toggle_button("reverseplay")->attach(toggle_button::proxy(m_reverse_play), 0, "");
	if(get_toggle_button("stop"))
		get_toggle_button("stop")->attach(toggle_button::proxy(m_stop), 0, "");
	if(get_toggle_button("play"))
		get_toggle_button("play")->attach(toggle_button::proxy(m_play), 0, "");
	if(get_toggle_button("loopplay"))
		get_toggle_button("loopplay")->attach(toggle_button::proxy(m_loop_play), 0, "");
	if(get_toggle_button("fastforward"))
		get_toggle_button("fastforward")->attach(toggle_button::proxy(m_fast_forward), 0, "");

	m_rewind.changed_signal().connect(SigC::slot(*this, &document_window::on_rewind));
	m_reverse_loop_play.changed_signal().connect(SigC::slot(*this, &document_window::on_reverse_loop_play));
	m_reverse_play.changed_signal().connect(SigC::slot(*this, &document_window::on_reverse_play));
	m_stop.changed_signal().connect(SigC::slot(*this, &document_window::on_stop));
	m_play.changed_signal().connect(SigC::slot(*this, &document_window::on_play));
	m_loop_play.changed_signal().connect(SigC::slot(*this, &document_window::on_loop_play));
	m_fast_forward.changed_signal().connect(SigC::slot(*this, &document_window::on_fast_forward));

	// Create the DAG control ...
	m_dag_control = new dag_control::control(m_document, this, "dag");
	Container("dag").Attach(m_dag_control->root_widget());

	// Create an event for our animation scrollbar ...
	sdpGtkAdjustment adjustment = Scrollbar(control_animationscrollbar).Adjustment();
	MapEvent("value-changed", control_animationscrollbar, false, adjustment, true);

	// Update our titlebar ...
	UpdateTitlebar();

	// We want to be notified whenever the undo/redo stack is modified ...
	m_document.state_recorder().stack_changed_signal().connect(SigC::slot(*this, &document_window::on_undo_stack_changed));
	m_document.state_recorder().mark_saved_signal().connect(SigC::slot(*this, &document_window::on_undo_mark_saved));

	// Setup plugins tab ...
	PluginsList().SetDragSource(GDK_BUTTON1_MASK, &dnd_create_object_target(), 1, GDK_ACTION_COPY);
	MapEvent("drag-data-get", control_dragdataget, false, PluginsList(), true);

	PluginsList().Clear();

	// Insert plugin types the new way ...
	std::for_each(application().plugins().begin(), application().plugins().end(), insert_factories(PluginsList()));

	PluginsList().Sort();

	// Update remaining state ...
	CallUpdateControls();

	// Make ourselves visible ...
	Show();
}

document_window::~document_window()
{
	// Kill the animation timer ...
	if(m_animation_timer)
		gtk_timeout_remove(m_animation_timer);

	delete m_dag_control;
}

bool document_window::safe_to_close_document()
{
	// If we don't have a user interface, go ahead ...
	if(!application().user_interface())
		return true;

	// If the UI is in batch mode, close away ... !
	if(application().user_interface()->batch_mode())
		return true;

	// If there aren't any unsaved changes, we're good to go ...
	if(!m_document.state_recorder().unsaved_changes())
		return true;

	// Prompt the user to save changes ...
	std::vector<std::string> buttons;
	buttons.push_back("Save Changes");
	buttons.push_back("Discard Changes");
	buttons.push_back("Cancel");

	const std::string message = "Close " + m_document.title() + "? Unsaved changes will be lost (No Undo)";

	switch(application().user_interface()->query_message(message, "Close Document:", 1, buttons))
		{
			case 0:
				return false;
			case 1:
				return on_file_save();
			case 2:
				return true;
			case 3:
				return false;
		}

	return false;
}

void document_window::on_document_closed()
{
	delete this;
}

void document_window::on_document_title_changed()
{
	UpdateTitlebar();
}

void document_window::on_undo_stack_changed()
{
	UpdateUndoStack();
}

void document_window::on_undo_mark_saved()
{
	UpdateUndoStack();
}

void document_window::OnEvent(sdpGtkEvent* Event)
{
	if(Event->Name() == control_undostack)
		OnUndoStack(Event);
	else if(Event->Name() == control_animationscrollbar)
		OnAnimationScrollbar();
	else if(Event->Name() == control_animationframe)
		OnAnimationFrame();
	else if(Event->Name() == control_pluginsclicked)
		OnPluginsClicked(Event);
	else if(Event->Name() == control_dragdataget)
		OnDragDataGet(Event);
	else
		base::OnEvent(Event);
}

void document_window::OnDelete(sdpGtkEvent* Event)
{
	// Cancel the normal window close ...
	((sdpGtkEventWidgetDeleteEvent*)Event)->SetResult(true);
	on_file_close();
}

void document_window::on_file_new_dag_window()
{
	new dag_window(m_document);
}

void document_window::on_file_new_viewport()
{
	return_if_fail(create_document_plugin("Viewport", m_document, "Viewport"));
}

void document_window::on_file_import()
{
	// Make sure we have some file formats to choose from ...
	if(0 == plugins<igeometry_read_format>().size())
		{
			error_message("No geometry import file filters available", "Import Geometry:");
			return;
		}

	// Prompt the user for a file ...
	boost::filesystem::path filepath;
	if(!get_file_path("geometry", "Import Geometry:", false, boost::filesystem::path(), filepath))
		return;

	// Prompt the user to choose an import filter ...
	filter_selector<igeometry_read_format> filterselector("Select Import File Format:", filepath);
	if(!filterselector.do_modal(RootWindow()))
		return;

	// Get the import filter that the user chose (could be NULL, if they chose "automatic" and there wasn't a match)
	auto_ptr<igeometry_read_format> filter(filterselector.filter());
	if(!filter.get())
		{
			error_message(
				"Couldn't find a filter for this file.  If you chose \"Automatic\" as the filter type,\n"
				"try choosing a specific filter that matches the type of file you're importing.",
				"Import Geometry:");
			return;
		}

	// Record undo/redo information ...
	record_state_change_set changeset(m_document, "Import " + filepath.native_file_string());

	// Import the file ...
	if(!import_file(m_document, *filter, filepath))
		{
			error_message(
				"Error importing geometry file.  If you chose \"Automatic\" as the filter type,\n"
				"try choosing a specific filter that matches the type of file you're importing.",
				"Import Geometry:");
			return;
		}

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

void document_window::on_file_export()
{
	// Make sure we have some file formats to choose from ...
	if(0 == plugins<igeometry_write_format>().size())
		{
			error_message("No geometry export file filters available", "Export Geometry:");
			return;
		}

	// Prompt the user for a file to export ...
	boost::filesystem::path filepath;
	if(!get_file_path("geometry", "Export Geometry:", true, boost::filesystem::path(), filepath))
		return;

	// Prompt the user to choose an export filter ...
	filter_selector<igeometry_write_format> filterselector("Select Export File Format:", filepath);
	if(!filterselector.do_modal(RootWindow()))
		return;

	// Get the import filter that the user chose (could be NULL, if they chose "automatic" and there wasn't a match)
	auto_ptr<igeometry_write_format> filter(filterselector.filter());
	if(!filter.get())
		{
			error_message(
				"Couldn't find a filter for this file.  If you chose \"Automatic\" as the filter type,\n"
				"try choosing a specific filter that matches the file type you want to export.",
				"Export Geometry:");
			return;
		}

	// Export the file ...
	if(!export_file(m_document, *filter, filepath))
		{
			error_message(
				"Error exporting geometry file.  If you chose \"Automatic\" as the filter type,\n"
				"try choosing a specific filter that matches the file type you want to export.",
				"Export Geometry:");
			return;
		}

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

bool document_window::on_file_save()
{
	// If the user hasn't chosen a name, yet, turn it into a "Save As" ...
	if(m_document.path().empty())
		return on_file_save_as();

	wait_cursor wc(RootWidget());
	
	return m_document.save(m_document.path());
}

bool document_window::on_file_save_as()
{
	boost::filesystem::path filepath;
	if(!get_file_path("document", "Save K-3D Document As:", true, boost::filesystem::path(), filepath))
		return false;

	wait_cursor wc(RootWidget());
	
	return m_document.save(filepath);
}

void document_window::on_file_close()
{
	// Don't close without asking!
	if(m_document.safe_to_close_signal().emit())
		application().close_document(m_document);
}

void document_window::on_edit_undo()
{
	// Ignore it if there aren't any actions to undo ...
	if(!m_document.state_recorder().undo_count())
		return;

	// Tell the document to undo the last action ...
	m_document.state_recorder().undo();

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

void document_window::on_edit_redo()
{
	// Ignore it if there aren't any actions to redo ...
	if(!m_document.state_recorder().redo_count())
		return;

	// Tell the document to redo the last action ...
	m_document.state_recorder().redo();

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}


void document_window::on_tools_play_script()
{
	boost::filesystem::path filepath;
	if(!get_file_path("script", "Play K-3D Script:", false, boost::filesystem::path(), filepath))
		return;

	// Make it happen ...
	bool recognized = false;
	bool executed = false;
	boost::filesystem::ifstream file(filepath);

	execute_script(file, filepath.native_file_string(), iscript_engine::context_t(1, &m_document), recognized, executed);

	if(!recognized)
		{
			error_message(
				"Could not determine scripting language.  K-3D supports multiple scripting languages, but the language for this script was\n"
				"not recognized. Most K-3D script engines use some type of \"magic token\" at the beginning of a script to recognize it, e.g. \"//javascript\"\n"
				"in the first 12 characters of a script for K-3D's built-in JavaScript engine.  If you are writing a K-3D script, check the documentation\n"
				"for the scripting language you're writing in to see how to make it recognizable.",
				"Play " + filepath.native_file_string() + ":");
			return;
		}

	if(!executed)
		{
			error_message("Error executing script", "Play " + filepath.native_file_string() + ":");
			return;
		}
}

void document_window::OnUndoStack(sdpGtkEvent* Event)
{
	// Get the row we clicked on ...
	int row;
	int column;
	sdpGtkEventWidgetButtonPressEvent* event = (sdpGtkEventWidgetButtonPressEvent*)Event;
	int rowselected = CList(control_undostack).GetHitInfo(int(event->Event()->x), int(event->Event()->y), &row, &column);

	// Cache a reference to the state recorder ...
	istate_recorder& staterecorder = m_document.state_recorder();

	// If the user clicked on a row, call Undo() or Redo() until we reach the selected state ...
	if(rowselected)
		{
			while((unsigned long)(row) < staterecorder.undo_count())
				staterecorder.undo();

			while((unsigned long)(row) > staterecorder.undo_count())
				staterecorder.redo();
		}

	// Otherwise, the user must have clicked after the last row, so redo everything ...
	else
		{
			while(staterecorder.redo_count())
				staterecorder.redo();
		}

	// Update the display ...
	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);

	gtk_signal_emit_stop_by_name(CList(control_undostack).Object(), "button-press-event");
}

void document_window::on_rewind()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(REWIND);
}

void document_window::on_reverse_loop_play()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(LOOP_REVERSE_PLAY);
}

void document_window::on_reverse_play()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(REVERSE_PLAY);
}

void document_window::on_stop()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(STOP);
}

void document_window::on_play()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(PLAY);
}

void document_window::on_loop_play()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(LOOP_PLAY);
}

void document_window::on_fast_forward()
{
	if(!m_ignore_playback_change)
		m_playback_mode.set_value(FAST_FORWARD);
}

void document_window::OnAnimationScrollbar()
{
	// Update our scrollbar ...
	sdpGtkAdjustment adjustment = Scrollbar(control_animationscrollbar).Adjustment();

	// If the document doesn't have a time source, we're done ...
	iproperty* const frame_rate_property = get_frame_rate(m_document);
	iwritable_property* const writable_time_property = dynamic_cast<iwritable_property*>(get_time(m_document));
	
	if(!frame_rate_property || !writable_time_property)
		return;

	const double frame_rate = boost::any_cast<double>(get_property_value(m_document.dag(), *frame_rate_property));

	writable_time_property->set_value(adjustment.Value() / frame_rate);

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

void document_window::OnAnimationFrame()
{
	// If the document doesn't have a time source, we're done ...
	iproperty* const frame_rate_property = get_frame_rate(m_document);
	iwritable_property* const writable_time_property = dynamic_cast<iwritable_property*>(get_time(m_document));
	
	if(!frame_rate_property || !writable_time_property)
		return;

	const double frame_rate = boost::any_cast<double>(get_property_value(m_document.dag(), *frame_rate_property));

	writable_time_property->set_value(atol(Entry(control_animationframe).GetText()) / frame_rate);

	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

void document_window::UpdateControls()
{
	UpdateUndoStack();
	on_playback_mode_changed();

	base::UpdateControls();
}

void document_window::UpdateTitlebar()
{
	// Get our title and stuff it in the titlebar ...
	RootWindow().SetTitle(Title().c_str());
}

std::string document_window::Title()
{
	return m_document.title();
}

/// Populates the contents of the Undo Stack with a current list of Undo-able changes
class document_windowPopulateUndoStack :
	public istate_recorder::ichange_set_visitor
{
public:
	document_windowPopulateUndoStack(sdpGtkCList List, istate_recorder& StateRecorder) :
		m_List(List)
	{
		// Setup our colors
		m_RedoForeground.red = 0x4444;
		m_RedoForeground.green = 0x4444;
		m_RedoForeground.blue = 0x4444;
		gdk_colormap_alloc_color(gdk_colormap_get_system(), &m_RedoBackground, FALSE, TRUE);

		m_RedoBackground.red = 0xffff;
		m_RedoBackground.green = 0xffff;
		m_RedoBackground.blue = 0xaaaa;
		gdk_colormap_alloc_color(gdk_colormap_get_system(), &m_RedoBackground, FALSE, TRUE);

		// Update the list ...
		m_List.Freeze();
		m_List.Clear();

		StateRecorder.visit_change_sets(*this);

		m_List.Thaw();
	}

	virtual ~document_windowPopulateUndoStack()
	{
	}

	void visit_undo_change_set(const std::string& UndoLabel, const bool Saved)
	{
		// If this is the channge set where we last saved, then append [Saved] to the name
		std::string name(UndoLabel);
		if(Saved)
			name += " [Saved]";

		m_List.Append(name.c_str());
	}

	void visit_redo_change_set(const std::string& RedoLabel, const bool Saved)
	{
		// If this is the channge set where we last saved, then append [Saved] to the name
		std::string name(RedoLabel);
		if(Saved)
			name += " [Saved]";

		m_List.Append(name.c_str());

		m_List.SetForegroundColor(m_List.RowCount() - 1, &m_RedoForeground);
		m_List.SetBackgroundColor(m_List.RowCount() - 1, &m_RedoBackground);
	}

private:
	sdpGtkCList m_List;
	GdkColor m_RedoForeground;
	GdkColor m_RedoBackground;
};

void document_window::UpdateUndoStack()
{
	// Setup the undo menu item ...
	if(m_document.state_recorder().undo_count())
		{
			get_menu_item("edit_undo")->set_text("Undo " + m_document.state_recorder().next_undo_label());
			get_menu_item("edit_undo")->set_sensitive(true);
			get_button("undo")->set_sensitive(true);
		}
	else
		{
			get_menu_item("edit_undo")->set_text("Can't Undo");
			get_menu_item("edit_undo")->set_sensitive(false);
			get_button("undo")->set_sensitive(false);
		}

	// Setup the redo menu item ...
	if(m_document.state_recorder().redo_count())
		{
			get_menu_item("edit_redo")->set_text("Redo " + m_document.state_recorder().next_redo_label());
			get_menu_item("edit_redo")->set_sensitive(true);
			get_button("redo")->set_sensitive(true);
		}
	else
		{
			get_menu_item("edit_redo")->set_text("Can't Redo");
			get_menu_item("edit_redo")->set_sensitive(false);
			get_button("redo")->set_sensitive(false);
		}

	document_windowPopulateUndoStack populateundostack(CList(control_undostack), m_document.state_recorder());
}

void document_window::on_playback_mode_changed()
{
	m_ignore_playback_change = true;

	// Set all our buttons to "un-toggled" ...
	m_rewind.set_value(false);
	m_reverse_loop_play.set_value(LOOP_REVERSE_PLAY == m_playback_mode.value());
	m_reverse_play.set_value(REVERSE_PLAY == m_playback_mode.value());
	m_stop.set_value(STOP == m_playback_mode.value());
	m_play.set_value(PLAY == m_playback_mode.value());
	m_loop_play.set_value(LOOP_PLAY == m_playback_mode.value());
	m_fast_forward.set_value(false);

	m_ignore_playback_change = false;

	// Start the animation timer ...
	if(!m_animation_timer)
		m_animation_timer = gtk_timeout_add(33, raw_animation_timer, this);
}

void document_window::update_time()
{
	// Make sure this document contains time information ...
	iproperty* const start_time_property = get_start_time(m_document);
	iproperty* const end_time_property = get_end_time(m_document);
	iproperty* const frame_rate_property = get_frame_rate(m_document);
	iproperty* const time_property = get_time(m_document);
	
	if(!start_time_property || !end_time_property || !frame_rate_property || !time_property)
		return;

	BlockAllEvents();

	// Convert from time to frames ...
	const double start_time = boost::any_cast<double>(get_property_value(m_document.dag(), *start_time_property));
	const double end_time = boost::any_cast<double>(get_property_value(m_document.dag(), *end_time_property));
	const double frame_rate = boost::any_cast<double>(get_property_value(m_document.dag(), *frame_rate_property));
	const double time = boost::any_cast<double>(get_property_value(m_document.dag(), *time_property));
	
	const double start_frame = round(frame_rate * start_time);
	const double view_frame = round(frame_rate * time);
	const double end_frame = round(frame_rate * end_time);

	// Display the frame number ...
	Entry(control_animationframe).SetText(sdpToString(view_frame));

	// Update our scrollbar ...
	const unsigned long page_size = static_cast<unsigned long>(round(frame_rate));

	sdpGtkAdjustment adjustment = Scrollbar(control_animationscrollbar).Adjustment();
	adjustment.SetLower(start_frame);
	adjustment.SetUpper(end_frame + page_size - 1);
	adjustment.SetValue(view_frame);
	adjustment.SetStepIncrement(1);
	adjustment.SetPageIncrement(page_size);
	adjustment.SetPageSize(page_size);
	adjustment.Changed();
	adjustment.ValueChanged();

	UnblockAllEvents();
}

bool document_window::SafeToClose()
{
	/// No UI, so it's safe ...
	if(!application().user_interface())
		return true;

	std::vector<std::string> buttons;
	buttons.push_back("Yes");
	buttons.push_back("No");

	return 1 == application().user_interface()->query_message("Close the document?  Unsaved changes will be lost (No Undo)", "Close Document:", 1, buttons);
}

void document_window::OnCreatePlugin()
{
	sdpGtkCList list = PluginsList();
	sdpGtkCList::Rows selection = list.GetSelectedRows();
	for(sdpGtkCList::RowIterator row = selection.begin(); row != selection.end(); row++)
		{
			iplugin_factory* const factory = dynamic_cast<iplugin_factory*>(reinterpret_cast<iunknown*>(list.GetRowData(*row)));
			if(factory)
				{
					// Create the requested object ...
					record_state_change_set changeset(m_document, "Create " + factory->name());
					const std::string object_name = unique_name(m_document.objects(), factory->name());
					iobject* const object = create_document_plugin(*factory, m_document, object_name);
					
					// If this object is a mesh source, create a MeshInstance object and attach it so it's immediately visible ...
					imesh_source* const mesh_source = dynamic_cast<imesh_source*>(object);
					if(mesh_source)
						{
							// But don't create a MeshInstance for a MeshInstance object!
							if(classes::MeshInstance() != factory->class_id())
								{
									imesh_sink* const mesh_sink = dynamic_cast<imesh_sink*>(create_document_plugin(classes::MeshInstance(), m_document, unique_name(m_document.objects(), object_name + " Instance")));
									if(mesh_sink)
										{
											idag::dependencies_t dependencies;
											dependencies.insert(std::make_pair(&mesh_sink->mesh_sink_input(), &mesh_source->mesh_source_output()));
											m_document.dag().set_dependencies(dependencies);
										}
								}
						}
						
					// If this object is a viewport, create a viewport window and attach it ...
					iviewport* const viewport = dynamic_cast<iviewport*>(object);
					if(viewport)
						{
							viewport::window* const window = new viewport::window(m_document);
							window->attach(*viewport);
						}
						
					// If the object is a viewport host, create a viewport and attach it ...
					iviewport_host* const viewport_host = dynamic_cast<iviewport_host*>(object);
					if(viewport_host)
						{
							iobject* const viewport_object = create_document_plugin("Viewport", m_document, unique_name(m_document.objects(), object->name() + " Viewport"));
							if(viewport_object)
								{
									iviewport* const viewport = dynamic_cast<iviewport*>(viewport_object);
									if(viewport)
										{
											viewport->set_host(viewport_host);
											
											iproperty* const output_mesh = get_typed_property<matrix4>(*viewport_host, "output_matrix");
											iproperty* const input_mesh = get_typed_property<matrix4>(*viewport, "input_matrix");
											if(input_mesh && output_mesh)
												{
													idag::dependencies_t dependencies;
													dependencies.insert(std::make_pair(input_mesh, output_mesh));
													m_document.dag().set_dependencies(dependencies);
												}
											
											viewport::window* const window = new viewport::window(m_document);
											window->attach(*viewport);
										}
								}
						}
					
					if(object && application().user_interface())
						application().user_interface()->show(*object);
				}
		}

	// Update the display ...
	viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
}

void document_window::OnDragDataGet(sdpGtkEvent* Event)
{
	// Sanity checks ...
	assert_warning(Event);

	sdpGtkEventWidgetDragDataGet* const event = static_cast<sdpGtkEventWidgetDragDataGet*>(Event);

	// We're going to generate a K3DML document ...
	sdpxml::Document xmldocument("k3dml");

	sdpxml::Element& application = xmldocument.Append(sdpxml::Element("application"));
	sdpxml::Element& objects = application.Append(sdpxml::Element("objects"));

	// For each selected object ...
	sdpGtkCList::Rows selection = PluginsList().GetSelectedRows();
	for(sdpGtkCList::RowIterator row = selection.begin(); row != selection.end(); row++)
		{
			iplugin_factory* const factory = dynamic_cast<iplugin_factory*>(reinterpret_cast<iunknown*>(PluginsList().GetRowData(*row)));
			assert_warning(factory);

			// Create an XML element for the object ...
			objects.Append(sdpxml::Element("object", "", sdpxml::Attribute("name", factory->name()), sdpxml::Attribute("class", sdpToString(factory->class_id()))));
		}

	// Buffer the data (since drag-and-drop is asynchronous) ...
	std::stringstream stream;
	stream << xmldocument;

	m_DNDBuffer = stream.str();

	// Hand the data off to the drag-and-drop mechanism ...
	gtk_selection_data_set(event->Selection(), GDK_SELECTION_TYPE_STRING, 8, reinterpret_cast<const unsigned char*>(m_DNDBuffer.c_str()), m_DNDBuffer.size());
}

void document_window::OnPluginsClicked(sdpGtkEvent* Event)
{
	// Sanity checks ...
	assert_warning(Event);

	// Get the row we clicked on ...
	int row;
	int column;
	sdpGtkEventWidgetButtonPressEvent* event = (sdpGtkEventWidgetButtonPressEvent*)Event;
	PluginsList().GetHitInfo(int(event->Event()->x), int(event->Event()->y), &row, &column);

	if(-1 == row)
		return;

	iplugin_factory* const factory = dynamic_cast<iplugin_factory*>(reinterpret_cast<iunknown*>(PluginsList().GetRowData(row)));
	if(factory)
		record_command(*this, icommand_node::command_t::USER_INTERFACE, control_highlight_plugin, factory->name());

	if(event->Event()->button == 1 && event->Event()->type == GDK_2BUTTON_PRESS)
		{
			PluginsList().SelectRow(row, 0);
			OnCreatePlugin();
		}
}

bool document_window::execute_command(const std::string& Command, const std::string& Arguments)
{
	if(Command == control_highlight_plugin)
		{
			// Lookup the matching plugin factory ...
			const iplugin_factory_collection::factories_t factories(plugins(Arguments));
			return_val_if_fail(1 == factories.size(), false);

			iplugin_factory* const factory = *factories.begin();

			// Find a row that matches ...
			sdpGtkCList list = PluginsList();
			int row = list.FindRowFromData(factory);

			// Move the pointer to the row ...
			list.InteractiveShow(application().options().tutorial_speed(), true);
			list.InteractiveWarpPointer(row, application().options().tutorial_speed(), true);
			list.SelectRow(row, 0);

			if(application().user_interface())
				application().user_interface()->tutorial_mouse_message("Highlight Plugin:", iuser_interface::LMB_CLICK, key_modifiers());

			return true;
		}

	return base::execute_command(Command, Arguments);
}

// Called by the animation interval timer
int document_window::raw_animation_timer(gpointer Data)
{
	// Call the object-instance version ...
	return static_cast<document_window*>(Data)->animation_timer();
}

// Called by the animation interval timer
int document_window::animation_timer()
{
	// We want to be notified whenever the current document time changes
	if(!m_time_connection.connected())
		{
			iproperty* const time_property = get_time(m_document);
			if(time_property)
				{
					m_time_connection = time_property->changed_signal().connect(SigC::slot(*this, &document_window::update_time));
					update_time();
				}
		}

	// We only pay attention to every-other timer signal ...
	m_ignore_animation_timer = !m_ignore_animation_timer;
	if(m_ignore_animation_timer)
		return true;

	// If the document doesn't have a time source, we're done ...
	iproperty* const start_time_property = get_start_time(m_document);
	iproperty* const end_time_property = get_end_time(m_document);
	iproperty* const frame_rate_property = get_frame_rate(m_document);
	iproperty* const time_property = get_time(m_document);
	iwritable_property* const writable_time_property = dynamic_cast<iwritable_property*>(time_property);
	
	if(!start_time_property || !end_time_property || !frame_rate_property || !time_property || !writable_time_property)
		return true;

	const double start_time = boost::any_cast<double>(get_property_value(m_document.dag(), *start_time_property));
	const double end_time = boost::any_cast<double>(get_property_value(m_document.dag(), *end_time_property));
	const double frame_rate = boost::any_cast<double>(get_property_value(m_document.dag(), *frame_rate_property));
	const double time = boost::any_cast<double>(get_property_value(m_document.dag(), *time_property));

	const double start_frame = round(start_time * frame_rate);
	const double view_frame = round(time * frame_rate);
	const double end_frame = round(end_time * frame_rate) - 1;

	switch(m_playback_mode.value())
		{
			case REWIND:
				writable_time_property->set_value(start_frame / frame_rate);
				m_playback_mode.set_value(STOP);
				viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				break;

			case LOOP_REVERSE_PLAY:
				{
					// Calculate our new time ...
					double new_view_frame = view_frame - 1;
					if(new_view_frame < start_frame)
						{
							// We passed the beginning of the animation, so wrap around ...
							new_view_frame = end_frame;
						}

					// Set the new time ...
					writable_time_property->set_value(new_view_frame / frame_rate);

					// Redraw everything ...
					viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				}
				break;

			case REVERSE_PLAY:
				{
					// Calculate our new time ...
					double new_view_frame = view_frame - 1;
					if(new_view_frame < start_frame)
						{
							// We passed the beginning of the animation, so stop ...
							new_view_frame = start_frame;
							m_playback_mode.set_value(STOP);
						}

					// Set the new time ...
					writable_time_property->set_value(new_view_frame / frame_rate);

					// Redraw everything ...
					viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				}
				break;

			case STOP:
				break;

			case PLAY:
				{
					// Calculate our new time ...
					double new_view_frame = view_frame + 1;
					if(new_view_frame > end_frame)
						{
							// We passed the end of the animation, so stop ...
							new_view_frame = end_frame;
							m_playback_mode.set_value(STOP);
						}

					// Set the new time ...
					writable_time_property->set_value(new_view_frame / frame_rate);

					// Redraw everything ...
					viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				}
				break;

			case LOOP_PLAY:
				{
					// Calculate our new time ...
					double new_view_frame = view_frame + 1;
					if(new_view_frame > end_frame)
						{
							// We passed the end of the animation, so wrap around ...
							new_view_frame = start_frame;
						}

					// Set the new time ...
					writable_time_property->set_value(new_view_frame / frame_rate);

					// Redraw everything ...
					viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				}
				break;

			case FAST_FORWARD:
				writable_time_property->set_value(end_frame / frame_rate);
				m_playback_mode.set_value(STOP);
				viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
				break;

			default:
				return_val_if_fail(0, true);
		}

	// Continue indefinitely ...
	return true;
}

} // namespace k3d

