% Panel is an alternative to Matlab's "subplot" function.
% 
% INSTALLATION. To install panel, place the file "panel.m"
% on your Matlab path.
% 
% DOCUMENTATION. Scan the introductory information in the
% folder "docs". Learn to use panel by working through the
% demonstration scripts in the folder "demo" (list the demos
% by typing "help panel/demo"). Reference information is
% available through "doc panel" or "help panel". For the
% change log, use "edit panel" to view the file "panel.m".



% CHANGE LOG
% 
% ############################################################
% 22/05/2011
% First Public Release Version 2.0
% ############################################################
% 
% 23/05/2011
% Incorporated an LP solver, since the one we were using
% "linprog()" is not available to users who do not have the
% Optimisation Toolbox installed.
% 
% 21/06/2011
% Added -opdf option, and changed PageSize to be equal to
% PaperPosition.
%
% 12/07/2011
% Made some linprog optimisations, inspired by "Ian" on
% Matlab central. Tested against subplot using
% demopanel2(N=9). Subplot is faster, by about 20%, but
% panel is better :). For my money, 20% isn't much of a hit
% for the extra functionality. NB: Using Jeff Stuart's
% linprog (unoptimised), panel is much slower (especially
% for large N problems); we will probably have to offer a
% faster solver at some point (optimise Jeff's?).
%
% NOTES: You will see a noticeable delay, also, on resize.
% That's the price of using physical units for the layout,
% because we have to recalculate everything when the
% physical canvas size changes. I suppose in the future, we
% could offer an option so that physical units are only used
% during export; that would make resizes fast, and the user
% may not care so much about layout on screen, if they are
% aiming for print figures. Or, you could have the ability
% to turn off auto-refresh on resize().
%
% ############################################################
% 20/07/2011
% Release Version 2.1
% ############################################################
%
% 05/10/2011
% Tidied in-file documentation (panel.m).
%
% 11/12/2011
% Added flag "no-manage-font" to constructor, as requested
% by Matlab Central user Mukhtar Ullah.
%
% ############################################################
% 13/12/2011
% Release Version 2.2
% ############################################################
%
% 21/01/2012
% Fixed bug in explicit height export option "-hX" which
% wasn't working right at all.
%
% 25/01/12
% Fixed bug in tick label display during print. _Think_ I've
% got it right, this time! Some notes below, search for
% "25/01/12".
%
% 25/01/12
% Fixed DPI bug in smoothed export figures. Bug was flagged
% up by Jesper at Matlab Central.
%
% ############################################################
% 26/01/2012
% Release Version 2.3
% ############################################################
%
% 09/03/12
% Fixed bug whereby re-positioning never got done if only
% one panel was created in an existing figure window.
%
% ############################################################
% 13/03/2012
% Release Version 2.4
% ############################################################
%
% 15/03/12
% NB: On 2008b, and possibly later versions, the fact that
% the resizeCallback() and closeCallback() are private makes
% things not work. You can fix this by removing the "Access
% = Private" modifier on that section of "methods". It works
% fine in later versions, they must have changed the access
% rules I guess.
%
% 19/07/12
% Modified so that more than one object can be managed by
% one axis. Just use p.select([h1 h2 ...]). Added function
% "getAllManagedAxes()" which returns only objects from the
% "object list" (h_object), as it now is, which represent
% axes. Suggested by Brendan Sullivan @ Matlab Central.
%
% 19/07/12
% Added support for zlabel() call (not applicable to parent
% panels, since they are implicitly 2D for axis labelling).
%
% 19/07/12
% Fixed another export bug - how did this one not get
% noticed? XLimMode (etc.) was not getting locked during
% export, so that axes without manual limits might get
% re-dimensioned during export, which is bad news. Added
% locking of limits as well as ticks, in storeAxisState().
% Hope this has no side effects!
%
% ############################################################
% 19/07/12
% Release Version 2.5
%
% NB: Owing to the introduction of management of multiple
% objects by each panel, this release should be considered
% possibly flaky. Revert to 2.4 if you have problems with
% 2.5.
% ############################################################
%
% 23/07/12
% Improved documentation for figure export in demopanelA.
%
% 24/07/12
% Added support for export to SVG, using "plot2svg" (Matlab
% Central File Exchange) as the renderer. Along the way,
% tidied the behaviour of export() a little, and improved
% reporting to the user. Changed default DPI for EPS to 600,
% since otherwise the output files are pretty shoddy, and
% the filesize is relatively unaffected.
%
% 24/07/12
% Updated documentation, particularly HTML pages and
% associated figures. Bit nicer, now.
%
% ############################################################
% 24/07/12
% Release Version 2.6
% ############################################################
%
% 22/09/12
% Added demopanelH, which illustrates how to do insets. Kudos
% to Ann Hickox for the idea.
%
% 20/03/13
% Added panel.plot() to work around poor rendering of dashed
% lines, etc. Added demopanelI to illustrate its use.
%
% 20/03/13
% Renamed setCallback to addCallback, so we can have more
% than one. Added "userdata" argument to addCallback(), and
% "event" field (and "userdata" field) to "data" passed when
% callback is fired.
%
% ############################################################
% 21/03/13
% Release Version 2.7
% ############################################################
%
% 21/03/13
% Fixed bug in panel.plot() which did not handle solid lines
% correctly.
%
% 12/04/13
% Added back setCallback() with appropriate semantics, for
% the use of legacy code (or, really, future code, these
% semantics might be useful to someone). Also added the
% function clearCallbacks().
%
% 12/04/13
% Removed panel.plot() because it just seemed to be too hard
% to manage. Instead, we'll let the user plot things in the
% usual way, but during export (when things are well under
% our control), we'll fix up any dashed lines that the user
% has requested using the call fixdash(). Thus, we apply the
% fix only where it's needed, when printing to an image
% file, and save all the faffing with resize callbacks.
%
% ############################################################
% 12/04/13
% Release Version 2.8
% ############################################################
%
% 13/04/13
% Changed panel.export() to infer image format from file
% extension, in the case that it is not explicitly specified
% and the passed filename has an extension.
%
% 03/05/13
% Changed term "render", where misused, to "layout", so as
% not to confuse users of the help/docs. Changed name of
% callback event from "render-complete" to "layout-updated",
% is the only functional effect.
%
% 03/05/13
% Added argument to panel constructor so that units can be
% set there, rather than through a separate call to the
% "units" property.
%
% 03/05/13
% Added set descriptor "family" to go with "children" and
% "descendants". This one should be of particular use for
% the construct p.fa.margin = 0.
%
% 03/05/13
% Updated demopanel9 to be a walkthrough of how to set
% margins. Will be useful to point users at this if they ask
% "how do I do margins?".
%
% 03/05/13
% Added panel.version().
%
% 03/05/13
% Added page size "LNCS" to export.
%
% ############################################################
% 03/05/13
% Release Version 2.9
% ############################################################
%
% 10/05/13
% Removed linprog solution in favour of recursive
% computation. This should speed things up for people who
% don't have the optimisation toolbox.
%
% 10/05/13
% Added support for panels of fixed physical size. See new
% documentation for panel/pack().
%
% 10/05/13
% Added support for packing into panels packed in absolute
% mode, which wasn't previously possible.
%
% 10/05/13
% Removed advertisement for 'defer' flag, since I suspect
% it's no longer needed now we've moved away from LP. There
% may be some optimisation required before this is true -
% defer still functions as before, it's just not advertised.
%
% ############################################################
% 10/05/13
% Release Version 2.10
% ############################################################
%
% 14/05/13
% Some minor optimisations, so now panel is not slower than
% subplot (see demopanelK).
%
% 14/01/15
% Various fixes to work correctly under R2014b. Essentially,
% checked the demos, added retirement notes to fixdash(), and
% added function "fignum()".
%
% ############################################################
% 14/01/15
% Release Version 2.11
% ############################################################
%
% 28/03/15
% Changed export() logic slightly so that if either -h or -w option is
% specified, direct sizing model is selected (and, therefore, /all/
% options from the paper sizing model are ignored). Thus, either -w or
% -h can be specified, with -a, and intuitively-correct behaviour
% results.
%
% 02/04/15
% Changed functions x/y/zlabel and title to return a handle to the
% referenced object so that caller can access its properties.
%
% ############################################################
% 02/04/15
% Release Version 2.12
% ############################################################



classdef (Sealed = true) panel < handle
	

	
	%% ---- PROPERTIES ----
	
	properties (Constant = true, Hidden = true)
		
		PANEL_TYPE_UNCOMMITTED = 0;
		PANEL_TYPE_PARENT = 1;
		PANEL_TYPE_OBJECT = 2;
		
	end
	
	properties (Constant = true)
		
		LAYOUT_MODE_NORMAL = 0;
		LAYOUT_MODE_PREPRINT = 1;
		LAYOUT_MODE_POSTPRINT = 2;
		
	end
	
	properties
		
		% these properties are only here for documentation. they
		% are actually stored in "prop". it's just some subsref
		% madness.
		
		% font name to use for axis text (inherited)
		%
		% access: read/write
		% default: normal
		fontname
		
		% font size to use for axis text (inherited)
		%
		% access: read/write
		% default: normal
		fontsize
		
		% font weight to use for axis text (inherited)
		%
		% access: read/write
		% default: normal
		fontweight
		
		% the units that are used when reading/writing margins
		%
		% units can be set to any of 'mm', 'cm', 'in' or 'pt'.
		% it only affects the read/write interface; values
		% stored already are not re-interpreted.
		%
		% access: read/write
		% default: mm
		units
		
		% the panel's margin vector in the form [left bottom right top]
		%
		% the margin is key to the layout process. the layout
		% algorithm makes all panels as large as possible whilst
		% not violating margin constraints. margins are
		% respected between panels within their parent and
		% between the root panel and the edges of the canvas
		% (figure or image file).
		%
		% access: read/write
		% default: [12 10 2 2] (mm)
		%
		% see also: marginleft, marginbottom, marginright, margintop
		margin
		
		% one element of the margin vector
		%
		% access: read/write
		% default: see margin
		%
		% see also: margin
		marginleft
		
		% one element of the margin vector
		%
		% access: read/write
		% default: see margin
		%
		% see also: margin
		marginbottom
		
		% one element of the margin vector
		%
		% access: read/write
		% default: see margin
		%
		% see also: margin
		marginright
		
		% one element of the margin vector
		%
		% access: read/write
		% default: see margin
		%
		% see also: margin
		margintop
		
		% return position of panel
		%
		% return the panel's position in normalized
		% coordinates (normalized to the figure window that
		% is associated with the panel). note that parent
		% panels have positions too, even though nothing is
		% usually rendered. uncommitted panels, too.
		%
		% access: read only
		position
		
		% return handle of associated figure
		%
		% access: read only
		figure
		
		% return handle of associated axis
		%
		% if the panel is not an axis panel, empty is returned.
		% object includes axis, but axis does not include
		% object.
		%
		% access: read only
		%
		% see also: object
		axis
		
		% return handle of associated object
		%
		% if the panel is not an object panel, empty is
		% returned. object includes axis, but axis does not
		% include object.
		%
		% access: read only
		%
		% see also: axis
		object
		
		% access properties of panel's children
		%
		% if the panel is a parent panel, "children" gives
		% access to some properties of its children (direct
		% descendants). "children" can be abbreviated "ch".
		% properties that can be accessed are as follows.
		%
		% axis: read-only, returns an array
		% object: read-only, returns an array
		%
		% margin: write-only
		% fontname: write-only
		% fontsize: write-only
		% fontweight: write-only
		%
		% EXAMPLE:
		%   h = p.ch.axis;
		%   p.ch.margin = 3;
		%
		% see also: descendants, family
		children
		
		% access properties of panel's descendants
		%
		% if the panel is a parent panel, "descendants" gives
		% access to some properties of its descendants
		% (children, grandchildren, etc.). "descendants" can be
		% abbreviated "de". properties that can be accessed are
		% as follows.
		%
		% axis: read-only, returns an array
		% object: read-only, returns an array
		%
		% margin: write-only
		% fontname: write-only
		% fontsize: write-only
		% fontweight: write-only
		%
		% EXAMPLE:
		%   h = p.de.axis;
		%   p.de.margin = 3;
		%
		% see also: children, family
		descendants
		
		% access properties of panel's family
		%
		% if the panel is a parent panel, "family" gives access
		% to some properties of its family (self, children,
		% grandchildren, etc.). "family" can be abbreviated
		% "fa". properties that can be accessed are as follows.
		%
		% axis: read-only, returns an array
		% object: read-only, returns an array
		%
		% margin: write-only
		% fontname: write-only
		% fontsize: write-only
		% fontweight: write-only
		%
		% EXAMPLE:
		%   h = p.fa.axis;
		%   p.fa.margin = 3;
		%
		% see also: children, descendants
		family
		
	end
	
	properties (Access = private)
		
		% associated figure window
		h_figure
		
		% parent graphics object
		h_parent
		
		% this is empty for the root PANEL, populated for all others
		parent
		
		% this is always the root panel associated with this
		m_root
		
		% packing specifier
		%
		% empty:              relative positioning mode (stretch)
		% scalar fraction:    relative positioning mode
		% scalar percentage:  relative positioning mode
		% 1x4 dimension:      absolute positioning mode
		packspec
		
		% packing dimension of children
		%
		% 1 : horizontal
		% 2 : vertical
		packdim
		
		% panel type
		m_panelType
		
		% fixdash lines
		m_fixdash
		m_fixdash_restore
		
		% associated managed graphics object (usually, an axis)
		h_object
		
		% show axis (only the root has this extra axis, if show() is active)
		h_showAxis
		
		% children (only a parent panel has non-empty, here)
		m_children
		
		% callback (any functions listed in this cell array are called when events occur)
		m_callback
		
		% local properties (actual properties is this overlaid on inherited/default properties)
		%
		% see getPropertyValue()
		prop
		
		% state
		%
		% private state information used during various processing
		state
		
		% layout context for this panel
		%
		% this is the layout context for the panel. this is
		% computed in the function recomputeLayout(), and used
		% to reposition the panel in applyLayout(). storage of
		% this data means that we can call applyLayout() to
		% layout only a branch of the panel tree without having
		% to recompute the whole thing. however, I don't know
		% how efficient this system is, might need some work.
		m_context
		
	end
	
	
	
	
	
	
	
	%% ---- PUBLIC CTOR/DTOR ----
	
	methods
		
		function p = panel(varargin)
			
			% create a new panel
			%
			% p = panel(...)
			%   create a new root panel. optional arguments listed
			%   below can be supplied in any order. if "h_parent"
			%   is not supplied, it is set to gcf - that is, the
			%   panel fills the current figure.
			%
			%   initially, the root panel is an "uncommitted
			%   panel". calling pack() or select() on it will
			%   commit it as a "parent panel" or an "object
			%   panel", respectively. the following arguments may
			%   be passed, in any order.
			%
			% h_parent
			%   a handle to a graphics object that will act as the
			%   parent of the new panel. this is usually a figure
			%   handle, but may be a handle to any graphics
			%   object, in principle. currently, an error is
			%   raised unless it's a figure or a uipanel - if you
			%   want to try other object types, edit the code
			%   where the error is raised, and let me know if you
			%   have positive results so I can update panel to
			%   allow other object types.
			%
			% 'add'
			%   usually, when you attach a new root panel to a
			%   figure, any existing attached root panels are
			%   first deleted to make way for it. if you pass this
			%   argument, this is not done, so that you can attach
			%   more than one root panel to the same figure. see
			%   demopanelE for an example of this use.
			%
			% 'no-manage-font'
			%   by default, a panel will manage fonts of titles
			%   and axis labels. this prevents the user from
			%   setting individual fonts on those items. pass this
			%   flag to disable font management for this panel.
			%
			% 'mm', 'cm', 'in', 'pt'
			%   by default, panel uses mm as the unit of
			%   communication with the user over margin sizes.
			%   pass any of these to change this (you can achieve
			%   the same effect after creating a panel by setting
			%   the property "units").
			%
			% see also: panel (overview), pack(), select()

			% PRIVATE DOCUMENTATION
			%
			% 'defer'
			%   THIS IS NO LONGER ADVERTISED since we replaced the
			%   LP solution with a procedural solution, but still
			%   functions as before, to provide legacy support.
			%   the panel will be created with layout disabled.
			%   the layout computations take a little while when
			%   large numbers of panels are involved, and are
			%   re-run every time you add a panel or change a
			%   margin, by default. this is tedious if you are
			%   preparing a complex layout; pass 'defer', and
			%   layout will not be computed at all until you call
			%   refresh() or export() on the root panel.
			%
			% 'pack'
			%   this constructor is called internally from pack()
			%   to create new panels when packing them into
			%   parents. the first argument is passed as 'pack' in
			%   this case, which allows us to do slightly quicker
			%   parsing of the arguments, since we know the
			%   calling convention (see immediately below).

			% default state
			p.state = [];
			p.state.name = '';
			p.state.defer = 0;
			p.state.manage_font = 1;
			p.m_callback = {};
			p.m_fixdash = {};
			p.packspec = [];
			p.packdim = 2;
			p.m_panelType = p.PANEL_TYPE_UNCOMMITTED;
			p.prop = panel.getPropertyInitialState();
			
			% handle call from pack() aqap
			if nargin && isequal(varargin{1}, 'pack')
				
				% since we know the calling convention, in this
				% case, we can handle this as quickly as possible,
				% so that large recursive layouts do not get held up
				% by spurious code, here.
				
				% parent is a panel
				passed_h_parent = varargin{2};
				
				% become its child
				indexInParent = int2str(length(passed_h_parent.m_children)+1);
				if passed_h_parent.isRoot()
					p.state.name = ['(' indexInParent ')'];
				else
					p.state.name = [passed_h_parent.state.name(1:end-1) ',' indexInParent ')'];
				end
				p.h_parent = passed_h_parent.h_parent;
				p.h_figure = passed_h_parent.h_figure;
				p.parent = passed_h_parent;
				p.m_root = passed_h_parent.m_root;
				
				% done!
				return
				
			end
				
			% default condition
			passed_h_parent = [];
			add = false;

			% peel off args
			while ~isempty(varargin)

				% get arg
				arg = varargin{1};
				varargin = varargin(2:end);

				% handle text
				if ischar(arg)

					switch arg

						case 'add'
							add = true;
							continue;

						case 'defer'
							p.state.defer = 1;
							continue;

						case 'no-manage-font'
							p.state.manage_font = 0;
							continue;

						case {'mm' 'cm' 'in' 'pt'}
							p.setPropertyValue('units', arg);
							continue;

						otherwise
							error('panel:InvalidArgument', ['unrecognised text argument "' arg '"']);

					end

				end

				% handle parent
				if isscalar(arg) && ishandle(arg)
					passed_h_parent = arg;
					continue;
				end

				% error
				error('panel:InvalidArgument', 'unrecognised argument to panel constructor');

			end

			% attach to current figure if no parent supplied
			if isempty(passed_h_parent)
				passed_h_parent = gcf;

				% this might cause a figure to be created - if so,
				% give it time to display now so we don't get a (or
				% two, in fact!) resize event(s) later
				drawnow
			end

			% we are a root panel
			p.state.name = 'root';
			p.parent = [];
			p.m_root = p;

			% get parent type
			parentType = get(passed_h_parent, 'type');

			% set handles
			switch parentType

				case 'uipanel'
					p.h_parent = passed_h_parent;
					p.h_figure = getParentFigure(passed_h_parent);

				case 'figure'
					p.h_parent = passed_h_parent;
					p.h_figure = passed_h_parent;

				otherwise
					error('panel:InvalidArgument', ...
						['panel() cannot be attached to an object of type "' parentType '"']);

			end

			% lay in callbacks
			addHandleCallback(p.h_figure, 'CloseRequestFcn', @panel.closeCallback);
			addHandleCallback(p.h_parent, 'ResizeFcn', @panel.resizeCallback);

			% register for callbacks
			if add
				panel.callbackDispatcher('registerNoClear', p);
			else
				panel.callbackDispatcher('register', p);
			end

			% lock class in memory (prevent persistent from being cleared by clear all)
			panel.lockClass();
			
		end
		
		function delete(p)
			
			% destroy a panel
			%
			% delete(p)
			%   destroy the passed panel, deleting all associated
			%   graphics objects.
			%
			% NB: you won't usually have to call this explicitly.
			% it is called automatically for all attached panels
			% when you close the associated figure.
			
			% debug output
