A panel which can display an image or two images superimposed
Inheritance: System.Windows.Forms.Panel
コード例 #1
0
        private void BuildView()
        {
            this.SuspendLayout();
            this.Text = this.StimulusClass.Settings.Name;
            var cols = GUIUtils.CreateTable(new double[] { .33, .33, .33 }, Direction.Horizontal);

            // settings
            var settingsConfig = new ConfigurationPanel(this.StimulusClass.Settings)
            {
                Dock = DockStyle.Fill
            };

            // image panel
            var imagePanel = new ImagePanel()
            {
                Dock = DockStyle.Fill
            };

            // dropdown
            var dropDown = new ComboBox()
            {
                DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Bottom
            };

            dropDown.MouseWheel += (sender, args) => ((HandledMouseEventArgs)args).Handled = true;
            dropDown.Items.Add(new DisplayPointer(() => this.StimulusClass.Settings.Answer1, true));
            dropDown.Items.Add(new DisplayPointer(() => this.StimulusClass.Settings.Answer2, false));
            dropDown.Items.Add(new DisplayPointer(GUIUtils.Strings.UNCLASSIFIED, null));

            // source folder
            var sourceFolderLink = new LinkLabel()
            {
                AutoSize = true, Text = Path.GetFileName(this.StimulusClass.SourceFolder), Dock = DockStyle.Top
            };

            sourceFolderLink.Click += (sender, args) =>
            {
                try { System.Diagnostics.Process.Start("explorer.exe", this.StimulusClass.SourceFolder); }
                catch (Exception) { GUIUtils.Alert("Failed to open " + this.StimulusClass.SourceFolder); }
            };
            this.ToolTip.SetToolTip(sourceFolderLink, "Open " + this.StimulusClass.SourceFolder);

            // image list
            var imageList = new CheckedListBox()
            {
                Dock = DockStyle.Fill
            };

            imageList.AddContextMenu();
            foreach (var stimulus in this.StimulusClass.Stimuli)
            {
                imageList.Items.Add(new StimulusItem(this.StimulusClass, stimulus), stimulus.Used);
            }
            EventHandler setImage = (sender, args) =>
            {
                if (imageList.Items.Count > 0)
                {
                    var stimulus = ((StimulusItem)(imageList.SelectedItem ?? imageList.Items[0])).Stimulus;
                    imagePanel.ImagePath = stimulus.PathOrText;
                    dropDown.Visible     = true;
                    switch (stimulus.Subclass)
                    {
                    case true: dropDown.SelectedIndex = 0; break;

                    case false: dropDown.SelectedIndex = 1; break;

                    case null: dropDown.SelectedIndex = 2; break;
                    }
                }
                else
                {
                    imagePanel.ImagePath = null;
                    dropDown.Visible     = false;
                }
            };

            setImage(imageList, EventArgs.Empty); // first set
            imageList.SelectedIndexChanged += setImage;
            settingsConfig.PropertyChanged += args =>
            {
                this.StimulusClass.Settings.SetProperty(args.Property, args.Getter());
                this.Text = this.StimulusClass.Settings.Name;
                imageList.Invalidate();

                // force a refresh
                int selectedIndex = dropDown.SelectedIndex;
                var items         = dropDown.Items.Cast <DisplayPointer>().ToArray();
                dropDown.Items.Clear();
                dropDown.Items.AddRange(items);
                dropDown.SelectedIndex = selectedIndex;
            };
            imageList.ItemCheck           += (sender, args) => ((StimulusItem)imageList.Items[args.Index]).Stimulus.Used = (args.NewValue == CheckState.Checked);
            dropDown.SelectedIndexChanged += (sender, args) =>
            {
                ((StimulusItem)(imageList.SelectedItem ?? imageList.Items[0])).Stimulus.Subclass =
                    (bool?)((DisplayPointer)dropDown.SelectedItem).Key;
                imageList.Invalidate();
            };
            this.getSelectedStimulus = () => imageList.SelectedItem == null ? null : ((StimulusItem)imageList.SelectedItem).Stimulus;

            // selection info label
            var selectionInfoLabel = new Label()
            {
                Dock = DockStyle.Bottom, AutoSize = true
            };
            PaintEventHandler updateSelectionInfoLabel = (sender, args) =>
            {
                var items = imageList.Items.Cast <StimulusItem>();
                selectionInfoLabel.Text = string.Format("{0}/{1} selected", items.Count(s => s.Stimulus.Used), imageList.Items.Count);
                if (!string.IsNullOrWhiteSpace(this.StimulusClass.Settings.Answer1) ||
                    !string.IsNullOrWhiteSpace(this.StimulusClass.Settings.Answer2))
                {
                    selectionInfoLabel.Text += string.Format(" ({0}/{1} {2}, {3}/{4} {5}, {6}/{7} {8})", items.Count(s => s.Stimulus.Subclass == true && s.Stimulus.Used),
                                                             items.Count(s => s.Stimulus.Subclass == true),
                                                             this.StimulusClass.Settings.Answer1,
                                                             items.Count(s => s.Stimulus.Subclass == false && s.Stimulus.Used),
                                                             items.Count(s => s.Stimulus.Subclass == false),
                                                             this.StimulusClass.Settings.Answer2,
                                                             items.Count(s => s.Stimulus.Subclass == null && s.Stimulus.Used),
                                                             items.Count(s => s.Stimulus.Subclass == null),
                                                             GUIUtils.Strings.UNCLASSIFIED);
                }
            };

            imageList.Paint += updateSelectionInfoLabel;
            updateSelectionInfoLabel(null, null);

            // button table
            var buttonTable = GUIUtils.CreateButtonTable(Direction.Horizontal, DockStyle.Bottom,
                                                         GUIUtils.CreateFlatButton("Classify", b =>
            {
                MainForm.Instance.Animate(new StimulusClassSetupProvider(this.StimulusClass), this.Invalidate);
            }, this.ToolTip, "Launch a tool to quickly answer this class's question for all stimuli"),
                                                         GUIUtils.CreateFlatButton("Refresh", b =>
            {
                this.StimulusClass.RefreshStimuli();
                this.Invalidate();
            }, this.ToolTip, "Reload the stimuli from the file system"),
                                                         GUIUtils.CreateFlatButton("Save", b =>
            {
                bool saved = this.StimulusClass.TrySave();
                GUIUtils.Alert((saved ? "Saved" : "Failed to save")
                               + " stimulus class info to " + this.StimulusClass.SavePath,
                               (saved ? MessageBoxIcon.Information : MessageBoxIcon.Error));
            }, this.ToolTip, "Save configuration information to " + this.StimulusClass.SavePath));

            // add all controls
            Panel panel;

            // left column
            panel = new Panel()
            {
                Dock = DockStyle.Fill
            };
            panel.Controls.Add(settingsConfig);
            panel.Controls.Add(sourceFolderLink);
            panel.Controls.Add("Folder".ToLabel());
            panel.Controls.Add(buttonTable);
            cols.Controls.Add(panel, 0, 0);

            // middle column
            panel = new Panel()
            {
                Dock = DockStyle.Fill
            };
            panel.Controls.Add(imageList);
            panel.Controls.Add(selectionInfoLabel);
            panel.Controls.Add("Stimuli".ToLabel());
            cols.Controls.Add(panel, 1, 0);

            // right column
            panel = new Panel()
            {
                Dock = DockStyle.Fill
            };
            panel.Controls.Add(imagePanel);
            panel.Controls.Add(dropDown);
            cols.Controls.Add(panel, 2, 0);

            this.Controls.Add(cols);
            this.ResumeLayout(false);
        }
