source: sasview/src/sas/guiframe/CategoryManager.py @ a12c0a6

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since a12c0a6 was 4faed25, checked in by butler, 9 years ago

Added help button to category manager panel

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