source: sasview/src/sas/sasgui/guiframe/CategoryManager.py @ 3ec78a1

Last change on this file since 3ec78a1 was 5251ec6, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

improved support for py37 in sasgui

  • Property mode set to 100644
File size: 20.2 KB
Line 
1#!/usr/bin/python
2
3"""
4This software was developed by Institut Laue-Langevin as part of
5Distributed Data Analysis of Neutron Scattering Experiments (DANSE).
6
7Copyright 2012 Institut Laue-Langevin
8
9"""
10
11
12import wx
13import sys
14import os
15import logging
16from wx.lib.mixins.listctrl import CheckListCtrlMixin, ListCtrlAutoWidthMixin
17from collections import defaultdict
18import json
19from sas.sasgui.guiframe.events import ChangeCategoryEvent
20from sas.sasgui.guiframe.CategoryInstaller import CategoryInstaller
21IS_MAC = (sys.platform == 'darwin')
22
23logger = logging.getLogger(__name__)
24
25""" Notes
26The category manager mechanism works from 3 data structures used:
27- self.master_category_dict: keys are the names of categories,
28the values are lists of tuples,
29the first being the model names (the models belonging to that
30category), the second a boolean
31of whether or not the model is enabled
32- self.by_model_dict: keys are model names, values are a list
33of categories belonging to that model
34- self.model_enabled_dict: keys are model names, values are
35bools of whether the model is enabled
36use self._regenerate_model_dict() to create the latter two
37structures from the former
38use self._regenerate_master_dict() to create the first
39structure from the latter two
40
41The need for so many data structures comes from the fact
42sometimes we need fast access
43to all the models in a category (eg user selection from the gui)
44and sometimes we need access to all the categories
45corresponding to a model (eg user modification of model categories)
46
47"""
48
49
50
51class CheckListCtrl(wx.ListCtrl, CheckListCtrlMixin, 
52                    ListCtrlAutoWidthMixin):
53    """
54    Taken from
55    http://zetcode.com/wxpython/advanced/
56    """
57
58    def __init__(self, parent, callback_func):
59        """
60        Initialization
61        :param parent: Parent window
62        :param callback_func: A function to be called when
63        an element is clicked
64        """
65        wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT \
66                                 | wx.SUNKEN_BORDER)
67        CheckListCtrlMixin.__init__(self)
68        ListCtrlAutoWidthMixin.__init__(self)
69
70        self.callback_func = callback_func
71       
72    def OnCheckItem(self, index, flag):
73        """
74        When the user checks the item we need to save that state
75        """
76        self.callback_func(index, flag)
77   
78
79class CategoryManager(wx.Frame):
80    """
81    A class for managing categories
82    """
83    def __init__(self, parent, win_id, title):
84        """
85        Category Manager Dialog class.  This is the class that is used to
86        bring up a dialog box allowing the user to create new model categories
87        and to add and remove models from a given category allowing complete
88        user customization of categories for models.  This and Category
89        Installer provide the mecahnisms for creating the category dictionary
90        which is saved as a json file so that categories remain persistent
91        from session to session
92        :param win_id: A new wx ID
93        :param title: Title for the window
94        """
95       
96        # make sure the category file is where it should be
97        self.performance_blocking = False
98
99        # get the current status of model categorization (from the dictionary)
100        self.master_category_dict = defaultdict(list)
101        self.by_model_dict = defaultdict(list)
102        self.model_enabled_dict = defaultdict(bool)
103
104        #----------Initialize panels, frames, and sizers ------------
105        # the whole panel is panel of hbox (a horizontal sizer and contains
106        # the left_pane (vbox2 sizer) which houses all the buttons and
107        # the right_pane (vbox sizer) which houses the current model/category
108        #list)
109        #     Comments added June 14, 2015 -PDB
110        wx.Frame.__init__(self, parent, win_id, title, size=(660, 400))
111
112        panel = wx.Panel(self, -1)
113        self.parent = parent
114
115        self._read_category_info()
116
117
118        vbox = wx.BoxSizer(wx.VERTICAL)
119        hbox = wx.BoxSizer(wx.HORIZONTAL)
120
121        left_panel = wx.Panel(panel, -1)
122        right_panel = wx.Panel(panel, -1)
123
124        self.cat_list = CheckListCtrl(right_panel, self._on_check)
125        self.cat_list.InsertColumn(0, 'Model', width = 280)
126        self.cat_list.InsertColumn(1, 'Category', width = 240)
127
128        self._fill_lists() 
129        self._regenerate_model_dict()
130        self._set_enabled()     
131
132        #----------button and button layout -----------------------
133        vbox2 = wx.BoxSizer(wx.VERTICAL)
134
135        #Create buttons
136        sel = wx.Button(left_panel, -1, 'Enable All', size=(100, -1))
137        des = wx.Button(left_panel, -1, 'Disable All', size=(100, -1))
138        modify_button = wx.Button(left_panel, -1, 'Modify', 
139                                  size=(100, -1))
140        ok_button = wx.Button(left_panel, -1, 'OK', size=(100, -1))
141        help_button = wx.Button(left_panel, -1, 'HELP', size=(100, -1))
142        cancel_button = wx.Button(left_panel, -1, 'Cancel', 
143                                  size=(100, -1))       
144
145       
146
147        #bind buttons to action method
148        self.Bind(wx.EVT_BUTTON, self._on_selectall, 
149                  id=sel.GetId())
150        self.Bind(wx.EVT_BUTTON, self._on_deselectall, 
151                  id=des.GetId())
152        self.Bind(wx.EVT_BUTTON, self._on_apply, 
153                  id = modify_button.GetId())
154        self.Bind(wx.EVT_BUTTON, self._on_ok, 
155                  id = ok_button.GetId())
156        self.Bind(wx.EVT_BUTTON, self._on_help, 
157                  id = help_button.GetId())
158        self.Bind(wx.EVT_BUTTON, self._on_cancel, 
159                  id = cancel_button.GetId())
160
161        #add buttons to sizer (vbox2) and convert to panel so displays well
162        #on all platforms
163        vbox2.Add(modify_button, 0, wx.TOP, 10)
164        vbox2.Add((-1, 20))
165        vbox2.Add(sel)
166        vbox2.Add(des)
167        vbox2.Add((-1, 20))
168        vbox2.Add(ok_button)
169        vbox2.Add(help_button)
170        vbox2.Add(cancel_button)
171
172        left_panel.SetSizer(vbox2)
173
174        #--------------------- layout of current cat/model list --------
175        vbox.Add(self.cat_list, 1, wx.EXPAND | wx.TOP, 3)
176        vbox.Add((-1, 10))
177
178
179        right_panel.SetSizer(vbox)
180
181        #-------------- put it all together -----------------
182        hbox.Add(left_panel, 0, wx.EXPAND | wx.RIGHT, 5)
183        hbox.Add(right_panel, 1, wx.EXPAND)
184        hbox.Add((3, -1))
185
186        panel.SetSizer(hbox)
187        self.performance_blocking = True
188
189
190        self.Centre()
191        self.Show(True)
192
193        # gui stuff finished
194
195    def _on_check(self, index, flag):
196        """
197        When the user checks an item we need to immediately save that state.
198        :param index: The index of the checked item
199        :param flag: True or False whether the item was checked
200        """
201        if self.performance_blocking:
202            # for computational reasons we don't want to
203            # call this function every time the gui is set up
204            model_name = self.cat_list.GetItem(index, 0).GetText()
205            self.model_enabled_dict[model_name] = flag
206            self._regenerate_master_dict()
207
208
209    def _fill_lists(self):
210        """
211        Expands lists on the GUI
212        """
213        ## This method loops through all the models in the category by model
214        ## list and for each one converts the dictionary item to a string
215        ## which has of course two terms: the model and the category (in that
216        ## order).  The text string however directly reads the quotes, brackets,
217        ## and encoding term (u in our case) and does not understand them
218        ## as dictionary and list separators.  Thus we then have to strip those
219        ## out.  Also note the text control box, cat_list, has already been made into
220        ## a two column list with a check box.
221        ##
222        ## This works but is ugly to me (should not have to manually strip).
223        ## had to add the u stripping for the json encoding
224        ##
225        ## - PDB April 26, 2014
226        ##
227        self.cat_list.DeleteAllItems()
228        model_name_list = [model for model in self.by_model_dict]
229        model_name_list.sort()
230
231        for model in model_name_list:
232            index = self.cat_list.InsertStringItem(sys.maxsize, model)
233            self.cat_list.SetStringItem(index, 1, \
234                                            str(self.by_model_dict[model]).\
235                                            replace("u'","").\
236                                            replace("'","").\
237                                            replace("[","").\
238                                            replace("]",""))
239
240
241           
242    def _set_enabled(self):
243        """
244        Updates enabled models from self.model_enabled_dict
245        """
246        num = self.cat_list.GetItemCount()
247        for i in range(num):
248            model_name = self.cat_list.GetItem(i, 0).GetText()
249            self.cat_list.CheckItem(i, 
250                                    self.model_enabled_dict[model_name] )
251                                   
252
253
254    def _on_selectall(self, event):
255        """
256        Callback for 'enable all'
257        """
258        self.performance_blocking = False
259        num = self.cat_list.GetItemCount()
260        for i in range(num):
261            self.cat_list.CheckItem(i)
262        for model in self.model_enabled_dict:
263            self.model_enabled_dict[model] = True
264        self._regenerate_master_dict()
265        self.performance_blocking = True
266
267    def _on_deselectall(self, event):
268        """
269        Callback for 'disable all'
270        """
271        self.performance_blocking = False
272        num = self.cat_list.GetItemCount()
273        for i in range(num):
274            self.cat_list.CheckItem(i, False)
275        for model in self.model_enabled_dict:
276            self.model_enabled_dict[model] = False
277        self._regenerate_master_dict()
278        self.performance_blocking = True
279
280    def _on_apply(self, event):
281        """
282        Call up the 'ChangeCat' dialog for category editing
283        """
284
285        if self.cat_list.GetSelectedItemCount() == 0:
286            wx.MessageBox('Please select a model', 'Error',
287                          wx.OK | wx.ICON_EXCLAMATION )
288
289        else:
290            selected_model = \
291                self.cat_list.GetItem(\
292                self.cat_list.GetFirstSelected(), 0).GetText()
293
294
295            modify_dialog = ChangeCat(self, selected_model, 
296                                      self._get_cat_list(),
297                                      self.by_model_dict[selected_model])
298           
299            if modify_dialog.ShowModal() == wx.ID_OK:
300                if not IS_MAC:
301                    self.dial_ok(modify_dialog, selected_model)
302
303    def dial_ok(self, dialog=None, model=None):
304        """
305        modify_dialog onclose
306        """
307        self.by_model_dict[model] = dialog.get_category()
308        self._regenerate_master_dict()
309        self._fill_lists()
310        self._set_enabled()
311
312
313    def _on_ok(self, event):
314        """
315        Close the manager
316        """
317        self._save_state()
318        evt = ChangeCategoryEvent()
319        wx.PostEvent(self.parent, evt)
320
321        self.Destroy()
322
323    def _on_help(self, event):
324        """
325        Bring up the Category Manager Panel Documentation whenever
326        the HELP button is clicked.
327
328        Calls DocumentationWindow with the path of the location within the
329        documentation tree (after /doc/ ....".  Note that when using old
330        versions of Wx (before 2.9) and thus not the release version of
331        installers, the help comes up at the top level of the file as
332        webbrowser does not pass anything past the # to the browser when it is
333        running "file:///...."
334
335    :param evt: Triggers on clicking the help button
336    """
337
338        #import documentation window here to avoid circular imports
339        #if put at top of file with rest of imports.
340        from documentation_window import DocumentationWindow
341
342        _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
343        _PageAnchor = "#category-manager"
344        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, _PageAnchor,
345                                          "Category Manager Help")
346
347    def _on_cancel(self, event):
348        """
349        On cancel
350        """
351        self.Destroy()
352
353    def _save_state(self):
354        """
355        Serializes categorization info to file
356        """
357
358        self._regenerate_master_dict()
359
360        cat_file = open(CategoryInstaller.get_user_file(), 'wb')
361
362        json.dump(self.master_category_dict, cat_file )
363       
364        cat_file.close()
365   
366    def _read_category_info(self):
367        """
368        Read in categorization info from file
369        """
370        try:
371            cat_file = CategoryInstaller.get_user_file()
372            self.master_category_dict = {}
373            if os.path.isfile(cat_file):
374                with open(cat_file, 'rb') as f:
375                    self.master_category_dict = json.load(f)
376        except IOError:
377            logger.error('Problem reading in category file.')
378
379        self._regenerate_model_dict()
380
381    def _get_cat_list(self):
382        """
383        Returns a simple list of categories
384        """
385        cat_list = list()
386        for category in self.master_category_dict.keys():
387            if not category == 'Uncategorized':
388                cat_list.append(category)
389   
390        return cat_list
391
392    def _regenerate_model_dict(self):
393        """
394        regenerates self.by_model_dict which has each model
395        name as the key
396        and the list of categories belonging to that model
397        along with the enabled mapping
398        """
399        self.by_model_dict = defaultdict(list)
400        for category in self.master_category_dict:
401            for (model, enabled) in self.master_category_dict[category]:
402                self.by_model_dict[model].append(category)
403                self.model_enabled_dict[model] = enabled
404
405    def _regenerate_master_dict(self):
406        """
407        regenerates self.master_category_dict from
408        self.by_model_dict and self.model_enabled_dict
409        """
410        self.master_category_dict = defaultdict(list)
411        for model in self.by_model_dict:
412            for category in self.by_model_dict[model]:
413                self.master_category_dict[category].append\
414                    ((model, self.model_enabled_dict[model]))
415   
416
417
418class ChangeCat(wx.Dialog):
419    """
420    dialog for changing the categories of a model
421    """
422
423    def __init__(self, parent, title, cat_list, current_cats):
424        """
425        Actual editor for a certain category
426        :param parent: Window parent
427        :param title: Window title
428        :param cat_list: List of all categories
429        :param current_cats: List of categories applied to current model
430        """
431        wx.Dialog.__init__(self, parent, title = 'Change Category: '+title, size=(485, 425))
432
433        self.current_cats = current_cats
434        if str(self.current_cats[0]) == 'Uncategorized':
435            self.current_cats = []
436        self.parent = parent
437        self.selcted_model = title
438        vbox = wx.BoxSizer(wx.VERTICAL)
439        self.add_sb = wx.StaticBox(self, label = "Add Category")
440        self.add_sb_sizer = wx.StaticBoxSizer(self.add_sb, wx.VERTICAL)
441        gs = wx.GridSizer(3, 2, 5, 5)
442        self.cat_list = cat_list
443       
444        self.cat_text = wx.StaticText(self, label = "Current categories: ")
445        self.current_categories = wx.ListBox(self, 
446                                             choices = self.current_cats
447                                             , size=(300, 100))
448        self.existing_check = wx.RadioButton(self, 
449                                             label = 'Choose Existing')
450        self.new_check = wx.RadioButton(self, label = 'Create new')
451        self.exist_combo = wx.ComboBox(self, style = wx.CB_READONLY, 
452                                       size=(220,-1), choices = cat_list)
453        self.exist_combo.SetSelection(0)
454       
455       
456        self.remove_sb = wx.StaticBox(self, label = "Remove Category")
457       
458        self.remove_sb_sizer = wx.StaticBoxSizer(self.remove_sb, 
459                                                 wx.VERTICAL)
460
461        self.new_text = wx.TextCtrl(self, size=(220, -1))
462        self.ok_button = wx.Button(self, wx.ID_OK, "Done")
463        self.add_button = wx.Button(self, label = "Add")
464        self.add_button.Bind(wx.EVT_BUTTON, self.on_add)
465        self.remove_button = wx.Button(self, label = "Remove Selected")
466        self.remove_button.Bind(wx.EVT_BUTTON, self.on_remove)
467
468        self.existing_check.Bind(wx.EVT_RADIOBUTTON, self.on_existing)
469        self.new_check.Bind(wx.EVT_RADIOBUTTON, self.on_newcat)
470        self.existing_check.SetValue(True)
471
472        vbox.Add(self.cat_text, flag = wx.LEFT | wx.TOP | wx.ALIGN_LEFT, 
473                 border = 10)
474        vbox.Add(self.current_categories, flag = wx.ALL | wx.EXPAND, 
475                 border = 10  )
476
477        gs.AddMany( [ (self.existing_check, 5, wx.ALL),
478                      (self.exist_combo, 5, wx.ALL),
479                      (self.new_check, 5, wx.ALL),
480                      (self.new_text, 5, wx.ALL ),
481                      ((-1,-1)),
482                      (self.add_button, 5, wx.ALL | wx.ALIGN_RIGHT) ] )
483
484        self.add_sb_sizer.Add(gs, proportion = 1, flag = wx.ALL, border = 5)
485        vbox.Add(self.add_sb_sizer, flag = wx.ALL | wx.EXPAND, border = 10)
486
487        self.remove_sb_sizer.Add(self.remove_button, border = 5, 
488                                 flag = wx.ALL | wx.ALIGN_RIGHT)
489        vbox.Add(self.remove_sb_sizer, 
490                 flag = wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 
491                 border = 10)
492        vbox.Add(self.ok_button, flag = wx.ALL | wx.ALIGN_RIGHT, 
493                 border = 10)
494       
495        if self.current_categories.GetCount() > 0:
496                self.current_categories.SetSelection(0)
497        self.new_text.Disable()
498        self.SetSizer(vbox)
499        self.Centre()
500        self.Show(True)
501        if IS_MAC:
502            self.ok_button.Bind(wx.EVT_BUTTON, self.on_ok_mac)
503
504    def on_ok_mac(self, event):
505        """
506        On OK pressed (MAC only)
507        """
508        event.Skip()
509        self.parent.dial_ok(self, self.selcted_model)
510        self.Destroy()
511
512    def on_add(self, event):
513        """
514        Callback for new category added
515        """
516        new_cat = ''
517        if self.existing_check.GetValue():
518            new_cat = str(self.exist_combo.GetValue())
519        else:
520            new_cat = str(self.new_text.GetValue())
521            if new_cat in self.cat_list:
522                wx.MessageBox('%s is already a model' % new_cat, 'Error',
523                              wx.OK | wx.ICON_EXCLAMATION )
524                return
525
526        if new_cat in self.current_cats:
527            wx.MessageBox('%s is already included in this model' \
528                              % new_cat, 'Error',
529                          wx.OK | wx.ICON_EXCLAMATION )
530            return
531
532        self.current_cats.append(new_cat)
533        self.current_categories.SetItems(self.current_cats)
534           
535       
536    def on_remove(self, event):
537        """
538        Callback for a category removed
539        """
540        if self.current_categories.GetSelection() == wx.NOT_FOUND:
541            wx.MessageBox('Please select a category to remove', 'Error',
542                          wx.OK | wx.ICON_EXCLAMATION )
543        else:
544            self.current_categories.Delete( \
545                self.current_categories.GetSelection())
546            self.current_cats = self.current_categories.GetItems()
547
548       
549
550    def on_newcat(self, event):
551        """
552        Callback for new category added
553        """
554        self.new_text.Enable()
555        self.exist_combo.Disable()
556
557
558    def on_existing(self, event):   
559        """
560        Callback for existing category selected
561        """
562        self.new_text.Disable()
563        self.exist_combo.Enable()
564
565    def get_category(self):
566        """
567        Returns a list of categories applying to this model
568        """
569        if not self.current_cats:
570            self.current_cats.append("Uncategorized")
571
572        ret = list()
573        for cat in self.current_cats:
574            ret.append(str(cat))
575        return ret
576
577if __name__ == '__main__':
578       
579   
580    if(len(sys.argv) > 1):
581        app = wx.App()
582        CategoryManager(None, -1, 'Category Manager', sys.argv[1])
583        app.MainLoop()
584    else:
585        app = wx.App()
586        CategoryManager(None, -1, 'Category Manager', sys.argv[1])
587        app.MainLoop()
588
Note: See TracBrowser for help on using the repository browser.