% 			panel.debugmsg(['deleting "' p.state.name '"...']);
			
			% delete managed graphics objects
			for n = 1:length(p.h_object)
				h = p.h_object(n);
				if ishandle(h)
					delete(h);
				end
			end
			
			% delete associated show axis
			if ~isempty(p.h_showAxis) && ishandle(p.h_showAxis)
				delete(p.h_showAxis);
			end
			
			% delete all children (child will remove itself from
			% "m_children" on delete())
			while ~isempty(p.m_children)
				delete(p.m_children(end));
			end
			
			% unregister...
			if p.isRoot()
				
				% ...for callbacks
				panel.callbackDispatcher('unregister', p);
				
			else
				
				% ...from parent
				p.parent.removeChild(p);
				
			end
			
			% debug output
% 			panel.debugmsg(['deleted "' p.state.name '"!']);
			
		end
		
	end
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	%% ---- PUBLIC DISPLAY ----

	methods (Hidden = true)
		
		function disp(p)
			
			display(p);
			
        end
        
		function display(p, indent)

			% default
			if nargin < 2
				indent = '';
			end
			
			% handle non-scalar (should not exist!)
			nels = numel(p);
			if nels > 1
				sz = size(p);
				sz = sprintf('%dx', sz);
				disp([sz(1:end-1) ' array of panel objects']);
				return
			end
			
			% header
			header = indent;
			if p.isObject()
				header = [header 'Object ' p.state.name ': '];
			elseif p.isParent()
				header = [header 'Parent ' p.state.name ': '];
			else
				header = [header 'Uncommitted ' p.state.name ': '];
			end
			if p.isRoot()
				pp = ['attached to Figure ' panel.fignum(p.h_figure)];
			else
				if isempty(p.packspec)
					pp = 'stretch';
				elseif iscell(p.packspec)
					units = p.getPropertyValue('units');
					val = panel.resolveUnits({p.packspec{1} 'mm'}, units);
					pp = sprintf('%.1f%s', val, units);
				elseif isscalar(p.packspec)
					if p.packspec > 1
						pp = sprintf('%.0f%%', p.packspec);
					else
						pp = sprintf('%.3f', p.packspec);
					end
				else
					pp = sprintf('%.3f ', p.packspec);
					pp = pp(1:end-1);
				end
			end
			header = [header '[' pp];
			if p.isParent()
				edges = {'hori' 'vert'};
				header = [header ', ' edges{p.packdim}];
			end
			header = [header ']'];

			% margin
			header = rpad(header, 60);
			header = [header '[ margin ' sprintf('%.3g ', p.getPropertyValue('margin')) p.getPropertyValue('units') ']'];
			
