#!/usr/bin/python
"""
This software was developed by Institut Laue-Langevin as part of
Distributed Data Analysis of Neutron Scattering Experiments (DANSE).
Copyright 2012 Institut Laue-Langevin
"""
import wx
import sys
import os
from wx.lib.mixins.listctrl import CheckListCtrlMixin, ListCtrlAutoWidthMixin
from collections import defaultdict
import json
from sas.guiframe.events import ChangeCategoryEvent
from sas.guiframe.CategoryInstaller import CategoryInstaller
IS_MAC = (sys.platform == 'darwin')
""" Notes
The category manager mechanism works from 3 data structures used:
- self.master_category_dict: keys are the names of categories,
the values are lists of tuples,
the first being the model names (the models belonging to that
category), the second a boolean
of whether or not the model is enabled
- self.by_model_dict: keys are model names, values are a list
of categories belonging to that model
- self.model_enabled_dict: keys are model names, values are
bools of whether the model is enabled
use self._regenerate_model_dict() to create the latter two
structures from the former
use self._regenerate_master_dict() to create the first
structure from the latter two
The need for so many data structures comes from the fact
sometimes we need fast access
to all the models in a category (eg user selection from the gui)
and sometimes we need access to all the categories
corresponding to a model (eg user modification of model categories)
"""
[docs]class CheckListCtrl(wx.ListCtrl, CheckListCtrlMixin,
ListCtrlAutoWidthMixin):
"""
Taken from
http://zetcode.com/wxpython/advanced/
"""
def __init__(self, parent, callback_func):
"""
Initialization
:param parent: Parent window
:param callback_func: A function to be called when
an element is clicked
"""
wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT \
| wx.SUNKEN_BORDER)
CheckListCtrlMixin.__init__(self)
ListCtrlAutoWidthMixin.__init__(self)
self.callback_func = callback_func
[docs] def OnCheckItem(self, index, flag):
"""
When the user checks the item we need to save that state
"""
self.callback_func(index, flag)
[docs]class CategoryManager(wx.Frame):
"""
A class for managing categories
"""
def __init__(self, parent, win_id, title):
"""
Category Manager Dialog class
:param win_id: A new wx ID
:param title: Title for the window
"""
# make sure the category file is where it should be
self.performance_blocking = False
self.master_category_dict = defaultdict(list)
self.by_model_dict = defaultdict(list)
self.model_enabled_dict = defaultdict(bool)
wx.Frame.__init__(self, parent, win_id, title, size=(660, 400))
panel = wx.Panel(self, -1)
self.parent = parent
self._read_category_info()
vbox = wx.BoxSizer(wx.VERTICAL)
hbox = wx.BoxSizer(wx.HORIZONTAL)
left_panel = wx.Panel(panel, -1)
right_panel = wx.Panel(panel, -1)
self.cat_list = CheckListCtrl(right_panel, self._on_check)
self.cat_list.InsertColumn(0, 'Model', width = 280)
self.cat_list.InsertColumn(1, 'Category', width = 240)
self._fill_lists()
self._regenerate_model_dict()
self._set_enabled()
vbox2 = wx.BoxSizer(wx.VERTICAL)
sel = wx.Button(left_panel, -1, 'Enable All', size=(100, -1))
des = wx.Button(left_panel, -1, 'Disable All', size=(100, -1))
modify_button = wx.Button(left_panel, -1, 'Modify',
size=(100, -1))
ok_button = wx.Button(left_panel, -1, 'OK', size=(100, -1))
cancel_button = wx.Button(left_panel, -1, 'Cancel',
size=(100, -1))
self.Bind(wx.EVT_BUTTON, self._on_selectall,
id=sel.GetId())
self.Bind(wx.EVT_BUTTON, self._on_deselectall,
id=des.GetId())
self.Bind(wx.EVT_BUTTON, self._on_apply,
id = modify_button.GetId())
self.Bind(wx.EVT_BUTTON, self._on_ok,
id = ok_button.GetId())
self.Bind(wx.EVT_BUTTON, self._on_cancel,
id = cancel_button.GetId())
vbox2.Add(modify_button, 0, wx.TOP, 10)
vbox2.Add((-1, 20))
vbox2.Add(sel)
vbox2.Add(des)
vbox2.Add((-1, 20))
vbox2.Add(ok_button)
vbox2.Add(cancel_button)
left_panel.SetSizer(vbox2)
vbox.Add(self.cat_list, 1, wx.EXPAND | wx.TOP, 3)
vbox.Add((-1, 10))
right_panel.SetSizer(vbox)
hbox.Add(left_panel, 0, wx.EXPAND | wx.RIGHT, 5)
hbox.Add(right_panel, 1, wx.EXPAND)
hbox.Add((3, -1))
panel.SetSizer(hbox)
self.performance_blocking = True
self.Centre()
self.Show(True)
# gui stuff finished
def _on_check(self, index, flag):
"""
When the user checks an item we need to immediately save that state.
:param index: The index of the checked item
:param flag: True or False whether the item was checked
"""
if self.performance_blocking:
# for computational reasons we don't want to
# call this function every time the gui is set up
model_name = self.cat_list.GetItem(index, 0).GetText()
self.model_enabled_dict[model_name] = flag
self._regenerate_master_dict()
def _fill_lists(self):
"""
Expands lists on the GUI
"""
## This method loops through all the models in the category by model
## list and for each one converts the dictionary item to a string
## which has of course two terms: the model and the category (in that
## order). The text string however directly reads the quotes, brackets,
## and encoding term (u in our case) and does not understand them
## as dictionary and list separators. Thus we then have to strip those
## out. Also note the text control box, cat_list, has already been made into
## a two column list with a check box.
##
## This works but is ugly to me (should not have to manually strip).
## had to add the u stripping for the json encoding
##
## - PDB April 26, 2014
##
self.cat_list.DeleteAllItems()
model_name_list = [model for model in self.by_model_dict]
model_name_list.sort()
for model in model_name_list:
index = self.cat_list.InsertStringItem(sys.maxint, model)
self.cat_list.SetStringItem(index, 1, \
str(self.by_model_dict[model]).\
replace("u'","").\
replace("'","").\
replace("[","").\
replace("]",""))
def _set_enabled(self):
"""
Updates enabled models from self.model_enabled_dict
"""
num = self.cat_list.GetItemCount()
for i in range(num):
model_name = self.cat_list.GetItem(i, 0).GetText()
self.cat_list.CheckItem(i,
self.model_enabled_dict[model_name] )
def _on_selectall(self, event):
"""
Callback for 'enable all'
"""
self.performance_blocking = False
num = self.cat_list.GetItemCount()
for i in range(num):
self.cat_list.CheckItem(i)
for model in self.model_enabled_dict:
self.model_enabled_dict[model] = True
self._regenerate_master_dict()
self.performance_blocking = True
def _on_deselectall(self, event):
"""
Callback for 'disable all'
"""
self.performance_blocking = False
num = self.cat_list.GetItemCount()
for i in range(num):
self.cat_list.CheckItem(i, False)
for model in self.model_enabled_dict:
self.model_enabled_dict[model] = False
self._regenerate_master_dict()
self.performance_blocking = True
def _on_apply(self, event):
"""
Call up the 'ChangeCat' dialog for category editing
"""
if self.cat_list.GetSelectedItemCount() == 0:
wx.MessageBox('Please select a model', 'Error',
wx.OK | wx.ICON_EXCLAMATION )
else:
selected_model = \
self.cat_list.GetItem(\
self.cat_list.GetFirstSelected(), 0).GetText()
modify_dialog = ChangeCat(self, selected_model,
self._get_cat_list(),
self.by_model_dict[selected_model])
if modify_dialog.ShowModal() == wx.ID_OK:
if not IS_MAC:
self.dial_ok(modify_dialog, selected_model)
[docs] def dial_ok(self, dialog=None, model=None):
"""
modify_dialog onclose
"""
self.by_model_dict[model] = dialog.get_category()
self._regenerate_master_dict()
self._fill_lists()
self._set_enabled()
def _on_ok(self, event):
"""
Close the manager
"""
self._save_state()
evt = ChangeCategoryEvent()
wx.PostEvent(self.parent, evt)
self.Destroy()
def _on_cancel(self, event):
"""
On cancel
"""
self.Destroy()
def _save_state(self):
"""
Serializes categorization info to file
"""
self._regenerate_master_dict()
cat_file = open(CategoryInstaller.get_user_file(), 'wb')
json.dump(self.master_category_dict, cat_file )
cat_file.close()
def _read_category_info(self):
"""
Read in categorization info from file
"""
try:
file = CategoryInstaller.get_user_file()
if os.path.isfile(file):
cat_file = open(file, 'rb')
# self.master_category_dict = pickle.load(cat_file)
self.master_category_dict = json.load(cat_file)
else:
cat_file = open(CategoryInstaller.get_default_file(), 'rb')
# self.master_category_dict = pickle.load(cat_file)
self.master_category_dict = json.load(cat_file)
cat_file.close()
except IOError:
print 'Problem reading in category file. Please review'
self._regenerate_model_dict()
def _get_cat_list(self):
"""
Returns a simple list of categories
"""
cat_list = list()
for category in self.master_category_dict.iterkeys():
if not category == 'Uncategorized':
cat_list.append(category)
return cat_list
def _regenerate_model_dict(self):
"""
regenerates self.by_model_dict which has each model
name as the key
and the list of categories belonging to that model
along with the enabled mapping
"""
self.by_model_dict = defaultdict(list)
for category in self.master_category_dict:
for (model, enabled) in self.master_category_dict[category]:
self.by_model_dict[model].append(category)
self.model_enabled_dict[model] = enabled
def _regenerate_master_dict(self):
"""
regenerates self.master_category_dict from
self.by_model_dict and self.model_enabled_dict
"""
self.master_category_dict = defaultdict(list)
for model in self.by_model_dict:
for category in self.by_model_dict[model]:
self.master_category_dict[category].append\
((model, self.model_enabled_dict[model]))
[docs]class ChangeCat(wx.Dialog):
"""
dialog for changing the categories of a model
"""
def __init__(self, parent, title, cat_list, current_cats):
"""
Actual editor for a certain category
:param parent: Window parent
:param title: Window title
:param cat_list: List of all categories
:param current_cats: List of categories applied to current model
"""
wx.Dialog.__init__(self, parent, title = 'Change Category: '+title, size=(485, 425))
self.current_cats = current_cats
if str(self.current_cats[0]) == 'Uncategorized':
self.current_cats = []
self.parent = parent
self.selcted_model = title
vbox = wx.BoxSizer(wx.VERTICAL)
self.add_sb = wx.StaticBox(self, label = "Add Category")
self.add_sb_sizer = wx.StaticBoxSizer(self.add_sb, wx.VERTICAL)
gs = wx.GridSizer(3, 2, 5, 5)
self.cat_list = cat_list
self.cat_text = wx.StaticText(self, label = "Current categories: ")
self.current_categories = wx.ListBox(self,
choices = self.current_cats
, size=(300, 100))
self.existing_check = wx.RadioButton(self,
label = 'Choose Existing')
self.new_check = wx.RadioButton(self, label = 'Create new')
self.exist_combo = wx.ComboBox(self, style = wx.CB_READONLY,
size=(220,-1), choices = cat_list)
self.exist_combo.SetSelection(0)
self.remove_sb = wx.StaticBox(self, label = "Remove Category")
self.remove_sb_sizer = wx.StaticBoxSizer(self.remove_sb,
wx.VERTICAL)
self.new_text = wx.TextCtrl(self, size=(220, -1))
self.ok_button = wx.Button(self, wx.ID_OK, "Done")
self.add_button = wx.Button(self, label = "Add")
self.add_button.Bind(wx.EVT_BUTTON, self.on_add)
self.remove_button = wx.Button(self, label = "Remove Selected")
self.remove_button.Bind(wx.EVT_BUTTON, self.on_remove)
self.existing_check.Bind(wx.EVT_RADIOBUTTON, self.on_existing)
self.new_check.Bind(wx.EVT_RADIOBUTTON, self.on_newcat)
self.existing_check.SetValue(True)
vbox.Add(self.cat_text, flag = wx.LEFT | wx.TOP | wx.ALIGN_LEFT,
border = 10)
vbox.Add(self.current_categories, flag = wx.ALL | wx.EXPAND,
border = 10 )
gs.AddMany( [ (self.existing_check, 5, wx.ALL),
(self.exist_combo, 5, wx.ALL),
(self.new_check, 5, wx.ALL),
(self.new_text, 5, wx.ALL ),
((-1,-1)),
(self.add_button, 5, wx.ALL | wx.ALIGN_RIGHT) ] )
self.add_sb_sizer.Add(gs, proportion = 1, flag = wx.ALL, border = 5)
vbox.Add(self.add_sb_sizer, flag = wx.ALL | wx.EXPAND, border = 10)
self.remove_sb_sizer.Add(self.remove_button, border = 5,
flag = wx.ALL | wx.ALIGN_RIGHT)
vbox.Add(self.remove_sb_sizer,
flag = wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND,
border = 10)
vbox.Add(self.ok_button, flag = wx.ALL | wx.ALIGN_RIGHT,
border = 10)
if self.current_categories.GetCount() > 0:
self.current_categories.SetSelection(0)
self.new_text.Disable()
self.SetSizer(vbox)
self.Centre()
self.Show(True)
if IS_MAC:
self.ok_button.Bind(wx.EVT_BUTTON, self.on_ok_mac)
[docs] def on_ok_mac(self, event):
"""
On OK pressed (MAC only)
"""
event.Skip()
self.parent.dial_ok(self, self.selcted_model)
self.Destroy()
[docs] def on_add(self, event):
"""
Callback for new category added
"""
new_cat = ''
if self.existing_check.GetValue():
new_cat = str(self.exist_combo.GetValue())
else:
new_cat = str(self.new_text.GetValue())
if new_cat in self.cat_list:
wx.MessageBox('%s is already a model' % new_cat, 'Error',
wx.OK | wx.ICON_EXCLAMATION )
return
if new_cat in self.current_cats:
wx.MessageBox('%s is already included in this model' \
% new_cat, 'Error',
wx.OK | wx.ICON_EXCLAMATION )
return
self.current_cats.append(new_cat)
self.current_categories.SetItems(self.current_cats)
[docs] def on_remove(self, event):
"""
Callback for a category removed
"""
if self.current_categories.GetSelection() == wx.NOT_FOUND:
wx.MessageBox('Please select a category to remove', 'Error',
wx.OK | wx.ICON_EXCLAMATION )
else:
self.current_categories.Delete( \
self.current_categories.GetSelection())
self.current_cats = self.current_categories.GetItems()
[docs] def on_newcat(self, event):
"""
Callback for new category added
"""
self.new_text.Enable()
self.exist_combo.Disable()
[docs] def on_existing(self, event):
"""
Callback for existing category selected
"""
self.new_text.Disable()
self.exist_combo.Enable()
[docs] def get_category(self):
"""
Returns a list of categories applying to this model
"""
if not self.current_cats:
self.current_cats.append("Uncategorized")
ret = list()
for cat in self.current_cats:
ret.append(str(cat))
return ret
if __name__ == '__main__':
if(len(sys.argv) > 1):
app = wx.App()
CategoryManager(None, -1, 'Category Manager', sys.argv[1])
app.MainLoop()
else:
app = wx.App()
CategoryManager(None, -1, 'Category Manager', sys.argv[1])
app.MainLoop()