dicomsort/dicomsort

View on GitHub
dicomsort/gui/widgets.py

Summary

Maintainability
C
7 hrs
Test Coverage
B
87%
import os
import re
import wx

import wx.grid
import wx.html

from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin, TextEditMixin

from dicomsort.gui import errors, events
from dicomsort.gui.dialogs import SeriesRemoveWarningDlg


class FileDropTarget(wx.FileDropTarget):
    def __init__(self, callback):
        super(FileDropTarget, self).__init__()
        self.callback = callback

    def OnDropFiles(self, x, y, filenames):
        self.callback(x, y, filenames)


class CustomDataTable(wx.grid.GridTableBase):
    def __init__(self, data):
        super(CustomDataTable, self).__init__()

        self.colLabels = ['', 'DICOM Property', 'Replacement Value']

        self.dataTypes = [
            wx.grid.GRID_VALUE_BOOL,
            wx.grid.GRID_VALUE_STRING,
            wx.grid.GRID_VALUE_STRING
        ]

        if data is None:
            data = [['', '', ''], ]

        self.data = data

    # --------------------------------------------------
    # required methods for the wxPyGridTableBase interface

    def GetNumberRows(self):
        return len(self.data)

    def GetNumberCols(self):
        return len(self.data[0])

    def IsEmptyCell(self, row, col):
        try:
            return not self.data[row][col]
        except IndexError:
            return True

    # Get/Set values in the table.  The Python version of these
    # methods can handle any data-type, (as long as the Editor and
    # Renderer understands the type too,) not just strings as in the
    # C++ version.
    def GetValue(self, row, col):
        try:
            return self.data[row][col]
        except IndexError:
            return ''

    def SetValue(self, row, col, value):
        def innerSetValue(row, col, value):
            try:
                self.data[row][col] = value
            except IndexError:
                # add a new row
                self.data.append([''] * self.GetNumberCols())
                innerSetValue(row, col, value)

                # tell the grid we've added a row
                msg = wx.grid.GridTableMessage(
                    self,
                    wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED,
                    1
                )

                view = self.GetView()

                if view:
                    view.ProcessTableMessage(msg)
        innerSetValue(row, col, value)

    # --------------------------------------------------
    # Some optional methods

    # Called when the grid needs to display labels
    def GetColLabelValue(self, col):
        return self.colLabels[col]

    # Called to determine the kind of editor/renderer to use by
    # default, doesn't necessarily have to be the same type used
    # natively by the editor/renderer if they know how to convert.
    def GetTypeName(self, row, col):
        return self.dataTypes[col]

    # Called to determine how the data can be fetched and stored by the
    # editor and renderer.  This allows you to enforce some type-safety
    # in the grid.
    def CanGetValueAs(self, row, col, typeName):
        colType = self.dataTypes[col].split(':')[0]
        if typeName == colType:
            return True
        else:
            return False

    def CanSetValueAs(self, row, col, typeName):
        return self.CanGetValueAs(row, col, typeName)


class CheckListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin, TextEditMixin):

    def __init__(self, parent):
        wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT)
        ListCtrlAutoWidthMixin.__init__(self)

        # The order here matter because we favor Check over Edit
        TextEditMixin.__init__(self)

        self.editColumns = []

        self.EnableCheckBoxes()

    def _GetCheckedIndexes(self):
        return [i for i in range(self.ItemCount) if self.IsItemChecked(i)]

    def ClearColumn(self, col):
        [self.SetItem(i, col, '') for i in range(self.ItemCount)]

    def SetColumnEditable(self, column, edit=True):
        if edit:
            if column not in self.editColumns:
                self.editColumns.append(column)
        else:
            if column in self.editColumns:
                self.editColumns.remove(column)

    def SetStringItems(self, items):
        self.DeleteAllItems()

        for item in items:
            if isinstance(item, str):
                item = [item, ]

            row = self.InsertItem(self.GetItemCount() + 1, item[0])

            for col in range(1, len(item)):
                self.SetStringItem(row, col, item[col])

    def CheckItems(self, itemIndex):
        [self.CheckItem(index) for index in itemIndex]

    def GetCheckedItems(self, col=None):
        return [self.GetItemList(r, col) for r in self._GetCheckedIndexes()]

    def UnCheckAll(self):
        [self.CheckItem(index, False) for index in range(self.ItemCount)]

    def GetCheckedStrings(self, col=None):
        return [self.GetStringItem(r, col) for r in self._GetCheckedIndexes()]

    def FindStrings(self, strings, col=0):
        fields = [item.Text for item in self.GetItemList(col)]

        indices = list()

        for string in strings:
            try:
                indices.append(fields.index(string))
            except ValueError:
                indices.append(None)

        return indices

    def GetItemList(self, column=None):
        if column is None:
            return [self.GetItemList(c) for c in range(self.ColumnCount)]
        else:
            return [self.GetItem(r, column) for r in range(self.ItemCount)]

    def GetStringItem(self, row, column=None):
        if column is None:
            return [
                self.GetStringItem(row, c) for c in range(self.ColumnCount)
            ]
        else:
            return self.GetItem(row, column).Text

    def OpenEditor(self, column, row):
        """
        Overloaded method to render certain columns un-editable
        """
        if column not in self.editColumns:
            return
        else:
            # Call superclass edit method
            TextEditMixin.OpenEditor(self, column, row)


