Exemple #1
0
    def __init__(self, parent, log):

        wx.Frame.__init__(self, parent, title="BalloonTip wxPython Demo ;-)")

        self.statusbar = self.CreateStatusBar(2)
        self.statusbar.SetStatusWidths([-2, -1])
        # statusbar fields
        statusbar_fields = [("Welcome To WxPython " + wx.VERSION_STRING),
                            ("BalloonTip Demo")]

        for i in range(len(statusbar_fields)):
            self.statusbar.SetStatusText(statusbar_fields[i], i)

        self.SetIcon(images.Mondrian.GetIcon())
        self.SetMenuBar(self.CreateMenuBar())

        panel = wx.Panel(self, -1)

        mainsizer = wx.FlexGridSizer(3, 4, hgap=2, vgap=2)

        # Add A Button
        button = wx.Button(panel, -1, "Press Me!")
        # Add A TextCtrl
        textctrl = wx.TextCtrl(panel, -1, "I Am A TextCtrl")
        # Add A CheckBox
        checkbox = wx.CheckBox(panel,
                               -1,
                               "3-State Checkbox",
                               style=wx.CHK_3STATE
                               | wx.CHK_ALLOW_3RD_STATE_FOR_USER)
        samplelist = [
            'One', 'Two', 'Three', 'Four', 'Kick', 'The', 'Demo', 'Out', 'The',
            'Door', ';-)'
        ]
        # Add A Choice
        choice = wx.Choice(panel, -1, choices=samplelist)
        # Add A Gauge
        gauge = wx.Gauge(panel, -1, 50, style=wx.GA_SMOOTH)
        # Add A ListBox
        listbox = wx.ListBox(panel, -1, choices=samplelist, style=wx.LB_SINGLE)
        # Add A TreeCtrl
        isz = (16, 16)
        treecontrol = wx.TreeCtrl(panel, -1)
        il = wx.ImageList(isz[0], isz[1])
        fldridx = il.Add(
            wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, isz))
        fldropenidx = il.Add(
            wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_OTHER, isz))
        fileidx = il.Add(
            wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, wx.ART_OTHER, isz))
        treecontrol.SetImageList(il)
        self.il = il
        root = treecontrol.AddRoot("ROOT")
        treecontrol.SetItemData(root, None)
        treecontrol.SetItemImage(root, fldridx, wx.TreeItemIcon_Normal)
        treecontrol.SetItemImage(root, fldropenidx, wx.TreeItemIcon_Expanded)
        for ii in range(11):
            child = treecontrol.AppendItem(root, samplelist[ii])
            treecontrol.SetItemData(child, None)
            treecontrol.SetItemImage(child, fldridx, wx.TreeItemIcon_Normal)
            treecontrol.SetItemImage(child, fldropenidx,
                                     wx.TreeItemIcon_Selected)

        # Add A Slider
        slider = wx.Slider(panel,
                           -1,
                           25,
                           1,
                           100,
                           style=wx.SL_HORIZONTAL
                           | wx.SL_AUTOTICKS)  # | wx.SL_LABELS)
        slider.SetTickFreq(5)
        # Add Another TextCtrl
        textctrl2 = wx.TextCtrl(panel, -1, "Another TextCtrl")
        # Add A GenStaticText
        statictext = StaticText(panel, -1, "Hello World!")
        statictext.SetFont(
            wx.Font(9, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                    wx.FONTWEIGHT_BOLD, False))
        bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_TOOLBAR,
                                       (16, 16))
        # Add A GenBitmapButton
        bitmapbutton = BitmapButton(panel, -1, bmp)
        button2 = wx.Button(panel, -1, "Disable BalloonTip")

        tbicon = TaskBarIcon()
        tbicon.SetIcon(images.Mondrian.GetIcon())

        controls = list(panel.GetChildren())
        controls.append(tbicon)
        self.tbicon = tbicon

        # Add The Controls To The Main FlexGridSizer
        mainsizer.Add(button, 0, wx.EXPAND | wx.ALL, 10)
        mainsizer.Add(textctrl, 0, wx.EXPAND | wx.ALL, 10)
        mainsizer.Add(checkbox, 0, wx.EXPAND | wx.ALL, 10)
        mainsizer.Add(choice, 0, wx.EXPAND | wx.ALL, 10)
        mainsizer.Add(gauge, 0, wx.ALL, 10)
        mainsizer.Add(listbox, 0, wx.EXPAND | wx.ALL, 10)
        mainsizer.Add(treecontrol, 0, wx.EXPAND, wx.ALL, 10)
        mainsizer.Add(slider, 0, wx.ALL, 10)
        mainsizer.Add(textctrl2, 0, wx.ALL, 10)
        mainsizer.Add(statictext, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, 10)
        mainsizer.Add(bitmapbutton, 0, wx.ALL, 10)
        mainsizer.Add(button2, 0, wx.ALL, 10)

        panel.SetSizer(mainsizer)
        mainsizer.Layout()

        # Declare The BalloonTip Background Colours
        bgcolours = [
            None, wx.WHITE, wx.GREEN, wx.BLUE, wx.CYAN, wx.RED, None, None,
            wx.LIGHT_GREY, None, wx.WHITE, None, None
        ]

        # Declare The BalloonTip Top-Left Icons
        icons = []
        for ii in range(4):
            bmp = wx.ArtProvider.GetBitmap(eval(ArtIDs[ii]), wx.ART_TOOLBAR,
                                           (16, 16))
            icons.append(bmp)

        icons.extend([None] * 5)

        for ii in range(4, 9):
            bmp = wx.ArtProvider.GetBitmap(eval(ArtIDs[ii]), wx.ART_TOOLBAR,
                                           (16, 16))
            icons.append(bmp)

        # Declare The BalloonTip Top Titles
        titles = [
            "Button Help", "Texctrl Help", "CheckBox Help", "Choice Help",
            "Gauge Help", "", "", "Read Me Carefully!", "SpinCtrl Help",
            "StaticText Help", "BitmapButton Help", "Button Help",
            "Taskbar Help"
        ]

        fontone = wx.Font(9, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                          wx.FONTWEIGHT_BOLD, True)
        fonttwo = wx.Font(14, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL,
                          wx.FONTWEIGHT_BOLD, False)
        fontthree = wx.Font(9, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_ITALIC,
                            wx.FONTWEIGHT_NORMAL, False)
        fontfour = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                           wx.FONTWEIGHT_BOLD, True)

        # Declare The BalloonTip Top Titles Fonts
        titlefonts = [
            None, None, fontone, None, fonttwo, fontthree, None, None, None,
            fontfour, fontthree, None, None
        ]

        # Declare The BalloonTip Top Titles Colours
        titlecolours = [
            None, None, wx.WHITE, wx.YELLOW, None, wx.WHITE, wx.BLUE, wx.RED,
            None, None, wx.LIGHT_GREY, None, None
        ]

        # Declare The BalloonTip Messages
        msg1 = "This Is The Default BalloonTip Window\nYou Can Customize It! "\
               "Look At The Demo!"
        msg2 = "You Can Change The Background Colour\n Of The Balloon Window."
        msg3 = "You Can Also Change The Font And The\nColour For The Title."
        msg4 = "I Have Nothing Special To Suggest!\n\nWelcome To wxPython " + \
               wx.VERSION_STRING + " !"
        msg5 = "What About If I Don't Want The Icon?\nNo Problem!"
        msg6 = "I Don't Have The Icon Nor The Title.\n\nDo You Love Me Anyway?"
        msg7 = "Some Comments On The Window Shape:\n\n- BT_ROUNDED: Creates A "\
               "Rounded Rectangle;\n- BT_RECTANGLE: Creates A Rectangle.\n"
        msg8 = "Some Comments On The BalloonTip Style:\n\n"\
               "BT_LEAVE: The BalloonTip Is Destroyed When\nThe Mouse Leaves"\
               "The Target Widget;\n\nBT_CLICK: The BalloonTip Is Destroyed When\n"\
               "You Click Any Region Of The BalloonTip;\n\nBT_BUTTON: The BalloonTip"\
               " Is Destroyed When\nYou Click On The Top-Right Small Button."
        msg9 = "Some Comments On Delay Time:\n\nBy Default, The Delay Time After Which\n"\
               "The BalloonTip Is Destroyed Is Very Long.\nYou Can Change It By Using"\
               " The\nSetEndDelay() Method."
        msg10 = "I Have Nothing Special To Suggest!\n\nRead Me FAST, You Have Only 3 "\
                "Seconds!"
        msg11 = "I Hope You Will Enjoy BalloonTip!\nIf This Is The Case, Please\n"\
                "Post Some Comments On wxPython\nMailing List!"
        msg12 = "This Button Enable/Disable Globally\nThe BalloonTip On Your Application."
        msg13 = "This Is A BalloonTip For The\nTaskBar Icon Of Your Application.\n"\
                "All The Styles For BalloonTip Work."

        messages = [
            msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11,
            msg12, msg13
        ]

        # Declare The BalloonTip Tip Messages Colours
        messagecolours = [
            None, None, None, wx.WHITE, wx.BLUE, None, wx.BLUE, None, None,
            wx.RED, wx.GREEN, wx.BLUE, None
        ]

        fontone = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                          wx.FONTWEIGHT_NORMAL, True)
        fonttwo = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_ITALIC,
                          wx.FONTWEIGHT_NORMAL, False)
        fontthree = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                            wx.FONTWEIGHT_BOLD, True)

        # Declare The BalloonTip Tip Messages Fonts
        messagefonts = [
            None, None, None, fontone, None, None, fonttwo, None, fontthree,
            None, None, None, None
        ]

        # Declare The BalloonTip Frame Shapes
        windowshapes = [
            BT.BT_ROUNDED, BT.BT_RECTANGLE, BT.BT_ROUNDED, BT.BT_RECTANGLE,
            BT.BT_ROUNDED, BT.BT_RECTANGLE, BT.BT_ROUNDED, BT.BT_ROUNDED,
            BT.BT_ROUNDED, BT.BT_RECTANGLE, BT.BT_ROUNDED, BT.BT_RECTANGLE,
            BT.BT_RECTANGLE
        ]

        # Declare The BalloonTip Destruction Style
        tipstyles = [
            BT.BT_LEAVE, BT.BT_CLICK, BT.BT_BUTTON, BT.BT_LEAVE, BT.BT_CLICK,
            BT.BT_LEAVE, BT.BT_CLICK, BT.BT_BUTTON, BT.BT_BUTTON, BT.BT_CLICK,
            BT.BT_LEAVE, BT.BT_LEAVE, BT.BT_BUTTON
        ]

        # Set The Targets/Styles For The BalloonTip
        for ii, widget in enumerate(controls):
            tipballoon = BT.BalloonTip(topicon=icons[ii],
                                       toptitle=titles[ii],
                                       message=messages[ii],
                                       shape=windowshapes[ii],
                                       tipstyle=tipstyles[ii])
            # Set The Target
            tipballoon.SetTarget(widget)
            # Set The Balloon Colour
            tipballoon.SetBalloonColour(bgcolours[ii])
            # Set The Font For The Top Title
            tipballoon.SetTitleFont(titlefonts[ii])
            # Set The Colour For The Top Title
            tipballoon.SetTitleColour(titlecolours[ii])
            # Set The Font For The Tip Message
            tipballoon.SetMessageFont(messagefonts[ii])
            # Set The Colour For The Tip Message
            tipballoon.SetMessageColour(messagecolours[ii])
            # Set The Delay After Which The BalloonTip Is Created
            tipballoon.SetStartDelay(1000)
            if ii == 9:
                # Set The Delay After Which The BalloonTip Is Destroyed
                tipballoon.SetEndDelay(3000)

        # Store The Last BalloonTip Reference To Enable/Disable Globall The
        # BalloonTip. You Can Store Any Of Them, Not Necessarily The Last One.
        self.lasttip = tipballoon
        self.gauge = gauge
        self.count = 0

        button2.Bind(wx.EVT_BUTTON, self.OnActivateBalloon)
        self.Bind(wx.EVT_IDLE, self.IdleHandler)

        frameSizer = wx.BoxSizer(wx.VERTICAL)
        frameSizer.Add(panel, 1, wx.EXPAND)
        self.SetSizer(frameSizer)
        frameSizer.Layout()
        self.Fit()

        self.CenterOnParent()