% 			% index
% 			if isfield(p.state, 'index')
% 				header = [header ' (' int2str(p.state.index) ')'];
% 			end

			% display
			disp(header);
			
			% children
			for c = 1:length(p.m_children)
				p.m_children(c).display([indent '  ']);
			end
						
		end
			
	end
	
	
	
	
	
	
	
	
	
	
	%% ---- PUBLIC METHODS ----

	methods
		
		function h = xlabel(p, text)
			
			% apply an xlabel to the panel (or group)
			%
			% p.xlabel(...)
			%   behaves just like xlabel() at the prompt (you can
			%   use that as an alternative) when called on an axis
			%   panel. when called on a parent panel, however, the
			%   group of objects within that parent have a label
			%   applied. when called on a non-axis object panel,
			%   an error is raised.
			
			h = get(p.getOrCreateAxis(), 'xlabel');
			set(h, 'string', text);
			if p.isParent()
				set(h, 'visible', 'on');
			end
			
		end
		
		function h = ylabel(p, text)
			
			% apply a ylabel to the panel (or group)
			%
			% p.ylabel(...)
			%   behaves just like ylabel() at the prompt (you can
			%   use that as an alternative) when called on an axis
			%   panel. when called on a parent panel, however, the
			%   group of objects within that parent have a label
			%   applied. when called on a non-axis object panel,
			%   an error is raised.
			
			h = get(p.getOrCreateAxis(), 'ylabel');
			set(h, 'string', text);
			if p.isParent()
				set(h, 'visible', 'on');
			end
			
		end
		
		function h = zlabel(p, text)
			
			% apply a zlabel to the panel (or group)
			%
			% p.zlabel(...)
			%   behaves just like zlabel() at the prompt (you can
			%   use that as an alternative) when called on an axis
			%   panel. when called on a parent panel, however,
			%   this method raises an error, since parent panels
			%   are assumed to be 2D, with respect to axes.
			
			if p.isParent()
				error('panel:ZLabelOnParentAxis', 'can only call zlabel() on an object panel');
			end
			
			h = get(p.getOrCreateAxis(), 'zlabel');
			set(h, 'string', text);
			
		end
		
		function h = title(p, text)
			
			% apply a title to the panel (or group)
			%
			% p.title(...)
			%   behaves just like title() at the prompt (you can
			%   use that as an alternative) when called on an axis
			%   panel. when called on a parent panel, however, the
			%   group of objects within that parent have a title
			%   applied. when called on a non-axis object panel,
			%   an error is raised.
			
			h = title(p.getOrCreateAxis(), text);
			if p.isParent()
				set(h, 'visible', 'on');
			end
			
		end
		
		function hold(p, state)
			
			% set the hold on/off state of the associated axis
			% 
			% p.hold('on' / 'off')
			%   you can use matlab's "hold" function with plots in
			%   panel, just like any other plot. there is,
			%   however, a very minor gotcha that is somewhat
			%   unlikely to ever come up, but for completeness
			%   this is the problem and the solutions:
			%
			% if you create a panel "p", change its font using
			% panel, e.g. "p.fontname = 'Courier New'", then call
			% "hold on", then "hold off", then plot into it, the
			% font is not respected. this situation is unlikely to
			% arise because there's usually no reason to call
			% "hold off" on a plot. however, there are three
			% solutions to get round it, if it does:
			%
			%   a) call p.refresh() when you're finished, to
			%   update all fonts to managed values.
			%
			%   b) if you're going to call p.export() anyway,
			%   fonts will get updated when you do.
			%
			%   c) if for some reason you can't do (a) OR (b) (I
			%   can't think why), you can use the hold() function
			%   provided by panel instead of that provided by
			%   Matlab. this will not affect your fonts. for
			%   example, call "p(2).hold('on')".
			
			% because the matlab "hold off" command sets an axis's
			% nextplot state to "replace", we lose control over
			% the axis properties (such as fontname). we set
			% nextplot to "replacechildren" when we create an
			% axis, but if the user does a "hold on, hold off"
			% cycle, we lose that. therefore, we offer this
			% alternative.
			
			% check
			if ~p.isObject()
				error('panel:HoldWhenNotObjectPanel', 'can only call hold() on an object panel');
			end
			
			% check
			h_axes = p.getAllManagedAxes();
			if isempty(h_axes)
				error('panel:HoldWhenNoAxes', 'can only call hold() on a panel that manages one or more axes');
			end
			
			% switch
			switch state
				case {'on' true 1}
					set(h_axes, 'nextplot', 'add');
				case {'off' false 0}
					set(h_axes, 'nextplot', 'replacechildren');
				otherwise
					error('panel:InvalidArgument', 'argument to hold() must be ''on'', ''off'', or boolean');
			end
			
		end
		
		function fixdash(p, hs, linestyle)
			
			% pass dashed lines to be fixed up during export
            %
            % NB: Matlab's difficulty with dotted/dashed lines on export
            % seems to be fixed in R2014b, so if using this version or a
            % later one, this functionality of panel will be of no
            % interest. Text below was from pre R2014b.
			%
			% p.fixdash(h, linestyle)
			%   add the lines specified as handles in "h" to the
			%   list of lines to be "fixed up" during export.
			%   panel will attempt to get the lines to look right
			%   during export to all formats where they would
			%   usually get mussed up. see demopanelI for an
			%   example of how it works.
			%
			%   the above is the usual usage of fixdash(), but
			%   you can get more control over linestyle by
			%   specifying the additional argument, "linestyle".
			%   if "linestyle" is supplied, it is used as the
			%   linestyle; if not, the current linestyle of the
			%   line (-, --, -., :) is used. "linestyle" can
			%   either be a text string or a series of numbers, as
			%   described below.
			%
			%     '-' solid
			%     '--' dashed, equal to [2 0.75]
			%     '-.' dash-dot, equal to [2 0.75 0.5 0.75]
			%     ':', '.' dotted, equal to [0.5 0.5]
			%
			%   a number series should be 1xN, where N is a
			%   multiple of 2, as in the examples above, and
			%   specifies the lengths of any number of dash
			%   components that are used before being repeated.
			%   for instance, '-.' generates a 2 unit segment
			%   (dash), a 0.75 unit gap, then a 0.5 unit segment
			%   (dot) and a final 0.75 unit gap. at present, the
			%   units are always millimetres. this system is
			%   extensible, so that the following examples are
			%   also valid:
			%
			%     '--..' dash-dash-dot-dot
			%     '-..-.' dash-dot-dot-dash-dot
			%     [2 1 4 1 6 1] 2 dash, 4 dash, 6 dash

			% default
			if nargin < 3
				linestyle = [];
			end
			
			% bubble up to root
			if ~p.isRoot()
				p.m_root.fixdash(hs, linestyle);
				return
			end
			
			% for each passed handle
			for h = (hs(:)')
				
				% check it's still a handle
				if ~ishandle(h)
					continue
				end
				
				% check it's a line
				if ~isequal(get(h, 'type'), 'line')
					continue
				end
				
				% update if in list
				found = false;
				for i = 1:length(p.m_fixdash)
					if h == p.m_fixdash{i}.h
						p.m_fixdash{i}.linestyle = linestyle;
						found = true;
						break
					end
				end
				
				% else add to list
				if ~found
					p.m_fixdash{end+1} = struct('h', h, 'linestyle', linestyle);
				end
				
			end
			
		end
		
		function show(p)
			
			% highlight the outline of the panel
			%
			% p.show()
			%   make the outline of the panel "p" show up in red
			%   in the figure window. this is useful for
			%   understanding a complex layout.
			%
			% see also: identify()

			r = p.getObjectPosition();
			
			if ~isempty(r)
				h = p.getShowAxis();
				delete(get(h, 'children'));
				xdata = [r(1) r(1)+r(3) r(1)+r(3) r(1) r(1)];
				ydata = [r(2) r(2) r(2)+r(4) r(2)+r(4) r(2)];
				plot(h, xdata, ydata, 'r-', 'linewidth', 5);
				axis([0 1 0 1])
			end
			
		end
		
		function export(p, varargin)
			
			% to export the root panel to an image file
			%
			% p.export(filename, ...)
			%
			% export the figure containing panel "p" to an image file.
			% you must supply the filename of this output file, with or
			% without a file extension. any further arguments must be
			% option strings starting with the dash ("-") character. "p"
			% should be the root panel.
			%
			% if the filename does not include an extension, the
			% appropriate extension will be added. if it does, the
			% output format will be inferred from it, unless overridden
			% by the "-o" option, described below.
			%
			% if you are targeting a print publication, you may find it
			% easiest to size your output using the "paper sizing model"
			% (below). if you prefer, you can use the "direct sizing
			% model", instead. these two sizing models are described
			% below. underneath these are listed the options unrelated
			% to sizing (which apply regardless of which sizing model
			% you use).
			%
			%
			%
			% PAPER SIZING MODEL:
			%
			% using the paper sizing model, you specify your target as a
			% region of a piece of paper, and the actual size in
			% millimeters is calculated for you. this is usually very
			% convenient, but if you find it unsuitable, the direct
			% sizing model (next section) is provided as an alternative.
			%
			% to specify the region, you specify the type (size) of
			% paper, the orientation, the number of columns, and the
			% aspect ratio of the figure (or the fraction of a column to
			% fill). usually, the remaining options can be left as
			% defaults.
			%
			% -pX
			%   X is the paper type, A2-A6 or letter (default is A4).
			%   NB: you can also specify paper type LNCS (Lecture Notes
			%   in Computer Science), using "-pLNCS". If you do this,
			%   the margins are also adjusted to match LNCS format.
			%
			% -l
			%   specify landscape mode (default is portrait).
			%
			% -mX
			%   X is the paper margins in mm. you can provide a scalar
			%   (same margins all round) or a comma-separated list of
			%   four values, specifying the left, bottom, right, top
			%   margins separately (default is 20mm all round).
			%
			% -iX
			%   X is the inter-column space in mm (default is
			%   5mm).
			%
			% -cX
			%   X is the number of columns (default is 1).
			%
			% NB: the following two options represent two ways to
			% specify the height of the figure relative to the space
			% defined by the above options. if you supply both,
			% whichever comes second will be used.
			%
			% -aX
			%   X is the aspect ratio of the resulting image file (width
			%   is set by the paper model). X can be one of the strings:
			%   s (square), g (landscape golden ratio), gp (portrait
			%   golden ratio), h (half-height), d (double-height); or, a
			%   number greater than zero, to specify the aspect ratio
			%   explicitly. note that, if using the numeric form, the
			%   ratio is expressed as the quotient of width over height,
			%   in the usual way. ratios greater than 10 or less than
			%   0.1 are disallowed, since these can cause a very large
			%   figure file to be created accidentally. default is to
			%   use the landscape golden ratio.
			%
			% -fX
			%   X is the fraction of the column (or page, if there are
			%   not columns) to fill. X can be one of the following
			%   strings - a (all), tt (two thirds), h (half), t (third),
			%   q (quarter) - or a fraction between 0 and 1, to specify
			%   the fraction of the space to fill as a number. default
			%   is to use aspect ratio, not fill fraction.
			%
			%
			%
			% DIRECT SIZING MODEL:
			%
			% if one of these two options is set, the output image is
			% sized according to that option and the aspect ratio (see
			% above) and the paper model is not used. if both are set,
			% the aspect ratio is not used either.
			%
			% -wX
			%   X is the explicit width in mm.
			%
			% -hX
			%   X is the explicit height in mm.
			%
			%
			%
			% NON-SIZING OPTIONS:
			%
			% finally, a few options are provided to control how
			% the prepared figure is exported. note that DPI below
			% 150 is only recommended for sizing drafts, since
			% font and line sizes are not rendered even vaguely
			% accurately in some cases. at the other end, DPI
			% above 600 is unlikely to be useful except when
			% submitting camera-ready copy.
			%
			% -rX
			%   X is the resolution (DPI) at which to produce the
			%   output file. X can be one of the following strings
			%   - d (draft, 75DPI), n (normal, 150DPI), h (high,
			%   300DPI), p (publication quality, 600DPI), x
			%   (extremely high quality, 1200DPI) - or just
			%   the DPI as a number (must be in 75-2400). the
			%   default depends on the output format (see below).
			%
			% -rX/S
			%   X is the DPI, S is the smoothing factor, which can
			%   be 2 or 4. the output file is produced at S times
			%   the specified DPI, and then reduced in size to the
			%   specified DPI by averaging. thus, the hard edges
			%   produced by the renderer are smoothed - the effect
			%   is somewhat like "anti-aliasing".
			%
			% NB: the DPI setting might be expected to have no
			% effect on vector formats. this is true for SVG, but
			% not for EPS, where the DPI affects the numerical
			% precision used as well as the size of some image
			% elements, but has little effect on file size. for
			% this reason, the default DPI is 150 for bitmap
			% formats but 600 for vector formats.
			%
			% -s
			%   print sideways (default is to print upright)
			%
			% -oX
			%   X is the output format - choose from most of the
			%   built-in image device drivers supported by "print"
			%   (try "help print"). this includes "png", "jpg",
			%   "tif", "eps" and "pdf". note that "eps"/"ps"
			%   resolve to "epsc2"/"psc2", for convenience. to use
			%   the "eps"/"ps" devices, use "-oeps!"/"-ops!". you
			%   may also specify "svg", if you have the tool
			%   "plot2svg" on your path (available at Matlab
			%   Central File Exchange). the default output format
			%   is inferred from the file extension, or "png" if
			%   the passed filename has no extension.
			%
			%
			%
			% EXAMPLES:
			%
			% default export of 'myfig', creates 'myfig.png' at a
			% size of 170x105mm (1004x620px). this size comes
			% from: A4 (210mm wide), minus two 20mm margins
			% (170mm), and using the golden aspect ratio to give a
			% height of 105mm, and finally 150DPI to give the
			% pixel size.
			%
			% p.export('myfig')
			%
			% when producing the final camera-ready image for a
			% square figure that will sit in one of the two
			% columns of a letter-size paper journal with default
			% margins and inter-column space, we might use this:
			%
			% p.export('myfig', '-pletter', '-c2', '-as', '-rp');

			% LEGACY
			%
			% (this is legacy since the 'defer' flag is no longer
			% needed - though it is still supported)
			%
			% NB: if you pass 'defer' to the constructor, calling
			% export() both exports the panel and releases the
			% defer mode. future changes to properties (e.g.
			% margins) will cause immediate recomputation of the
			% layout.
			
			% check
			if ~p.isRoot()
				error('panel:ExportWhenNotRoot', 'cannot export() this panel - it is not the root panel');
			end
			
			% used below
			default_margin = 20;
			
			% parameters
			pars = [];
			pars.filename = '';
			pars.fmt = '';
			pars.ext = '';
			pars.dpi = [];
			pars.smooth = 1;
			pars.paper = 'A4';
			pars.landscape = false;
			pars.fill = -1.618;
			pars.cols = 1;
			pars.intercolumnspacing = 5;
			pars.margin = default_margin;
			pars.sideways = false;
			pars.width = 0;
			pars.height = 0;
			invalid = false;
			
			% interpret args
			for a = 1:length(varargin)
				
				% extract
				arg = varargin{a};
				
				% all arguments must be non-empty strings
				if ~isstring(arg)
					error('panel:InvalidArgument', ...
						'all arguments to export() must be non-empty strings');
				end
				
				% is filename?
				if arg(1) ~= '-'
					
					% error if already set
					if ~isempty(pars.filename)
						error('panel:InvalidArgument', ...
							['at argument "' arg '", the filename is already set ("' pars.filename '")']);
					end
					
					% ok, continue
					pars.filename = arg;
					continue
					
				end

				% split off option key and option value
				if length(arg) < 2
					error('panel:InvalidArgument', ...
						['at argument "' arg '", no option specified']);
				end
				key = arg(2);
				val = arg(3:end);
				
				% switch on option key
				switch key

					case 'p'
						pars.paper = validate_par(val, arg, {'A2' 'A3' 'A4' 'A5' 'A6' 'letter' 'LNCS'});

					case 'l'
						pars.landscape = true;
						validate_par(val, arg, 'empty');

					case 'm'
						pars.margin = validate_par(str2num(val), arg, 'dimension', 'nonneg');

					case 'i'
						pars.intercolumnspacing = validate_par(str2num(val), arg, 'scalar', 'nonneg');

					case 'c'
						pars.cols = validate_par(str2num(val), arg, 'scalar', 'integer');

					case 'f'
						switch val
							case 'a', pars.fill = 1;      % all
							case 'w', pars.fill = 1;      % whole (legacy, not documented)
							case 'tt', pars.fill = 2/3;   % two thirds
							case 'h', pars.fill = 1/2;    % half
							case 't', pars.fill = 1/3;    % third
							case 'q', pars.fill = 1/4;    % quarter
							otherwise
								pars.fill = validate_par(str2num(val), arg, 'scalar', [0 1]);
						end

					case 'a'
						switch val
							case 's', pars.fill = -1;         % square
							case 'g', pars.fill = -1.618;     % golden ratio (landscape)
							case 'gp', pars.fill = -1/1.618;  % golden ratio (portrait)
							case 'h', pars.fill = -2;         % half height
							case 'd', pars.fill = -0.5;       % double height
							otherwise
								pars.fill = -validate_par(str2num(val), arg, 'scalar', [0.1 10]);
						end

					case 'w'
						pars.width = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]);

					case 'h'
						pars.height = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]);

					case 'r'
						% peel off smoothing ("/...")
						if any(val == '/')
							f = find(val == '/', 1);
							switch val(f+1:end)
								case '2', pars.smooth = 2;
								case '4', pars.smooth = 4;
								otherwise, error('panel:InvalidArgument', ...
										['invalid argument "' arg '", part after / must be "2" or "4"']);
							end
							val = val(1:end-2);
						end

						switch val
							case 'd', pars.dpi = 75;      % draft
							case 'n', pars.dpi = 150;     % normal
							case 'h', pars.dpi = 300;     % high
							case 'p', pars.dpi = 600;     % publication quality
							case 'x', pars.dpi = 1200;    % extremely high quality
							otherwise
								pars.dpi = validate_par(str2num(val), arg, 'scalar', [75 2400]);
						end

					case 's'
						pars.sideways = true;
						validate_par(val, arg, 'empty');

					case 'o'
						fmts = {
							'png' 'png' 'png'
							'tif' 'tiff' 'tif'
							'tiff' 'tiff' 'tif'
							'jpg' 'jpeg' 'jpg'
							'jpeg' 'jpeg' 'jpg'
							'ps' 'psc2' 'ps'
							'ps!' 'psc' 'ps'
							'psc' 'psc' 'ps'
							'ps2' 'ps2' 'ps'
							'psc2' 'psc2' 'ps'
							'eps' 'epsc2' 'eps'
							'eps!' 'eps' 'eps'
							'epsc' 'epsc' 'eps'
							'eps2' 'eps2' 'eps'
							'epsc2' 'epsc2' 'eps'
							'pdf' 'pdf' 'pdf'
							'svg' 'svg' 'svg'
							};
						validate_par(val, arg, fmts(:, 1)');
						index = isin(fmts(:, 1), val);
						pars.fmt = fmts(index, 2:3);

					otherwise
						error('panel:InvalidArgument', ...
							['invalid argument "' argtext '", option is not recognised']);

				end
				
			end
			
			% if not specified, infer format from filename
			if isempty(pars.fmt)
				[path, base, ext] = fileparts(pars.filename);
				if ~isempty(ext)
					ext = ext(2:end);
				end
				switch ext
					case {'tif' 'tiff'}
						pars.fmt = {'tiff' 'tif'};
					case {'jpg' 'jpeg'}
						pars.fmt = {'jpeg' 'jpg'};
					case 'eps'
						pars.fmt = {'epsc2' 'eps'};
					case {'png' 'pdf' 'svg'}
						pars.fmt = {ext ext};
					case ''
						pars.fmt = {'png' 'png'};
					otherwise
						warning('panel:CannotInferImageFormat', ...
							['unable to infer image format from file extension "' ext '" (PNG assumed)']);
						pars.fmt = {'png' 'png'};
				end
			end
			
			% extract
			pars.ext = pars.fmt{2};
			pars.fmt = pars.fmt{1};
			
			% extract
			is_bitmap = ismember(pars.fmt, {'png' 'jpeg' 'tiff'});
			
			% default DPI
			if isempty(pars.dpi)
				if is_bitmap
					pars.dpi = 150;
				else
					pars.dpi = 600;
				end
			end

			% validate
			if isequal(pars.fmt, 'svg') && isempty(which('plot2svg'))
				error('panel:Plot2SVGMissing', 'export to SVG requires plot2svg (Matlab Central File Exchange)');
			end
			
			% validate
			if ~is_bitmap && pars.smooth ~= 1
				pars.smooth = 1;
				warning('panel:NoSmoothVectorFormat', 'requested smoothing will not be performed (chosen export format is not a bitmap format)');
			end
			
			% validate
			if isempty(pars.filename)
				error('panel:InvalidArgument', 'filename not supplied');
			end
			
			% make sure filename has extension
			if ~any(pars.filename == '.')
				pars.filename = [pars.filename '.' pars.ext];
			end
			
			
			
%%%% GET TARGET DIMENSIONS (BEGIN)
			
			% get space for figure
			switch pars.paper
				case 'A0', sz = [841 1189];
				case 'A1', sz = [594 841];
				case 'A2', sz = [420 594];
				case 'A3', sz = [297 420];
				case 'A4', sz = [210 297];
				case 'A5', sz = [148 210];
				case 'A6', sz = [105 148];
				case 'letter', sz = [216 279];
				case 'LNCS', sz = [152 235];
					% if margin is still at default, set it to LNCS
					% margin size
					if isequal(pars.margin, default_margin)
						pars.margin = [15 22 15 20];
					end
				otherwise
					error(['unrecognised paper size "' pars.paper '"'])
			end
			
			% orientation of paper
			if pars.landscape
				sz = sz([2 1]);
			end
			
			% paper margins (scalar or quad)
			if isscalar(pars.margin)
				pars.margin = pars.margin * [1 1 1 1];
			end
			sz = sz - pars.margin(1:2) - pars.margin(3:4);
			
			% divide by columns
			w = (sz(1) + pars.intercolumnspacing) / pars.cols - pars.intercolumnspacing;
			sz(1) = w;
			
			% apply fill / aspect ratio
			if pars.fill > 0
				% fill fraction
				sz(2) = sz(2) * pars.fill;
			elseif pars.fill < 0
				% aspect ratio
				sz(2) = sz(1) * (-1 / pars.fill);
			end
			
			% direct sizing model is used if either of width or height
			% is set
			if pars.width || pars.height
				
				% use aspect ratio to fill in either one that is not
				% specified
 				if ~pars.width || ~pars.height
					
					% aspect ratio must not be a fill
					if pars.fill >= 0
						error('cannot use fill fraction with direct sizing model');
					end

					% compute width
					if ~pars.width
	 					pars.width = pars.height * -pars.fill;
					end
					
					% compute height
					if ~pars.height
	 					pars.height = pars.width / -pars.fill;
					end
					
				end
				
				% store
				sz = [pars.width pars.height];
				
			end
			
%%%% GET TARGET DIMENSIONS (END)

			
			
			% orientation of figure is upright, unless printing
			% sideways, in which case the printing space is rotated too
			if pars.sideways
				set(p.h_figure, 'PaperOrientation', 'landscape')
				sz = fliplr(sz);
			else
				set(p.h_figure, 'PaperOrientation', 'portrait')
			end
			
			% report export size
			msg = ['exporting to ' int2str(sz(1)) 'x' int2str(sz(2)) 'mm'];
			if is_bitmap
				psz = sz / 25.4 * pars.dpi;
				msg = [msg ' (' int2str(psz(1)) 'x' int2str(psz(2)) 'px @ ' int2str(pars.dpi) 'DPI)'];
			else
				msg = [msg ' (vector format @ ' int2str(pars.dpi) 'DPI)'];
			end
			disp(msg);
			
			% if we are in defer state, we need to do a clean
			% recompute first so that axes get positioned so that
			% axis ticks get set correctly (if they are in
			% automatic mode), since the LAYOUT_MODE_PREPRINT
			% recompute will store the tick states.
			if p.state.defer
				p.state.defer = 0;
				p.recomputeLayout([]);
			end

			% turn off defer, if it is on
			p.state.defer = 0;
			
			% do a pre-print layout
			context.mode = panel.LAYOUT_MODE_PREPRINT;
			context.size_in_mm = sz;
			context.rect = [0 0 1 1];
			p.recomputeLayout(context);
			
			% need also to disable the warning that we should set
			% PaperPositionMode to auto during this operation -
			% we're setting it explicitly.
			w = warning('off', 'MATLAB:Print:CustomResizeFcnInPrint');
			
			% handle smoothing
			pars.write_dpi = pars.dpi;
			if pars.smooth > 1
				pars.write_dpi = pars.write_dpi * pars.smooth;
				print_filename = [pars.filename '-temp'];
			else
				print_filename = pars.filename;
			end

			% disable layout so it doesn't get computed during any
			% figure resize operations that occur during printing.
			p.state.defer = 1;
			
			% set size of figure now. it's important we do this
			% after the pre-print layout, because in SVG export
			% mode the on-screen figure size is changed and that
			% would otherwise affect ticks and limits.
			switch pars.fmt
				
				case 'svg'
					% plot2svg (our current SVG export mechanism) uses
					% 'Units' and 'Position' (i.e. on-screen position)
					% rather than the Paper- prefixed ones used by the
					% Matlab export functions.
					
					% store old on-screen position
					svg_units = get(p.h_figure, 'Units');
					svg_pos = get(p.h_figure, 'Position');
					
					% update on-screen position
					set(p.h_figure, 'Units', 'centimeters');
					pos = get(p.h_figure, 'Position');
					pos(3:4) = sz / 10;
					set(p.h_figure, 'Position', pos);
					
				otherwise
					set(p.h_figure, ...
						'PaperUnits', 'centimeters', ...
						'PaperPosition', [0 0 sz] / 10, ...
						'PaperSize', sz / 10 ... % * 1.5 / 10 ... % CHANGED 21/06/2011 so that -opdf works correctly - why was this * 1.5, anyway? presumably was spurious...
						);
					
			end
			
			% do fixdash (not for SVG, since plot2svg does a nice
			% job of dashed lines without our meddling...)
			if ~isequal(pars.fmt, 'svg')
				p.do_fixdash(context);
			end
			
			% do the export
			switch pars.fmt
				case 'svg'
					plot2svg(print_filename, p.h_figure);
				otherwise
					print(p.h_figure, '-loose', ['-d' pars.fmt], ['-r' int2str(pars.write_dpi)], print_filename)
			end

			% undo fixdash
			if ~isequal(pars.fmt, 'svg')
				p.do_fixdash([]);
			end
			
			% set on-screen figure size back to what it was, if it
			% was changed.
			switch pars.fmt
				case 'svg'
					set(p.h_figure, 'Units', svg_units);
					set(p.h_figure, 'Position', svg_pos);
			end
			
			% enable layout again (it was disabled, above, during
			% printing).
			p.state.defer = 0;
			
			% enable warnings
			warning(w);
			
			% do a post-print layout
			context.mode = panel.LAYOUT_MODE_POSTPRINT;
			context.size_in_mm = [];
			context.rect = [0 0 1 1];
			p.recomputeLayout(context);
			
			% handle smoothing
			if pars.smooth > 1
				psz = sz * pars.smooth / 25.4 * pars.dpi;
				msg = [' (reducing from ' int2str(psz(1)) 'x' int2str(psz(2)) 'px)'];
				disp(['smoothing by factor ' int2str(pars.smooth) msg]);
				im1 = imread(print_filename);
				delete(print_filename);
				sz = size(im1);
				sz = [sz(1)-mod(sz(1),pars.smooth) sz(2)-mod(sz(2),pars.smooth)] / pars.smooth;
				im = zeros(sz(1), sz(2), 3);
				mm = 1:pars.smooth:(sz(1) * pars.smooth);
				nn = 1:pars.smooth:(sz(2) * pars.smooth);
				for m = 0:pars.smooth-1
					for n = 0:pars.smooth-1
						im = im + double(im1(mm+m, nn+n, :));
					end
				end
				im = uint8(im / (pars.smooth^2));
				
				% set the DPI correctly in the new file
				switch pars.fmt
					case 'png'
						dpm = pars.dpi / 25.4 * 1000; 
						imwrite(im, pars.filename, ... 
							'XResolution', dpm, ... 
							'YResolution', dpm, ... 
							'ResolutionUnit', 'meter');
					case 'tiff'
						imwrite(im, pars.filename, ... 
							'Resolution', pars.dpi * [1 1]);
					otherwise
						imwrite(im, pars.filename);
				end
			end
			
		end

		function clearCallbacks(p)
			
			% clear all callback functions for the panel
			%
			% p.clearCallbacks()
			p.m_callback = {};
			
		end
		
		function setCallback(p, func, userdata)
			
			% set the callback function for the panel
			%
			% p.setCallback(myCallbackFunction, userdata)
			%
			% NB: this function clears all current callbacks, then
			%   calls addCallback(myCallbackFunction, userdata).
			p.clearCallbacks();
			p.addCallback(func, userdata);
			
		end
		
		function addCallback(p, func, userdata)
			
			% attach a callback function to receive panel events
			%
			% p.addCallback(myCallbackFunction, userdata)
			%   register myCallbackFunction() to be called when
			%   events occur on the panel. at present, the only
			%   event is "layout-updated", which usually occurs
			%   after the figure is resized. myCallbackFunction()
			%   should accept one argument, "data", which will
			%   have the following fields.
			%
			% "userdata": the userdata passed to this function, if
			%     any was supplied, else empty.
			%
			% "panel": a reference to the panel on which the
			%     callback was set. this object can be queried in
			%     the usual way.
			%
			% "event": name of event (currently only
			%	    "layout-updated").
			%
			% "context": the layout context for the panel. this
			%	    includes a field "size_in_mm" which is the
			%	    physical size of the rendering surface (screen
			%	    real estate, or image file) and "rect" which is
			%	    the relative size of the rectangle within that
			%	    occupied by the panel which is the context of
			%	    the callback (in [left, bottom, width, height]
			%	    format).
			
			invalid = ~isscalar(func) || ~isa(func, 'function_handle');
			if invalid
				error('panel:InvalidArgument', 'argument to callback() must be a function handle');
			end
			if nargin == 2
				p.m_callback{end+1} = {func []};
			else
				p.m_callback{end+1} = {func userdata};
			end
			
		end
		
		function identify(p)

			% add annotations to help identify individual panels
			%
			% p.identify()
			%   when creating a complex layout, it can become
			%   confusing as to which panel is which. this
			%   function adds a text label to each axis panel
			%   indicating how to reference the axis panel through
			%   the root panel. for instance, if "(2, 3)" is
			%   indicated, you can find that panel at p(2, 3).
			%
			% see also: show()
			
			if p.isObject()
				
				% get managed axes
				h_axes = p.getAllManagedAxes();
			
				% if no axes, ignore
				if isempty(h_axes)
					return
				end
				
				% mark first axis
				h_axes = h_axes(1);
				cla(h_axes);
				text(0.5, 0.5, p.state.name, 'fontsize', 12, 'hori', 'center', 'parent', h_axes);
				axis(h_axes, [0 1 0 1]);
				grid(h_axes, 'off')

			else
				
				% recurse
				for c = 1:length(p.m_children)
					p.m_children(c).identify();
				end
				
			end
			
		end
		
		function repack(p, packspec)
			
			% change the packing specifier for an existing panel
			%
			% p.repack(packspec)
			%   repack() is a convenience function provided to
			%   allow easy development of a layout from the
			%   command prompt. packspec can be any packing
			%   specifier accepted by pack().
			%
			% see also: pack()
			
			% deny repack() on root
			if p.isRoot()
				
				% let's deny this. I'm not sure it makes anyway. you
				% could always pack into root with a panel with
				% absolute positioning, so let's deny first, and
				% allow later if we're sure it's a good idea.
				error('panel:InvalidArgument', 'root panel cannot be repack()ed');
				
			end
			
			% validate
			validate_packspec(packspec);
			
			% handle units
			if iscell(packspec)
				units = p.getPropertyValue('units');
				packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm');
			end
			
			% update the packspec
			p.packspec = packspec;
			
			% and recomputeLayout
			p.recomputeLayout([]);
			
		end
		
		function pack(p, varargin)
			
			% add (pack) child panel(s) into an existing panel
			%
			% p.pack(...)
			%   add children to the panel "p", committing it as a
			%   "parent" panel (if it is not already). new (child)
			%   panels are created using this call - they start as
			%   "uncommitted" panels. if the parent already has
			%   children, the new children are appended. The
			%   following arguments are understood:
			%
			% 'h'/'v' - switch "p" to pack in the horizontal or
			%   vertical packing dimension for relative packing
			%   mode (default for new panels is vertical).
			%
			% {a, b, c, ...} (a cell row vector) - pack panels
			%   into "p" with "packing specifiers" a, b, c, etc.
			%   packing specifiers are detailed below.
			%
			% PACKING MODES
			%   panels can be packed into their parent in two
			%   modes, dependent on their packing specifier. you
			%   can see a visual representation of these modes on
			%   the HTML documentation page "Layout".
			%
			% (i) Relative Mode - panels are packed into the space
			%   occupied by their parent. size along the parent's
			%   "packing dimension" is dictated by the packing
			%   specifier; along the other dimension size matches
			%   the parent. the following packing specifiers
			%   indicate Relative Mode.
			%
			%   a) Fixed Size: the specifier is a scalar double in
			%   a cell {d}. The panel will be of size d in the
			%   current units of "p" (see the property "p.units"
			%   for details, but default units are mm).
			%
			%   b) Fractional Size: the specifier is a scalar
			%   double between 0 and 1 (or between 1 and 100, as a
			%   percentage). The panel is sized as a fraction of
			%   the space remaining in its parent after Fixed Size
			%   panels and inter-panel margins have been subtracted.
			%
			%   c) Stretchable: the specifier is the empty matrix
			%   []. remaining space in the parent after Fixed and
			%   Fractional Size panels have been subtracted is
			%   shared out amongst Stretchable Size panels.
			%
			% (ii) Absolute Mode - panels hover above their
			%   parent and do not take up space, as if using the
			%   position:absolute property in CSS. The packing
			%   specifier is a 1x4 double vector indicating the
			%   [left bottom width height] of the panel in
			%   normalised coordinates of its parent. for example,
			%   the specifier [0 0 1 1] generates a child panel
			%   that fills its parent.
			%
			% SHORTCUTS
			%
			% ** a small scalar integer, N, (1 to 32) is expanded
			%    to {[], [], ... []}, with N entries. that is, it
			%    packs N panels in Relative Mode (Stretchable) and
			%    shares the available space between them.
			%
			% ** the call to pack() is recursive, so following a
			%    packing specifier list, an additional list will
			%    be used to generate a separate call to pack() on
			%    each of the children created by the first. hence:
			%
			%      p.pack({[] []}, {[] []})
			%
			%    will create a 2x2 grid of panels that share the
			%    space of their parent, "p". since the argument
			%    "2" expands to {[] []} (see above), the same grid
			%    can be created using:
			%
			%      p.pack(2, 2)
			%
			%    which is a common idiom in the demos. NB: on
			%    recursion, the packing dimension is flipped
			%    automatically, so that a grid is formed.
			%
			% ** if no arguments are passed at all, a single
			%    argument {[]} is assumed, so that a single
			%    additional panel is packed into the parent in
			%    relative packing mode and with stretchable size.
			%
			% see also: panel (overview), panel/panel(), select()
			%
			% LEGACY
			%
			%   the interface to pack() was changed at release
			%   2.10 to add support for panels of fixed physical
			%   size. the interface offered at 2.9 and earlier is
			%   still available (look inside panel.m - search for
			%   text "LEGACY" - for details).

			% LEGACY
			%
			%  releases of panel prior to 2.10 did not support
			%  panels of fixed physical size, and therefore had
			%  developed a different argument form to that used in
			%  2.10 and beyond. specifically, the following
			%  additional arguments are accepted, for legacy
			%  support:
			%
			% 'abs'
			%   the next argument will be an absolute position, as
			%   described below. you should avoid using absolute
			%   positioning mode, in general, since this does not
			%   take advantage of panel's automatic layout.
			%   however, on occasion, you may need to take manual
			%   control of the position of one or more panels. see
			%   demopanelH for an example.
			%
			% 1xN row vector (without 'abs')
			%   pack N new panels along the packing dimension in
			%   relative mode, with the relative size of each
			%   given by the elements of the vector. -1 can be
			%   passed for any elements to mark those panel as
			%   'stretchable', so that they fill available space
			%   left over by other panels packed alongside. the
			%   sum of the vector (apart from any -1 entries)
			%   should not come to more than 1, or a warning will
			%   be generated during laying out. an example would
			%   be [1/4 1/4 -1], to pack 3 panels, at 25, 25 and
			%   50% relative sizes. though, NB, you can use
			%   percentages instead of fractions if you prefer, in
			%   which case they should not sum to over 100. so
			%   that same pack() would be [25 25 -1].
			%
			% 1x4 row vector (after 'abs')
			%   pack 1 new panel using absolute positioning. the
			%   argument indicates the [left bottom width height]
			%   of the new panel, in normalised coordinates, as a
			%   fraction of its parent's position. panels using
			%   absolute positioning mode are ignored for the sake
			%   of layout, much like items using
			%   'position:absolute' in CSS.
			
			% handle legacy, parse arguments from varargin into args
			args = {};
			while ~isempty(varargin)
				
				% peel
				arg = varargin{1};
				varargin = varargin(2:end);
				
				% handle shortcut (small integer) on current interface
				if isa(arg, 'double') && isscalar(arg) && round(arg) == arg && arg >= 1 && arg <= 32
					arg = cell(1, arg);
				end
					
				% handle current interface - note that the argument
				% "recursive" is private and not advertised to the
				% user.
				if isequal(arg, 'h') || isequal(arg, 'v') || (iscell(arg) && isrow(arg)) || isequal(arg, 'recursive')
					args{end+1} = arg;
					continue
				end
				
				% report (DEBUG)