コード例 #2
0
        private void BuildView()
        {
            this.SuspendLayout();
            this.StimulusClass1 = this.StimulusClass2 = null;

            // tab control
            var tabs = new CustomTabControl()
            {
                Dock = DockStyle.Fill
            };

            tabs.DisplayStyleProvider = new TabStyleVisualStudioProvider(tabs)
            {
                ShowTabCloser = true
            };
            tabs.TabClosing += (sender, args) => ((CustomTab)args.TabPage).RaiseClosingSafe(args);

            // start tab
            var startTab = new CustomTab()
            {
                Text = "Classes"
            };

            // columns
            var cols = GUIUtils.CreateTable(new double[] { .33, .33, .33 }, Direction.Horizontal);

            // image config
            var imageConfig = ConfigurationPanel.Create <ImageDisplaySettings>();

            // image panel
            var imagePanel = new ImagePanel()
            {
                Dock = DockStyle.Fill, UseNativeSize = false
            };
            bool cycle = true;
            var  rand  = new Random();
            Func <StimulusClass, string> getImageForClass = stimulusClass =>
            {
                var tab = this.stimulusClassTabs.First(t => t.StimulusClass == stimulusClass);
                if (tab.StimulusClass.Stimuli.Count == 0)
                {
                    return(null);
                }
                if (!((ImageDisplaySettings)imageConfig.GetConfiguredObject()).CycleThroughImages)
                {
                    return((tab.SelectedStimulus ?? tab.StimulusClass.Stimuli.First()).PathOrText);
                }
                return(tab.StimulusClass.Stimuli.ElementAt(rand.Next(tab.StimulusClass.Stimuli.Count)).PathOrText);
            };
            Action setImage = () =>
            {
                imagePanel.ImagePath = this.StimulusClass1 == null
                    ? null
                    : getImageForClass(this.StimulusClass1);
                imagePanel.SecondaryImagePath = this.StimulusClass2 == null
                    ? null
                    : getImageForClass(this.StimulusClass2);
            };

            setImage();
            var timer = new Timer()
            {
                Interval = 2500, Enabled = true
            };

            timer.Tick += (sender, args) =>
            {
                // just return if we're not cycling to avoid flicker
                if (!cycle && !timer.Enabled)
                {
                    return;
                }

                // if the form is valid, set a new image
                var activeTextBox = this.FindForm().ActiveControl as TextBox;
                if (activeTextBox == null || activeTextBox.IsValid())
                {
                    setImage();
                }
            };
            Action <ImageDisplaySettings> configurePanel = settings =>
            {
                imagePanel.Configure(settings);
                if (settings.CycleThroughImages != cycle)
                {
                    cycle = settings.CycleThroughImages;
                    setImage();
                }
                this.ImageDisplaySettings = settings;
            };

            configurePanel((ImageDisplaySettings)imageConfig.GetConfiguredObject());
            imageConfig.PropertyChanged += args => configurePanel((ImageDisplaySettings)imageConfig.GetConfiguredObject());

            // class list
            var classList = new CheckedListBox()
            {
                Dock = DockStyle.Fill, AllowDrop = true, CheckOnClick = true
            };

            classList.AddContextMenu();
            ItemCheckEventHandler refreshSelectedClasses = (sender, args) =>
            {
                // get the list of checked indices, including the possibly not-yet-changed item
                List <int> checkedIndices = classList.CheckedIndices.Cast <int>().ToList();
                if (args != null)
                {
                    if (args.NewValue == CheckState.Checked)
                    {
                        checkedIndices.Add(args.Index);
                        checkedIndices.Sort();
                    }
                    else
                    {
                        checkedIndices.Remove(args.Index);
                    }
                }

                this.StimulusClass1 = this.StimulusClass2 = null;
                if (checkedIndices.Count > 0)
                {
                    this.StimulusClass1 = ((StimulusClassTab)classList.Items[checkedIndices[0]]).StimulusClass;
                    if (checkedIndices.Count > 1)
                    {
                        this.StimulusClass2 = ((StimulusClassTab)classList.Items[checkedIndices[1]]).StimulusClass;
                    }
                }
                setImage();
            };
            Action <string> addClass = path =>
            {
                StimulusClass stimulusClass;
                if (!StimulusClass.TryLoad(path, out stimulusClass))
                {
                    GUIUtils.Alert("Failed to load stimulus class from " + path, MessageBoxIcon.Error);
                }
                else if (this.stimulusClassTabs
                         .Count(tp => tp.StimulusClass.SourceFolder.Equals(path, StringComparison.OrdinalIgnoreCase) ||
                                tp.StimulusClass.SavePath.Equals(path, StringComparison.OrdinalIgnoreCase)) > 0)
                {
                    GUIUtils.Alert("A class from " + path + " is already loaded!", MessageBoxIcon.Exclamation);
                }
                else
                {
                    // get a unique marker unless this was the load of a saved class
                    if (!File.Exists(stimulusClass.SavePath))
                    {
                        stimulusClass.Settings.Marker = this.stimulusClassTabs.Count == 0
                            ? 1
                            : this.stimulusClassTabs.Max(s => s.StimulusClass.Settings.Marker) + 1;
                    }
                    var classTab = new StimulusClassTab(stimulusClass);
                    classTab.TextChanged += (sender, args) => classList.Invalidate();
                    classTab.Closing     += (sender, args) =>
                    {
                        this.stimulusClassTabs.Remove(classTab);
                        classList.Items.Remove(classTab);
                        refreshSelectedClasses(classList, null);
                    };

                    this.stimulusClassTabs.Add(classTab);
                    tabs.TabPages.Add(classTab);
                    classList.Items.Add(classTab, true);
                    refreshSelectedClasses(classList, null);
                }
            };

            classList.ItemCheck += refreshSelectedClasses;
            classList.DragEnter += (sender, args) =>
            {
                if (args.Data.GetDataPresent(DataFormats.FileDrop, false) &&
                    ((string[])args.Data.GetData(DataFormats.FileDrop)).Where(StimulusClass.IsValidLoadPath).Count() > 0)
                {
                    args.Effect = DragDropEffects.All;
                }
            };
            classList.DragDrop += (sender, args) =>
            {
                // check that the form is in a valid state
                var activeTextBox = this.FindForm().ActiveControl as TextBox;
                if (activeTextBox != null && !activeTextBox.IsValid())
                {
                    GUIUtils.Alert("All entered data must be valid in order for drag and drop to be enabled", MessageBoxIcon.Error);
                    return;
                }

                string[] data = (string[])args.Data.GetData(DataFormats.FileDrop);

                foreach (string path in data.Where(StimulusClass.IsValidLoadPath))
                {
                    addClass(path);
                }
            };

            // button table
            var buttonTable = GUIUtils.CreateButtonTable(Direction.Horizontal, DockStyle.Bottom,
                                                         GUIUtils.CreateFlatButton("New", b =>
            {
                if (this.folderDialog.ShowDialog() == DialogResult.OK)
                {
                    addClass(this.folderDialog.SelectedPath);
                }
            }, startTab.ToolTip, "Create a new stimulus class from a folder of images"),
                                                         GUIUtils.CreateFlatButton("Load", b =>
            {
                if (this.fileDialog.ShowDialog() == DialogResult.OK)
                {
                    addClass(this.fileDialog.FileName);
                }
            }, startTab.ToolTip, "Load a previously saved stimulus class settings file"));

            startTab.Closing += (sender, args) =>
            {
                args.Cancel = true;
                if (GUIUtils.IsUserSure("Reset stimulus classes?"))
                {
                    this.stimulusClassTabs.Clear();
                    this.Controls.Remove(tabs);
                    tabs.Dispose();
                    timer.Enabled = false;
                    timer.Dispose();
                    this.BuildView();
                    this.OnSizeChanged(EventArgs.Empty);
                }
            };

            // add all controls

            // left column
            var panel = new Panel()
            {
                Dock = DockStyle.Fill
            };

            panel.Controls.Add(classList);
            panel.Controls.Add(buttonTable);
            cols.Controls.Add(panel, 0, 0);

            // middle column
            cols.Controls.Add(imageConfig, 1, 0);

            // right column
            cols.Controls.Add(imagePanel, 2, 0);

            startTab.Controls.Add(cols);
            tabs.Controls.Add(startTab);
            this.Controls.Add(tabs);

            this.ResumeLayout(false);
        }
