diff --git a/enaml/image.py b/enaml/image.py index d97c380d3..2f1691a7d 100644 --- a/enaml/image.py +++ b/enaml/image.py @@ -32,9 +32,14 @@ class Image(Atom): 'pgm', # Portable Graymap 'ppm', # Portable Pixmap 'tiff', # Tagged Image File Format - # 'array', # A numpy array with an appropriate image dtype. + 'argb32', # Raw data in the 0xAARRGGBB format. + # The `raw_size` of the image must be provided. ) + #: The (width, height) raw size of the image. This must be provided + #: for images where the size is not encoded in the data stream. + raw_size = Coerced(Size, (0, 0)) + #: The (width, height) size of the image. An invalid size indicates #: that the size of the image should be automatically inferred. A #: valid size indicates that the toolkit image should be scaled to diff --git a/enaml/qt/q_resource_helpers.py b/enaml/qt/q_resource_helpers.py index 1edd54c75..a24edd3f5 100644 --- a/enaml/qt/q_resource_helpers.py +++ b/enaml/qt/q_resource_helpers.py @@ -69,9 +69,13 @@ def QImage_from_Image(image): """ format = image.format - if format == 'auto': - format = '' - qimage = QImage.fromData(image.data, format) + if format == 'argb32': + w, h = image.raw_size + qimage = QImage(image.data, w, h, QImage.Format_ARGB32) + else: + if format == 'auto': + format = '' + qimage = QImage.fromData(image.data, format) if -1 not in image.size and not qimage.isNull(): qsize = QSize(*image.size) if qsize != qimage.size(): diff --git a/enaml/qt/qt_container.py b/enaml/qt/qt_container.py index 11668ec5f..002bb09e9 100644 --- a/enaml/qt/qt_container.py +++ b/enaml/qt/qt_container.py @@ -347,8 +347,12 @@ def _update_sizes(self): """ widget = self.widget widget.setSizeHint(self.compute_best_size()) - widget.setMinimumSize(self.compute_min_size()) - widget.setMaximumSize(self.compute_max_size()) + if not isinstance(widget.parent(), QContainer): + # Only set min and max size if the parent is not a container. + # The layout manager needs to be the ultimate authority when + # dealing with nested containers. + widget.setMinimumSize(self.compute_min_size()) + widget.setMaximumSize(self.compute_max_size()) def _build_refresher(self, manager): """ Build the refresh function for the container. diff --git a/enaml/qt/qt_push_button.py b/enaml/qt/qt_push_button.py index c162b46c0..83892fa04 100644 --- a/enaml/qt/qt_push_button.py +++ b/enaml/qt/qt_push_button.py @@ -31,6 +31,13 @@ def create_widget(self): """ self.widget = QPushButton(self.parent_widget()) + def init_widget(self): + """ Initialize the state of the widget. + + """ + super(QtPushButton, self).init_widget() + self.set_default(self.declaration.default) + def init_layout(self): """ Handle layout initialization for the push button. @@ -72,3 +79,14 @@ def child_removed(self, child): super(QtPushButton, self).child_removed(child) if isinstance(child, QtMenu): self.widget.setMenu(self.menu()) + + #-------------------------------------------------------------------------- + # ProxyPushButton API + #-------------------------------------------------------------------------- + def set_default(self, default): + """ Set the default button behavior for the widget. + + """ + self.widget.setDefault(default) + if default: + self.widget.setFocus() diff --git a/enaml/src/winutil.cpp b/enaml/src/winutil.cpp new file mode 100644 index 000000000..c91219e12 --- /dev/null +++ b/enaml/src/winutil.cpp @@ -0,0 +1,192 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file COPYING.txt, distributed with this software. +|----------------------------------------------------------------------------*/ +// Get access to the icon defines in Winuser.h +#ifndef OEMRESOURCE +#define OEMRESOURCE +#endif +#include +#include "pythonhelpersex.h" + + +using namespace PythonHelpers; + + +// Builtin Icons +static PyObject* Py_OIC_SAMPLE; +static PyObject* Py_OIC_HAND; +static PyObject* Py_OIC_QUES; +static PyObject* Py_OIC_BANG; +static PyObject* Py_OIC_NOTE; +static PyObject* Py_OIC_WINLOGO; +static PyObject* Py_OIC_WARNING; +static PyObject* Py_OIC_ERROR; +static PyObject* Py_OIC_INFORMATION; +#if(WINVER >= 0x0600) +static PyObject* Py_OIC_SHIELD; +#endif + + +typedef struct { + PyObject_HEAD; + UINT value; +} WinEnum; + + +static PyTypeObject +WinEnum_Type = { + PyObject_HEAD_INIT( 0 ) + 0, /* ob_size */ + "winutil.WinEnum", /* tp_name */ + sizeof( WinEnum ), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyObject_Del, /* tp_dealloc */ + (printfunc)0, /* tp_print */ + (getattrfunc)0, /* tp_getattr */ + (setattrfunc)0, /* tp_setattr */ + (cmpfunc)0, /* tp_compare */ + (reprfunc)0, /* tp_repr */ + (PyNumberMethods*)0, /* tp_as_number */ + (PySequenceMethods*)0, /* tp_as_sequence */ + (PyMappingMethods*)0, /* tp_as_mapping */ + (hashfunc)0, /* tp_hash */ + (ternaryfunc)0, /* tp_call */ + (reprfunc)0, /* tp_str */ + (getattrofunc)0, /* tp_getattro */ + (setattrofunc)0, /* tp_setattro */ + (PyBufferProcs*)0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + 0, /* Documentation string */ + (traverseproc)0, /* tp_traverse */ + (inquiry)0, /* tp_clear */ + (richcmpfunc)0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + (getiterfunc)0, /* tp_iter */ + (iternextfunc)0, /* tp_iternext */ + (struct PyMethodDef*)0, /* tp_methods */ + (struct PyMemberDef*)0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + (descrgetfunc)0, /* tp_descr_get */ + (descrsetfunc)0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)0, /* tp_init */ + (allocfunc)PyType_GenericAlloc, /* tp_alloc */ + (newfunc)0, /* tp_new */ + (freefunc)0, /* tp_free */ + (inquiry)0, /* tp_is_gc */ + 0, /* tp_bases */ + 0, /* tp_mro */ + 0, /* tp_cache */ + 0, /* tp_subclasses */ + 0, /* tp_weaklist */ + (destructor)0 /* tp_del */ +}; + + +static PyObject* +PyString_FromHICON( HICON icon, int& width_out, int& height_out ) +{ + HDC screen_device = GetDC( 0 ); + HDC hdc = CreateCompatibleDC( screen_device ); + ReleaseDC( 0, screen_device ); + + ICONINFO icon_info; + GetIconInfo( icon, &icon_info ); + int w = icon_info.xHotspot * 2; + int h = icon_info.yHotspot * 2; + + BITMAPINFO bmi; + memset( &bmi, 0, sizeof( bmi ) ); + bmi.bmiHeader.biSize = sizeof( BITMAPINFOHEADER ); + bmi.bmiHeader.biWidth = w; + bmi.bmiHeader.biHeight = -h; // flip the origin to top-left + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + VOID* bits; + + HBITMAP win_bitmap = CreateDIBSection( hdc, &bmi, DIB_RGB_COLORS, &bits, 0, 0 ); + HGDIOBJ old_hdc = ( HBITMAP )SelectObject( hdc, win_bitmap ); + DrawIconEx( hdc, 0, 0, icon, w, h, 0, 0, DI_NORMAL ); + + PyObject* result = PyString_FromStringAndSize( ( const char* )bits, w * h * 4 ); + + // dispose resources created by GetIconInfo + DeleteObject( icon_info.hbmMask ); + DeleteObject( icon_info.hbmColor ); + + SelectObject( hdc, old_hdc ); // restore state + DeleteObject( win_bitmap ); + DeleteDC( hdc ); + + width_out = w; + height_out = h; + return result; +} + + +static PyObject* +load_icon( PyObject* mod, PyObject* args ) +{ + WinEnum* win_enum; + if( !PyArg_ParseTuple( args, "O!", &WinEnum_Type, &win_enum ) ) + return 0; + HANDLE hicon = LoadImage( + 0, MAKEINTRESOURCE( win_enum->value ), IMAGE_ICON, 0, 0, LR_SHARED + ); + if( !hicon ) + return Py_BuildValue( "(s, (i, i))", "", -1, -1 ); + int width, height; + PyObjectPtr result( PyString_FromHICON( ( HICON )hicon, width, height ) ); + if( !result ) + return 0; + return Py_BuildValue( "(O, (i, i))", result.get(), width, height ); +} + + +static PyMethodDef +winutil_methods[] = { + { "load_icon", ( PyCFunction )load_icon, METH_VARARGS, + "Load a builtin Windows icon" }, + { 0 } // Sentinel +}; + + +#define MAKE_ENUM( TOKEN, VALUE ) \ + do { \ + TOKEN = PyType_GenericNew( &WinEnum_Type, 0, 0 ); \ + if( !TOKEN ) \ + return; \ + reinterpret_cast( TOKEN )->value = VALUE; \ + if( PyModule_AddObject( mod, #VALUE, newref( TOKEN ) ) < 0 ) \ + return; \ + } while( 0 ) + + +PyMODINIT_FUNC +initwinutil( void ) +{ + PyObject* mod = Py_InitModule( "winutil", winutil_methods ); + if( !mod ) + return; + if( PyType_Ready( &WinEnum_Type ) ) + return; + MAKE_ENUM( Py_OIC_SAMPLE, OIC_SAMPLE ); + MAKE_ENUM( Py_OIC_HAND, OIC_HAND ); + MAKE_ENUM( Py_OIC_QUES, OIC_QUES ); + MAKE_ENUM( Py_OIC_BANG, OIC_BANG ); + MAKE_ENUM( Py_OIC_NOTE, OIC_NOTE ); + MAKE_ENUM( Py_OIC_WINLOGO, OIC_WINLOGO ); + MAKE_ENUM( Py_OIC_WARNING, OIC_WARNING ); + MAKE_ENUM( Py_OIC_ERROR, OIC_ERROR ); + MAKE_ENUM( Py_OIC_INFORMATION, OIC_INFORMATION ); + #if(WINVER >= 0x0600) + MAKE_ENUM( Py_OIC_SHIELD, OIC_SHIELD ); + #endif +} diff --git a/enaml/stdlib/dialog_buttons.enaml b/enaml/stdlib/dialog_buttons.enaml new file mode 100644 index 000000000..88e7ed427 --- /dev/null +++ b/enaml/stdlib/dialog_buttons.enaml @@ -0,0 +1,95 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#------------------------------------------------------------------------------ +from atom.api import Atom, Bool, Enum, Unicode +from enaml.core.looper import Looper +from enaml.layout.layout_helpers import hbox +from enaml.widgets.container import Container +from enaml.widgets.push_button import PushButton + + +class DialogButton(Atom): + """ A class for specifying a button in a button box. + + Instances of this class are created by users to specify the buttons + which will be shown in a DialogButtonBox. + + """ + #: The text for the button. + text = Unicode() + + #: The dialog action to perform when the button is clicked. + action = Enum('accept', 'reject') + + #: Whether or not the button is the default button for the dialog. + default = Bool(False) + + #: Whether or not the button is enabled. The button will subscribe + #: to this attribute, and reflect the changes at runtime. + enabled = Bool(True) + + #: Whether or not the button was clicked by the user. + was_clicked = Bool(False) + + def __init__(self, text, action, **kwargs): + """ Initialize a DialogButton. + + Parameters + ---------- + text : unicode + The unicode label for the button. + + action : 'accept' or 'reject' + The dialog action to perform when the button is clicked. + + **kwargs + Additional optional state to apply to the button. + + """ + super(DialogButton, self).__init__(text=text, action=action, **kwargs) + + +enamldef DialogButtonBox(Container): box: + """ A component for defining a button box for a dialog. + + The dialog button box must be used as a decendant of a Dialog, and + relies on dynamic scoping to invoke the dialog action when a button + is clicked. The button widgets created by the dialog can be styled + using the style class 'dialog-box-button'. + + Attributes + ---------- + buttons : list + A list of DialogButton objects which represent the buttons to + create for the dialog box. This value should be set before the + widget is shown. Dynamic changes will not update the UI. + + Events + ------ + clicked + This event will be emitted when a button is clicked, but before + the dialog action is taken. The payload will be the DialogButton + instance for the button which was clicked. + + """ + attr buttons: list = [] + event clicked: DialogButton + padding = 0 + hug_width = 'strong' + hug_height = 'strong' + constraints = [hbox(*sum(looper.items, []), spacing=6)] + Looper: looper: + iterable = buttons + PushButton: + style_class = 'dialog-box-button' + text = loop_item.text + default = loop_item.default + enabled << loop_item.enabled + clicked :: + loop_item.was_clicked = True + box.clicked(loop_item) + nonlocals[loop_item.action]() diff --git a/enaml/stdlib/message_box.enaml b/enaml/stdlib/message_box.enaml new file mode 100644 index 000000000..059d333d8 --- /dev/null +++ b/enaml/stdlib/message_box.enaml @@ -0,0 +1,309 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#------------------------------------------------------------------------------ +import sys + +from enaml.core.dynamic_template import DynamicTemplate +from enaml.image import Image +from enaml.layout.layout_helpers import align, hbox, spacer +from enaml.stdlib.dialog_buttons import DialogButtonBox, DialogButton +from enaml.stdlib.task_dialog import (TaskDialogBody, TaskDialogContentArea, + TaskDialogCommandArea, TaskDialogDetailsArea, TaskDialogIconArea, + TaskDialogInstructionArea, TaskDialogStyleSheet) +from enaml.widgets.check_box import CheckBox +from enaml.widgets.dialog import Dialog +from enaml.widgets.html import Html +from enaml.widgets.image_view import ImageView +from enaml.widgets.label import Label + + +enamldef MessageBox(Dialog): + """ A dialog which provides the functionality of a message box. + + This element uses the task dialog components to assemble a dialog + based on the provided attribute values. + + Simple versions of this dialog can be launched with the functions + 'about', 'critical', 'information', 'question', and 'warning' + defined in this module. These functions are intended to be simple + and do not provide all options available on this element. If full + control over the message box is needed, use this element directly. + + Attributes + ---------- + image: Image, optional + The image to use for the icon in the dialog body icon area. If + this is not provided, no icon area will be generated. + + text : basetring + The text to display in the instruction area of the dialog. This + should always be provided. + + content : basestring, optional + The secondary text to place in the content area of the dialog. + + details : basestring, optional + Extra information to show in the details area. This string will + be provided to an Html area, and can therefore contain html + markup. If this is provided, the details area will be togglable + via a check box placed in the dialog command area. + + buttons : list + The list of DialogButton objects which define the buttons to + display in a DialogButtonBox which is placed in the dialog + command area. This should always be provided. + + """ + attr image: Image + attr text: basestring + attr content: basestring + attr details: basestring + attr buttons: list = [] + TaskDialogStyleSheet: + pass + TaskDialogBody: + attr show_details = False + DynamicTemplate: + base = _MessageBoxBody + args = (bool(image), bool(content), bool(details)) + + +def about(parent, title, text): + """ Display a simple about box with title and text. + + Parameters + ---------- + parent : Widget or None + The Enaml widget which should be the parent of the dialog. + + title : basestring + The text for the window title bar. + + text : basestring + The main text to display in the dialog. + + """ + buttons = [DialogButton('OK', 'accept')] + _exec_box(parent, title, text, buttons, '') + + +def critical(parent, title, text, buttons=None): + """ Display an critical message box with title and text. + + On Windows, the dialog will display the stock critical icon. + + Parameters + ---------- + parent : Widget or None + The Enaml widget which should be the parent of the dialog. + + title : basestring + The text for the window title bar. + + buttons : list, optional + The list of DialogButton instances to display in the dialog. + If this is not provided, an 'OK' button will be created. + + Returns + ------- + result : DialogButton or None + The dialog button object which was clicked, or None if the + dialog was closed without clicking a dialog button. + + """ + buttons = buttons or [DialogButton('OK', 'accept')] + return _exec_box(parent, title, text, buttons, 'critical') + + +def information(parent, title, text, buttons=None): + """ Display an information message box with title and text. + + On Windows, the dialog will display the stock information icon. + + Parameters + ---------- + parent : Widget or None + The Enaml widget which should be the parent of the dialog. + + title : basestring + The text for the window title bar. + + buttons : list, optional + The list of DialogButton instances to display in the dialog. + If this is not provided, an 'OK' button will be created. + + Returns + ------- + result : DialogButton or None + The dialog button object which was clicked, or None if the + dialog was closed without clicking a dialog button. + + """ + buttons = buttons or [DialogButton('OK', 'accept')] + return _exec_box(parent, title, text, buttons, 'information') + + +def question(parent, title, text, buttons=None): + """ Display a question message box with title and text. + + On Windows, the dialog will display the stock question icon. + + Parameters + ---------- + parent : Widget or None + The Enaml widget which should be the parent of the dialog. + + title : basestring + The text for the window title bar. + + buttons : list, optional + The list of DialogButton instances to display in the dialog. + If this is not provided, 'Yes'|'No' buttons will be created. + + Returns + ------- + result : DialogButton or None + The dialog button object which was clicked, or None if the + dialog was closed without clicking a dialog button. + + """ + buttons = buttons or [DialogButton('Yes', 'accept'), + DialogButton('No', 'reject')] + return _exec_box(parent, title, text, buttons, 'question') + + +def warning(parent, title, text, buttons=None): + """ Display a warning message box with title and text. + + On Windows, the dialog will display the stock warning icon. + + Parameters + ---------- + parent : Widget or None + The Enaml widget which should be the parent of the dialog. + + title : basestring + The text for the window title bar. + + buttons : list, optional + The list of DialogButton instances to display in the dialog. + If this is not provided, an 'OK' button will be created. + + Returns + ------- + result : DialogButton or None + The dialog button object which was clicked, or None if the + dialog was closed without clicking a dialog button. + + """ + buttons = buttons or [DialogButton('OK', 'accept')] + return _exec_box(parent, title, text, buttons, 'warning') + + +#------------------------------------------------------------------------------ +# Private API +#------------------------------------------------------------------------------ +template _BodyImage(HasImage: False): pass +template _BodyImage(HasImage: True): + TaskDialogIconArea: + ImageView: + hug_width = 'strong' + hug_height = 'strong' + image = nonlocals(1).image + + +template _BodyContent(HasContent: False): pass +template _BodyContent(HasContent: True): + TaskDialogContentArea: + Label: + text = content + style_class = 'task-dialog-content' + + +template _BodyDetails(HasDetails: False): pass +template _BodyDetails(HasDetails: True): + TaskDialogDetailsArea: + visible << show_details + hug_height = 'ignore' + Html: + source = details + + +template _BodyCommand(HasDetails: False): + TaskDialogCommandArea: + constraints = [hbox(spacer, buttons)] + DialogButtonBox: buttons: + buttons = nonlocals(1).buttons + + +template _BodyCommand(HasDetails: True): + TaskDialogCommandArea: + constraints = [ + hbox(details_cb, spacer, buttons), + align('v_center', details_cb, buttons), + ] + CheckBox: details_cb: + text = 'More details' + checked := show_details + DialogButtonBox: buttons: + buttons = nonlocals(1).buttons + + +template _MessageBoxBody(HasImage, HasContent, HasDetails): + TaskDialogInstructionArea: + Label: + text = nonlocals(1).text or u'' + style_class = 'task-dialog-instructions' + _BodyImage(HasImage): pass + _BodyContent(HasContent): pass + _BodyDetails(HasDetails): pass + _BodyCommand(HasDetails): pass + + +def _null_icon(): + return None + + +_ICONS = { + '': _null_icon, + 'critical': _null_icon, + 'information': _null_icon, + 'question': _null_icon, + 'warning': _null_icon, +} + + +if sys.platform == 'win32': + + from enaml import winutil + + def _win_icon(which): + image = [None] + def _maker(): + if image[0] is None: + data, size = winutil.load_icon(which) + image[0] = Image(data=data, raw_size=size, format='argb32') + return image[0] + return _maker + + _ICONS['critical'] = _win_icon(winutil.OIC_ERROR) + _ICONS['information'] = _win_icon(winutil.OIC_INFORMATION) + _ICONS['question'] = _win_icon(winutil.OIC_QUES) + _ICONS['warning'] = _win_icon(winutil.OIC_WARNING) + + +def _exec_box(parent, title, text, buttons, icon): + box = MessageBox() + box.title = title + box.text = text + box.image = _ICONS[icon]() + box.buttons = buttons + box.set_parent(parent) + box.exec_() + for button in box.buttons: + if button.was_clicked: + return button diff --git a/enaml/stdlib/task_dialog.enaml b/enaml/stdlib/task_dialog.enaml new file mode 100644 index 000000000..292bca702 --- /dev/null +++ b/enaml/stdlib/task_dialog.enaml @@ -0,0 +1,230 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#------------------------------------------------------------------------------ +from atom.api import set_default +from enaml.layout.layout_helpers import align, vertical +from enaml.styling import StyleSheet, Style, Setter +from enaml.widgets.container import Container +from enaml.widgets.separator import Separator + + +class _RigidContainer(Container): + """ A container which can never be size smaller than its size hint. + + This is used as a base class for the task dialog areas. + + """ + resist_width = set_default('required') + resist_height = set_default('required') + + +class TaskDialogIconArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide icon area content. + + """ + #: The icon area hugs its width hint strongly by default. + hug_width = set_default('strong') + + #: The default padding is set appropriate for common use cases. + padding = set_default((10, 0, 15, 10)) + + +class TaskDialogInstructionArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide instruction area content. + + """ + #: The instruction area hugs its height hint strongly by default. + hug_height = set_default('strong') + + #: The default padding is set appropriate for common use cases. + padding = set_default((10, 10, 15, 10)) + + +class TaskDialogContentArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide content area content. + + """ + #: The content area hugs its height hint strongly by default. + hug_height = set_default('strong') + + #: The default padding is set appropriate for common use cases. + padding = set_default((0, 10, 15, 10)) + + +class TaskDialogCommandArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide command area content. + + """ + #: The command area hugs its height hint strongly by default. + hug_height = set_default('strong') + + +class TaskDialogDetailsArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide details area content. + + """ + #: The details area hugs its height hint strongly by default. + hug_height = set_default('strong') + + #: The default padding is set appropriate for common use cases. + padding = set_default((0, 10, 15, 10)) + + def _observe_visible(self, change): + if change['type'] == 'update': + self.parent.request_relayout() + + +class TaskDialogFootnoteArea(_RigidContainer): + """ A custom container for use in a task dialog. + + User code can declare and instance of this element in the body of a + TaskDialogBody in order to provide footnote area content. + + """ + #: The footnote area hugs its height hint strongly by default. + hug_height = set_default('strong') + + +class TaskDialogBody(Container): + """ A custom container used to create a task dialog body. + + A TaskDialogBody element should be declared as the central widget + of a Dialog widget. The element layout logic which automatically + arranges the dialog area containers declared as children. + + The dialog body supports the following area children, all of which + are optional. User code should declare at most one of each type + of child: + + - TaskDialogIconArea + - TaskDialogInstructionArea + - TaskDialogContentArea + - TaskDialogDetailsArea + - TaskDialogCommandArea + - TaskDialogFootnoteArea + + No other widget types should be used as children of the dialog body. + + """ + #: The dialog body has no padding by default. + padding = set_default(0) + + #: The dialog body hugs its height hint strongly by default. + hug_height = set_default('strong') + + def initialize(self): + """ A reimplemented initialization method. + + This initializer will add a horizontal separator as a child of + the element if it detects a child footnote area. + + """ + super(TaskDialogBody, self).initialize() + for child in self.children: + if isinstance(child, TaskDialogFootnoteArea): + separator = Separator(self) + separator.style_class = 'footnote-separator' + separator.initialize() + break + + def layout_constraints(self): + """ A reimplemented constraints generation method. + + This method will setup a grid layout which is appropriate for + the task dialog area children defined for the dialog body. The + 'constraints' attribute is ignored entirely. + + """ + icon = inst = ctnt = dtls = cmnd = sepr = ftnt = None + for item in self.widgets(): + if isinstance(item, TaskDialogIconArea): + icon = item + elif isinstance(item, TaskDialogInstructionArea): + inst = item + elif isinstance(item, TaskDialogContentArea): + ctnt = item + elif isinstance(item, TaskDialogCommandArea): + cmnd = item + elif isinstance(item, TaskDialogDetailsArea): + dtls = item + elif isinstance(item, TaskDialogFootnoteArea): + ftnt = item + elif isinstance(item, Separator): + sepr = item + if dtls is not None and not dtls.visible: + dtls = None + t = self.contents_top + r = self.contents_right + b = self.contents_bottom + l = self.contents_left + cns = [ + vertical(t, inst, ctnt, dtls, cmnd, sepr, ftnt, b, spacing=0), + align('right', r, inst, ctnt, dtls, cmnd, sepr, ftnt), + align('left', l, cmnd, sepr, ftnt), + align('left', inst, ctnt, dtls), + ] + if icon is None: + cns.append(align('left', l, inst or ctnt or dtls)) + else: + cns.extend([ + vertical(t, icon, cmnd, spacing=0), + align('left', l, icon), + align('left', icon.right, inst or ctnt or dtls), + ]) + return cns + + +enamldef TaskDialogStyleSheet(StyleSheet): + """ A style sheet which provides basic styling for a task dialog. + + This stylesheet can be declared, and optionally extended, in the + body of a Dialog which contains a TaskDialogBody as its central + widget. It provides a styling theme which is consistent with the + look and feel of Windows 7/8 task dialogs. + + """ + Style: + element = ('TaskDialogIconArea, TaskDialogInstructionArea, ' + 'TaskDialogContentArea, TaskDialogDetailsArea') + Setter: + field = 'background' + value = 'white' + Style: + element = 'TaskDialogCommandArea' + Setter: + field = 'border-top' + value = '1px solid #D9D9D9' + Style: + element = 'Label' + style_class = 'task-dialog-instructions' + Setter: + field = 'font' + value = '12pt "Segoe UI"' + Setter: + field = 'color' + value = '#003399' + Style: + element = 'Label' + style_class = 'task-dialog-content' + Setter: + field = 'font' + value = '9pt "Segoe UI"' diff --git a/enaml/widgets/page.py b/enaml/widgets/page.py index db4e619c6..318d26e4d 100644 --- a/enaml/widgets/page.py +++ b/enaml/widgets/page.py @@ -76,7 +76,7 @@ def _update_proxy(self, change): """ Send the member state change to the proxy. """ - # The superclass implementation is suffient + # The superclass implementation is sufficient super(Page, self)._update_proxy(change) #-------------------------------------------------------------------------- diff --git a/enaml/widgets/push_button.py b/enaml/widgets/push_button.py index b4854655e..37d392a60 100644 --- a/enaml/widgets/push_button.py +++ b/enaml/widgets/push_button.py @@ -5,7 +5,9 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ -from atom.api import Typed, ForwardTyped +from atom.api import Bool, Typed, ForwardTyped, observe + +from enaml.core.declarative import d_ from .abstract_button import AbstractButton, ProxyAbstractButton from .menu import Menu @@ -18,11 +20,17 @@ class ProxyPushButton(ProxyAbstractButton): #: A reference to the PushButton declaration. declaration = ForwardTyped(lambda: PushButton) + def set_default(self, default): + raise NotImplementedError + class PushButton(AbstractButton): """ A button control represented by a standard push button widget. """ + #: Whether this button is the default action button in a dialog. + default = d_(Bool(False)) + #: A reference to the ProxyPushButton object. proxy = Typed(ProxyPushButton) @@ -33,3 +41,14 @@ def menu(self): for child in reversed(self.children): if isinstance(child, Menu): return child + + #-------------------------------------------------------------------------- + # Observers + #-------------------------------------------------------------------------- + @observe('default') + def _update_proxy(self, change): + """ Send the member state change to the proxy. + + """ + # The superclass implementation is sufficient + super(PushButton, self)._update_proxy(change) diff --git a/enaml/wx/wx_push_button.py b/enaml/wx/wx_push_button.py index c6b138f7f..ff4942e79 100644 --- a/enaml/wx/wx_push_button.py +++ b/enaml/wx/wx_push_button.py @@ -59,3 +59,12 @@ def set_checked(self, checked): """ pass + + #-------------------------------------------------------------------------- + # ProxyPushButton API + #-------------------------------------------------------------------------- + def set_default(self, default): + """ This is not supported on Wx. + + """ + pass diff --git a/setup.py b/setup.py index 59dbb5f0b..65f838edc 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ +import sys from setuptools import setup, find_packages, Extension @@ -52,6 +53,17 @@ ] +if sys.platform == 'win32': + ext_modules.append( + Extension( + 'enaml.winutil', + ['enaml/src/winutil.cpp'], + libraries=['user32', 'gdi32'], + language='c++' + ) + ) + + setup( name='enaml', version='0.8.7',