% 				panel.debugmsg('use of LEGACY interface to pack()', 1);
				
				% handle legacy case
				if isequal(arg, 'abs')
					if length(varargin) ~= 1 || ~isnumeric(varargin{1}) || ~isofsize(varargin{1}, [1 4])
						error('panel:LegacyAbsNotFollowedBy1x4', 'the argument "abs" on the legacy interface should be followed by a [1x4] row vector');
					end
					abs = varargin{1};
					varargin = varargin(2:end);
					args{end+1} = {abs};
					continue
				end
				
				% handle legacy case
				if isa(arg, 'double') && isrow(arg)
					arg_ = {};
					for a = 1:length(arg)
						aa = arg(a);
						if isequal(aa, -1)
							arg_{end+1} = [];
						else
							arg_{end+1} = aa;
						end
					end
					args{end+1} = arg_;
					continue
				end
				
				% unrecognised argument
				error('panel:InvalidArgument', 'argument to pack() not recognised');
				
			end
			
			% check m_panelType
			if p.isObject()
				error('panel:PackWhenObjectPanel', ...
					'cannot pack() into this panel - it is already committed as an object panel');
			end
			
			% if no arguments, simulate an argument of [], to pack
			% a single panel of stretchable size
			if isempty(args)
				args = {{[]}};
			end
			
			% state
			recursive = false;
			
			% handle arguments one by one
			while ~isempty(args) && ischar(args{1})
				
				% extract
				arg = args{1};
				args = args(2:end);
				
				% handle string arguments
				switch arg
					case 'h'
						p.packdim = 1;
					case 'v'
						p.packdim = 2;
					case 'recursive'
						recursive = true;
					otherwise
						error('panel:InvalidArgument', ['pack() did not recognise the argument "' arg '"']);
				end
					
			end
			
			% if no more arguments that's weird but not bad
			if isempty(args)
				return
			end
			
			% next argument now must be a cell
			arg = args{1};
			args = args(2:end);
			if ~iscell(arg)
				panel.error('InternalError');
			end

			% commit as parent
			p.commitAsParent();				

			% for each element
			for i = 1:length(arg)

				% get packspec
				packspec = arg{i};

				% validate
				validate_packspec(packspec);
				
				% handle units
				if iscell(packspec)
					units = p.getPropertyValue('units');
					packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm');
				end

				% create a child
				child = panel('pack', p);
				child.packspec = packspec;

				% store it in the parent
				if isempty(p.m_children)
					p.m_children = child;
				else
					p.m_children(end+1) = child;
				end

				% recurse (further argumens are passed on)
				if ~isempty(args)
					child_packdim = flippackdim(p.packdim);
					edges = 'hv';
					child.pack('recursive', edges(child_packdim), args{:});
				end

			end
				
			% this must generate a recomputeLayout(), since the
			% addition of new panels may affect the layout. any
			% recursive call passes 'recursive', so that only the
			% root call actually bothers doing a layout.
			if ~recursive
				p.recomputeLayout([]);
			end
			
		end
		
		function h_out = select(p, h_object)
			
			% create or select an axis or object panel
			%
			% h = p.select(h)
			%   this call will return the handle of the object
			%   associated with the panel. if the panel is not yet
			%   committed, this will involve first committing it
			%   as an "object panel". if a list of objects ("h")
			%   is passed, these are the objects associated with
			%   the panel; if not, a new axis is created by the
			%   panel when this function is called.
			%
			%   if the object list includes axes, then the "object
			%   panel" is also known as an "axis panel". in this
			%   case, the call to select() will make the (first)
			%   axis current, unless an output argument is
			%   requested, in which case the handle of the axes
			%   are returned but no axis is made current.
			%
			%   the passed objects can be user-created axes (e.g.
			%   a colorbar) or any graphics object that is to have
			%   its position managed (e.g. a uipanel). your
			%   mileage may vary with different types of graphics
			%   object, please let me know.
			%
			% see also: panel (overview), panel/panel(), pack()
			
			% handle "all" and "data"
			if nargin == 2 && isstring(h_object) && (strcmp(h_object, 'all') || strcmp(h_object, 'data'))
				
				% collect
				h_out = [];
				
				% commit all uncommitted panels as axis panels by
				% selecting them once
				if p.isParent()

					% recurse
					for c = 1:length(p.m_children)
						h_out = [h_out p.m_children(c).select(h_object)];
					end

				elseif p.isUncommitted()

					% select in an axis
					h_out = p.select();
					
					% plot some data
					if strcmp(h_object, 'data')
						plot(h_out, randn(100, 1), 'k-');
					end

				end
				
				% ok
				return
				
			end
			
			% check m_panelType
			if p.isParent()
				error('panel:SelectWhenParent', 'cannot select() this panel - it is already committed as a parent panel');
			end
			
			% commit as object
			p.commitAsObject();

			% assume not a new object
			newObject = false;
			
			% use passed graphics object
			if nargin >= 2
				
				% validate
				if ~all(ishandle(h_object))
					error('panel:InvalidArgument', 'argument to select() must be a list of handles to graphics objects');
				end
				
				% validate
				if ~isempty(p.h_object)
					error('panel:SelectWithObjectWhenObject', 'cannot select() new objects into this panel - it is already managing objects');
				end
				
				% store
				p.h_object = h_object;
				newObject = true;
				
				% make sure it has the correct parent - this doesn't
				% seem to affect axes, so we do it for all
 				set(p.h_object, 'parent', p.h_parent);
				
			end
			
			% create new axis if necessary
			if isempty(p.h_object)
				% 'NextPlot', 'replacechildren'
				%   make sure fonts etc. don't get changed when user
				%   plots into it
				p.h_object = axes( ...
					'Parent', p.h_parent, ...
					'NextPlot', 'replacechildren' ...
					);
				newObject = true;
			end
			
			% if wrapped objects include an axis, and no output args, make it current
			h_axes = p.getAllManagedAxes();
			if ~isempty(h_axes) && ~nargout
				set(p.h_figure, 'CurrentAxes', h_axes(1));
				
				% 12/07/11: this call is slow, because it implies "drawnow"
