Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| % 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, what a load of shit | |
| 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, what a load of shit | |
| 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, for fuck's sake. 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 | |