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

Last change on this file since d619341 was 212bfc2, checked in by mathieu, 8 years ago

Pull categories from models. Get rid of default categories. Fixes #535

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