コード例 #3
0
        private void BuildView()
        {
            this.SuspendLayout();
            this.Text = this.StimulusClass.Settings.Name;
            var cols = GUIUtils.CreateTable(new double[] { .33, .33, .33 }, Direction.Horizontal);

            // settings
            var settingsConfig = new ConfigurationPanel(this.StimulusClass.Settings) { Dock = DockStyle.Fill };

            // image panel
            var imagePanel = new ImagePanel() { Dock = DockStyle.Fill };

            // dropdown
            var dropDown = new ComboBox() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Bottom };
            dropDown.MouseWheel += (sender, args) => ((HandledMouseEventArgs)args).Handled = true;
            dropDown.Items.Add(new DisplayPointer(() => this.StimulusClass.Settings.Answer1, true));
            dropDown.Items.Add(new DisplayPointer(() => this.StimulusClass.Settings.Answer2, false));
            dropDown.Items.Add(new DisplayPointer(GUIUtils.Strings.UNCLASSIFIED, null));

            // source folder
            var sourceFolderLink = new LinkLabel() { AutoSize = true, Text = Path.GetFileName(this.StimulusClass.SourceFolder), Dock = DockStyle.Top };
            sourceFolderLink.Click += (sender, args) =>
            {
                try { System.Diagnostics.Process.Start("explorer.exe", this.StimulusClass.SourceFolder); }
                catch (Exception) { GUIUtils.Alert("Failed to open " + this.StimulusClass.SourceFolder); }
            };
            this.ToolTip.SetToolTip(sourceFolderLink, "Open " + this.StimulusClass.SourceFolder);

            // image list
            var imageList = new CheckedListBox() { Dock = DockStyle.Fill };
            imageList.AddContextMenu();
            foreach (var stimulus in this.StimulusClass.Stimuli)
                imageList.Items.Add(new StimulusItem(this.StimulusClass, stimulus), stimulus.Used);
            EventHandler setImage = (sender, args) =>
            {
                if (imageList.Items.Count > 0)
                {
                    var stimulus = ((StimulusItem)(imageList.SelectedItem ?? imageList.Items[0])).Stimulus;
                    imagePanel.ImagePath = stimulus.PathOrText;
                    dropDown.Visible = true;
                    switch (stimulus.Subclass)
                    {
                        case true: dropDown.SelectedIndex = 0; break;
                        case false: dropDown.SelectedIndex = 1; break;
                        case null: dropDown.SelectedIndex = 2; break;
                    }
                }
                else
                {
                    imagePanel.ImagePath = null;
                    dropDown.Visible = false;
                }
            };
            setImage(imageList, EventArgs.Empty); // first set
            imageList.SelectedIndexChanged += setImage;
            settingsConfig.PropertyChanged += args =>
            {
                this.StimulusClass.Settings.SetProperty(args.Property, args.Getter());
                this.Text = this.StimulusClass.Settings.Name;
                imageList.Invalidate();

                // force a refresh
                int selectedIndex = dropDown.SelectedIndex;
                var items = dropDown.Items.Cast<DisplayPointer>().ToArray();
                dropDown.Items.Clear();
                dropDown.Items.AddRange(items);
                dropDown.SelectedIndex = selectedIndex;
            };
            imageList.ItemCheck += (sender, args) => ((StimulusItem)imageList.Items[args.Index]).Stimulus.Used = (args.NewValue == CheckState.Checked);
            dropDown.SelectedIndexChanged += (sender, args) =>
            {
                ((StimulusItem)(imageList.SelectedItem ?? imageList.Items[0])).Stimulus.Subclass =
                    (bool?)((DisplayPointer)dropDown.SelectedItem).Key;
                imageList.Invalidate();
            };
            this.getSelectedStimulus = () => imageList.SelectedItem == null ? null : ((StimulusItem)imageList.SelectedItem).Stimulus;

            // selection info label
            var selectionInfoLabel = new Label() { Dock = DockStyle.Bottom, AutoSize = true };
            PaintEventHandler updateSelectionInfoLabel = (sender, args) =>
            {
                var items = imageList.Items.Cast<StimulusItem>();
                selectionInfoLabel.Text = string.Format("{0}/{1} selected", items.Count(s => s.Stimulus.Used), imageList.Items.Count);
                if (!string.IsNullOrWhiteSpace(this.StimulusClass.Settings.Answer1)
                    || !string.IsNullOrWhiteSpace(this.StimulusClass.Settings.Answer2))
                    selectionInfoLabel.Text += string.Format(" ({0}/{1} {2}, {3}/{4} {5}, {6}/{7} {8})", items.Count(s => s.Stimulus.Subclass == true && s.Stimulus.Used),
                        items.Count(s => s.Stimulus.Subclass == true),
                        this.StimulusClass.Settings.Answer1,
                        items.Count(s => s.Stimulus.Subclass == false && s.Stimulus.Used),
                        items.Count(s => s.Stimulus.Subclass == false),
                        this.StimulusClass.Settings.Answer2,
                        items.Count(s => s.Stimulus.Subclass == null && s.Stimulus.Used),
                        items.Count(s => s.Stimulus.Subclass == null),
                        GUIUtils.Strings.UNCLASSIFIED);
            };
            imageList.Paint += updateSelectionInfoLabel;
            updateSelectionInfoLabel(null, null);

            // button table
            var buttonTable = GUIUtils.CreateButtonTable(Direction.Horizontal, DockStyle.Bottom,
            GUIUtils.CreateFlatButton("Classify", b =>
            {
                MainForm.Instance.Animate(new StimulusClassSetupProvider(this.StimulusClass), this.Invalidate);
            }, this.ToolTip, "Launch a tool to quickly answer this class's question for all stimuli"),
            GUIUtils.CreateFlatButton("Refresh", b =>
            {
                this.StimulusClass.RefreshStimuli();
                this.Invalidate();
            }, this.ToolTip, "Reload the stimuli from the file system"),
            GUIUtils.CreateFlatButton("Save", b =>
            {
                bool saved = this.StimulusClass.TrySave();
                GUIUtils.Alert((saved ? "Saved" : "Failed to save")
                    + " stimulus class info to " + this.StimulusClass.SavePath,
                    (saved ? MessageBoxIcon.Information : MessageBoxIcon.Error));
            }, this.ToolTip, "Save configuration information to " + this.StimulusClass.SavePath));

            // add all controls
            Panel panel;

            // left column
            panel = new Panel() { Dock = DockStyle.Fill };
            panel.Controls.Add(settingsConfig);
            panel.Controls.Add(sourceFolderLink);
            panel.Controls.Add("Folder".ToLabel());
            panel.Controls.Add(buttonTable);
            cols.Controls.Add(panel, 0, 0);

            // middle column
            panel = new Panel() { Dock = DockStyle.Fill };
            panel.Controls.Add(imageList);
            panel.Controls.Add(selectionInfoLabel);
            panel.Controls.Add("Stimuli".ToLabel());
            cols.Controls.Add(panel, 1, 0);

            // right column
            panel = new Panel() { Dock = DockStyle.Fill };
            panel.Controls.Add(imagePanel);
            panel.Controls.Add(dropDown);
            cols.Controls.Add(panel, 2, 0);

            this.Controls.Add(cols);
            this.ResumeLayout(false);
        }
