EnvironmentPrefs

A PyObjC Example without documentation

Sources

ShellEnv.py

"""
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()

setup.py

"""
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",
    ],
)