Exemple #2
0
class GooeyApplication(wx.Frame):
    """
    Main window for Gooey.
    """
    def __init__(self, buildSpec, *args, **kwargs):
        super(GooeyApplication, self).__init__(None, *args, **kwargs)
        self._state = {}
        self.buildSpec = buildSpec

        self.applyConfiguration()
        self.menu = MenuBar(buildSpec)
        self.SetMenuBar(self.menu)
        self.header = FrameHeader(self, buildSpec)
        self.configs = self.buildConfigPanels(self)
        self.navbar = self.buildNavigation()
        self.footer = Footer(self, buildSpec)
        self.console = Console(self, buildSpec)
        self.layoutComponent()

        self.clientRunner = ProcessController(
            self.buildSpec.get('progress_regex'),
            self.buildSpec.get('progress_expr'),
            self.buildSpec.get('hide_progress_msg'),
            self.buildSpec.get('encoding'),
            self.buildSpec.get('requires_shell'),
        )

        pub.subscribe(events.WINDOW_START, self.onStart)
        pub.subscribe(events.WINDOW_RESTART, self.onStart)
        pub.subscribe(events.WINDOW_STOP, self.onStopExecution)
        pub.subscribe(events.WINDOW_CLOSE, self.onClose)
        pub.subscribe(events.WINDOW_CANCEL, self.onCancel)
        pub.subscribe(events.WINDOW_EDIT, self.onEdit)
        pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput)
        pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete)
        pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar)
        # Top level wx close event
        self.Bind(wx.EVT_CLOSE, self.onClose)

        if self.buildSpec['poll_external_updates']:
            self.fetchExternalUpdates()

        if self.buildSpec.get('auto_start', False):
            self.onStart()

    def applyConfiguration(self):
        self.SetTitle(self.buildSpec['program_name'])
        self.SetBackgroundColour(self.buildSpec.get('body_bg_color'))

    def onStart(self, *args, **kwarg):
        """
        Verify user input and kick off the client's program if valid
        """
        with transactUI(self):
            config = self.navbar.getActiveConfig()
            config.resetErrors()
            if config.isValid():
                if self.buildSpec['clear_before_run']:
                    self.console.clear()
                self.clientRunner.run(self.buildCliString())
                self.showConsole()
            else:
                config.displayErrors()
                self.Layout()

    def onEdit(self):
        """Return the user to the settings screen for further editing"""
        with transactUI(self):
            if self.buildSpec['poll_external_updates']:
                self.fetchExternalUpdates()
            self.showSettings()

    def buildCliString(self):
        """
        Collect all of the required information from the config screen and
        build a CLI string which can be used to invoke the client program
        """
        config = self.navbar.getActiveConfig()
        group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()]
        positional = config.getPositionalArgs()
        optional = config.getOptionalArgs()
        return cli.buildCliString(
            self.buildSpec['target'],
            group['command'],
            positional,
            optional,
            suppress_gooey_flag=self.buildSpec['suppress_gooey_flag'])

    def onComplete(self, *args, **kwargs):
        """
        Display the appropriate screen based on the success/fail of the
        host program
        """
        with transactUI(self):
            if self.clientRunner.was_success():
                if self.buildSpec.get('return_to_config', False):
                    self.showSettings()
                else:
                    self.showSuccess()
                    if self.buildSpec.get('show_success_modal', True):
                        wx.CallAfter(modals.showSuccess)
            else:
                if self.clientRunner.wasForcefullyStopped:
                    self.showForceStopped()
                else:
                    self.showError()
                    if self.buildSpec.get('show_failure_modal'):
                        wx.CallAfter(modals.showFailure)

    def onStopExecution(self):
        """Displays a scary message and then force-quits the executing
        client code if the user accepts"""
        if self.buildSpec['show_stop_warning'] and modals.confirmForceStop():
            self.clientRunner.stop()

    def fetchExternalUpdates(self):
        """
        !Experimental!
        Calls out to the client code requesting seed values to use in the UI
        !Experimental!
        """
        seeds = seeder.fetchDynamicProperties(self.buildSpec['target'],
                                              self.buildSpec['encoding'])
        for config in self.configs:
            config.seedUI(seeds)

    def onCancel(self):
        """Close the program after confirming"""
        if modals.confirmExit():
            self.onClose()

    def onClose(self, *args, **kwargs):
        """Cleanup the top level WxFrame and shutdown the process"""
        self.Destroy()
        sys.exit()

    def layoutComponent(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.header, 0, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)

        sizer.Add(self.navbar, 1, wx.EXPAND)
        sizer.Add(self.console, 1, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)
        sizer.Add(self.footer, 0, wx.EXPAND)
        self.SetMinSize((400, 300))
        self.SetSize(self.buildSpec['default_size'])
        self.SetSizer(sizer)
        self.console.Hide()
        self.Layout()
        # Program Icon (Windows)
        icon = wx.Icon(self.buildSpec['images']['programIcon'],
                       wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)
        if sys.platform != 'win32':
            # OSX needs to have its taskbar icon explicitly set
            # bizarrely, wx requires the TaskBarIcon to be attached to the Frame
            # as instance data (self.). Otherwise, it will not render correctly.
            self.taskbarIcon = TaskBarIcon(iconType=wx.adv.TBI_DOCK)
            self.taskbarIcon.SetIcon(icon)

    def buildNavigation(self):
        """
        Chooses the appropriate layout navigation component based on user prefs
        """
        if self.buildSpec['navigation'] == constants.TABBED:
            navigation = Tabbar(self, self.buildSpec, self.configs)
        else:
            navigation = Sidebar(self, self.buildSpec, self.configs)
            if self.buildSpec['navigation'] == constants.HIDDEN:
                navigation.Hide()
        return navigation

    def buildConfigPanels(self, parent):
        page_class = TabbedConfigPage if self.buildSpec[
            'tabbed_groups'] else ConfigPage

        return [
            page_class(parent, widgets, self.buildSpec)
            for widgets in self.buildSpec['widgets'].values()
        ]

    def showSettings(self):
        self.navbar.Show(True)
        self.console.Show(False)
        self.header.setImage('settings_img')
        self.header.setTitle(_("settings_title"))
        self.header.setSubtitle(self.buildSpec['program_description'])
        self.footer.showButtons('cancel_button', 'start_button')
        self.footer.progress_bar.Show(False)

    def showConsole(self):
        self.navbar.Show(False)
        self.console.Show(True)
        self.header.setImage('running_img')
        self.header.setTitle(_("running_title"))
        self.header.setSubtitle(_('running_msg'))
        self.footer.showButtons('stop_button')
        self.footer.progress_bar.Show(True)
        if not self.buildSpec['progress_regex']:
            self.footer.progress_bar.Pulse()

    def showComplete(self):
        self.navbar.Show(False)
        self.console.Show(True)
        buttons = (['edit_button', 'restart_button', 'close_button']
                   if self.buildSpec.get('show_restart_button', True) else
                   ['edit_button', 'close_button'])
        self.footer.showButtons(*buttons)
        self.footer.progress_bar.Show(False)

    def showSuccess(self):
        self.showComplete()
        self.header.setImage('check_mark')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_msg'))
        self.Layout()

    def showError(self):
        self.showComplete()
        self.header.setImage('error_symbol')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_error'))

    def showForceStopped(self):
        self.showComplete()
        if self.buildSpec.get('force_stop_is_error', True):
            self.showError()
        else:
            self.showSuccess()
        self.header.setSubtitle(_('finished_forced_quit'))