コード例 #4
0
        private void BuildView()
        {
            this.SuspendLayout();
            this.StimulusClass1 = this.StimulusClass2 = null;

            // tab control
            var tabs = new CustomTabControl() { Dock = DockStyle.Fill };
            tabs.DisplayStyleProvider = new TabStyleVisualStudioProvider(tabs) { ShowTabCloser = true };
            tabs.TabClosing += (sender, args) => ((CustomTab)args.TabPage).RaiseClosingSafe(args);

            // start tab
            var startTab = new CustomTab() { Text = "Classes" };

            // columns
            var cols = GUIUtils.CreateTable(new double[] { .33, .33, .33 }, Direction.Horizontal);

            // image config
            var imageConfig = ConfigurationPanel.Create<ImageDisplaySettings>();

            // image panel
            var imagePanel = new ImagePanel() { Dock = DockStyle.Fill, UseNativeSize = false };
            bool cycle = true;
            var rand = new Random();
            Func<StimulusClass, string> getImageForClass = stimulusClass =>
            {
                var tab = this.stimulusClassTabs.First(t => t.StimulusClass == stimulusClass);
                if (tab.StimulusClass.Stimuli.Count == 0)
                    return null;
                if (!((ImageDisplaySettings)imageConfig.GetConfiguredObject()).CycleThroughImages)
                    return (tab.SelectedStimulus ?? tab.StimulusClass.Stimuli.First()).PathOrText;
                return tab.StimulusClass.Stimuli.ElementAt(rand.Next(tab.StimulusClass.Stimuli.Count)).PathOrText;
            };
            Action setImage = () =>
            {
                imagePanel.ImagePath = this.StimulusClass1 == null
                    ? null
                    : getImageForClass(this.StimulusClass1);
                imagePanel.SecondaryImagePath = this.StimulusClass2 == null
                    ? null
                    : getImageForClass(this.StimulusClass2);
            };
            setImage();
            var timer = new Timer() { Interval = 2500, Enabled = true };
            timer.Tick += (sender, args) =>
            {
                // just return if we're not cycling to avoid flicker
                if (!cycle && !timer.Enabled)
                    return;

                // if the form is valid, set a new image
                var activeTextBox = this.FindForm().ActiveControl as TextBox;
                if (activeTextBox == null || activeTextBox.IsValid())
                    setImage();
            };
            Action<ImageDisplaySettings> configurePanel = settings =>
            {
                imagePanel.Configure(settings);
                if (settings.CycleThroughImages != cycle)
                {
                    cycle = settings.CycleThroughImages;
                    setImage();
                }
                this.ImageDisplaySettings = settings;
            };
            configurePanel((ImageDisplaySettings)imageConfig.GetConfiguredObject());
            imageConfig.PropertyChanged += args => configurePanel((ImageDisplaySettings)imageConfig.GetConfiguredObject());

            // class list
            var classList = new CheckedListBox() { Dock = DockStyle.Fill, AllowDrop = true, CheckOnClick = true };
            classList.AddContextMenu();
            ItemCheckEventHandler refreshSelectedClasses = (sender, args) =>
            {
                // get the list of checked indices, including the possibly not-yet-changed item
                List<int> checkedIndices = classList.CheckedIndices.Cast<int>().ToList();
                if (args != null)
                {
                    if (args.NewValue == CheckState.Checked)
                    {
                        checkedIndices.Add(args.Index);
                        checkedIndices.Sort();
                    }
                    else
                        checkedIndices.Remove(args.Index);
                }

                this.StimulusClass1 = this.StimulusClass2 = null;
                if (checkedIndices.Count > 0)
                {
                    this.StimulusClass1 = ((StimulusClassTab)classList.Items[checkedIndices[0]]).StimulusClass;
                    if (checkedIndices.Count > 1)
                        this.StimulusClass2 = ((StimulusClassTab)classList.Items[checkedIndices[1]]).StimulusClass;
                }
                setImage();
            };
            Action<string> addClass = path =>
            {
                StimulusClass stimulusClass;
                if (!StimulusClass.TryLoad(path, out stimulusClass))
                    GUIUtils.Alert("Failed to load stimulus class from " + path, MessageBoxIcon.Error);
                else if (this.stimulusClassTabs
                    .Count(tp => tp.StimulusClass.SourceFolder.Equals(path, StringComparison.OrdinalIgnoreCase)
                        || tp.StimulusClass.SavePath.Equals(path, StringComparison.OrdinalIgnoreCase)) > 0)
                    GUIUtils.Alert("A class from " + path + " is already loaded!", MessageBoxIcon.Exclamation);
                else
                {
                    // get a unique marker unless this was the load of a saved class
                    if (!File.Exists(stimulusClass.SavePath))
                        stimulusClass.Settings.Marker = this.stimulusClassTabs.Count == 0
                            ? 1
                            : this.stimulusClassTabs.Max(s => s.StimulusClass.Settings.Marker) + 1;
                    var classTab = new StimulusClassTab(stimulusClass);
                    classTab.TextChanged += (sender, args) => classList.Invalidate();
                    classTab.Closing += (sender, args) =>
                    {
                        this.stimulusClassTabs.Remove(classTab);
                        classList.Items.Remove(classTab);
                        refreshSelectedClasses(classList, null);
                    };

                    this.stimulusClassTabs.Add(classTab);
                    tabs.TabPages.Add(classTab);
                    classList.Items.Add(classTab, true);
                    refreshSelectedClasses(classList, null);
                }
            };
            classList.ItemCheck += refreshSelectedClasses;
            classList.DragEnter += (sender, args) =>
            {
                if (args.Data.GetDataPresent(DataFormats.FileDrop, false)
                    && ((string[])args.Data.GetData(DataFormats.FileDrop)).Where(StimulusClass.IsValidLoadPath).Count() > 0)
                    args.Effect = DragDropEffects.All;
            };
            classList.DragDrop += (sender, args) =>
            {
                // check that the form is in a valid state
                var activeTextBox = this.FindForm().ActiveControl as TextBox;
                if (activeTextBox != null && !activeTextBox.IsValid())
                {
                    GUIUtils.Alert("All entered data must be valid in order for drag and drop to be enabled", MessageBoxIcon.Error);
                    return;
                }

                string[] data = (string[])args.Data.GetData(DataFormats.FileDrop);

                foreach (string path in data.Where(StimulusClass.IsValidLoadPath))
                    addClass(path);
            };

            // button table
            var buttonTable = GUIUtils.CreateButtonTable(Direction.Horizontal, DockStyle.Bottom,
            GUIUtils.CreateFlatButton("New", b =>
            {
                if (this.folderDialog.ShowDialog() == DialogResult.OK)
                    addClass(this.folderDialog.SelectedPath);
            }, startTab.ToolTip, "Create a new stimulus class from a folder of images"),
            GUIUtils.CreateFlatButton("Load", b =>
            {
                if (this.fileDialog.ShowDialog() == DialogResult.OK)
                    addClass(this.fileDialog.FileName);
            }, startTab.ToolTip, "Load a previously saved stimulus class settings file"));

            startTab.Closing += (sender, args) =>
            {
                args.Cancel = true;
                if (GUIUtils.IsUserSure("Reset stimulus classes?"))
                {
                    this.stimulusClassTabs.Clear();
                    this.Controls.Remove(tabs);
                    tabs.Dispose();
                    timer.Enabled = false;
                    timer.Dispose();
                    this.BuildView();
                    this.OnSizeChanged(EventArgs.Empty);
                }
            };

            // add all controls

            // left column
            var panel = new Panel() { Dock = DockStyle.Fill };
            panel.Controls.Add(classList);
            panel.Controls.Add(buttonTable);
            cols.Controls.Add(panel, 0, 0);

            // middle column
            cols.Controls.Add(imageConfig, 1, 0);

            // right column
            cols.Controls.Add(imagePanel, 2, 0);

            startTab.Controls.Add(cols);
            tabs.Controls.Add(startTab);
            this.Controls.Add(tabs);

            this.ResumeLayout(false);
        }