% 				figure(p.h_figure);

				% 12/07/11: this call is fast, because it doesn't
				set(0, 'CurrentFigure', p.h_figure);
				
			end
			
			% and return object list
			if nargout
				h_out = p.h_object;
			end
			
			% this must generate a applyLayout(), since the axis
			% will need positioning appropriately
			if newObject
				% 09/03/12 mitch
				% if there isn't a context yet, we'll have to
				% recomputeLayout(), in fact, to generate a context first.
				% this will happen, for instance, if a single panel
				% is generated in a window that was already open
				% (no resize event will fire, and since pack() is
				% not called, it will not call recomputeLayout() either).
				% nonetheless, we have to reposition this object, so
				% this forces us to recomputeLayout() now and generate
				% that context we need.
				if isempty(p.m_context)
					p.recomputeLayout([]);
				else
					p.applyLayout();
				end
			end
			
		end
		
	end
	
	
	
	
	
	
	
	
	
	
	
	
	
	%% ---- HIDDEN OVERLOADS ----
	
	methods (Hidden = true)
		
		function out = vertcat(p, q)
			error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)');
		end
		
		function out = horzcat(p, q)
			error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)');
		end
		
		function out = cat(dim, p, q)
			error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)');
		end
		
		function out = ge(p, q)
			error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel');
		end
		
		function out = le(p, q)
			error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel');
		end
		
		function out = lt(p, q)
			error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel');
		end
		
		function out = gt(p, q)
			error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel');
		end
		
		function out = eq(p, q)
			out = eq@handle(p, q);
		end
		
		function out = ne(p, q)
			out = ne@handle(p, q);
		end
		
	end
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	%% ---- PUBLIC HIDDEN GET/SET ----
	
	methods (Hidden = true)
		
		function p = descend(p, indices)
			
			while ~isempty(indices)

				% validate
				if numel(p) > 1
					error('panel:InvalidIndexing', 'you can only use () on a single (scalar) panel');
				end

				% validate
				if ~p.isParent()
					error('panel:InvalidIndexing', 'you can only use () on a parent panel');
				end
				
				% extract
				index = indices{1};
				indices = indices(2:end);

				% only accept numeric
				if ~isnumeric(index) || ~isscalar(index)
					error('panel:InvalidIndexing', 'you can only use () with scalar indices');
				end
					
				% do the reference
				p = p.m_children(index);
					
			end
			
		end

		function p_out = subsasgn(p, refs, value)
			
			% output is always subject
			p_out = p;
			
			% handle () indexing
			if strcmp(refs(1).type, '()')
				p = p.descend(refs(1).subs);
				refs = refs(2:end);
			end
				
			% is that it?
			if isempty(refs)
				error('panel:InvalidIndexing', 'you cannot assign to a child panel');
			end
			
 			% next ref must be .
 			if ~strcmp(refs(1).type, '.')
				panel.error('InvalidIndexing');
			end
			
			% either one (.X) or two (.ch.X)
			switch numel(refs)
				
				case 2
			
					% validate
					if ~strcmp(refs(2).type, '.')
						panel.error('InvalidIndexing');
					end
					
					% validate
					switch refs(2).subs
						case {'fontname' 'fontsize' 'fontweight'}
						case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'}
						otherwise
							panel.error('InvalidIndexing');
					end
					
					% avoid computing layout whilst setting descendant
					% properties
					p.defer();
					
					% recurse
					switch refs(1).subs
						case {'children' 'ch'}
							cs = p.m_children;
							for c = 1:length(cs)
								subsasgn(cs(c), refs(2:end), value);
							end
						case {'descendants' 'de'}
							cs = p.getPanels('*');
							for c = 1:length(cs)
								if cs{c} ~= p
									subsasgn(cs{c}, refs(2:end), value);
								end
							end
						case {'family' 'fa'}
							cs = p.getPanels('*');
							for c = 1:length(cs)
								subsasgn(cs{c}, refs(2:end), value);
							end
					end
					
					% release for laying out
					p.undefer();

					% mark for appropriate updates
					refs(1).subs = refs(2).subs;
					
				case 1

					% delegate
					p.setPropertyValue(refs(1).subs, value);
					
				otherwise
					panel.error('InvalidIndexing');
	
			end
			
			% update layout as necessary
			switch refs(1).subs
				case {'fontname' 'fontsize' 'fontweight'}
					p.applyLayout('recurse');
				case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'}
					p.recomputeLayout([]);
			end

		end
		
		function out = subsref(p, refs)
			
			% handle () indexing
			if strcmp(refs(1).type, '()')
				p = p.descend(refs(1).subs);
				refs = refs(2:end);
			end
				
			% is that it?
			if isempty(refs)
				out = p;
				return
			end

 			% next ref must be .
 			if ~strcmp(refs(1).type, '.')
				panel.error('InvalidIndexing');
			end
			
			% switch on "fieldname"
			switch refs(1).subs
				
				case { ...
						'fontname' 'fontsize' 'fontweight' ...
						'margin' 'marginleft' ...
						'marginbottom' 'marginright' 'margintop' ...
 						'units' ...
						}

					% delegate this property get
					out = p.getPropertyValue(refs(1).subs);
					
				case 'position'
					out = p.getObjectPosition();
					
				case 'figure'
					out = p.h_figure;
					
				case 'packspec'
					out = p.packspec;
					
				case 'axis'
					if p.isObject()
						out = p.getAllManagedAxes();
					else
						out = [];
					end
					
				case 'object'
					if p.isObject()
						h = p.h_object;
						ih = ishandle(h);
						out = h(ih);
					else
						out = [];
					end
					
 				case {'ch' 'children' 'de' 'descendants' 'fa' 'family'}
					
					% get the set
					switch refs(1).subs
						case {'children' 'ch'}
							out = p.m_children;
						case {'descendants' 'de'}
							out = p.getPanels('*');
							for c = 1:length(out)
								if out{c} == p
									out = out([1:c-1 c+1:end]);
									break
								end
							end
						case {'family' 'fa'}
							out = p.getPanels('*');
					end
					
					% we handle a special case of deeper reference
					% here, because we are abusing matlab's syntax to
					% do it. other cases (non-abusing) will be handled
					% recursively, as usual. this is when we go:
					%
					% p.ch.axis
					%
					% which isn't syntactically sound since p.ch is a
					% cell array (and potentially a non-singular one
					% at that). we re-interpret this to mean
					% [p.ch{1}.axis p.ch{2}.axis ...], as follows.
					if length(refs) > 1 && isequal(refs(2).type, '.')
						switch refs(2).subs
							case {'axis' 'object'}
								pp = out;
								out = [];
								for i = 1:length(pp)
									out = cat(2, out, subsref(pp{i}, refs(2)));
								end
								refs = refs(2:end); % used up!
							otherwise
								% give an informative error message
								panel.error('InvalidIndexing');
						end
					end
					
				case { ...
						'addCallback' 'setCallback' 'clearCallbacks' ...
						'hold' ...
						'refresh' 'export' ...
						'pack' 'repack' ...
						'identify' 'show' ...
						}

					% validate
					if length(refs) ~= 2 || ~strcmp(refs(2).type, '()')
						error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']);
					end
					
					% delegate this function call with no output
					builtin('subsref', p, refs);
					return
					
				case { ...
						'select' 'fixdash' ...
						'xlabel' 'ylabel' 'zlabel' 'title' ...
						}
					
					% validate
					if length(refs) ~= 2 || ~strcmp(refs(2).type, '()')
						error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']);
					end
					
					% delegate this function call with output
					if nargout
						out = builtin('subsref', p, refs);
					else
						builtin('subsref', p, refs);
					end
					return
					
				otherwise
					panel.error('InvalidIndexing');
							
			end
			
			% continue
			if length(refs) > 1
				out = subsref(out, refs(2:end));
			end
			
		end
		
	end
	
	
	
	
	
	
	
	
	
	
	
	
	
	%% ---- UTILITY METHODS ----
	
	methods (Access = private)
		
		function b = ismanagefont(p)
			
			% ask root
			b = p.m_root.state.manage_font;
			
		end
		
		function b = isdefer(p)
			
			% ask root
			b = p.m_root.state.defer ~= 0;
			
		end
		
		function defer(p)
			
			% increment
			p.m_root.state.defer = p.m_root.state.defer + 1;
			
		end

		function undefer(p)
			
			% decrement
			p.m_root.state.defer = p.m_root.state.defer - 1;
			
		end

		function cs = getPanels(p, panelTypes, edgespec, all)
			
			% return all the panels that match the specification.
			%
			% panelTypes "*": return all panels
			% panelTypes "s": return all sizeable panels (parent,
			%		object and uncommitted)
			% panelTypes "p": return only physical panels (object
			%   or uncommitted)
			% panelTypes "o": return only object panels
			%
			% if edgespec/all is specified, only panels matching
			% the edgespec are returned (all of them if "all" is
			% true, or any of them - the first one, in fact - if
			% "all" is false).
			
			cs = {};
			
			% do not include any that use absolute positioning -
			% they stand outside of the sizing model
			skip = (numel(p.packspec) == 4) && ~any(panelTypes == '*');
			
			if p.isParent()
				
				% return if appropriate type
 				if any(panelTypes == '*s') && ~skip
					cs = {p};
 				end
				
				% if edgespec was supplied
				if nargin == 4

					% if we are perpendicular to the specified edge
					if p.packdim ~= edgespec(1)

						if all
							
							% return all matching
							for c = 1:length(p.m_children)
								ppp = p.m_children(c).getPanels(panelTypes, edgespec, all);
								cs = cat(2, cs, ppp);
							end
							
						else
							
							% return only the first one
							cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all));
							
						end

					else

						% if we are parallel to the specified edge
						if edgespec(2) == 2
							
							% use last
							ppp = p.m_children(end).getPanels(panelTypes, edgespec, all);
							cs = cat(2, cs, ppp);
							
						else
							
							% use first
							cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all));
							
						end

					end
					
				else
					
					% else, return all
					for c = 1:length(p.m_children)
						ppp = p.m_children(c).getPanels(panelTypes);
						cs = cat(2, cs, ppp);
					end
					
				end
				
			elseif p.isObject()
				
				% return if appropriate type
				if any(panelTypes == '*spo') && ~skip
					cs = {p};
				end
				
			else
				
				% return if appropriate type
				if any(panelTypes == '*sp') && ~skip
					cs = {p};
				end
				
			end
			
		end
		
		function commitAsParent(p)
			
			if p.isUncommitted()
				p.m_panelType = p.PANEL_TYPE_PARENT;
			elseif p.isObject()
				error('panel:AlreadyCommitted', 'cannot make this panel a parent panel, it is already an object panel');
			end

		end
		
		function commitAsObject(p)
			
			if p.isUncommitted()
				p.m_panelType = p.PANEL_TYPE_OBJECT;
			elseif p.isParent()
				error('panel:AlreadyCommitted', 'cannot make this panel an object panel, it is already a parent panel');
			end

		end
		
		function b = isRoot(p)
			
			b = isempty(p.parent);
			
		end
		
		function b = isParent(p)
			
			b = p.m_panelType == p.PANEL_TYPE_PARENT;
			
		end
		
		function b = isObject(p)
			
			b = p.m_panelType == p.PANEL_TYPE_OBJECT;
			
		end
		
		function b = isUncommitted(p)
			
			b = p.m_panelType == p.PANEL_TYPE_UNCOMMITTED;
			
		end

		function h_axes = getAllManagedAxes(p)
			
			h_axes = [];
			for n = 1:length(p.h_object)
				h = p.h_object(n);
				if isaxis(h)
					h_axes = [h_axes h];
				end
			end
			
		end
		
		function h_object = getOrCreateAxis(p)
			
			switch p.m_panelType
				
				case p.PANEL_TYPE_PARENT
					
					% create if not present
					if isempty(p.h_object)
						
						% 'Visible', 'off'
						%   this is the hidden axis of a parent panel,
						%   used for displaying a parent panel's xlabel,
						%   ylabel and title, but not as a plotting axis
						%
						% 'NextPlot', 'replacechildren'
						%   make sure fonts etc. don't get changed when user
						%   plots into it
						p.h_object = axes( ...
							'Parent', p.h_parent, ...
							'Visible', 'off', ...
							'NextPlot', 'replacechildren' ...
							);
						
						% make sure it's unitary, to help us in
						% positioning labels and title
						axis(p.h_object, [0 1 0 1]);
						
						% refresh this axis position
						p.applyLayout();
						
					end
					
					% ok
					h_object = p.h_object;
					
				case p.PANEL_TYPE_OBJECT
					
					% ok
					h_object = p.getAllManagedAxes();
					if isempty(h_object)
						error('panel:ManagedObjectNotAnAxis', 'this object panel does not manage an axis');
					end
					
				case p.PANEL_TYPE_UNCOMMITTED
					
					panel.error('PanelUncommitted');
					
			end
			
		end
		
		function removeChild(p, child)
			
			% if not a parent, fail but warn (shouldn't happen)
			if ~p.isParent()
				warning('panel:NotParentOnRemoveChild', 'i am not a parent (in removeChild())');
				return
			end
			
			% remove from children
			for c = 1:length(p.m_children)
				if p.m_children(c) == child
					p.m_children = p.m_children([1:c-1 c+1:end]);
					return
				end
			end
			
			% warn
			warning('panel:ChildAbsentOnRemoveChild', 'child not found (in removeChild())');
			
		end
		
		function h = getShowAxis(p)
			
			if p.isRoot()
				if isempty(p.h_showAxis)
					
					% create
					p.h_showAxis = axes( ...
						'Parent', p.h_parent, ...
						'units', 'normalized', ...
						'position', [0 0 1 1] ...
						);
					
					% move to bottom
					c = get(p.h_parent, 'children');
					c = [c(2:end); c(1)];
					set(p.h_parent, 'children', c);
					
					% finalise axis
					set(p.h_showAxis, ...
						'xtick', [], 'ytick', [], ...
						'color', 'none', 'box', 'off' ...
						);
					axis(p.h_showAxis, [0 1 0 1]);
					
					% hold
					hold(p.h_showAxis, 'on');
					
				end
				
				% return it
				h = p.h_showAxis;
				
			else
				h = p.parent.getShowAxis();
			end
			
		end
		
		function fireCallbacks(p, event)
		
			% for each attached callback
			for c = 1:length(p.m_callback)
				
				% extract
				callback = p.m_callback{c};
				func = callback{1};
				userdata = callback{2};
				
				% fire
				data = [];
				data.panel = p;
				data.event = event;
				data.context = p.m_context;
				data.userdata = userdata;
				func(data);
				
			end
				
		end
		
	end
	
	
	
	

		
	
	
	
	
	
		
	%% ---- LAYOUT METHODS ----
	
	methods

		function refresh(p)
			
			% recompute layout of all panels
			%
			% p.refresh()
			%   recompute the layout of all panels from scratch.
			%   this should not usually be required, and is
			%   provided primarily for legacy support.
			
			% LEGACY
			%
			% NB: if you pass 'defer' to the constructor, calling
			% refresh() both recomputes the layout and releases
			% the defer mode. future changes to properties (e.g.
			% margins) will cause immediate recomputation of the
			% layout, so only call refresh() when you're done.
			
			% bubble up to root
			if ~p.isRoot()
				p.m_root.refresh();
				return
			end
			
			% release defer
			p.state.defer = 0;

			% debug output
% 			panel.debugmsg(['refresh "' p.state.name '"...']);
			
			% call recomputeLayout
			p.recomputeLayout([]);
			
		end
		
	end
		
	methods (Access = private)
		
		function do_fixdash(p, context)
			
			% if context is [], this is _after_ the layout for
			% export, so we need to restore
			if isempty(context)
				
				% restore lines we changed to their original state
				for r = 1:length(p.m_fixdash_restore)
					
					% get
					restore = p.m_fixdash_restore{r};
					
					% if empty, no change was made
					if ~isempty(restore)
						set(restore.h_line, ...
							'xdata', restore.xdata, 'ydata', restore.ydata);
						delete([restore.h_supp restore.h_mark]);
					end
					
				end
				
			else
				
% 				% get handles to objects that still exist
% 				h_lines = p.m_fixdash(ishandle(p.m_fixdash));
				
				% no restores
				p.m_fixdash_restore = {};
				
				% for each line
				for i = 1:length(p.m_fixdash)
					
					% get
					fix = p.m_fixdash{i};
					
					% final check
					if ~ishandle(fix.h) || ~isequal(get(fix.h, 'type'), 'line')
						continue
					end
					
					% apply dashstyle
					p.m_fixdash_restore{end+1} = dashstyle_line(fix, context);

				end
				
			end

		end

		function p = recomputeLayout(p, context)
			
			% this function recomputes the layout from scratch.
			% this means calculating the sizes of the root panel
			% and all descendant panels. after this is completed,
			% the function calls applyLayout to effect the new
			% layout.
			
			% if not root, bubble up to root
			if ~p.isRoot()
				p.m_root.recomputeLayout(context);
				return
			end
			
			% if in defer mode, do not compute layout
			if p.isdefer()
				return
			end
			
			% if no context supplied (e.g. on resize events), use
			% the figure window (a context is supplied if
			% exporting to an image file).
			if isempty(context)
				context.mode = panel.LAYOUT_MODE_NORMAL;
				context.size_in_mm = [];
				context.rect = [0 0 1 1];
			end				
			
			% debug output
% 			panel.debugmsg(['recomputeLayout "' p.state.name '"...']);

% 			% root may have a packspec of its own
% 			if ~isempty(p.packspec)
% 				if isscalar(p.packspec)
% 					% this should never happen, because it should be
% 					% caught when the packspec is set in repack()
% 					warning('panel:RootPanelCannotUseRelativeMode', 'the root panel uses relative positioning mode - this is ignored');
% 				else
% 					context.rect = p.packspec;
% 				end
% 			end
			
			% if not given a context size, use the size on screen
			% of the parent figure
			if isempty(context.size_in_mm)
				
				% get context (whole parent) size in its units
				pp = get(p.h_figure, 'position');
				context_size = pp(3:4);

				% defaults, in case this fails for any reason
				screen_size = [1280 1024];
				if ismac
					screen_dpi = 72;
				else
					screen_dpi = 96;
				end

				% get screen DPI
				try
					local_screen_dpi = get(0, 'ScreenPixelsPerInch');
					if ~isempty(local_screen_dpi)
						screen_dpi = local_screen_dpi;
					end
				end

				% get screen size
				try
					local_screen_size = get(0, 'ScreenSize');
					if ~isempty(local_screen_size)
						screen_size = local_screen_size;
					end
				end
				
				% get figure width and height on screen
				switch get(p.h_figure, 'Units')
					
					case 'points'
						points_per_inch = 72;
						context.size_in_mm = context_size / points_per_inch * 25.4;
						
					case 'inches'
						context.size_in_mm = context_size * 25.4;
						
					case 'centimeters'
						context.size_in_mm = context_size * 10.0;
						
					case 'pixels'
						context.size_in_mm = context_size / screen_dpi * 25.4;
						
					case 'characters'
						context_size = context_size .* [5 13]; % convert to pixels (based on empirical measurement)
						context.size_in_mm = context_size / screen_dpi * 25.4;
						
					case 'normalized'
						context_size = context_size .* screen_size(3:4); % convert to pixels (based on screen size)
						context.size_in_mm = context_size / screen_dpi * 25.4;
						
					otherwise
						error('panel:CaseNotCoded', ['case not coded, (Parent Units are ' get(p.h_figure, 'Units') ')']);
						
				end
				
			end
			
			% that's the figure size, now we need the size of our
			% parent, if it's not the figure too
			if p.h_parent ~= p.h_figure
				units = get(p.h_parent, 'units');
				set(p.h_parent, 'units', 'normalized');
				pos = get(p.h_parent, 'position');
				set(p.h_parent, 'units', units);
				context.size_in_mm = context.size_in_mm .* pos(3:4);
			end
			
			% for the root, we apply the margins here, since it's
			% a special case because there's always exactly one of
			% it
			margin = p.getPropertyValue('margin', 'mm');
			m = margin([1 3]) / context.size_in_mm(1);
			context.rect = context.rect + [m(1) 0 -sum(m) 0];
			m = margin([2 4]) / context.size_in_mm(2);
			context.rect = context.rect + [0 m(1) 0 -sum(m)];
			
			% now, recurse
			p.recurseComputeLayout(context);
			
			% clear h_showAxis when we recompute the layout
			if ~isempty(p.h_showAxis)
				delete(p.h_showAxis);
				p.h_showAxis = [];
			end

			% having computed the layout, we now apply it,
			% starting at the root panel.
			p.applyLayout('recurse');
			
		end
		
		function recurseComputeLayout(p, context)
			
			% store context
			p.m_context = context;
			
			% if no children, do nothing further
			if isempty(p.m_children)
				return
			end
			
			% else, we're going to recompute the layout for our
			% children
			margins = [];
			
			% get size to pack into
			mm_canvas = context.size_in_mm(p.packdim);
			mm_context = mm_canvas * context.rect(2+p.packdim);
			
			% get list of children that are packed relative - we
			% do this because the computation only handles these
			% relative children; absolute packed children are
			% ignored through the computation, and are just packed
			% as specified when the time comes.
			rel_list = [];
			
			% for each child
			for i = 1:length(p.m_children)

				% get child
				c = p.m_children(i);			
			
				% is it packed abs?
				if isofsize(c.packspec, [1 4])
					continue
				end
				
				% if not, it's packed relative, so add to list
				rel_list(end+1) = i;
				
			end
				
			% array of actual sizes as fraction of parent (note we
			% only represent the rel_list).
			zz = zeros(1, length(rel_list));
			sz_phys = zz;
			sz_frac = zz;
			i_stretch = zz;
			
			% for each child that is packed relative
			for i = 1:length(rel_list)

				% get child
				c = p.m_children(rel_list(i));

				% get internal margin
				margin = c.getPropertyValue('margin', 'mm');
				if p.packdim == 2
					margin = margin([2 4]);
					margin = fliplr(margin); % doclink FLIP_PACKDIM_2 - same reason, here!
				else
					margin = margin([1 3]);
				end
				margins(i:i+1, i) = margin';
				
				% subtract fixed size packspec from packing size
				if iscell(c.packspec)
					% NB: fixed size is always _stored_ in mm!
					sz_phys(i) = c.packspec{1};
				end
				
				% get relative packing sizes
				if isnumeric(c.packspec) && isscalar(c.packspec)
					% NB: relative size is a scalar numeric
					sz_frac(i) = c.packspec;
					% convert perc to frac
					if sz_frac(i) > 1
						sz_frac(i) = sz_frac(i) / 100;
					end
				end
				
				% get stretch packing size
				if isempty(c.packspec)
					% NB: these will be filled later
					i_stretch(i) = 1;
				end
				
				% else, it's an abs packing size, and we can ignore
				% it for this phase of layout
				
			end
			
			% finalise internal margins (that is, the margin at
			% each boundary between two adjacent relative packed
			% panels is the maximum of the margins specified by
			% each of the pair).
			margins = max(margins, [], 2);
			margins = margins(2:end-1)';
			
			% subtract internal margins to give available space
			% for objects (in mm)
			mm_objects = mm_context - sum(margins);
			
			% now, subtract physically sized objects to give
			% available space to share out amongst panels that
			% specify their size as a fraction.
			mm_share = mm_objects - sum(sz_phys);
			
			% and now stretch items can be given their actual
			% fractional size, since we now know who they are
			% sharing space with.
			sz_frac(find(i_stretch)) = (1 - sum(sz_frac)) / sum(i_stretch);
			
			% and we can now get the real physical size of all the
			% fractionally-sized panels in mm.
			sz_frac = sz_frac * mm_share;
			
			% finally, we've got the physical boundaries of
			% everything; let's just tidy that up.
			sz = [[sz_phys + sz_frac]; margins 0];
			sz = sz(1:end-1);
			
			% and let's normalise the physical boundaries, because
			% we're actually going to specify them to matlab in
			% normalised form, even though we computed them in mm.
			if ~isempty(sz)
				
				% do it
				sz_norm = reshape([0 cumsum(sz / mm_context)]', 2, [])';
			
				% for packdim 2, we pack from the top, whereas
				% matlab's position property packs from the bottom, so
				% we have to flip these. doclink FLIP_PACKDIM_2.
				if p.packdim == 2
					sz_norm = fliplr(1 - sz_norm);
				end
				
			end
			
			% recurse
			for i = 1:length(p.m_children)
				
				% get child
 				c = p.m_children(i);
				
				% handle abs packed panels
				if isofsize(c.packspec, [1 4])
					
					% child context
					child_context = context;
					rect = child_context.rect;
					rect([1 3]) = c.packspec([1 3]) * rect(3) + [rect(1) 0];
					rect([2 4]) = c.packspec([2 4]) * rect(4) + [rect(2) 0];
					child_context.rect = rect;

				else
				
					% child context
					child_context = context;
					rr = sz_norm(1, :);
					sz_norm = sz_norm(2:end, :); % sz_norm has only as many entries as there are rel-packed panels
					ri = p.packdim + [0 2];
					a = child_context.rect(ri(1));
					b = child_context.rect(ri(2));
					child_context.rect(ri) = [a+rr(1)*b diff(rr)*b];
					
				end
				
				% recurse
 				c.recurseComputeLayout(child_context);
				
			end
			
		end
		
		function applyLayout(p, varargin)
			
			% this function applies the layout that is stored in
			% each panel objects "m_context" member, and fixes up
			% the position of any associated objects (such as axis
			% group labels).
			
			% skip if disabled
			if p.isdefer()
				return
			end
			
			% debug output
% 			panel.debugmsg(['applyLayout "' p.state.name '"...']);
			
			% defaults
			recurse = false;
			
			% handle arguments
			while ~isempty(varargin)
				
				% get
				arg = varargin{1};
				varargin = varargin(2:end);
				
				% handle
				switch arg
					case 'recurse'
						recurse = true;
					otherwise
						panel.error('InternalError');
				end
				
			end
			
			% recurse
			if recurse
				pp = p.getPanels('*');
			else
				pp = {p};
			end
			
			% why do we have to split the applyLayout() operation
			% into two?
			%
			% because the "group labels" are positioned with
			% respect to the axes in their group depending on
			% whether those axes have tick labels, and what those
			% tick labels are. if those tick labels are in
			% automatic mode (as they usually are), they may
			% change when those axes are positioned. since an axis
			% group may contain many of these nested deep, we have
			% to position all axes (step 1) first, then (step 2)
			% position any group labels.
			
			% step 1
			for pi = 1:length(pp)
				pp{pi}.applyLayout1();
			end
			
			% step 2
			for pi = 1:length(pp)
				pp{pi}.applyLayout2();
			end
			
			% callbacks
			for pi = 1:length(pp)
				fireCallbacks(pp{pi}, 'layout-updated');
			end
			
		end
		
		function r = getObjectPosition(p)
			
			% get packed position
			r = p.m_context.rect;
			
			% if empty, must be absolute position
			if isempty(r)
				r = p.packspec;
				pp = getObjectPosition(p.parent);
				r = panel.getRectangleOfRectangle(pp, r);
			end
			
		end
		
		function applyLayout1(p)
			
			% if no context yet, skip this call
			if isempty(p.m_context)
				return
			end
			
			% if no managed objects, skip this call
			if isempty(p.h_object)
				return
			end

			% debug output
% 			panel.debugmsg(['applyLayout1 "' p.state.name '"...']);
			
			% handle LAYOUT_MODE
			switch p.m_context.mode

				case panel.LAYOUT_MODE_PREPRINT

					% if in LAYOUT_MODE_PREPRINT, store current axis
					% layout (ticks and ticklabels) and lock them into
					% manual mode so they don't get changed during the
					% print operation
					h_axes = p.getAllManagedAxes();
					for n = 1:length(h_axes)
						p.state.store{n} = storeAxisState(h_axes(n));
					end

				case panel.LAYOUT_MODE_POSTPRINT

					% if in LAYOUT_MODE_POSTPRINT, restore axis
					% layout, leaving it as it was before we ran
					% export
					h_axes = p.getAllManagedAxes();
					for n = 1:length(h_axes)
						restoreAxisState(h_axes(n), p.state.store{n});
					end

			end
			
			% position it
			try
				set(p.h_object, 'position', p.getObjectPosition(), 'units', 'normalized');
			catch err
				if strcmp(err.identifier, 'MATLAB:hg:set_chck:DimensionsOutsideRange')
					w = warning('query', 'backtrace');
					warning off backtrace
					warning('panel:PanelZeroSize', 'a panel had zero size, and the managed object was hidden');
					set(p.h_object, 'position', [-0.3 -0.3 0.2 0.2]);
					if strcmp(w.state, 'on')
						warning on backtrace
					end
				elseif strcmp(err.identifier, 'MATLAB:class:InvalidHandle')
					% this will happen if the user deletes the managed
					% objects manually. an obvious way that this
					% happens is if the user select()s some panels so
					% that axes get created, then calls clf. it would
					% be nice if we could clear the panels attached to
					% a figure in response to a clf call, but there
					% doesn't seem any obvious way to pick up the clf
					% call, only the delete(objects) that follows, and
					% this is indistinguishable from a call by the
					% user to delete(my_axis), for instance. how are
					% we to respond if the user deletes the axis the
					% panel is managing? it's not clear. so, we'll
					% just fail silently, for now, and these panels
					% will either never be used again (and will be
					% destroyed when the figure is closed) or will be
					% destroyed when the user creates a new panel on
					% this figure. either way, i think, no real harm
					% done.
% 					w = warning('query', 'backtrace');
% 					warning off backtrace
% 					warning('panel:PanelObjectDestroyed', 'the object managed by a panel has been destroyed');
% 					if strcmp(w.state, 'on')
% 						warning on backtrace
% 					end
% 					panel.debugmsg('***WARNING*** the object managed by a panel has been destroyed');
					return
				else
					rethrow(err)
				end
			end

			% if managing fonts
			if p.ismanagefont()
				
				% apply properties to objects
				h = p.h_object;
				
				% get those which are axes
				h_axes = p.getAllManagedAxes();

				% and labels/title objects, for any that are axes
				for n = 1:length(h_axes)
					h = [h ...
						get(h_axes(n), 'xlabel') ...
						get(h_axes(n), 'ylabel') ...
						get(h_axes(n), 'zlabel') ...
						get(h_axes(n), 'title') ...
						];
				end
				
				% apply font properties
				set(h, ...
					'fontname', p.getPropertyValue('fontname'), ...
					'fontsize', p.getPropertyValue('fontsize'), ...
					'fontweight', p.getPropertyValue('fontweight') ...
					);
				
			end

		end
			
		function applyLayout2(p)
			
			% if no context yet, skip this call
			if isempty(p.m_context)
				return
			end
			
			% if no object, skip this call
			if isempty(p.h_object)
				return
			end

			% if not a parent, skip this call
			if ~p.isParent()
				return
			end

			% if not an axis, skip this call - NB: this is not a
			% displayed and managed object, rather it is the
			% invisible axis used to display parent labels/titles.
			% we checked above if this panel is a parent. thus,
			% the member h_object must be scalar, if it is
			% non-empty.
			if ~isaxis(p.h_object)
				return
			end

			% debug output
% 			panel.debugmsg(['applyLayout2 "' p.state.name '"...']);
			
			% matlab moves x/ylabels around depending on
			% whether the axis in question has any x/yticks,
			% so that the label is always "near" the axis.
			% we try to do the same, but it's hack-o-rama.

			% calibration offsets - i measured these
			% empirically
			font_fudge = [2 1/3];
			nofont_fudge = [2 0];

			% do xlabel
			cs = p.getPanels('o', [2 2], true);
			y = 0;
			for c = 1:length(cs)
				ch = cs{c};
				h_axes = ch.getAllManagedAxes();
				for h_axis = h_axes
					% only if there are some tick labels, and they're
					% at the bottom...
					if ~isempty(get(h_axis, 'xticklabel')) && ~isempty(get(h_axis, 'xtick')) ...
							&& strcmp(get(h_axis, 'xaxislocation'), 'bottom')
						fontoffset_mm = get(h_axis, 'fontsize') * font_fudge(2) + font_fudge(1);
						y = max(y, fontoffset_mm);
					end
				end
			end
			y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1));

			% convert and lay in
			axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4);
			y = y / axisheight_mm;
			set(get(p.h_object, 'xlabel'), ...
				'VerticalAlignment', 'Cap', ...
				'Units', 'Normalized', ...
				'Position', [0.5 -y 1]);

			% calibration offsets - i measured these
			% empirically
			font_fudge = [3 1/6];
			nofont_fudge = [2 0];

			% do ylabel
			cs = p.getPanels('o', [1 1], true);
			x = 0;
			for c = 1:length(cs)
				ch = cs{c};
				h_axes = ch.getAllManagedAxes();
				for h_axis = h_axes
					% only if there are some tick labels, and they're
					% at the left...
					if ~isempty(get(h_axis, 'yticklabel')) && ~isempty(get(h_axis, 'ytick')) ...
							&& strcmp(get(h_axis, 'yaxislocation'), 'left')
						yt = get(h_axis, 'yticklabel');
						if ischar(yt)
							ml = size(yt, 2);
						else
							ml = 0;
							for i = 1:length(yt)
								ml = max(ml, length(yt{i}));
							end
						end
						fontoffset_mm = get(h_axis, 'fontsize') * ml * font_fudge(2) + font_fudge(1);
						x = max(x, fontoffset_mm);
					end
				end
			end
			x = max(x, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1));

			% convert and lay in
			axisheight_mm = p.m_context.size_in_mm(1) * p.m_context.rect(3);
			x = x / axisheight_mm;
			set(get(p.h_object, 'ylabel'), ...
				'VerticalAlignment', 'Bottom', ...
				'Units', 'Normalized', ...
				'Position', [-x 0.5 1]);

			% calibration offsets - made up based on the
			% ones i measured for the labels
			nofont_fudge = [2 0];

			% get y position
			y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1));

			% convert and lay in
			axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4);
			y = y / axisheight_mm;
			set(get(p.h_object, 'title'), ...
				'VerticalAlignment', 'Bottom', ...
				'Position', [0.5 1+y 1]);

		end
		
	end
	
	
	
	
	
	
	%% ---- PROPERTY METHODS ----
	
	methods (Access = private)
		
		function value = getPropertyValue(p, key, units)

			value = p.prop.(key);
			
			if isempty(value)

				% inherit
				if ~isempty(p.parent)
					switch key
						case {'fontname' 'fontsize' 'fontweight' 'margin' 'units'}
							if nargin == 3
								value = p.parent.getPropertyValue(key, units);
							else
								value = p.parent.getPropertyValue(key);
							end
							return
					end
				end
				
				% default
				if isempty(value)
					value = panel.getPropertyDefault(key);
				end
				
			end
			
			% translate dimensions
			switch key
				case {'margin'}
					if nargin < 3
						units = p.getPropertyValue('units');
					end
					value = panel.resolveUnits(value, units);
			end
			
		end
		
		function setPropertyValue(p, key, value)
			
			% root properties
			switch key
				case 'units'
					if ~isempty(p.parent)
						p.parent.setPropertyValue(key, value);
						return
					end
			end
			
			% value validation
			switch key
				case 'units'
					invalid = ~( (isstring(value) && isin({'mm' 'in' 'cm' 'pt'}, value)) || isempty(value) );
				case 'fontname'
					invalid = ~( isstring(value) || isempty(value) );
				case 'fontsize'
					invalid = ~( (isnumeric(value) && isscalar(value) && value >= 4 && value <= 60) || isempty(value) );
				case 'fontweight'
					invalid = ~( (isstring(value) && isin({'normal' 'bold'}, value)) || isempty(value) );
				case 'margin'
					invalid = ~( (isdimension(value)) || isempty(value) );
				case {'marginleft' 'marginbottom' 'marginright' 'margintop'}
					invalid = ~isscalardimension(value);
				otherwise
					error('panel:UnrecognisedProperty', ['unrecognised property "' key '"']);
			end
			
			% value validation
			if invalid
				error('panel:InvalidValueForProperty', ['invalid value for property "' key '"']);
			end
			
			% marginX properties
			switch key
				case {'marginleft' 'marginbottom' 'marginright' 'margintop'}
					index = isin({'left' 'bottom' 'right' 'top'}, key(7:end));
					element = value;
					value = p.getPropertyValue('margin');
					value(index) = element;
					key = 'margin';
			end
			
			% translate dimensions
			switch key
				case {'margin'}
					if isscalar(value)
						value = value * [1 1 1 1];
					end
					if ~isempty(value)
						units = p.getPropertyValue('units');
						value = {panel.resolveUnits({value units}, 'mm') 'mm'};
					end
			end
			
			% lay in
			p.prop.(key) = value;
			
		end
		
	end	
		
	methods (Static = true, Access = private)
	
        function s = fignum(h)
            
            % handled differently pre/post 2014b
            if isa(h, 'matlab.ui.Figure')
                % R2014b
                s = num2str(h.Number);
            else
                % pre-R2014b
                s = num2str(h);
            end
        end
		
		function prop = getPropertyInitialState()
			
			prop = panel.getPropertyDefaults();
			for key = fieldnames(prop)'
				prop.(key{1}) = [];
			end
			
		end
		
		function value = getPropertyDefault(key)
			
			persistent defprop
			
			if isempty(defprop)
				defprop = panel.getPropertyDefaults();
			end
			
			value = defprop.(key);
			
		end
		
		function defprop = getPropertyDefaults()
			
			% root properties
			defprop.units = 'mm';
			
			% inherited properties
			defprop.fontname = get(0, 'defaultAxesFontName');
			defprop.fontsize = get(0, 'defaultAxesFontSize');
			defprop.fontweight = 'normal';
			defprop.margin = {[15 15 5 5] 'mm'};
			
			% not inherited properties
			% CURRENTLY, NONE!
