EnvironmentPrefs
================

* :download:`Download example <PyObjCExample-EnvironmentPrefs.zip>`

This ``NSPreferencePane`` can be used to edit the default environment for the current user. It also is a simple example of a localized application.


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

ShellEnv.py
...........

.. sourcecode:: python

    """
    EnvironmentPane - PreferencePane for changing the global user environment
    
    This PreferencePane provides an easy to use UI for changing
    ~/.MacOSX/environment.plist. This plist is loaded by the loginwindow application
    when the user logs in and is used to initialize the "shell" environment.
    
    Note that these variables are also available outside of the Terminal, but not
    when the user logs in using SSH.
    
    TODO:
    - Undo
    """
    
    import os
    
    import Cocoa
    import objc
    import PreferencePanes
    from objc import super  # noqa: A004
    from PyObjCTools import AppHelper
    
    # Uncomment this during development, you'll get exception tracebacks when
    # the Python code fails.
    # objc.setVerbose(1)
    
    # Location of the environment.plist
    ENVPLIST = "~/.MacOSX/environment.plist"
    
    # Template for new keys
    NEWTMPL = Cocoa.NSLocalizedString("New_Variable_%d", "")
    
    
    class EnvironmentPane(PreferencePanes.NSPreferencePane):
        """
        The 'model/controller' for the "Shell Environment" preference pane
        """
    
        deleteButton = objc.IBOutlet()
        mainTable = objc.IBOutlet()
    
        # __slots__ = (
        #    'environ',  # The actual environment, as a NSMutableDictionary
        #    'keys',     # The list of keys, in the right order for the tableView
        #    'changed',  # True if we should save before exiting
        # )
    
        def initWithBundle_(self, bundle):
            # Our bundle has been loaded, initialize the instance variables.
            # We don't load the environment.plist yet, do that when we're
            # actually selected. That way we can easier pick up manual changes.
    
            self = super().initWithBundle_(bundle)
            if self is None:
                return None
    
            self.keys = ()
            self.environ = None
            self.changed = False
    
            return self
    
        def mainViewDidLoad(self):
            self.deleteButton.setEnabled_(False)
    
        def didSelect(self):
            # We are the selected preference pane. Load the environment.plist.
    
            self.environ = Cocoa.NSMutableDictionary.dictionaryWithContentsOfFile_(
                os.path.expanduser(ENVPLIST)
            )
            if self.environ is None:
                self.environ = Cocoa.NSMutableDictionary.dictionary()
            self.keys = list(self.environ.keys())
            self.keys.sort()
            self.changed = False
            self.mainTable.reloadData()
    
        def shouldUnselect(self):
            # The user wants to select another preference pane. If we have
            # unsaved changes ask if they should be saved right now.
    
            if self.changed:
                Cocoa.NSBeginAlertSheet(
                    Cocoa.NSLocalizedString("Save changes?", ""),
                    Cocoa.NSLocalizedString("Cancel", ""),
                    Cocoa.NSLocalizedString("Don't Save", ""),
                    Cocoa.NSLocalizedString("Save", ""),
                    self.mainView().window(),
                    self,
                    None,
                    "sheetDidDismiss:returnCode:contextInfo:",
                    0,
                    Cocoa.NSLocalizedString(
                        "There are unsaved changed, should these be saved?", ""
                    ),
                )
                return PreferencePanes.NSUnselectLater
            return PreferencePanes.NSUnselectNow
    
        @AppHelper.endSheetMethod
        def sheetDidDismiss_returnCode_contextInfo_(self, sheet, code, info):
            # Sheet handler for saving unsaved changes.
    
            if code == Cocoa.NSAlertDefaultReturn:  # 'Cancel'
                self.replyToShouldUnselect_(PreferencePanes.NSUnselectCancel)
                return
    
            elif code == Cocoa.NSAlertAlternateReturn:  # 'Don't Save'
                pass
    
            elif code == Cocoa.NSAlertOtherReturn:  # 'Save'
                r = self.saveEnvironment()
                if not r:
                    self.runAlertSheet(
                        Cocoa.NSLocalizedString("Cannot save changes", ""),
                        Cocoa.NSLocalizedString(
                            "It was not possible to save your changes", ""
                        ),
                    )
                    self.replyToShouldUnselect_(PreferencePanes.NSUnselectCancel)
                    return
    
            self.keys = ()
            self.environ = None
            self.changed = False
            self.replyToShouldUnselect_(PreferencePanes.NSUnselectNow)
    
        def saveEnvironment(self):
            """
            Save the data to environment.plist
            """
            fname = os.path.expanduser(ENVPLIST)
            dname = os.path.dirname(fname)
            if not os.path.isdir(dname):
                try:
                    os.mkdir(dname)
                except OSError:
                    return False
    
            if not self.environ.writeToFile_atomically_(fname, True):
                return False
    
            return True
    
        @objc.IBAction
        def deleteVariable_(self, sender):
            """
            Delete the selected variable
            """
            r = self.mainTable.selectedRow()
    
            envname = self.keys[r]
            del self.environ[envname]
            self.keys.remove(envname)
            self.mainTable.reloadData()
            self.changed = True
    
        @objc.IBAction
        def newVariable_(self, sender):
            """
            Add a new variable
            """
            i = 0
            name = NEWTMPL % (i,)
            while name in self.environ:
                i += 1
                name = NEWTMPL % (i,)
            self.environ[name] = Cocoa.NSLocalizedString("New Value", "")
            self.keys = list(self.environ.keys())
            self.keys.sort()
            self.mainTable.reloadData()
            self.changed = True
    
        def numberOfRowsInTableView_(self, aView):
            """Return the number of environment variables"""
            return len(self.keys)
    
        def tableView_objectValueForTableColumn_row_(self, aView, aCol, rowIndex):
            """Get the name of value of an environment variable"""
            name = aCol.identifier()
            envname = self.keys[rowIndex]
    
            if name == "name":
                return envname
            elif name == "value":
                return self.environ[envname]
    
        def tableView_setObjectValue_forTableColumn_row_(
            self, aView, value, aCol, rowIndex
        ):
            """Change the name or value of an environment variable"""
            if self.environ is None:
                aView.reloadData()
                return
    
            name = aCol.identifier()
            envname = self.keys[rowIndex]
    
            if name == "name":
                if value != envname:
                    if value in self.environ:
                        self.runAlertSheet(
                            Cocoa.NSLocalizedString("Name exists", ""),
                            Cocoa.NSLocalizedString("The name %s is already used", "")
                            % (value,),
                        )
                        aView.reloadData()
                        return
    
                    val = self.environ[envname]
                    del self.environ[envname]
                    self.environ[value] = val
                    self.keys = list(self.environ.keys())
                    self.keys.sort()
                    aView.reloadData()
                    self.changed = True
    
            elif name == "value":
                val = self.environ[envname]
                if val != value:
                    self.environ[envname] = value
                    self.changed = True
    
        def tableViewSelectionDidChange_(self, notification):
            """The delete button should only be active if a row is selected"""
            if self.mainTable.numberOfSelectedRows() == 0:
                self.deleteButton.setEnabled_(False)
            else:
                self.deleteButton.setEnabled_(True)
    
        @objc.python_method
        def runAlertSheet(self, title, message):
            """Run an alertsheet without callbacks"""
            Cocoa.NSBeginAlertSheet(
                title,
                Cocoa.NSLocalizedString("OK", ""),
                None,
                None,
                self.mainView().window(),
                self,
                None,
                None,
                0,
                message,
            )
    
    
    objc.removeAutoreleasePool()

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

    """
    Script for building the example.
    
    Usage:
        python3 setup.py py2app
    """
    
    from setuptools import setup
    
    infoPlist = {
        "CFBundleName": "Shell Environment",
        "CFBundleGetInfoString": "Shell Environment Panel",
        "CFBundleVersion": "0.1",
        "CFBundleShortVersionString": "0.1",
        "NSPrefPaneIconLabel": "Shell Environment",
        "NSPrefPaneIconFile": "ShellEnv.icns",
        "NSPrincipalClass": "EnvironmentPane",
        "NSMainNibFile": "EnvironmentPane",
    }
    
    setup(
        name="Shell Environment",
        plugin=["ShellEnv.py"],
        data_files=["English.lproj", "Dutch.lproj", "ShellEnv.icns"],
        options={"py2app": {"extension": ".prefPane", "plist": infoPlist}},
        setup_requires=[
            "py2app",
            "pyobjc-framework-Cocoa",
            "pyobjc-framework-PreferencePanes",
        ],
    )