Exemple #3
0
class KlipFrame(wx.Frame):
    """
    """
    def __init__(self, model_):
        """
        """
        self.model = model_
        self._book = None
        self._book_id = None
        self._search_target = None

        super(KlipFrame, self).__init__(None,
                                        size=wx.Size(1200, 760),
                                        title='Klip',
                                        name='Klip')
        # ensure the parent's __init__ is called

        icon = icons.klip.GetIcon()
        self.SetIcon(icon)

        self.tb = TaskBarIcon(wx.adv.TBI_DOCK)
        self.tb.SetIcon(icon)

        sp = wx.SplitterWindow(self, style=wx.SP_BORDER | wx.SP_3DBORDER)
        sp.SetSplitMode(wx.SPLIT_VERTICAL)
        sp.SetMinimumPaneSize(50)
        self.detailPanel = KlipDetailWindow(self)
        self.SetMinSize(wx.Size(800, 600))

        # create a panel in the frame
        pnl_books = wx.Panel(sp)
        pnl_clips = wx.Panel(sp)
        pnl_clips.SetBackgroundColour(wx.Colour(255, 255, 255))

        sp.SplitVertically(pnl_books, pnl_clips,
                           int(round(self.GetSize().GetWidth() * 0.3)))

        sizer = wx.BoxSizer(wx.VERTICAL)

        self.search = wx.SearchCtrl(pnl_books,
                                    size=(200, -1),
                                    style=wx.TE_PROCESS_ENTER)
        self.search.ShowSearchButton(True)
        self.search.ShowCancelButton(True)

        self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.OnSearch, self.search)
        self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnCancelSearch,
                  self.search)

        sizer.Add(self.search, 0, wx.EXPAND, 0)

        self.book_list = KlipListCtrl(pnl_books,
                                      wx.ID_ANY,
                                      style=wx.LC_REPORT | wx.LC_NO_HEADER
                                      | wx.LC_HRULES | wx.LC_SINGLE_SEL)

        font = self.book_list.GetFont()
        font.PointSize += 5
        # font = font.Bold()
        self.book_list.SetFont(font)
        self.book_list.SetForegroundColour(wx.Colour(0x43, 0x43, 0x43))
        self.book_list.ClearAll()
        self.book_list.SetBackgroundColour(wx.Colour(0xf6, 0xf6, 0xf6))

        self.bl_width = self.book_list.GetSize().GetWidth() * 0.5

        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnBookSelected,
                  self.book_list)

        self._total_books = wx.StaticText(pnl_books,
                                          label="BOOKS (%d)" % 0,
                                          pos=(25, 25))

        sizer.Add(self._total_books, 0, wx.LEFT, 0)

        sizer.Add(self.book_list, 1, wx.EXPAND | wx.ALL, 0)

        pnl_books.SetSizer(sizer)

        # init right panel, show clippings.
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.book_title = wx.StaticText(pnl_clips, label='')

        font = self.book_title.GetFont()
        font.PointSize += 10
        font.Bold()
        self.book_title.SetFont(font)
        self.book_title.SetForegroundColour(wx.Colour(0x25, 0x91, 0xff))
        sizer.Add(self.book_title, 0, wx.EXPAND, 10)

        self.book_info = wx.StaticText(pnl_clips, label='')

        font = self.book_info.GetFont()
        font.PointSize += 5
        font.Bold()
        self.book_info.SetFont(font)
        # self.book_info.SetForegroundColour(wx.Colour(0x25, 0x91, 0xff))

        sizer.Add(self.book_info, 0, wx.EXPAND, 10)

        self.clip_list = KlipListCtrl(pnl_clips,
                                      wx.ID_ANY,
                                      style=wx.LC_REPORT | wx.LC_NO_HEADER
                                      | wx.LC_HRULES | wx.LC_SINGLE_SEL)

        self.clip_list.InsertColumn(0, "Clip")

        # books = self.fillBooks()

        sizer.Add(self.clip_list, 1, wx.EXPAND | wx.ALL, 0)

        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.showClipDetail,
                  self.clip_list)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnClipSelected,
                  self.clip_list)

        pnl_clips.SetSizer(sizer)

        font = self.clip_list.GetFont()
        font.PointSize += 3
        self.clip_list.SetFont(font)

        width = self.clip_list.GetSize().GetWidth() * 0.5
        PDEBUG('Update Column Width: %d' % width)
        self.clip_list.SetColumnWidth(0, width)

        self.clip_list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK,
                            self.OnRightClickClips)

        self.refreshContents()

        # create a menu bar
        self.makeMenuBar()

    def makeMenuBar(self):
        """
        A menu bar is composed of menus, which are composed of menu items.
        This method builds a set of menus and binds handlers to be called
        when the menu item is selected.
        """

        # Make a file menu with Hello and Exit items
        fileMenu = wx.Menu()
        # The "\t..." syntax defines an accelerator key that also triggers
        # the same event
        loadItem = fileMenu.Append(-1, "&Load...", "Load clippings...")

        cleanItem = fileMenu.Append(-1, "&Clean...", "Clean clippings...")

        fileMenu.AppendSeparator()
        # When using a stock ID we don't need to specify the menu item's
        # label
        exitItem = fileMenu.Append(wx.ID_EXIT)

        # Now a help menu for the about item
        helpMenu = wx.Menu()
        aboutItem = helpMenu.Append(wx.ID_ABOUT)

        # Make the menu bar and add the two menus to it. The '&' defines
        # that the next letter is the "mnemonic" for the menu item. On the
        # platforms that support it those letters are underlined and can be
        # triggered from the keyboard.
        menuBar = wx.MenuBar()
        menuBar.Append(fileMenu, "&File")
        menuBar.Append(helpMenu, "&Help")

        # Give the menu bar to the frame
        self.SetMenuBar(menuBar)

        # Finally, associate a handler function with the EVT_MENU event for
        # each of the menu items. That means that when that menu item is
        # activated then the associated handler function will be called.
        self.Bind(wx.EVT_MENU, self.OnLoadFile, loadItem)
        self.Bind(wx.EVT_MENU, self.cleanRecords, cleanItem)
        self.Bind(wx.EVT_MENU, self.OnExit, exitItem)
        self.Bind(wx.EVT_MENU, self.OnAbout, aboutItem)

    def OnExit(self, event):
        """Close the frame, terminating the application."""
        self.Close(True)

    def cleanRecords(self, event):
        """Close the frame, terminating the application."""
        total = self.model.cleanUpBooks()
        if total > 0:
            pass  # Add dialog??

        self.refreshContents()

    def refreshContents(self):
        """
        """
        self.fillBooks()
        total_books = self.book_list.GetItemCount()
        PDEBUG('BOOK_ID: %s, total: %s', self._book_id, total_books)
        if self._book_id is None:
            self.book_list.Select(0)
        elif self._book_id >= total_books:
            self.book_list.Select(total_books - 1)
        else:
            self.book_list.Select(self._book_id)
        pass

    def OnLoadFile(self, event):
        """Load clips from file.."""

        (books, clips) = self.model.loadFile(getClipPath())
        if books > 0:
            self.fillBooks()
        if clips > 0 or books > 0:
            self.showClipsOfBook()

        wx.MessageBox("Loaded %d books with %d clips." % (books, clips))

    def OnAbout(self, event):
        """Display an About Dialog"""
        wx.MessageBox("This is a wxPython Hello World sample",
                      "About Hello World 2", wx.OK | wx.ICON_INFORMATION)

    def OnCancelSearch(self, evt):
        PDEBUG('ENTER')
        self.showClipsOfBook()

    def OnSearch(self, evt):
        PDEBUG('ENTER')
        target = self.search.GetValue()
        args = target.split()
        if not args:
            PDEBUG('empty search target...')
            return

        it = self.model.searchClips(args)
        self.showClips('Result for "%s"' % target, it)
        pass

    def fillBooks(self):
        """Fill book list.
        """

        self.book_list.DeleteAllItems()

        self.book_list.InsertColumn(0, "Book")
        # 0 will insert at the start of the list

        iter = self.model.getBooks()
        idx = 0
        while iter.next():
            item = wx.ListItem()
            item.SetData(iter.id)
            item.SetId(idx)
            item.SetText(u"    %s" % (iter.name))
            self.book_list.InsertItem(item)
            idx += 1

        self._total_books.SetLabel('BOOKS (%d)' % idx)
        self.book_list.SetColumnWidth(0, self.bl_width)

        return idx

    def OnBookSelected(self, event):
        """
        """
        self._book_id = event.GetItem().GetData()
        PDEBUG('BOOK_ID: %d', self._book_id)
        self.showClipsOfBook()
        self._clip = None
        pass

    def OnClipSelected(self, event):
        self._cur_item = event.GetItem()
        id = self._cur_item.GetData()
        txt = self._cur_item.GetText()
        PDEBUG('ID: %d -- %s', id, txt)
        self._clip = self.model.getClipById(id)
        PDEBUG('CLIP: %s', self._clip)
        pass

    def OnRightClickClips(self, evt):
        """
        """
        PDEBUG('enter: %s', evt)

        # only do this part the first time so the events are only bound once
        if not hasattr(self, "popupID1"):
            self.popupID1 = wx.NewIdRef()
            self.popupID2 = wx.NewIdRef()
            self.popupID3 = wx.NewIdRef()
            self.popupID5 = wx.NewIdRef()
            self.popupID6 = wx.NewIdRef()

            self.Bind(wx.EVT_MENU, self.OnNew, id=self.popupID1)
            self.Bind(wx.EVT_MENU, self.OnEdit, id=self.popupID2)
            self.Bind(wx.EVT_MENU, self.OnDelete, id=self.popupID3)

        # make a menu
        menu = wx.Menu()
        # add some items
        menu.Append(self.popupID1, "New Clip")
        menu.Append(self.popupID2, "Edit Selected")
        menu.Append(self.popupID3, "Delete Selected")

        # Popup the menu.  If an item is selected then its handler
        # will be called before PopupMenu returns.
        self.PopupMenu(menu)

        menu.Destroy()

        pass

    def OnNew(self, evt):
        """
        """
        self.detailPanel.Show(True)
        self.detailPanel.OnNew(evt)
        pass

    def OnEdit(self, evt):
        """
        """
        PDEBUG('NOT IMPLEMENT.')
        self.detailPanel.Show(True)
        self.detailPanel.OnNew(evt)

        pass

    def OnDelete(self, evt):
        PDEBUG('NOT IMPLEMENT.')
        if self._clip is None:
            PDEBUG('oops')

        self.dropClip(self._clip)
        self._clip = None

    def showClipsOfBook(self):
        """Show clips of book.
        """

        PDEBUG('BOOK_ID: %d', self._book_id)

        book_iter = self.model.getBookById(self._book_id)
        if not book_iter.next():
            print('Failed to load book info, book_id: %d' % (self._book_id))
            sys.exit(1)

        book = book_iter.name
        author = book_iter.author

        iter = self.model.getClipsByBookId(self._book_id)
        self.showClips(book, author, iter)
        pass

    def showClips(self, title, author, it):
        """Show contents in clip_li.s
        """
        self.book_title.SetLabel("  %s -- %s" % (title, author))
        self.clip_list.DeleteAllItems()

        idx = 0
        while it.next():
            item = wx.ListItem()
            item.SetData(it.id)
            item.SetId(idx)
            item.SetText(u"    %s" % (it.content))
            self.clip_list.InsertItem(item)
            idx += 1

        self.book_info.SetLabel('      Total clips: %d' % idx)

        pass

    def showClipDetail(self, event):

        self._cur_item = event.GetItem()
        id = self._cur_item.GetData()
        clip = self.model.getClipById(id)
        self.detailPanel.UpdateContent(clip)

        # TODO:  adjust position of detail window.
        # self.detailPanel.Position(wx.Point(0,0), wx.Size(0,0))
        self.detailPanel.Show(True)
        pass

    def updateClip(self, clip, text):
        self.model.updateClip(clip, text)
        self.clip_list.SetItemText(self._cur_item.GetId(), u"    %s" % text)
        pass

    def dropClip(self, clip):
        idx = self._cur_item.GetId()
        PDEBUG('DELETE IDX: %d', idx)
        self.model.dropClip(clip)
        self.clip_list.DeleteItem(idx)
        if idx < self.clip_list.GetItemCount():
            self.clip_list.Select(idx)

        self.Refresh()
        self.clip_list.SetFocus()

        # If there are no clips left for current book, ask if we should remove
        # current book...
        if self.clip_list.GetItemCount() == 0:
            self.refreshContents()
        pass

    def newClip(self, content, typ, date):
        self.model.newClip(self._book, content, typ, date)
        self.showClipsOfBook()

        pass