% 			defprop.align = false;
			
		end
		
	end	
		
	
	

	
	
	
	%% ---- STATIC PUBLIC METHODS ----
	
	methods (Static = true)
		
		function p = recover(h_figure)
			
			% get a handle to the root panel associated with a figure
			%
			% p = recover(h_fig)
			%   if you have not got a handle to the root panel of
			%   the figure h_fig, this call will retrieve it. if
			%   h_fig is not supplied, gcf is used.
			
			if nargin < 1
				h_figure = gcf;
			end
			
			p = panel.callbackDispatcher('recover', h_figure);
			
		end
		
		function version()
			
			% report the version of panel that is active
			%
			% panel.version()
			
			fid = fopen(which('panel'));
			tag = '% Release Version';
			ltag = length(tag);
			tagline = 'Unable to determine Release Version';
			while 1
				line = fgetl(fid);
				if ~ischar(line)
					break
				end
				if length(line) > ltag && strcmp(line(1:ltag), tag)
					tagline = line(3:end);
				end
			end
			fclose(fid);
			disp(tagline)
			
		end
		
		function panic()
			
			% call delete on all children of the global workspace,
			% to recover from bugs that leave us with uncloseable
			% figures. call this as "panel.panic()".
			%
			% NB: if you have to call panic(), something has gone
			% wrong. if you are able to reproduce the problem,
			% please contact me to report the bug.
			delete(allchild(0));
			
		end
		
	end
	
	
	
	
	
	
	%% ---- STATIC PRIVATE METHODS ----
	
	methods (Static = true, Access = private)
		
		function error(id)

			switch id
				case 'PanelUncommitted'
					throwAsCaller(MException('panel:PanelUncommitted', 'this action cannot be performed on an uncommitted panel'));
				case 'InvalidIndexing'
					throwAsCaller(MException('panel:InvalidIndexing', 'you cannot index a panel object in this way'));
				case 'InternalError'
					throwAsCaller(MException('panel:InternalError', 'an internal error occurred'));
				otherwise
					throwAsCaller(MException('panel:UnknownError', ['an unknown error was generated with id "' id '"']));
			end
				
		end
		
		function lockClass()
			
			persistent hasLocked
			if isempty(hasLocked)
				
				% only lock if not in debug mode
				if ~panel.isDebug()
					% in production code, must mlock() file at this point,
					% to avoid persistent variables being cleared by user
					if strcmp(getenv('USERDOMAIN'), 'BERGEN')
						% my machine, do nothing
					else
						mlock
					end
				end
				
				% mark that we've handled this
				hasLocked = true;
				
			end
			
		end
		
		function debugmsg(msg, focus)
			
			% focus can be supplied to force only focussed
			% messages to be shown
			if nargin < 2
				focus = 1;
			end
			
			% display, if in debug mode
			if focus
				if panel.isDebug()
					disp(msg);
				end
			end
			
		end
		
		function state = isDebug()
			
			% persistent
			persistent debug
			
			% create
			if isempty(debug)
				try
					debug = panel_debug_state();
				catch
	 				debug = false;
				end
			end
			
			% ok
			state = debug;
			
		end
		
		function r = getFractionOfRectangle(r, dim, range)
			
			switch dim
				case 1
					r = [r(1)+range(1)*r(3) r(2) range(2)*r(3) r(4)];
				case 2
					r = [r(1) r(2)+(1-sum(range))*r(4) r(3) range(2)*r(4)];
				otherwise
					error('panel:CaseNotCoded', ['case not coded, dim = ' dim ' (internal error)']);
			end
			
		end
		
		function r = getRectangleOfRectangle(r, s)
			
			w = r(3);
			h = r(4);
			r = [r(1)+s(1)*w r(2)+s(2)*h s(3)*w s(4)*h];
			
		end
		
		function a = getUnionRect(a, b)
			
			if isempty(a)
				a = b;
			end
			if ~isempty(b)
				d = a(1) - b(1);
				if d > 0
					a(1) = a(1) - d;
					a(3) = a(3) + d;
				end
				d = a(2) - b(2);
				if d > 0
					a(2) = a(2) - d;
					a(4) = a(4) + d;
				end
				d = b(1) + b(3) - (a(1) + a(3));
				if d > 0
					a(3) = a(3) + d;
				end
				d = b(2) + b(4) - (a(2) + a(4));
				if d > 0
					a(4) = a(4) + d;
				end
			end
			
		end
		
		function r = reduceRectangle(r, margin)
			
			r(1:2) = r(1:2) + margin(1:2);
			r(3:4) = r(3:4) - margin(1:2) - margin(3:4);
			
		end
		
		function v = normaliseDimension(v, space_size_in_mm)
			
			v = v ./ [space_size_in_mm space_size_in_mm];
			
		end
		
		function v = resolveUnits(d, units)
			
			% first, convert into mm
			v = d{1};
			switch d{2}
				case 'mm'
					% ok
				case 'cm'
					v = v * 10.0;
				case 'in'
					v = v * 25.4;
				case 'pt'
					v = v / 72.0 * 25.4;
				otherwise
					error('panel:CaseNotCoded', ['case not coded, storage units = ' units ' (internal error)']);
			end
			
			% then, convert to specified units
			switch units
				case 'mm'
					% ok
				case 'cm'
					v = v / 10.0;
				case 'in'
					v = v / 25.4;
				case 'pt'
					v = v / 25.4 * 72.0;
				otherwise
					error('panel:CaseNotCoded', ['case not coded, requested units = ' units ' (internal error)']);
			end
			
		end
		
		function resizeCallback(obj, evt)
			
			panel.callbackDispatcher('resize', obj);
			
		end
		
		function closeCallback(obj, evt)
			
			panel.callbackDispatcher('delete', obj);
			delete(obj);
			
		end
		
		function out = callbackDispatcher(op, data)
			
			% debug output
