# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2005-2012 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public License
## as published by the Free Software Foundation; either version 2
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
""" Basic dialogs definition """
import inspect
import logging
import gtk
from kiwi.ui.dialogs import error, warning, info, yesno
from kiwi.ui.delegates import GladeDelegate
from kiwi.ui.views import BaseView
from kiwi.utils import gsignal
from zope.interface import implementer
from stoqlib.lib.decorators import public
from stoqlib.lib.translation import stoqlib_gettext
from stoqlib.lib.interfaces import ISystemNotifier
from stoqlib.gui.base.gtkadds import change_button_appearance
from stoqlib.gui.base.messagebar import MessageBar
from stoqlib.gui.events import DialogCreateEvent, RunDialogEvent
_ = stoqlib_gettext
_toplevel_stack = []
log = logging.getLogger(__name__)
#
# Helper classes
#
[docs]class RunnableView:
"""A mixin class for any View or GladeDelegate that offers run/close"""
retval = None
[docs] def close(self, *args):
"""Handles action to be performed when window is closed."""
self.get_toplevel().hide()
[docs] def destroy(self):
self.get_toplevel().destroy()
[docs] def run(self):
"""Handles action to be performed when window is opened. Defaults to
_open(), which blocks in a mainloop. Returns the value of the retval
attribute.
"""
self.show()
[docs] def get_current_toplevel(self):
return self.get_toplevel()
[docs] def set_transient_for(self, window):
self.get_toplevel().set_transient_for(window)
[docs]class BasicDialog(GladeDelegate, RunnableView):
"""Abstract class that offers a Dialog with two buttons. It should be
subclassed and customized.
"""
domain = 'stoq'
help_section = None
gsignal('confirm', object, retval=bool)
gsignal('cancel', object)
def __init__(self, main_label_text=None, title=" ",
header_text="", size=None, hide_footer=False,
delete_handler=None, help_section=None):
self.enable_confirm_validation = False
self._message_bar = None
self._create_dialog_ui()
if delete_handler is None:
delete_handler = self._delete_handler
GladeDelegate.__init__(self, delete_handler=delete_handler,
gladefile=self.gladefile)
help_section = help_section or self.help_section
if help_section:
self._add_help_button(help_section)
# FIXME: Create more widgets lazily when needed
self.set_title(title)
if size:
self.get_toplevel().set_size_request(*size)
if main_label_text:
self.main_label.set_text(main_label_text)
if header_text:
header_label = gtk.Label()
header_label.set_markup(header_text)
self.header.add(header_label)
header_label.show()
if hide_footer:
self.hide_footer()
DialogCreateEvent.emit(self)
#
# Private
#
def _create_dialog_ui(self):
self.toplevel = gtk.Dialog()
self._main_vbox = self.toplevel.get_content_area()
self.vbox = gtk.VBox()
self._main_vbox.pack_start(self.vbox, True, True)
self.vbox.show()
# FIXME
# stoqlib/gui/base/search.py - hides the header
self.header = gtk.EventBox()
self.vbox.pack_start(self.header, False, False)
self.header.show()
self.main = gtk.EventBox()
self.vbox.pack_start(self.main, padding=6)
self.main.show()
# FIXME
# stoqlib/gui/dialogs/importerdialog.py - setting
# stoqlib/gui/base/lists.py - removes the label
# stoqlib/gui/base/search.py - removes the label
# plugins/ecf/deviceconstanteditor.py - removes the label
self.main_label = gtk.Label()
self.main.add(self.main_label)
self.main_label.show()
hbox1 = gtk.HBox()
self.vbox.pack_start(hbox1, False, 6)
hbox1.show()
# FIXME
# stoqlib/gui/dialogs/paymentmethod.py
# stoqlib/gui/search/salesearch.py
self.extra_holder = gtk.EventBox()
hbox1.pack_start(self.extra_holder, True, True, 6)
self.extra_holder.show()
# FIXME
# stoqlib/gui/search/productsearch.py
# stoqlib/gui/search/servicesearch.py
self.print_holder = gtk.EventBox()
hbox1.pack_start(self.print_holder, True, True)
self.print_holder.show()
# FIXME
# stoqlib/gui/base/search.py
# stoqlib/gui/slaves/productslave.py
self.details_holder = gtk.EventBox()
hbox1.pack_end(self.details_holder, False, False, 6)
self.details_holder.show()
# FIXME
# stoqlib/gui/dialogs/quotedialog.py
self.notice = gtk.EventBox()
hbox1.pack_start(self.notice, False, 6)
self.notice.show()
action_area = self.toplevel.get_action_area()
action_area.set_border_width(6)
action_area.set_layout(gtk.BUTTONBOX_END)
self.cancel_button = gtk.Button(stock=gtk.STOCK_CANCEL)
action_area.pack_start(self.cancel_button, True, True, 6)
self.cancel_button.show()
self.ok_button = gtk.Button(stock=gtk.STOCK_OK)
self.ok_button.set_use_underline(True)
action_area.pack_start(self.ok_button, True, True, 6)
self.ok_button.show()
def _try_confirm(self, *args):
"""Only confirm if ok button is actually enabled.
This is so that this dialog doesn't get confirmed in case the ok
button was specifically disabled.
"""
# FIXME: There should be a better way to findout valid status
if self.ok_button.get_sensitive():
self.confirm()
def _add_help_button(self, section):
def on_help__clicked(button):
from stoqlib.gui.utils.help import show_section
show_section(section)
self.action_area.set_layout(gtk.BUTTONBOX_END)
self.help_button = gtk.Button(stock=gtk.STOCK_HELP)
self.help_button.connect('clicked', on_help__clicked)
self.action_area.pack_start(self.help_button, False, False, 6)
self.action_area.set_child_secondary(self.help_button, True)
self.help_button.show()
#
# Public API
#
[docs] def confirm(self):
# SearchDialog and SellableSearch overrides this
self.retval = True
# FIXME: Confirm validation should be enabled by default,
# but we need to change the dialog API and existing
# callsites for that to work.
if (self.enable_confirm_validation and not
self.emit('confirm', self.retval)):
return
self.close()
[docs] def cancel(self):
# SearchDialog overrides this
self.retval = False
# FIXME: See fixme on confirm
if (self.enable_confirm_validation and
not self.emit('cancel', self.retval)):
return
self.close()
[docs] def enable_ok(self):
self.ok_button.set_sensitive(True)
[docs] def disable_ok(self):
self.ok_button.set_sensitive(False)
[docs] def set_ok_label(self, text, icon=None):
if not icon:
icon = gtk.STOCK_OK
change_button_appearance(self.ok_button, icon, text)
[docs] def set_cancel_label(self, text, icon=None):
if not icon:
icon = gtk.STOCK_CANCEL
change_button_appearance(self.cancel_button, icon, text)
[docs] def add(self, widget):
for child in self.main.get_children():
self.main.remove(child)
self.main.add(widget)
@property
def action_area(self):
return self.get_toplevel().action_area
[docs] def set_message(self, message, message_type=None):
"""Sets a message for this editor
:param message: message to add or None to remove previous message
:param message_type: type of message to add
"""
if message_type is None:
message_type = gtk.MESSAGE_INFO
if self._message_bar is not None:
self._message_bar.destroy()
self._message_bar = None
if message is None:
return
self._message_bar = MessageBar(message, message_type)
self._main_vbox.pack_start(self._message_bar, False, False)
self._main_vbox.reorder_child(self._message_bar, 0)
self._message_bar.show_all()
return self._message_bar
#
# Kiwi handlers
#
def _delete_handler(self, window, event):
self.cancel()
#
# General methods
#
[docs]def get_current_toplevel():
if _toplevel_stack:
return _toplevel_stack[-1]
[docs]def add_current_toplevel(toplevel):
_toplevel_stack.append(toplevel)
def _pop_current_toplevel():
if _toplevel_stack:
_toplevel_stack.pop()
[docs]def get_dialog(parent, dialog, *args, **kwargs):
"""Returns a dialog.
:param parent: the window which is opening the dialog
:param dialog: the dialog class or instance
:param args: custom positional arguments
:param kwargs: custom keyword arguments
"""
if callable(dialog):
dialog = dialog(*args, **kwargs)
# If parent is a BaseView, use GTK+ calls to get the toplevel
# window. This is a bit of a hack :-/
if isinstance(parent, BaseView):
parent = parent.get_toplevel().get_toplevel()
if parent and not _fullscreen:
dialog.set_transient_for(parent)
return dialog
[docs]def has_modal_window():
"""Verifies if any of the currently open windows are modal.
:returns: ``True`` if there are any modal windows open, otherwise ``False``
"""
windows = _toplevel_stack
return any(window.get_modal() for window in windows)
@public(since="1.5.0")
[docs]def run_dialog(dialog, parent=None, *args, **kwargs):
"""Runs a dialog and return the return value of it.
If dialog is a class it will be instantiated before running the dialog.
:param dialog: the dialog class
:param parent: parent of the dialog
:param args: custom positional arguments
:param kwargs: custom keyword arguments
"""
if dialog is None:
raise TypeError("dialog cannot be None")
dialog = RunDialogEvent.emit(dialog, parent, *args, **kwargs)
if not issubclass(dialog, RunnableView):
raise TypeError("dialog %r must be subclass of RunnableView" % (
dialog, ))
# FIXME: Move this into RunnableView
#XXX: When create a new Stoq window, even using the parent correctly:
# The called dialog will be a modal of the latest created window
# (not necessarily the window that called).
parent = getattr(parent, 'main_dialog', parent)
parent = parent or get_current_toplevel()
if inspect.isclass(dialog):
dialog_name = dialog.__name__
else:
dialog_name = dialog.__class__.__name__
dialog = get_dialog(parent, dialog, *args, **kwargs)
orig_dialog = dialog
toplevel = dialog.get_current_toplevel()
add_current_toplevel(toplevel)
if _fullscreen is not None:
toplevel.set_position(gtk.WIN_POS_CENTER)
elif parent and isinstance(parent, gtk.Window) and parent.props.visible:
toplevel.set_transient_for(parent)
toplevel.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
else:
if parent:
toplevel.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
else:
toplevel.set_position(gtk.WIN_POS_CENTER)
# FIXME: This should not be necessary, but gnome shell hides window
# decorations for HINT_DIALOG. We should study what dialogs should
# have HINT_NORMAL (with window decorations) and what can have
# HINT_DIALOG
toplevel.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL)
log.info("%s: Opening" % dialog_name)
if hasattr(parent, 'on_dialog__opened'):
parent.on_dialog__opened(orig_dialog)
retval = run_dialog_internal(dialog, parent, toplevel)
_pop_current_toplevel()
return retval
# Mainly for mocking
[docs]def run_dialog_internal(dialog, parent, toplevel):
# FIXME: We should avoid calling dialog.run() here
# See http://tinyurl.com/lj6ske4
toplevel.run()
retval = dialog.retval
dialog.destroy()
return retval
_fullscreen = None
[docs]def push_fullscreen(window):
global _fullscreen
_fullscreen = window
[docs]def pop_fullscreen(window):
global _fullscreen
_fullscreen = None
@implementer(ISystemNotifier)
[docs]class DialogSystemNotifier:
[docs] def info(self, short, description):
info(short, description, get_current_toplevel())
[docs] def warning(self, short, description, *args, **kwargs):
return warning(short, description, get_current_toplevel(), *args,
**kwargs)
[docs] def error(self, short, description):
error(short, description, get_current_toplevel())
[docs] def yesno(self, text, default=None, *verbs):
if default is None:
default = gtk.RESPONSE_YES
if len(verbs) != 2:
raise ValueError(
"Button descriptions must be a tuple with 2 items")
if verbs == (_("Yes"), _("No")):
buttons = gtk.BUTTONS_YES_NO
else:
buttons = ((verbs[0], gtk.RESPONSE_YES),
(verbs[1], gtk.RESPONSE_NO))
return (yesno(text, get_current_toplevel(), default, buttons)
== gtk.RESPONSE_YES)