class PathEditCtrl(wx.Panel):
    def __init__(self, *args, **kwargs):
        wx.Panel.__init__(self, *args, **kwargs)
        self.create()
        self.path = ''
        self.Bind(wx.EVT_TEXT_ENTER, self.ValidatePath, self.edit)

        dt = FileDropTarget(self.ValidateDropFiles)
        self.edit.SetDropTarget(dt)

    def ValidateDropFiles(self, x, y, filenames):
        self.SetPaths(filenames)

    def ValidatePath(self, *evnt):
        paths = self.edit.GetValue()

        self.SetPaths(paths.split(';'))

    def create(self):
        hbox = wx.BoxSizer(wx.HORIZONTAL)

        opts = wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT | wx.TOP

        self.edit = wx.TextCtrl(self, -1, "", style=wx.TE_PROCESS_ENTER)
        hbox.Add(self.edit, 1, opts, 5)

        self.browse = wx.Button(self, -1, label="Browse")
        hbox.Add(self.browse, 0, opts, 10)

        self.browse.Bind(wx.EVT_BUTTON, self.BrowsePaths)

        self.SetSizer(hbox)

    def SetPaths(self, paths):
        if not isinstance(paths, list):
            paths = [paths, ]

        # Determine if there are any invalid paths
        badPaths = [path for path in paths if not os.path.isdir(path)]

        if len(badPaths):
            p = ', '.join(badPaths)
            errorMsg = 'The Following directories are invalid paths: %s' % p
            errors.throw_error(errorMsg, 'Invalid Paths', parent=self.Parent)
            return

        self.path = paths
        self.edit.SetValue(';'.join(paths))

        # Trigger an EVT_PATH event
        self.Notify()

    def Notify(self, *evnt):
        event = events.PathEvent(path=self.path)
        wx.PostEvent(self, event)

    def BrowsePaths(self, *evnt):
        # Open the dialog and search
        if len(self.path):
            defaultPath = self.path[0]
        else:
            defaultPath = ''

        pathDlg = wx.DirDialog(self.GetParent(), "Please Select Directory",
                               defaultPath)
        pathDlg.CenterOnParent()

        if pathDlg.ShowModal() == wx.ID_OK:
            path = pathDlg.GetPath()
            self.SetPaths([path, ])

        pathDlg.Destroy()