% 			panel.debugmsg(['callbackDispatcher(' op ')...'])
			
			% persistent store
			persistent registeredPanels
			
			% switch on operation
			switch op
				
				case {'register' 'registerNoClear'}
					
					% if a root panel is already attached to this
					% figure, we could throw an error and refuse to
					% create the new object, we could delete the
					% existing panel, or we could allow multiple
					% panels to be attached to the same figure.
					%
					% we should allow multiple panels, because they
					% may have different parents within the same
					% figure (e.g. uipanels). but by default we don't,
					% unless the panel.add() static constructor is
					% used.
					
					if strcmp(op, 'register')
						
						argument_h_figure = data.h_figure;
						i = 0;
						while i < length(registeredPanels)
							i = i + 1;
							if registeredPanels(i).h_figure == argument_h_figure
								delete(registeredPanels(i));
								i = 0;
							end
						end
						
					end
					
					% register the new panel
					if isempty(registeredPanels)
						registeredPanels = data;
					else
						registeredPanels(end+1) = data;
					end
					
					% debug output
% 					panel.debugmsg(['panel registered (' int2str(length(registeredPanels)) ' now registered)']);
					
				case 'unregister'
					
					% debug output
% 					panel.debugmsg(['on unregister, ' int2str(length(registeredPanels)) ' registered']);
					
					for r = 1:length(registeredPanels)
						if registeredPanels(r) == data
							registeredPanels = registeredPanels([1:r-1 r+1:end]);

							% debug output
% 							panel.debugmsg(['panel unregistered (' int2str(length(registeredPanels)) ' now registered)']);
							
							return
						end
					end
					
					% warn
					warning('panel:AbsentOnCallbacksUnregister', 'panel was absent from the callbacks register when it tried to unregister itself');
					
				case 'resize'
					
					argument_h_parent = data;
					for r = 1:length(registeredPanels)
						if registeredPanels(r).h_parent == argument_h_parent
							registeredPanels(r).recomputeLayout([]);
						end
					end
					
				case 'recover'
					
					argument_h_figure = data;
					out = [];
					for r = 1:length(registeredPanels)
						if registeredPanels(r).h_figure == argument_h_figure
							if isempty(out)
								out = registeredPanels(r);
							else
								out(end+1) = registeredPanels(r);
							end
						end
					end
					
				case 'delete'
					
					argument_h_figure = data;
					i = 0;
					while i < length(registeredPanels)
						i = i + 1;
						if registeredPanels(i).h_figure == argument_h_figure
							delete(registeredPanels(i));
							i = 0;
						end
					end
					
			end
			
		end
		
	end
	
	
	
	