class ImageGallery(wx.Frame):
    """
    GUI Interface and functionality for Image Gallery.
    """

    def __init__(self, properties=None, parent=None, id=ID_IMAGE_GALLERY, **kwargs):

        if properties is not None:
            global p
            p = properties
            global db
            db = dbconnect.DBConnect()

        wx.Frame.__init__(self, parent, id=id, title='CPA/ImageGallery - %s' % \
                                                     (os.path.basename(p._filename)), size=(800, 600), **kwargs)
        if parent is None and not sys.platform.startswith('win'):
            from wx.adv import TaskBarIcon
            self.tbicon = TaskBarIcon()
            self.tbicon.SetIcon(icons.get_cpa_icon(), 'CPA/ImageGallery')
        else:
            self.SetIcon(icons.get_cpa_icon())
        self.SetName('ImageGallery')

        db.register_gui_parent(self)

        global dm
        dm = DataModel()

        if not p.is_initialized():
            logging.critical('ImageGallery requires a properties file. Exiting.')
            raise Exception('ImageGallery requires a properties file. Exiting.')

        self.pmb = None
        self.worker = None
        self.trainingSet = None
        self.classBins = []
        self.binsCreated = 0
        self.chMap = p.image_channel_colors[:]
        self.toggleChMap = p.image_channel_colors[
                           :]  # used to store previous color mappings when toggling colors on/off with ctrl+1,2,3...
        self.brightness = 1.0
        self.scale = 1.0 
        
        self.contrast = 'Linear'
        self.defaultTSFileName = None
        self.defaultModelFileName = None
        self.lastScoringFilter = None

        self.menuBar = wx.MenuBar()
        self.SetMenuBar(self.menuBar)
        self.CreateMenus()

        self.CreateStatusBar()

        #### Create GUI elements
        # Top level - three split windows
        self.splitter = wx.SplitterWindow(self, style=wx.NO_BORDER | wx.SP_3DSASH)
        self.bins_splitter = wx.SplitterWindow(self.splitter, style=wx.NO_BORDER | wx.SP_3DSASH)

        # fetch & rules
        self.fetch_panel = wx.Panel(self.splitter)

        # sorting bins
        self.gallery_panel = wx.Panel(self.bins_splitter)
        o_label = p.object_name[0] if p.classification_type == 'image' else '' + ' image gallery'
        self.gallery_box = wx.StaticBox(self.gallery_panel, label=o_label)
        self.gallery_sizer = wx.StaticBoxSizer(self.gallery_box, wx.VERTICAL)
        self.galleryBin = sortbin.SortBin(parent=self.gallery_panel,
                                               classifier=self,
                                               label='image gallery',
                                               parentSizer=self.gallery_sizer)
        self.gallery_sizer.Add(self.galleryBin, proportion=1, flag=wx.EXPAND)
        self.gallery_panel.SetSizer(self.gallery_sizer)
        self.objects_bin_panel = wx.Panel(self.bins_splitter)

        # fetch objects interface
        self.startId = wx.TextCtrl(self.fetch_panel, id=-1, value='1', size=(60, -1), style=wx.TE_PROCESS_ENTER)
        self.endId = wx.TextCtrl(self.fetch_panel, id=-1, value='100', size=(60, -1), style=wx.TE_PROCESS_ENTER)
        self.fetchChoice = wx.Choice(self.fetch_panel, id=-1, choices=['range','all','individual'])
        self.fetchChoice.SetSelection(0)
        self.filterChoice = wx.Choice(self.fetch_panel, id=-1,
                                      choices=['experiment'] + p._filters_ordered + p._groups_ordered + [
                                          CREATE_NEW_FILTER])
        self.fetchFromGroupSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.fetchBtn = wx.Button(self.fetch_panel, -1, 'Fetch!')

        #### Create Sizers
        self.fetchSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.classified_bins_sizer = wx.BoxSizer(wx.HORIZONTAL)

        #### Add elements to sizers and splitters
        # fetch panel
        self.fetchSizer.AddStretchSpacer()
        self.fetchSizer.Add(wx.StaticText(self.fetch_panel, -1, 'Fetch '), flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(self.fetchChoice, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchTxt = wx.StaticText(self.fetch_panel, -1, label='of image IDs:')
        self.fetchSizer.Add(self.fetchTxt, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(self.startId, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchTxt2 = wx.StaticText(self.fetch_panel, -1, label='to')
        self.fetchSizer.Add(self.fetchTxt2, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(self.endId, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        #self.fetchSizer.Add(self.obClassChoice, flag=wx.ALIGN_CENTER_VERTICAL)
        #self.fetchSizer.Add(5, 20, 0)
        self.fetchTxt3 = wx.StaticText(self.fetch_panel, -1, label='images')
        self.fetchSizer.Add(self.fetchTxt3, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(wx.StaticText(self.fetch_panel, -1, 'from'), flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(self.filterChoice, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(10, 20, 0)
        self.fetchSizer.Add(self.fetchFromGroupSizer, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.Add(5, 20, 0)
        self.fetchSizer.Add(self.fetchBtn, flag=wx.ALIGN_CENTER_VERTICAL)
        self.fetchSizer.AddStretchSpacer()
        self.fetch_panel.SetSizerAndFit(self.fetchSizer)

        # classified bins panel
        self.objects_bin_panel.SetSizer(self.classified_bins_sizer)

        # splitter windows
        self.splitter.SplitHorizontally(self.fetch_panel, self.bins_splitter,
                                        self.fetch_panel.GetMinSize()[1])
        self.bins_splitter.SplitHorizontally(self.gallery_panel, self.objects_bin_panel)

        self.splitter.SetSashGravity(0.0)
        self.bins_splitter.SetSashGravity(0.5)

        self.splitter.SetMinimumPaneSize(max(50, self.fetch_panel.GetMinHeight()))
        self.bins_splitter.SetMinimumPaneSize(50)
        self.SetMinSize((self.fetch_panel.GetMinWidth(), 4 * 50 + self.fetch_panel.GetMinHeight()))

        # Set initial state
        self.filterChoice.SetSelection(0)

        # JEN - Start Add
        # self.openDimensReduxBtn.Disable()
        # JEN - End Add
        self.fetchSizer.Hide(self.fetchFromGroupSizer)

        #####################
        #### GUI Section ####
        #####################

        # add the default classes
        #for class in range(1, num_classes+1):
        self.AddSortClass('objects of selected image')
        #self.AddSortClass('negative')

        self.Layout()

        self.Center()
        self.MapChannels(p.image_channel_colors[:])
        self.BindMouseOverHelpText()

        #self.Bind(wx.EVT_BUTTON, self.OnInspect, self.inspectBtn)
        # JEN - Start Add
        # self.Bind(wx.EVT_BUTTON, self.OpenDimensRedux, self.openDimensReduxBtn)
        # JEN - End Add

        self.Bind(wx.EVT_BUTTON, self.OnFetch, self.fetchBtn)
        self.startId.Bind(wx.EVT_TEXT, self.ValidateIntegerField)
        self.startId.Bind(wx.EVT_TEXT_ENTER, self.OnFetch)

        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_CHAR, self.OnKey)  # Doesn't work for windows
        tilecollection.EVT_TILE_UPDATED(self, self.OnTileUpdated)
        self.Bind(sortbin.EVT_QUANTITY_CHANGED, self.QuantityChanged)

        self.Bind(wx.EVT_CHOICE, self.OnSelectFetchChoice, self.fetchChoice)
        self.Bind(wx.EVT_CHOICE, self.OnSelectFilter, self.filterChoice)


    # JK - End Add

    def BindMouseOverHelpText(self):
        self.startId.SetToolTip(wx.ToolTip('The number of %s to fetch.' % (p.object_name[1])))
        #self.obClassChoice.SetToolTip(wx.ToolTip('The phenotype of the %s.' % (p.object_name[1])))
        #self.obClassChoice.GetToolTip().SetDelay(3000)
        self.filterChoice.SetToolTip(wx.ToolTip(
            'Filters fetched %s to be from a subset of your images. (See groups and filters in the properties file)' % (
            p.object_name[1])))
        self.filterChoice.GetToolTip().SetDelay(3000)
        self.fetchBtn.SetToolTip(wx.ToolTip('Fetches images of %s to be sorted.' % (p.object_name[1])))
        self.galleryBin.SetToolTip(
            wx.ToolTip('%s gallery of our dataset' % (p.object_name[1].capitalize())))

    def OnKey(self, evt):
        ''' Keyboard shortcuts '''
        keycode = evt.GetKeyCode()
        chIdx = keycode - 49
        if evt.ControlDown() or evt.CmdDown():
            # ctrl+N toggles channel #N on/off
            if len(self.chMap) > chIdx >= 0:
                self.ToggleChannel(chIdx)
            else:
                evt.Skip()
        else:
            evt.Skip()

    def ToggleChannel(self, chIdx):
        if self.chMap[chIdx] == 'None':
            for (idx, color, item, menu) in list(self.chMapById.values()):
                if idx == chIdx and color.lower() == self.toggleChMap[chIdx].lower():
                    item.Check()
            self.chMap[chIdx] = self.toggleChMap[chIdx]
            self.MapChannels(self.chMap)
        else:
            for (idx, color, item, menu) in list(self.chMapById.values()):
                if idx == chIdx and color.lower() == 'none':
                    item.Check()
            self.chMap[chIdx] = 'None'
            self.MapChannels(self.chMap)

    def CreateMenus(self):
        ''' Create file menu and menu items '''
        # View Menu

        viewMenu = wx.Menu()

        self.fileMenu = wx.Menu()
        loadMenuItem = self.fileMenu.Append(-1, item='Load Image Set\tCtrl+O',
                                                   helpString='Loads images specfied in a csv file.')
        # JEN - End Add
        exitMenuItem = self.fileMenu.Append(id=wx.ID_EXIT, item='Exit\tCtrl+Q', helpString='Exit Image Gallery')
        self.GetMenuBar().Append(self.fileMenu, 'File')

        imageControlsMenuItem = viewMenu.Append(-1, item='Image Controls\tCtrl+Shift+I',
                                                helpString='Launches a control panel for adjusting image brightness, size, etc.')
        self.GetMenuBar().Append(viewMenu, 'View')

        # Rules menu
        # rulesMenu = wx.Menu()
        # rulesEditMenuItem = rulesMenu.Append(-1, text=u'Edit…', help='Lets you edit the rules')
        # self.GetMenuBar().Append(rulesMenu, 'Rules')

        # Channel Menus
        self.CreateChannelMenus()

        # Advanced menu
        advancedMenu = wx.Menu()
        fetchObjMenuItem = advancedMenu.Append(-1, item='Fetch all objects from displayed images', helpString='Fetch all objects from displayed images')
        fetchAllObjMenuItem = advancedMenu.Append(-1, item='Fetch all objects', helpString='Fetch all objects')
        saveImgMenuItem = advancedMenu.Append(-1, item='Save image thumbnails as PNG', helpString='Save image thumbnails as PNG')
        saveObjMenuItem = advancedMenu.Append(-1, item='Save object thumbnails as PNG', helpString='Save object thumbnails as PNG')
        self.GetMenuBar().Append(advancedMenu, 'Advanced')

        self.GetMenuBar().Append(cpa.helpmenu.make_help_menu(self, manual_url="https://cellprofiler.org/xiv-image-gallery"), 'Help')

        self.Bind(wx.EVT_MENU, self.OnShowImageControls, imageControlsMenuItem)
        self.Bind(wx.EVT_MENU, self.OnFetchObjThumbnails ,fetchObjMenuItem)
        self.Bind(wx.EVT_MENU, self.OnFetchAllObjThumbnails ,fetchAllObjMenuItem)
        self.Bind(wx.EVT_MENU, self.OnSaveImgThumbnails ,saveImgMenuItem)
        self.Bind(wx.EVT_MENU, self.OnSaveObjThumbnails ,saveObjMenuItem)
        self.Bind(wx.EVT_MENU, self.OnClose, exitMenuItem)
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_MENU, self.OnLoadImageSet, loadMenuItem)




    def CreateChannelMenus(self):
        ''' Create color-selection menus for each channel. '''

        # Clean up existing channel menus
        try:
            menus = set([items[2].Menu for items in list(self.chMapById.values())])
            for menu in menus:
                for i, mbmenu in enumerate(self.MenuBar.Menus):
                    if mbmenu[0] == menu:
                        self.MenuBar.Remove(i)
            for menu in menus:
                menu.Destroy()
            if 'imagesMenu' in self.__dict__:
                self.MenuBar.Remove(self.MenuBar.FindMenu('Images'))
                self.imagesMenu.Destroy()
        except:
            pass

        # Initialize variables
        self.imagesMenu = wx.Menu()
        chIndex = 0
        self.chMapById = {}
        self.imMapById = {}
        channel_names = []
        startIndex = 0
        channelIds = []

        for i, chans in enumerate(p.channels_per_image):
            chans = int(chans)
            # Construct channel names, for RGB images, append a # to the end of
            # each channel.
            name = p.image_names[i]
            if chans == 1:
                channel_names += [name]
            elif chans == 3:  # RGB
                channel_names += ['%s [%s]' % (name, x) for x in 'RGB']
            elif chans == 4:  # RGBA
                channel_names += ['%s [%s]' % (name, x) for x in 'RGBA']
            else:
                channel_names += ['%s [%s]' % (name, x + 1) for x in range(chans)]

        # Zip channel names with channel map
        zippedChNamesChMap = list(zip(channel_names, self.chMap))

        # Loop over all the image names in the properties file
        for i, chans in enumerate(p.image_names):
            channelIds = []
            # Loop over all the channels
            for j in range(0, int(p.channels_per_image[i])):
                (channel, setColor) = zippedChNamesChMap[chIndex]
                channel_menu = wx.Menu()
                for color in ['Red', 'Green', 'Blue', 'Cyan', 'Magenta', 'Yellow', 'Gray', 'None']:
                    id = wx.NewId()
                    # Create a radio item that maps an id and a color.
                    item = channel_menu.AppendRadioItem(id, color)
                    # Add a new chmapbyId object
                    self.chMapById[id] = (chIndex, color, item, channel_menu)
                    # If lowercase color matches what it was originally set to...
                    if color.lower() == setColor.lower():
                        # Check off the item
                        item.Check()
                    # Bind
                    self.Bind(wx.EVT_MENU, self.OnMapChannels, item)
                    # Add appropriate Ids to imMapById
                    if ((int(p.channels_per_image[i]) == 1 and color == 'Gray') or
                            (int(p.channels_per_image[i]) > 1 and j == 0 and color == 'Red') or
                            (int(p.channels_per_image[i]) > 1 and j == 2 and color == 'Blue') or
                            (int(p.channels_per_image[i]) > 1 and j == 1 and color == 'Green')):
                        channelIds = channelIds + [id]
                # Add new menu item
                self.GetMenuBar().Append(channel_menu, channel)
                chIndex += 1
            # New id for the image as a whole
            id = wx.NewId()
            item = self.imagesMenu.AppendRadioItem(id, p.image_names[i])
            # Effectively this code creates a data structure that stores relevant info with ID as a key
            self.imMapById[id] = (int(p.channels_per_image[i]), item, startIndex, channelIds)
            # Binds the event menu to OnFetchImage (below) and item
            self.Bind(wx.EVT_MENU, self.OnFetchImage, item)
            startIndex += int(p.channels_per_image[i])
        # Add the "none" image and check it off.
        id = wx.NewId()
        item = self.imagesMenu.AppendRadioItem(id, 'None')
        self.Bind(wx.EVT_MENU, self.OnFetchImage, item)
        item.Check()  # Add new "Images" menu bar item
        self.GetMenuBar().Append(self.imagesMenu, 'Images')

    #######################################
    # OnFetchImage
    #
    # Allows user to display one image at a time.  If image is single channel,
    # displays the image as gray.  If image is multichannel, displays image as
    # RGB.
    # @param self, evt
    #######################################
    def OnFetchImage(self, evt=None):

        # Set every channel to black and set all the toggle options to 'none'
        for ids in list(self.chMapById.keys()):
            (chIndex, color, item, channel_menu) = self.chMapById[ids]
            if (color.lower() == 'none'):
                item.Check()
        for ids in list(self.imMapById.keys()):
            (cpi, itm, si, channelIds) = self.imMapById[ids]
            if cpi == 3:
                self.chMap[si] = 'none'
                self.chMap[si + 1] = 'none'
                self.chMap[si + 2] = 'none'
                self.toggleChMap[si] = 'none'
                self.toggleChMap[si + 1] = 'none'
                self.toggleChMap[si + 2] = 'none'
            else:
                self.chMap[si] = 'none'
                self.toggleChMap[si] = 'none'

        # Determine what image was selected based on the event.  Set channel to appropriate color(s)
        if evt.GetId() in self.imMapById:

            (chanPerIm, item, startIndex, channelIds) = self.imMapById[evt.GetId()]

            if chanPerIm == 1:
                # Set channel map and toggleChMap values.
                self.chMap[startIndex] = 'gray'
                self.toggleChMap[startIndex] = 'gray'

                # Toggle the option for the independent channel menu
                (chIndex, color, item, channel_menu) = self.chMapById[channelIds[0]]
                item.Check()
            else:
                RGB = ['red', 'green', 'blue'] + ['none'] * chanPerIm
                for i in range(chanPerIm):
                    # Set chMap and toggleChMap values
                    self.chMap[startIndex + i] = RGB[i]
                    self.toggleChMap[startIndex + i] = RGB[i]
                    # Toggle the option in the independent channel menus
                    (chIndex, color, item, channel_menu) = self.chMapById[channelIds[i]]
                    item.Check()

        self.MapChannels(self.chMap)
        #######################################
        # /OnFetchImage
        #######################################

    def OnLoadImageSet(self, evt):
        '''
        Present user with file select dialog, then load selected training set.
        '''
        dlg = wx.FileDialog(self, "Select the file containing your classifier training set.",
                            defaultDir=os.getcwd(),
                            wildcard='CSV files (*.csv)|*.csv',
                            style=wx.FD_OPEN | wx.FD_CHANGE_DIR)
        if dlg.ShowModal() == wx.ID_OK:
            filename = dlg.GetPath()
            name, file_extension = os.path.splitext(filename)
            if '.csv' == file_extension:
                self.LoadImageSet(filename)
            else:
                logging.error("Couldn't load the file! Make sure it is .csv")

    # Loads a CSV file with ImageNumber column and fetches all images with this number
    def LoadImageSet(self, filename):
        '''
        Loads the selected file, parses out object keys, and fetches the tiles for CSV
        '''
        # pause tile loading
        with tilecollection.load_lock():
            self.PostMessage('Loading image set from %s' % filename)
            import pandas as pd
            df = pd.read_csv(filename)
            def cb():
                keys = [tuple([k,-1]) for k in df['ImageNumber']]    
                self.galleryBin.AddObjects(keys, self.chMap, pos='last', display_whole_image=True)
                self.PostMessage('Image set loaded')
            if df.shape[0] > 100:
                dlg = wx.MessageDialog(self,
                                       'The whole collection consists of %s images. Downloading could be slow. Do you still want to continue?' % (
                                       df.shape[0]),
                                       'Load whole image set?', wx.YES_NO | wx.ICON_QUESTION)
                response = dlg.ShowModal()
                # Call fetch filter with all keys
                if response == wx.ID_YES:
                    self.galleryBin.SelectAll()
                    self.galleryBin.RemoveSelectedTiles()
                    # Need to run this after removing all tiles!
                    wx.CallAfter(cb)
            else:
                self.galleryBin.SelectAll()
                self.galleryBin.RemoveSelectedTiles()
                # Need to run this after removing all tiles!
                wx.CallAfter(cb)

    def OnFetch(self, evt):
        start = int(self.startId.Value)
        end = int(self.endId.Value)
        fltr_sel = self.filterChoice.GetStringSelection()
        fetch_sel = self.fetchChoice.GetStringSelection()
        statusMsg = 'Fetched images %d - %d ' % (start, end)

        # Need to flatten it due to the fact that img key can look like this:
        # (image_id,) or this (table_id, image_id)
        def flatten(*args):
            for x in args:
                if hasattr(x, '__iter__'):
                    for y in flatten(*x):
                        yield y
                else:
                    yield x

        # Fetching all images with filter
        if fetch_sel == 'all':
            # Easy just fetch all images
            if fltr_sel == 'experiment':
                self.FetchAll()
                return
            # Fetch all images with self defined filter
            elif fltr_sel in p._filters_ordered:
                imKeys = db.GetFilteredImages(fltr_sel)
                if imKeys == []:
                    self.PostMessage('No images were found in filter "%s"' % (fltr_sel))
                    return

                # Are you sure?
                if len(imKeys) > 100:
                    dlg = wx.MessageDialog(self,
                                       'The whole collection consists of %s images. Downloading could be slow. Do you still want to continue?' % (
                                       len(imKeys)),
                                       'Load whole image set?', wx.YES_NO | wx.ICON_QUESTION)
                    response = dlg.ShowModal()
                    # Call fetch filter with all keys
                    if response == wx.ID_YES:
                        self.galleryBin.SelectAll()
                        self.galleryBin.RemoveSelectedTiles()
                        # Need to run this after removing all tiles!
                        def cb():
                            filteredImKeys = db.GetFilteredImages(fltr_sel)
                            imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]
                            self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                        wx.CallAfter(cb)
                        statusMsg += ' from filter "%s"' % (fltr_sel)

                # data set is small, lets go for it!
                else:
                    self.galleryBin.SelectAll()
                    self.galleryBin.RemoveSelectedTiles()
                    # Need to run this after removing all tiles!
                    def cb():
                        filteredImKeys = db.GetFilteredImages(fltr_sel)
                        imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]

                        self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                    wx.CallAfter(cb)
                    statusMsg += ' from filter "%s"' % (fltr_sel)

            # fetching all images for predefined filter
            elif fltr_sel in p._groups_ordered:

                
                groupName = fltr_sel
                groupKey = self.GetGroupKeyFromGroupSizer(groupName)
                imKeys = dm.GetImagesInGroupWithWildcards(groupName, groupKey)
                colNames = dm.GetGroupColumnNames(groupName)

                if imKeys == []:
                    self.PostMessage('No images were found in group %s: %s' % (groupName,
                                                                               ', '.join(['%s=%s' % (n, v) for n, v in
                                                                                          zip(colNames, groupKey)])))
                    return

                # Are you sure?
                if len(imKeys) > 100:
                    dlg = wx.MessageDialog(self,
                                       'The whole collection consists of %s images. Downloading could be slow. Do you still want to continue?' % (
                                       len(imKeys)),
                                       'Load whole image set?', wx.YES_NO | wx.ICON_QUESTION)
                    response = dlg.ShowModal()
                    # Yes, I am sure!
                    if response == wx.ID_YES:
                        self.galleryBin.SelectAll()
                        self.galleryBin.RemoveSelectedTiles()
                        groupName = fltr_sel
                        groupKey = self.GetGroupKeyFromGroupSizer(groupName)
                        filteredImKeys = dm.GetImagesInGroupWithWildcards(groupName, groupKey)
                        colNames = dm.GetGroupColumnNames(groupName)
                        def cb():
                            imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]
                            self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                        
                        statusMsg += ' from group %s: %s' % (groupName,
                                                             ', '.join(['%s=%s' % (n, v) for n, v in zip(colNames, groupKey)]))
                        wx.CallAfter(cb)

                # dataset is small, lets go for it!
                else:
                    self.galleryBin.SelectAll()
                    self.galleryBin.RemoveSelectedTiles()
                    groupName = fltr_sel
                    groupKey = self.GetGroupKeyFromGroupSizer(groupName)
                    filteredImKeys = dm.GetImagesInGroupWithWildcards(groupName, groupKey)
                    colNames = dm.GetGroupColumnNames(groupName)
                    def cb():
                        imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]
                        self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                    
                    statusMsg += ' from group %s: %s' % (groupName,
                                                         ', '.join(['%s=%s' % (n, v) for n, v in zip(colNames, groupKey)]))
                    wx.CallAfter(cb)

        # Fetching individual images
        elif fetch_sel == 'individual':

            if p.table_id:
                imgKey = [(start,end,-1)]
            else:
                imgKey = [(end,-1)]

            self.galleryBin.AddObjects(imgKey, self.chMap, pos='last', display_whole_image=True)
            return

        # Fetching images with range
        elif fltr_sel == 'experiment':
            self.galleryBin.SelectAll()
            self.galleryBin.RemoveSelectedTiles()
            # Need to run this after removing all tiles!
            def cb():
                imKeys = db.GetAllImageKeys()
                imKeys = [tuple(list(flatten(x,-1))) for x in imKeys]
                self.galleryBin.AddObjects(imKeys[(start - 1):end], self.chMap, pos='last', display_whole_image=True)
            wx.CallAfter(cb)
            statusMsg += ' from whole experiment'
        elif fltr_sel in p._filters_ordered:
            self.galleryBin.SelectAll()
            self.galleryBin.RemoveSelectedTiles()
            # Need to run this after removing all tiles!
            def cb():
                filteredImKeys = db.GetFilteredImages(fltr_sel)
                if filteredImKeys == []:
                    self.PostMessage('No images were found in filter "%s"' % (fltr_sel))
                    return
                imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]
                self.galleryBin.AddObjects(imKeys[(start - 1):end], self.chMap, pos='last', display_whole_image=True)
            wx.CallAfter(cb)
            statusMsg += ' from filter "%s"' % (fltr_sel)
        elif fltr_sel in p._groups_ordered:
            # if the filter name is a group then it's actually a group
            self.galleryBin.SelectAll()
            self.galleryBin.RemoveSelectedTiles()
            groupName = fltr_sel
            groupKey = self.GetGroupKeyFromGroupSizer(groupName)
            filteredImKeys = dm.GetImagesInGroupWithWildcards(groupName, groupKey)
            colNames = dm.GetGroupColumnNames(groupName)
            def cb():
                if filteredImKeys == []:
                    self.PostMessage('No images were found in group %s: %s' % (groupName,
                                                                               ', '.join(['%s=%s' % (n, v) for n, v in
                                                                                          zip(colNames, groupKey)])))
                    return
                
                imKeys = [tuple(list(flatten(x,-1))) for x in filteredImKeys]
                self.galleryBin.AddObjects(imKeys[(start - 1):end], self.chMap, pos='last', display_whole_image=True)
            
            statusMsg += ' from group %s: %s' % (groupName,
                                                 ', '.join(['%s=%s' % (n, v) for n, v in zip(colNames, groupKey)]))

            wx.CallAfter(cb)
           

        self.PostMessage(statusMsg)


    def FetchAll(self):

        def flatten(*args):
            for x in args:
                if hasattr(x, '__iter__'):
                    for y in flatten(*x):
                        yield y
                else:
                    yield x

        imKeys = db.GetAllImageKeys()
        # A lot of images
        if len(imKeys) > 200:
            # double check
            dlg = wx.MessageDialog(self,
                                   'The whole collection consists of %s images. Downloading could be slow. Do you still want to continue?' % (
                                   len(imKeys)),
                                   'Load whole image set?', wx.YES_NO | wx.ICON_QUESTION)
            response = dlg.ShowModal()
            if response == wx.ID_YES:
                self.galleryBin.SelectAll()
                self.galleryBin.RemoveSelectedTiles()
                # Need to run this after removing all tiles!
                def cb():
                    imKeys = db.GetAllImageKeys()
                    imKeys = [tuple(list(flatten(x,-1))) for x in imKeys]
                    self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                    self.PostMessage("Loaded all images")
                wx.CallAfter(cb)
        else: 
            self.galleryBin.SelectAll()
            self.galleryBin.RemoveSelectedTiles()
            # Need to run this after removing all tiles!
            def cb():
                imKeys = db.GetAllImageKeys()
                imKeys = [tuple(list(flatten(x,-1))) for x in imKeys]
                self.galleryBin.AddObjects(imKeys, self.chMap, pos='last', display_whole_image=True)
                self.PostMessage("Loaded all images")
            wx.CallAfter(cb)


    def AddSortClass(self, label):
        ''' Create a new SortBin in a new StaticBoxSizer with the given label.
        This sizer is then added to the classified_bins_sizer. '''
        bin = sortbin.SortBin(parent=self.objects_bin_panel, label=label,
                              classifier=self)

        box = wx.StaticBox(self.objects_bin_panel, label=label)
        # NOTE: bin must be created after sizer or drop events will occur on the sizer
        sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
        bin.parentSizer = sizer

        sizer.Add(bin, proportion=1, flag=wx.EXPAND)
        self.classified_bins_sizer.Add(sizer, proportion=1, flag=wx.EXPAND)
        self.classBins.append(bin)
        self.objects_bin_panel.Layout()
        self.binsCreated += 1
        self.QuantityChanged()
        # IMPORTANT: required for drag and drop to work on Linux
        # see: http://trac.wxwidgets.org/ticket/2763
        box.Lower()
 
    def RemoveSortClass(self, label, clearModel=True):
        pass

    def all_sort_bins(self):
        return [self.galleryBin] + self.classBins

    def QuantityChanged(self, evt=None):
        pass

    def OnTileUpdated(self, evt):
        '''
        When the tile loader returns the tile image update the tile.
        '''
        self.galleryBin.UpdateTile(evt.data)
        for bin in self.classBins:
            bin.UpdateTile(evt.data)

    def OnMapChannels(self, evt):
        ''' Responds to selection from the color mapping menus. '''
        (chIdx, color, item, menu) = self.chMapById[evt.GetId()]
        item.Check()
        self.chMap[chIdx] = color.lower()
        if color.lower() != 'none':
            self.toggleChMap[chIdx] = color.lower()
        self.MapChannels(self.chMap)

    def MapChannels(self, chMap):
        ''' Tell all bins to apply a new channel-color mapping to their tiles. '''
        # TODO: Need to update color menu selections
        self.chMap = chMap
        for bin in self.all_sort_bins():
            bin.MapChannels(chMap)

    def ValidateImageKey(self, evt):
        # Unused? Mar 2021
        ''' Checks that the image field specifies an existing image. '''
        txtCtrl = evt.GetEventObject()
        try:
            if p.table_id:
                imKey = (int(self.tableTxt.Value), int(self.imageTxt.Value))
            else:
                imKey = (int(self.imageTxt.Value),)
            if dm.GetObjectCountFromImage(imKey) > 0:
                txtCtrl.SetForegroundColour('#000001')
                self.SetStatusText('Image contains %s %s.' % (dm.GetObjectCountFromImage(imKey), p.object_name[1]))
            else:
                txtCtrl.SetForegroundColour('#888888')  # Set field to GRAY if image contains no objects
                self.SetStatusText('Image contains zero %s.' % (p.object_name[1]))
        except(Exception):
            txtCtrl.SetForegroundColour('#FF0000')  # Set field to red if image doesn't exist
            self.SetStatusText('No such image.')

    def OnSelectFetchChoice(self, evt):
        ''' Handler for fetch filter selection. '''
        fetchChoice = self.fetchChoice.GetStringSelection()
        # Select from a specific image
        if fetchChoice == 'range':
            self.fetchTxt.SetLabel('of image IDs:')
            self.fetchTxt2.SetLabel('to')
            self.fetchTxt2.Show()
            self.fetchTxt3.SetLabel('images')
            self.fetchTxt3.Show()
            self.startId.Show()
            self.endId.Show()
            self.filterChoice.Enable()
            self.fetch_panel.Layout()

        elif fetchChoice == 'all':
            self.fetchTxt.SetLabel('')
            self.fetchTxt2.Hide()
            self.fetchTxt3.SetLabel('images')
            self.fetchTxt3.Show()
            self.startId.Hide()
            self.endId.Hide()
            self.filterChoice.Enable()
            self.fetch_panel.Layout()

        elif fetchChoice == 'individual':

            self.fetchTxt.SetLabel('image ID:')
            if p.table_id:
                self.startId.Show()
            else:
                self.startId.Hide()
            self.endId.Show()
            self.fetchTxt2.Hide()
            self.fetchTxt3.Hide()
            self.filterChoice.Disable()
            self.fetch_panel.Layout()


    def OnSelectFilter(self, evt):
        ''' Handler for fetch filter selection. '''
        filter = self.filterChoice.GetStringSelection()
        # Select from a specific image
        if filter == 'experiment' or filter in p._filters_ordered:
            self.fetchSizer.Hide(self.fetchFromGroupSizer, True)
        elif filter == 'image' or filter in p._groups_ordered:
            self.SetupFetchFromGroupSizer(filter)
            self.fetchSizer.Show(self.fetchFromGroupSizer, True)
        elif filter == CREATE_NEW_FILTER:
            self.fetchSizer.Hide(self.fetchFromGroupSizer, True)
            from .columnfilter import ColumnFilterDialog
            cff = ColumnFilterDialog(self, tables=[p.image_table], size=(600, 300))
            if cff.ShowModal() == wx.OK:
                fltr = cff.get_filter()
                fname = cff.get_filter_name()
                p._filters[fname] = fltr
                items = self.filterChoice.GetItems()
                self.filterChoice.SetItems(items[:-1] + [fname] + items[-1:])
                self.filterChoice.Select(len(items) - 1)
            else:
                self.filterChoice.Select(0)
            cff.Destroy()
        self.fetch_panel.Layout()
        self.fetch_panel.Refresh() 

    def SetupFetchFromGroupSizer(self, group):
        '''
        This sizer displays input fields for inputting each element of a
        particular group's key. A group with 2 columns: Gene, and Well,
        would be represented by two combo boxes.
        '''
        if group == 'image':
            fieldNames = ['table', 'image'] if p.table_id else ['image']
            fieldTypes = [int, int]
            validKeys = dm.GetAllImageKeys()
        else:
            fieldNames = dm.GetGroupColumnNames(group)
            fieldTypes = dm.GetGroupColumnTypes(group)
            validKeys = dm.GetGroupKeysInGroup(group)

        self.groupInputs = []
        self.groupFieldValidators = []
        self.fetchFromGroupSizer.Clear(True)
        for i, field in enumerate(fieldNames):
            label = wx.StaticText(self.fetch_panel, wx.NewId(), field + ':')
            # Values to be sorted BEFORE being converted to str
            validVals = list(set([col[i] for col in validKeys]))
            validVals.sort()
            validVals = [str(col) for col in validVals]
            if group == 'image' or fieldTypes[i] == int or fieldTypes[i] == int:
                fieldInp = wx.TextCtrl(self.fetch_panel, -1, value=validVals[0], size=(80, -1))
            else:
                fieldInp = wx.Choice(self.fetch_panel, -1, size=(80, -1),
                                       choices=['__ANY__'] + validVals)
            validVals = ['__ANY__'] + validVals
            fieldInp.SetSelection(0)
            # Create and bind to a text Validator
            def ValidateGroupField(evt, validVals=validVals):
                ctrl = evt.GetEventObject()
                if ctrl.GetValue() in validVals:
                    ctrl.SetForegroundColour('#000001')
                else:
                    ctrl.SetForegroundColour('#FF0000')

            self.groupFieldValidators += [ValidateGroupField]
            fieldInp.Bind(wx.EVT_TEXT, self.groupFieldValidators[-1])
            self.groupInputs += [fieldInp]
            self.fetchFromGroupSizer.Add(label)
            self.fetchFromGroupSizer.Add(fieldInp)
            self.fetchFromGroupSizer.Add(10, 20, 0)

    def ValidateIntegerField(self, evt):
        ''' Validates an integer-only TextCtrl '''
        txtCtrl = evt.GetEventObject()
        # NOTE: textCtrl.SetBackgroundColor doesn't work on Mac
        #   and foreground color only works when not setting to black.
        try:
            int(txtCtrl.GetValue())
            txtCtrl.SetForegroundColour('#000001')
        except(Exception):
            txtCtrl.SetForegroundColour('#FF0000')

    def GetGroupKeyFromGroupSizer(self, group=None):
        ''' Returns the text in the group text inputs as a group key. '''
        if group is not None:
            fieldTypes = dm.GetGroupColumnTypes(group)
        else:
            fieldTypes = [int for input in self.groupInputs]
        groupKey = []
        for input, ftype in zip(self.groupInputs, fieldTypes):
            # GetValue returns unicode from ComboBox, but we need a string
            val = str(input.GetStringSelection())
            # if the value is blank, don't bother typing it, it is a wildcard
            if val != '__ANY__':
                val = ftype(val)
            groupKey += [val]
        return tuple(groupKey)

    def OnShowImageControls(self, evt):
        ''' Shows the image adjustment control panel in a new frame. '''
        self.imageControlFrame = wx.Frame(self, size=(470, 155))
        ImageControlPanel(self.imageControlFrame, self, brightness=self.brightness, scale=self.scale,
                          contrast=self.contrast)
        self.imageControlFrame.Show(True)


    # Saving image thumbnails
    def OnSaveImgThumbnails(self, evt):

        saveDialog = wx.DirDialog(self, "Choose input directory", 
                                   style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        if saveDialog.ShowModal() == wx.ID_OK:
            directory = saveDialog.GetPath()

            if not os.path.exists(directory + '/image_gallery'):
                os.makedirs(directory + '/image_gallery')

            for tile in self.galleryBin.tiles:
                imagetools.SaveBitmap(tile.bitmap, directory + '/image_gallery/' + str(tile.obKey) + '.png')

    # Fetch all object thumbnails from displayed images
    def OnFetchObjThumbnails(self, evt):

        self.classBins[0].SelectAll()
        self.classBins[0].RemoveSelectedTiles()
        # Need to run this after removing all tiles!
        def cb():
            for tile in self.galleryBin.tiles:
                pseudo_obKeys = tile.obKey
                imKey = pseudo_obKeys[:-1] # Get image key
                obKeys = db.GetObjectsFromImage(imKey)
                self.classBins[0].AddObjects(obKeys, self.chMap, pos='last', display_whole_image=False)
            
        wx.CallAfter(cb)

    # Fetch all object thumbnails from displayed images
    def OnFetchAllObjThumbnails(self, evt):

        self.classBins[0].SelectAll()
        self.classBins[0].RemoveSelectedTiles()
        # Need to run this after removing all tiles!
        def flatten(*args):
            for x in args:
                if hasattr(x, '__iter__'):
                    for y in flatten(*x):
                        yield y
                else:
                    yield x


        def cb():
            imKeys = db.GetAllImageKeys()
            imKeys = [tuple(list(flatten(x,-1))) for x in imKeys]

            for imKey in imKeys:
                pseudo_obKeys = imKey
                imKey = pseudo_obKeys[:-1] # Get image key
                obKeys = db.GetObjectsFromImage(imKey)
                self.classBins[0].AddObjects(obKeys, self.chMap, pos='last', display_whole_image=False)
            
        wx.CallAfter(cb)



    # Saving image thumbnails
    def OnSaveObjThumbnails(self, evt):

        saveDialog = wx.DirDialog(self, "Choose input directory", 
                                   style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        if saveDialog.ShowModal() == wx.ID_OK:
            directory = saveDialog.GetPath()

            if not os.path.exists(directory + '/object_gallery'):
                os.makedirs(directory + '/object_gallery')

            for tile in self.classBins[0].tiles:
                imagetools.SaveBitmap(tile.bitmap, directory + '/object_gallery/' + str(tile.obKey) + '.png')


    def SetBrightness(self, brightness):
        ''' Updates the global image brightness across all tiles. '''
        self.brightness = brightness
        [t.SetBrightness(brightness) for bin in self.all_sort_bins() for t in bin.tiles]

    def SetScale(self, scale):
        ''' Updates the global image scaling across all tiles. '''
        self.scale = scale
        [t.SetScale(scale) for bin in self.all_sort_bins() for t in bin.tiles]
        [bin.UpdateSizer() for bin in self.all_sort_bins()]

    def SetContrastMode(self, mode):
        self.contrast = mode
        [t.SetContrastMode(mode) for bin in self.all_sort_bins() for t in bin.tiles]

    def PostMessage(self, message):
        ''' Updates the status bar text and logs to info. '''
        self.SetStatusText(message)
        logging.info(message)

    def OnClose(self, evt):
        ''' Prompt to save training set before closing. '''
        self.Destroy()

    def Destroy(self):
        ''' Kill off all threads before combusting. '''
        super(ImageGallery, self).Destroy()
        import threading
        t = tilecollection.TileCollection()
        if self in t.loader.notify_window:
            t.loader.notify_window.remove(self)
        # If no other windows are attached to the loader we shut it down and delete the tilecollection.
        if len(t.loader.notify_window) == 0:
            for thread in threading.enumerate():
                if thread != threading.currentThread() and thread.getName().lower().startswith('tileloader'):
                    logging.debug('Aborting thread %s' % thread.getName())
                    try:
                        thread.abort()
                    except:
                        pass
            tilecollection.TileCollection.forget()
Exemple #5
0
class GooeyApplication(wx.Frame):
    """
    Main window for Gooey.
    """
    def __init__(self, buildSpec, *args, **kwargs):
        super(GooeyApplication, self).__init__(None, *args, **kwargs)
        self._state = {}
        self.buildSpec = buildSpec

        self.applyConfiguration()
        self.menu = MenuBar(buildSpec)
        self.SetMenuBar(self.menu)
        self.header = FrameHeader(self, buildSpec)
        self.configs = self.buildConfigPanels(self)
        self.navbar = self.buildNavigation()
        self.footer = Footer(self, buildSpec)
        self.console = Console(self, buildSpec)

        self.props = {
            'background_color': self.buildSpec['header_bg_color'],
            'title': self.buildSpec['program_name'],
            'subtitle': self.buildSpec['program_description'],
            'height': self.buildSpec['header_height'],
            'image_uri': self.buildSpec['images']['configIcon'],
            'image_size': (six.MAXSIZE, self.buildSpec['header_height'] - 10)
        }

        state = form_page(initial_state(self.buildSpec))

        self.fprops = {
            'buttons': state['buttons'],
            'progress': state['progress'],
            'timing': state['timing'],
            'bg_color': self.buildSpec['footer_bg_color']
        }

        # self.hhh = render(create_element(RHeader, self.props), self)
        # self.fff = render(create_element(RFooter, self.fprops), self)
        # patch(self.hhh, create_element(RHeader, {**self.props, 'image_uri': self.buildSpec['images']['runningIcon']}))
        self.layoutComponent()
        self.timer = Timing(self)

        self.clientRunner = ProcessController(
            self.buildSpec.get('progress_regex'),
            self.buildSpec.get('progress_expr'),
            self.buildSpec.get('hide_progress_msg'),
            self.buildSpec.get('encoding'),
            self.buildSpec.get('requires_shell'),
            self.buildSpec.get('shutdown_signal', signal.SIGTERM))

        pub.subscribe(events.WINDOW_START, self.onStart)
        pub.subscribe(events.WINDOW_RESTART, self.onStart)
        pub.subscribe(events.WINDOW_STOP, self.onStopExecution)
        pub.subscribe(events.WINDOW_CLOSE, self.onClose)
        pub.subscribe(events.WINDOW_CANCEL, self.onCancel)
        pub.subscribe(events.WINDOW_EDIT, self.onEdit)
        pub.subscribe(events.CONSOLE_UPDATE, self.console.logOutput)
        pub.subscribe(events.EXECUTION_COMPLETE, self.onComplete)
        pub.subscribe(events.PROGRESS_UPDATE, self.footer.updateProgressBar)
        pub.subscribe(events.TIME_UPDATE, self.footer.updateTimeRemaining)
        # Top level wx close event
        # self.Bind(wx.EVT_CLOSE, self.onClose)

        # TODO: handle child focus for per-field level validation.
        # self.Bind(wx.EVT_CHILD_FOCUS, self.handleFocus)

        if self.buildSpec.get('auto_start', False):
            self.onStart()

    def applyConfiguration(self):
        self.SetTitle(self.buildSpec['program_name'])
        self.SetBackgroundColour(self.buildSpec.get('body_bg_color'))

    def onStart(self, *args, **kwarg):
        """
        Verify user input and kick off the client's program if valid
        """
        # navigates away from the button because a
        # disabled focused button still looks enabled.
        self.footer.cancel_button.Disable()
        self.footer.start_button.Disable()
        self.footer.start_button.Navigate()
        if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []):
            # TODO: make this wx thread safe so that it can
            # actually run asynchronously
            Thread(target=self.onStartAsync).run()
        else:
            Thread(target=self.onStartAsync).run()

    def onStartAsync(self, *args, **kwargs):
        with transactUI(self):
            try:
                errors = self.validateForm().getOrThrow()
                if errors:  # TODO
                    config = self.navbar.getActiveConfig()
                    config.setErrors(errors)
                    self.Layout()
                    # TODO: account for tabbed layouts
                    # TODO: scroll the first error into view
                    # TODO: rather than just snapping to the top
                    self.configs[0].Scroll(0, 0)
                else:
                    if self.buildSpec['clear_before_run']:
                        self.console.clear()
                    self.clientRunner.run(self.buildCliString())
                    self.showConsole()
            except CalledProcessError as e:
                self.showError()
                self.console.appendText(str(e))
                self.console.appendText(
                    '\n\nThis failure happens when Gooey tries to invoke your '
                    'code for the VALIDATE_FORM event and receives an expected '
                    'error code in response.')
                wx.CallAfter(modals.showFailure)
            except JSONDecodeError as e:
                self.showError()
                self.console.appendText(str(e))
                self.console.appendText(
                    '\n\nGooey was unable to parse the response to the VALIDATE_FORM event. '
                    'This can happen if you have additional logs to stdout beyond what Gooey '
                    'expects.')
                wx.CallAfter(modals.showFailure)
            # for some reason, we have to delay the re-enabling of
            # the buttons by a few ms otherwise they pickup pending
            # events created while they were disabled. Trial and error
            # let to this solution.
            wx.CallLater(20, self.footer.start_button.Enable)
            wx.CallLater(20, self.footer.cancel_button.Enable)

    def onEdit(self):
        """Return the user to the settings screen for further editing"""
        with transactUI(self):
            for config in self.configs:
                config.resetErrors()
            self.showSettings()

    def onComplete(self, *args, **kwargs):
        """
        Display the appropriate screen based on the success/fail of the
        host program
        """
        with transactUI(self):
            if self.clientRunner.was_success():
                if self.buildSpec.get('return_to_config', False):
                    self.showSettings()
                else:
                    self.showSuccess()
                    if self.buildSpec.get('show_success_modal', True):
                        wx.CallAfter(modals.showSuccess)
            else:
                if self.clientRunner.wasForcefullyStopped:
                    self.showForceStopped()
                else:
                    self.showError()
                    if self.buildSpec.get('show_failure_modal'):
                        wx.CallAfter(modals.showFailure)

    def onCancel(self):
        """Close the program after confirming

        We treat the behavior of the "cancel" button slightly
        differently than the general window close X button only
        because this is 'part of' the form.
        """
        if modals.confirmExit():
            self.onClose()

    def onStopExecution(self):
        """Displays a scary message and then force-quits the executing
        client code if the user accepts"""
        if self.shouldStopExecution():
            self.clientRunner.stop()

    def onClose(self, *args, **kwargs):
        """Stop any actively running client program, cleanup the top
        level WxFrame and shutdown the current process"""
        # issue #592 - we need to run the same onStopExecution machinery
        # when the exit button is clicked to ensure everything is cleaned
        # up correctly.
        if self.clientRunner.running():
            if self.shouldStopExecution():
                self.clientRunner.stop()
                self.destroyGooey()
        else:
            self.destroyGooey()

    def buildCliString(self) -> str:
        """
        Collect all of the required information from the config screen and
        build a CLI string which can be used to invoke the client program
        """
        cmd = self.getCommandDetails()
        return cli.cliCmd(
            cmd.target,
            cmd.subcommand,
            cmd.positionals,
            cmd.optionals,
            suppress_gooey_flag=self.buildSpec['suppress_gooey_flag'])

    def validateForm(self) -> Try[Mapping[str, str]]:
        config = self.navbar.getActiveConfig()
        localErrors: Mapping[str, str] = config.getErrors()
        dynamicResult: Try[Mapping[str, str]] = self.fetchDynamicValidations()

        combineErrors = lambda m: merge(localErrors, m)
        return dynamicResult.map(combineErrors)

    def fetchDynamicValidations(self) -> Try[Mapping[str, str]]:
        # only run the dynamic validation if the user has
        # specifically subscribed to that event
        if Events.VALIDATE_FORM in self.buildSpec.get('use_events', []):
            cmd = self.getCommandDetails()
            return seeder.communicate(
                cli.formValidationCmd(cmd.target, cmd.subcommand,
                                      cmd.positionals, cmd.optionals),
                self.buildSpec['encoding'])
        else:
            # shim response if nothing to do.
            return Success({})

    def getCommandDetails(self) -> CommandDetails:
        """
        Temporary helper for getting the state of the current Config.

        To be deprecated upon (the desperately needed) refactor.
        """
        config = self.navbar.getActiveConfig()
        group = self.buildSpec['widgets'][self.navbar.getSelectedGroup()]
        return CommandDetails(
            self.buildSpec['target'],
            group['command'],
            config.getPositionalValues(),
            config.getOptionalValues(),
        )

    def shouldStopExecution(self):
        return not self.buildSpec[
            'show_stop_warning'] or modals.confirmForceStop()

    def destroyGooey(self):
        self.Destroy()
        sys.exit()

    def block(self, **kwargs):
        pass

    def layoutComponent(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        # sizer.Add(self.hhh, 0, wx.EXPAND)
        sizer.Add(self.header, 0, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)

        sizer.Add(self.navbar, 1, wx.EXPAND)
        sizer.Add(self.console, 1, wx.EXPAND)
        sizer.Add(wx_util.horizontal_rule(self), 0, wx.EXPAND)
        # sizer.Add(self.fff, 0, wx.EXPAND)
        sizer.Add(self.footer, 0, wx.EXPAND)
        self.SetMinSize((400, 300))
        self.SetSize(self.buildSpec['default_size'])
        self.SetSizer(sizer)
        self.console.Hide()
        self.Layout()
        if self.buildSpec.get('fullscreen', True):
            self.ShowFullScreen(True)
        # Program Icon (Windows)
        icon = wx.Icon(self.buildSpec['images']['programIcon'],
                       wx.BITMAP_TYPE_PNG)
        self.SetIcon(icon)
        if sys.platform != 'win32':
            # OSX needs to have its taskbar icon explicitly set
            # bizarrely, wx requires the TaskBarIcon to be attached to the Frame
            # as instance data (self.). Otherwise, it will not render correctly.
            self.taskbarIcon = TaskBarIcon(iconType=wx.adv.TBI_DOCK)
            self.taskbarIcon.SetIcon(icon)

    def buildNavigation(self):
        """
        Chooses the appropriate layout navigation component based on user prefs
        """
        if self.buildSpec['navigation'] == constants.TABBED:
            navigation = Tabbar(self, self.buildSpec, self.configs)
        else:
            navigation = Sidebar(self, self.buildSpec, self.configs)
            if self.buildSpec['navigation'] == constants.HIDDEN:
                navigation.Hide()
        return navigation

    def buildConfigPanels(self, parent):
        page_class = TabbedConfigPage if self.buildSpec[
            'tabbed_groups'] else ConfigPage

        return [
            page_class(parent, widgets, self.buildSpec)
            for widgets in self.buildSpec['widgets'].values()
        ]

    def showSettings(self):
        self.navbar.Show(True)
        self.console.Show(False)
        self.header.setImage('settings_img')
        self.header.setTitle(_("settings_title"))
        self.header.setSubtitle(self.buildSpec['program_description'])
        self.footer.showButtons('cancel_button', 'start_button')
        self.footer.progress_bar.Show(False)
        self.footer.time_remaining_text.Show(False)

    def showConsole(self):
        self.navbar.Show(False)
        self.console.Show(True)
        self.header.setImage('running_img')
        self.header.setTitle(_("running_title"))
        self.header.setSubtitle(_('running_msg'))
        self.footer.showButtons('stop_button')
        if not self.buildSpec.get('disable_progress_bar_animation', False):
            self.footer.progress_bar.Show(True)
        self.footer.time_remaining_text.Show(False)
        if self.buildSpec.get('timing_options')['show_time_remaining']:
            self.timer.start()
            self.footer.time_remaining_text.Show(True)
        if not self.buildSpec['progress_regex']:
            self.footer.progress_bar.Pulse()

    def showComplete(self):
        self.navbar.Show(False)
        self.console.Show(True)
        buttons = (['edit_button', 'restart_button', 'close_button']
                   if self.buildSpec.get('show_restart_button', True) else
                   ['edit_button', 'close_button'])
        self.footer.showButtons(*buttons)
        self.footer.progress_bar.Show(False)
        if self.buildSpec.get('timing_options')['show_time_remaining']:
            self.timer.stop()
        self.footer.time_remaining_text.Show(True)
        if self.buildSpec.get(
                'timing_options')['hide_time_remaining_on_complete']:
            self.footer.time_remaining_text.Show(False)

    def showSuccess(self):
        self.showComplete()
        self.header.setImage('check_mark')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_msg'))
        self.Layout()

    def showError(self):
        self.showComplete()
        self.header.setImage('error_symbol')
        self.header.setTitle(_('finished_title'))
        self.header.setSubtitle(_('finished_error'))

    def showForceStopped(self):
        self.showComplete()
        if self.buildSpec.get('force_stop_is_error', True):
            self.showError()
        else:
            self.showSuccess()
        self.header.setSubtitle(_('finished_forced_quit'))