class FieldSelector(wx.Panel):

    def __init__(self, parent, id=-1, size=(250, 300), choices=None,
                 titles=None):
        wx.Panel.__init__(self, parent, id=id, size=size)

        self.choices = choices or []
        self.titles = titles or ['', '']

        self.create()

    def Filter(self, string=None):
        if string and isinstance(string, str):
            self.search.SetValue(string)
        else:
            string = self.search.GetValue()

        newVals = [p for p in self.choices if re.search(string, p,
                                                        re.IGNORECASE)]

        self.options.SetItems(newVals)

    def _return_focus(self):
        return

    def _sort_callback(self, *evnt):
        event = events.SortEvent(
            anon=self.anonQ.IsChecked(), fields=self.GetFormatFields()
        )
        wx.PostEvent(self, event)

    def GetSelectedItems(self):
        return self.selected.GetItems()

    def GetFormatFields(self):
        items = self.GetSelectedItems()
        return ['%(' + item + ')s' for item in items]

    def _anon_tick(self, args):
        if self.anonQ.IsChecked():
            self.Parent.QuickRename()

    def create(self):

        self.titleL = wx.StaticText(self, -1, self.titles[0])
        self.options = wx.ListBox(self, -1, choices=self.choices)

        self.search = wx.SearchCtrl(
            self, -1, "", style=wx.TE_PROCESS_ENTER)

        self.titleR = wx.StaticText(self, -1, self.titles[1])
        self.selected = wx.ListBox(self, -1, choices=[])

        self.sortBtn = wx.Button(self, -1, label="Sort Images")
        self.anonQ = wx.CheckBox(self, -1, label="Anonymize Data")

        # Make it so that when we check the checkbox it brings up the quick
        # rename dialog
        self.anonQ.Bind(wx.EVT_CHECKBOX, self._anon_tick)

        self.sortBtn.Bind(wx.EVT_BUTTON, self._sort_callback)

        # Setup double-click callbacks
        self.options.Bind(wx.EVT_LISTBOX_DCLICK, self.SelectItem)
        self.selected.Bind(wx.EVT_LISTBOX_DCLICK, self.DeselectItem)

        # Setup Search Control
        self.search.Bind(wx.EVT_TEXT, self.Filter)
        self.search.Bind(wx.EVT_TEXT_ENTER, self._return_focus)

        # Setup controls:
        self.bAdd = wx.Button(self, -1, label=">>", size=(50, -1))
        self.bRemove = wx.Button(self, -1, label="<<", size=(50, -1))
        self.bUp = wx.Button(self, -1, label="Up", size=(50, -1))
        self.bDown = wx.Button(self, -1, label="Down", size=(50, -1))

        self.bAdd.Bind(wx.EVT_BUTTON, self.SelectItem)
        self.bRemove.Bind(wx.EVT_BUTTON, self.DeselectItem)
        self.bUp.Bind(wx.EVT_BUTTON, self.PromoteSelection)
        self.bDown.Bind(wx.EVT_BUTTON, self.DemoteSelection)

        self._initialize_layout()

    def WidgetList(self):
        a = [self.options, self.search, self.selected,
             self.bAdd, self.bRemove, self.bUp, self.bDown]
        return a

    def DisableAll(self):
        [item.Disable() for item in self.WidgetList()]

    def EnableAll(self):
        [item.Enable(True) for item in self.WidgetList()]

    def _initialize_layout(self):
        # BoxSizer containing the options to select
        vboxOptions = wx.BoxSizer(wx.VERTICAL)

        vboxOptions.Add(self.titleL, 0, wx.ALIGN_CENTER_HORIZONTAL)
        vboxOptions.Add(self.options, 1,
                        wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10)
        vboxOptions.Add(self.search, 0, wx.EXPAND | wx.ALL, 10)

        # BoxSizer containing the selected options
        vboxSelect = wx.BoxSizer(wx.VERTICAL)

        vboxSelect.Add(self.titleR, 0, wx.ALIGN_CENTER_HORIZONTAL)
        vboxSelect.Add(self.selected, 1,
                       wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10)
        vboxSelect.Add(self.anonQ, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)
        vboxSelect.Add(
            self.sortBtn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10)

        # BoxSizer housing the controls
        vboxControl = wx.BoxSizer(wx.VERTICAL)

        vboxControl.Add(self.bAdd, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 30)
        vboxControl.Add(
            self.bRemove, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 15)
        vboxControl.Add(self.bUp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 15)
        vboxControl.Add(self.bDown, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 15)

        # Horizontal Box that contains all elements
        hbox = wx.BoxSizer(wx.HORIZONTAL)

        hbox.Add(vboxOptions, 1, wx.EXPAND | wx.ALL, 10)
        hbox.Add(vboxControl, 0, wx.ALL, 0)
        hbox.Add(vboxSelect, 1, wx.EXPAND | wx.ALL, 10)

        self.SetSizer(hbox)

    def has_default(self):
        if self.selected.GetCount() == 0:
            return False

        if self.selected.GetItems()[-1] == 'SeriesDescription':
            return True
        else:
            return False

    def PromoteSelection(self, *evnt):
        self._move_selection(-1)

    def DemoteSelection(self, *evnt):
        self._move_selection(1)

    def _move_selection(self, inc):
        """
        Shared method for moving an item around the list.
        There really has to be a better way...
        """

        index = self.selected.GetSelection()

        if index == self.selected.GetCount() - 1 and self.has_default():
            return

        if inc < 0 and (self.selected.Count == 1 or index == 0):
            return
        elif (inc > 0 and index == self.selected.Count - 1):
            return

        prop = self.selected.GetStringSelection()

        self.selected.Delete(index)
        self.selected.Insert(prop, index + inc)
        self.selected.Select(index + inc)

    def SelectItem(self, *evnt):
        item = self.options.GetStringSelection()
        if self.has_default():
            self.selected.Insert(item, self.selected.GetCount() - 1)
        else:
            self.selected.Append(item)

    def DeselectItem(self, *evnt):
        index = self.selected.GetSelection()

        if index == -1:
            return

        if index == self.selected.GetCount() - 1 and self.has_default():
            warn = SeriesRemoveWarningDlg(None)
            warn.ShowModal()

            if warn.choice == 0:
                return
            elif warn.choice == 1:
                # change to original filenames
                cfg = self.GetParent().config
                cfg['FilenameFormat']['Selection'] = 1
                page = self.GetParent().prefDlg.pages['FilenameFormat']
                page.UpdateFromConfig(cfg)

        self.selected.Delete(index)

        self.selected.Select(index - 1)

    def SetTitles(self, titleL, titleR):
        self.leftBox

    def SetOptions(self, optionList=[]):
        # clear
        self.options.SetItems(optionList)
        self.choices = optionList