end

















% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% HELPERS
%
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function restore = dashstyle_line(fix, context)

% get axis size in mm
h_line = fix.h;
h_axis = get(h_line, 'parent');
u = get(h_axis, 'units');
set(h_axis, 'units', 'norm');
pos = get(h_axis, 'position');
set(h_axis, 'units', u);
axis_in_mm = pos(3:4) .* context.size_in_mm;

% recover data
xdata = get(h_line, 'xdata');
ydata = get(h_line, 'ydata');
zdata = get(h_line, 'zdata');
linestyle = get(h_line, 'linestyle');
marker = get(h_line, 'marker');

% empty restore
restore = [];

% do not handle 3D
if ~isempty(zdata)
	warning('panel:NoFixdash3D', 'panel cannot fixdash() a 3D line - no action taken');
	return
end

% get range of axis
ax = axis(h_axis);

% get scale in each dimension (mm per unit)
sc = axis_in_mm ./ (ax([2 4]) - ax([1 3]));

% create empty line
data = NaN;

% override linestyle
if ~isempty(fix.linestyle)
	linestyle = fix.linestyle;
end

% transcribe linestyle
linestyle = dashstyle_parse_linestyle(linestyle);
if isempty(linestyle)
	return
end

% scale
scale = 1;
dashes = linestyle * scale;

% store for restore
restore.h_line = h_line;
restore.xdata = xdata;
restore.ydata = ydata;

% create another, separate, line to overlay on the original
% line and render the fixed-up dashes.
restore.h_supp = copyobj(h_line, h_axis);

% if the original line has markers, we'll have to create yet
% another separate line instance to represent them, because
% they shouldn't be "dashed", as it were. note that we don't
% currently attempt to get the z-order right for these
% new lines.
if ~isequal(marker, 'none')
	restore.h_mark = copyobj(h_line, h_axis);
	set(restore.h_mark, 'linestyle', 'none');
	set(restore.h_supp, 'marker', 'none');
else
	restore.h_mark = [];
end

% hide the original line. this line remains in existence so
% that if there is a legend, it doesn't get messed up.
set(h_line, 'xdata', NaN, 'ydata', NaN);

% extract pattern length
patlen = sum(dashes);

% position within pattern is initially zero
pos = 0;

% linedata
line_xy = complex(xdata, ydata);

% for each line segment
while length(line_xy) > 1
	
	% get line segment
	xy = line_xy(1:2);
	line_xy = line_xy(2:end);
	
	% any NaNs, and we're outta here
	if any(isnan(xy))
		continue
	end
	
	% get start etc.
	O = xy(1);
	V = xy(2) - xy(1);
	
	% get mm length of this line segment
	d = sqrt(sum(([real(V) imag(V)] .* sc) .^ 2));
	
	% and mm unit vector
	U = V / d;
	
	% generate a long-enough pattern for this segment
	n = ceil((pos + d) / patlen);
	pat = [0 cumsum(repmat(dashes, [1 n]))] - pos;
	pos = d - (pat(end) - patlen);
	pat = [pat(1:2:end-1); pat(2:2:end)];
	
	% trim spurious segments
	pat = pat(:, any(pat >= 0) & any(pat <= d));
	
	% skip if that's it
	if isempty(pat)
		continue
	end
	
	% and reduce ones that are oversized
	pat(1) = max(pat(1), 0);
	pat(end) = min(pat(end), d);

	% finally, add these segments to the line data
	seg = [O + pat * U; NaN(1, size(pat, 2))];
	data = [data seg(:).'];
	
end

% update line
set(restore.h_supp, 'xdata', real(data), 'ydata', imag(data), ...
	'linestyle', '-');

end


function linestyle = dashstyle_parse_linestyle(linestyle)

if isequal(linestyle, 'none') || isequal(linestyle, '-')
	linestyle = [];
	return
end

while 1

	% if numbers
	if isnumeric(linestyle)
		if ~isa(linestyle, 'double') || ~isrow(linestyle) || mod(length(linestyle), 2) ~= 0
			break
		end
		% no need to parse
		return
	end

	% else, must be char
	if ~ischar(linestyle) || ~isrow(linestyle)
		break
	end
	
	% translate matlab non-standard codes into codes we can
	% easily parse
	switch linestyle
		case ':'
			linestyle = '.';
		case '--'
			linestyle = '-';
	end
	
	% must be only - and .
	if any(linestyle ~= '.' & linestyle ~= '-')
		break
	end
	
	% transcribe
	c = linestyle;
	linestyle = [];
	for l = c
		switch l
			case '-'
				linestyle = [linestyle 2 0.75];
			case '.'
				linestyle = [linestyle 0.5 0.75];
		end
	end
	return

end

warning('panel:BadFixdashLinestyle', 'unusable linestyle in fixdash()');
linestyle = [];

end



% MISCELLANEOUS

function index = isin(list, value)

for i = 1:length(list)
	if strcmp(value, list{i})
		index = i;
		return
	end
end

index = 0;

end

function dim = flippackdim(dim)

% this function, used between arguments in a recursive call,
% causes the dim to be switched with each recurse, so that
% we build a grid, rather than a long, long row.
dim = 3 - dim;

end



% STRING PADDING FUNCTIONS

function s = rpad(s, l)

if nargin < 2
	l = 16;
end

if length(s) < l
	s = [s repmat(' ', 1, l - length(s))];
end

end

function s = lpad(s, l)

if nargin < 2
	l = 16;
end

if length(s) < l
	s = [repmat(' ', 1, l - length(s)) s];
end

end



% HANDLE GRAPHICS HELPERS

function h = getParentFigure(h)

if strcmp(get(h, 'type'), 'figure')
	return
else
	h = getParentFigure(get(h, 'parent'));
end

end

function addHandleCallback(h, name, func)

% % get current list of callbacks
% callbacks = get(h, name);
%
% % if empty, turn into a cell
% if isempty(callbacks)
% 	callbacks = {};
% elseif iscell(callbacks)
% 	% only add ourselves once
% 	for c = 1:length(callbacks)
% 		if callbacks{c} == func
% 			return
% 		end
% 	end
% else
% 	callbacks = {callbacks};
% end
%
% % and add ours (this is friendly, in case someone else has a
% % callback attached)
% callbacks{end+1} = func;
%
% % lay in
% set(h, name, callbacks);

% the above isn't as simple as i thought - for now, we'll
% just stamp on any existing callbacks
set(h, name, func);

end

function store = storeAxisState(h)

% LOCK TICKS AND LIMITS
%
% (LOCK TICKS)
%
% lock state so that the ticks and labels do not change when
% the figure is resized for printing. this is what the user
% will expect, which is why we go through this palaver.
%
% however the following code illustrates
% an idiosyncrasy of matlab (i would call this an
% inconsistency, myself, but there you go).
%
% figure
% axis([0 1 0 1])
% set(gca, 'ytick', [-1 0 1 2])
% get(gca, 'yticklabel')
% set(gca, 'yticklabelmode', 'manual')
%
% now, resize the figure window. at least in R2011b, the
% tick labels change on the first resize event. presumably,
% this is because matlab treats the ticklabel value
% differently depending on if the ticklabelmode is auto or
% manual. if it's manual, the value is used as documented,
% and [0 1] is used to label [-1 0 1 2], cyclically.
% however, if the ticklabelmode is auto, and the ticks
% extend outside the figure, then the ticklabels are set
% sensibly, but the _value_ of ticklabel is not consistent
% with what it would need to be to get this tick labelling
% were the mode manual. and, in a final bizarre twist, this
% doesn't become evident until the resize event. i think
% this is a bug, no other way of looking at it; at best it's
% an inconsistency that is either tedious or impossible to
% work around in the general case.
%
% in any case, we have to lock the ticks to manual as we go
% through the print cycle, so that the ticks do not get
% changed if they were in automatic mode. but we mustn't fix
% the tick labels to manual, since if we do we may encounter
% this inconsistency and end up with the wrong tick labels
% in the print out. i can't, at time of writing, think of a
% case where we'd have to fix the tick labels to manual too.
% the possible cases are:
%
% ticks auto, labels auto: in this case, fixing the ticks to
%		manual should be enough.
%
% ticks manual, labels auto: leave as is.
%
% ticks manual, labels manual: leave as is.
%
% the only other case is ticks auto, labels manual, which is
% a risky case to use, but in any case we can also fix the
% ticks to manual in that case. thus, our preferred solution
% is to always switch the ticks to manual, if they're not
% already, and otherwise leave things be.
%
% (LOCK LIMITS)
%
% the other thing that may get modified, if the user hasn't
% fixed it, is the axis limits. so we lock them too, any
% that are set to auto, and mark them for unlocking when the
% print is complete.

store = '';

% manual-ise ticks on any axis where they are currently
% automatic, and indicate that we need to switch them back
% afterwards.
if strcmp(get(h, 'XTickMode'), 'auto')
	store = [store 'X'];
	set(h, 'XTickMode', 'manual');
end
if strcmp(get(h, 'YTickMode'), 'auto')
	store = [store 'Y'];
	set(h, 'YTickMode', 'manual');
end
if strcmp(get(h, 'ZTickMode'), 'auto')
	store = [store 'Z'];
	set(h, 'ZTickMode', 'manual');
end

% manual-ise limits on any axis where they are currently
% automatic, and indicate that we need to switch them back
% afterwards.
if strcmp(get(h, 'XLimMode'), 'auto')
	store = [store 'x'];
	set(h, 'XLimMode', 'manual');
end
if strcmp(get(h, 'YLimMode'), 'auto')
	store = [store 'y'];
	set(h, 'YLimMode', 'manual');
end
if strcmp(get(h, 'ZLimMode'), 'auto')
	store = [store 'z'];
	set(h, 'ZLimMode', 'manual');
end

% % OLD CODE OBSOLETED 25/01/12 - see notes above
% 
% % store current state
% store.XTick = get(h, 'XTick');
% store.XTickMode = get(h, 'XTickMode');
% store.XTickLabel = get(h, 'XTickLabel');
% store.XTickLabelMode = get(h, 'XTickLabelMode');
% store.YTickMode = get(h, 'YTickMode');
% store.YTick = get(h, 'YTick');
% store.YTickLabel = get(h, 'YTickLabel');
% store.YTickLabelMode = get(h, 'YTickLabelMode');
% store.ZTick = get(h, 'ZTick');
% store.ZTickMode = get(h, 'ZTickMode');
% store.ZTickLabel = get(h, 'ZTickLabel');
% store.ZTickLabelMode = get(h, 'ZTickLabelMode');
% 
% % lock state to manual
% set(h, 'XTickLabelMode', 'manual');
% set(h, 'XTickMode', 'manual');
% set(h, 'YTickLabelMode', 'manual');
% set(h, 'YTickMode', 'manual');
% set(h, 'ZTickLabelMode', 'manual');
% set(h, 'ZTickMode', 'manual');

end

function restoreAxisState(h, store)

% unmanualise
for item = store
	switch item
		case {'X' 'Y' 'Z'}
			set(h, [item 'TickMode'], 'auto');
		case {'x' 'y' 'z'}
			set(h, [upper(item) 'TickMode'], 'auto');
	end
end

% % OLD CODE OBSOLETED 25/01/12 - see notes above
% 
% % restore passed state
% set(h, 'XTick', store.XTick);
% set(h, 'XTickMode', store.XTickMode);
% set(h, 'XTickLabel', store.XTickLabel);
% set(h, 'XTickLabelMode', store.XTickLabelMode);
% set(h, 'YTick', store.YTick);
% set(h, 'YTickMode', store.YTickMode);
% set(h, 'YTickLabel', store.YTickLabel);
% set(h, 'YTickLabelMode', store.YTickLabelMode);
% set(h, 'ZTick', store.ZTick);
% set(h, 'ZTickMode', store.ZTickMode);
% set(h, 'ZTickLabel', store.ZTickLabel);
% set(h, 'ZTickLabelMode', store.ZTickLabelMode);

end



% DIM AND EDGE HANDLING

% we describe each edge of a panel in terms of "dim" (1 or
% 2, horizontal or vertical) and "edge" (1 or 2, former or
% latter). together, [dim edge] is an "edgespec".

function s = edgestr(edgespec)

s = 'lbrt';
s = s(edgeindex(edgespec));

end

function i = edgeindex(edgespec)

% edge indices. margins are stored as [l b r t] but
% dims are packed left to right and top to bottom, so
% relationship between 'dim' and 'end' and index into
% margin is non-trivial. we call the index into the margin
% the "edgeindex". an "edgespec" is just [dim end], in a
% single array.
i = [1 3; 4 2];
i = i(edgespec(1), edgespec(2));

end



% VARIABLE TYPE HELPERS

function val = validate_par(val, argtext, varargin)

% this helper validates arguments to some functions in the
% main body

for n = 1:length(varargin)
	
	% get validation constraint
	arg = varargin{n};
	
	% handle string list
	if iscell(arg)
		% string list
		if ~isin(arg, val)
			error('panel:InvalidArgument', ...
				['invalid argument "' argtext '", "' val '" is not a recognised data value for this option']);
		end
		continue;
	end
	
	% handle strings
	if isstring(arg)
		switch arg
			case 'empty'
				if ~isempty(val)
					error('panel:InvalidArgument', ...
						['invalid argument "' argtext '", option does not expect any data']);
				end
			case 'dimension'
				if ~isdimension(val)
					error('panel:InvalidArgument', ...
						['invalid argument "' argtext '", option expects a dimension']);
				end
			case 'scalar'
				if ~(isnumeric(val) && isscalar(val) && ~isnan(val))
					error('panel:InvalidArgument', ...
						['invalid argument "' argtext '", option expects a scalar value']);
				end
			case 'nonneg'
				if any(val(:) < 0)
					error('panel:InvalidArgument', ...
						['invalid argument "' argtext '", option expects non-negative values only']);
				end
			case 'integer'
				if any(val(:) ~= round(val(:)))
					error('panel:InvalidArgument', ...
						['invalid argument "' argtext '", option expects integer values only']);
				end
		end
		continue;
	end
	
	% handle numeric range
	if isnumeric(arg) && isofsize(arg, [1 2])
		if any(val(:) < arg(1)) || any(val(:) > arg(2))
			error('panel:InvalidArgument', ...
				['invalid argument "' argtext '", option data must be between ' num2str(arg(1)) ' and ' num2str(arg(2))]);
		end
		continue;
	end
	
	% not recognised
	arg
	error('panel:InternalError', 'internal error - bad argument to validate_par (above)');
	
end

end

function b = checkpar(value, mn, mx)

b = isscalar(value) && isnumeric(value) && ~isnan(value);
if b
	if nargin >= 2
		b = b && value >= mn;
	end
	if nargin >= 3
		b = b && value <= mx;
	end
end

end

function b = isintegral(v)

b = all(all(v == round(v)));

end

function b = isstring(value)

sz = size(value);
b = ischar(value) && length(sz) == 2 && sz(1) == 1 && sz(2) >= 1;

end

function b = isdimension(value)

b = isa(value, 'double') && (isscalar(value) || isofsize(value, [1 4]));

end

function b = isscalardimension(value)

b = isa(value, 'double') && isscalar(value);

end

function b = isofsize(value, siz)

sz = size(value);
b = length(sz) == length(siz) && all(sz == siz);

end

function b = isaxis(h)

b = ishandle(h) && strcmp(get(h, 'type'), 'axes');

end

function validate_packspec(packspec)

	% stretchable
	if isempty(packspec)
		return
	end

	% scalar
	if isa(packspec, 'double') && isscalar(packspec)

		% fraction
		if packspec > 0 && packspec <= 1
			return
		end

		% percentage
		if packspec > 1 && packspec <= 100
			return
		end

	end

	% fixed
	if iscell(packspec) && isscalar(packspec)

		% delve
		d = packspec{1};
		if isa(d, 'double') && isscalar(d) && d > 0
			return
		end

	end

	% abs
	if isa(packspec, 'double') && isofsize(packspec, [1 4]) && all(packspec(3:4)>0)
		return
	end

	% otherwise, bad form
	error('panel:BadPackingSpecifier', 'the packing specifier was not valid - see help panel/pack');

end