# -*- Mode: Python; coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2011-2013 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU 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 General Public License for more details.
##
## You should have received a copy of the GNU 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>
##
import datetime
import locale
import logging
import platform
import os
import operator
import gtk
import glib
from kiwi.component import get_utility
from kiwi.environ import environ
from kiwi.ui.delegates import GladeDelegate
from stoqlib.api import api
from stoqlib.domain.views import ClientWithSalesView
from stoqlib.gui.base.dialogs import (add_current_toplevel,
get_current_toplevel,
run_dialog)
from stoqlib.gui.base.messagebar import MessageBar
from stoqlib.gui.editors.preferenceseditor import PreferencesEditor
from stoqlib.gui.events import StartApplicationEvent, StopApplicationEvent
from stoqlib.gui.utils.help import show_contents, show_section
from stoqlib.gui.utils.introspection import introspect_slaves
from stoqlib.gui.utils.keybindings import get_accel, get_accels
from stoqlib.gui.utils.logo import render_logo_pixbuf
from stoqlib.gui.utils.openbrowser import open_browser
from stoqlib.lib.interfaces import IAppInfo, IApplicationDescriptions
from stoqlib.lib.message import error, yesno
from stoqlib.lib.permissions import PermissionManager
from stoqlib.lib.pluginmanager import InstalledPlugin, get_plugin_manager
from stoqlib.lib.translation import (stoqlib_gettext, stoqlib_ngettext,
locale_sorted)
from stoqlib.lib.webservice import WebService
from stoqlib.gui.stockicons import STOQ_LAUNCHER
from stoqlib.gui.widgets.toolmenuaction import ToolMenuAction
from stoq.gui.shell.statusbar import ShellStatusbar
from stoq.lib.applist import get_application_icon, Application
import stoq
_ = stoqlib_gettext
log = logging.getLogger(__name__)
[docs]class ShellWindow(GladeDelegate):
"""
A Shell window is a
- Window
- Menubar
- Toolbar
- Application box
- Statusbar w/ Feedback button
It contain common menu items for:
- Opening a new Window
- Signing out
- Changing password
- Closing the application
- Printing
- Editing user preferences
- Spreedshet
- Toggle toolbar and statusbar visibility
- View Fullscreen
- Help menu (Chat, Content, Translation, Support, About)
The main function is to create the common ui and switch between different
applications.
"""
app_title = _('Stoq')
action_permissions = {}
# FIXME: we should have a common structure for all information regarding
# Actions
common_action_permissions = {
'HelpChat': ('app.common.help_chat', PermissionManager.PERM_ACCESS),
'HelpTranslate': ('app.common.help_translate', PermissionManager.PERM_ACCESS),
'HelpContents': ('app.common.help_contents', PermissionManager.PERM_ACCESS),
'HelpSupport': ('app.common.help_support', PermissionManager.PERM_ACCESS),
'HelpHelp': ('app.common.help', PermissionManager.PERM_ACCESS),
}
def __init__(self, options, shell, store):
"""Creates a new window
:param options: optparse options
:param shell: the shell
:param store: a store
"""
self._action_groups = {}
self._help_ui = None
self._osx_app = None
self.current_app = None
self.current_app_widget = None
self.shell = shell
self.uimanager = gtk.UIManager()
self.in_ui_test = False
self.tool_items = []
self.options = options
self.store = store
self._pre_launcher_init()
GladeDelegate.__init__(self,
gladefile=self.gladefile)
self._create_ui()
self._launcher_ui_bootstrap()
def _pre_launcher_init(self):
if platform.system() == 'Darwin':
import gtk_osxapplication
self._osx_app = gtk_osxapplication.OSXApplication()
self._osx_app.connect(
'NSApplicationBlockTermination',
self._on_osx__block_termination)
self._osx_app.set_use_quartz_accelerators(True)
self._app_settings = api.user_settings.get('app-ui', {})
self._create_shared_actions()
if self.options.debug:
self.add_debug_ui()
self.main_vbox = gtk.VBox()
#
# Private
#
def _create_shared_actions(self):
group = get_accels('app.common')
actions = [
('menubar', ),
('toolbar', ),
# Menus
('FileMenu', None, _("_File")),
('FileMenuNew', None),
("NewMenu", None, _("New")),
("HomeMenu", STOQ_LAUNCHER, _("Home"), None, _("Go back to launcher")),
('NewWindow', None, _("_Window"),
group.get('new_window'),
_('Opens up a new window')),
('Close', None, _('Close'),
group.get('close_window'),
_('Close the current view and go back to the initial screen')),
('ChangePassword', None, _('Change password...'),
group.get('change_password'),
_('Change the password for the currently logged in user')),
('SignOut', None, _('Sign out...'),
group.get('sign_out'),
_('Sign out the currently logged in user and login as another')),
('Print', gtk.STOCK_PRINT, _("Print..."),
group.get('print')),
('ExportSpreadSheet', gtk.STOCK_SAVE_AS, _('Export to spreadsheet...')),
("Quit", gtk.STOCK_QUIT, _('Quit'),
group.get('quit'),
_('Exit the application')),
# Edit
('EditMenu', None, _("_Edit")),
('Preferences', None, _("_Preferences"),
group.get('preferences'),
_('Show preferences')),
# View
('ViewMenu', None, _("_View")),
# Search
('SearchMenu', None, _("_Search")),
# Help
("HelpMenu", None, _("_Help")),
("HelpContents", gtk.STOCK_HELP, _("Contents"),
group.get('help_contents')),
("HelpTranslate", None, _("Translate Stoq..."), None,
_("Translate this application online")),
("HelpSupport", None, _("Get support online..."), None,
_("Get support for Stoq online")),
("HelpChat", None, _("Online chat..."), None,
_("Talk about Stoq online")),
("HelpAbout", gtk.STOCK_ABOUT),
# Toolbar
("NewToolMenu", None, _("New")),
("SearchToolMenu", None, _("Search")),
]
self.add_ui_actions(None, actions, filename='shellwindow.xml')
self.Close.set_sensitive(False)
toggle_actions = [
('ToggleToolbar', None, _("_Toolbar"),
group.get('toggle_toolbar'),
_('Show or hide the toolbar')),
('ToggleStatusbar', None, _("_Statusbar"),
group.get('toggle_statusbar'),
_('Show or hide the statusbar')),
('ToggleFullscreen', None, _("_Fullscreen"),
group.get('toggle_fullscreen'),
_('Enter or leave fullscreen mode')),
]
self.add_ui_actions('', toggle_actions, 'ToggleActions',
'toggle')
self.Print.set_short_label(_("Print"))
self.add_tool_menu_actions([
("NewToolItem", _("New"), '', gtk.STOCK_NEW),
("SearchToolItem", _("Search"), None, gtk.STOCK_FIND),
("HomeToolItem", _("Home"), None, STOQ_LAUNCHER),
])
self.NewToolItem.props.is_important = True
self.SearchToolItem.props.is_important = True
def _create_application_actions(self):
def callback(action, name):
self.switch_application(name)
self.application_actions = {}
actions = []
for app in self.get_available_applications():
action = gtk.Action(app.name, app.fullname, app.description, app.icon)
action.connect('activate', callback, app.name)
actions.append(action)
self.application_actions[app.name] = action
# By default, the menu comes with an 'Empty' item that we must hide
self.HomeToolItem.get_proxies()[0].get_menu().get_children()[-1].hide()
self.HomeToolItem.add_actions(self.uimanager, actions,
add_separator=False)
def _create_shared_ui(self):
self.ToggleToolbar.connect(
'notify::active', self._on_ToggleToolbar__notify_active)
self.ToggleStatusbar.connect(
'notify::active', self._on_ToggleStatusbar__notify_active)
self.ToggleFullscreen.connect(
'notify::active', self._on_ToggleFullscreen__notify_active)
self.toplevel.add(self.main_vbox)
self.main_vbox.show()
self.application_box = gtk.VBox()
self.main_vbox.pack_start(self.application_box)
self.application_box.show()
menubar = self.uimanager.get_widget('/menubar')
if self._osx_app:
self._osx_app.set_menu_bar(menubar)
else:
self.main_vbox.pack_start(menubar, False, False)
self.main_vbox.reorder_child(menubar, 0)
toolbar = self.uimanager.get_widget('/toolbar')
self.main_vbox.pack_start(toolbar, False, False)
self.main_vbox.reorder_child(toolbar, len(self.main_vbox) - 2)
self.statusbar = self._create_statusbar()
self.main_vbox.pack_start(self.statusbar, False, False)
self.main_vbox.reorder_child(self.statusbar, len(self.main_vbox) - 1)
menu_tool_button = self.SearchToolItem.get_proxies()[0]
# This happens when we couldn't set the GType of ToolMenuAction
# properly
if not hasattr(menu_tool_button, 'get_menu'):
return
search_tool_menu = menu_tool_button.get_menu()
# FIXME: For some reason, without this hack, some apps like Stock and
# Purchase shows an extra search tool menu labeled 'empty'
for child in search_tool_menu.get_children():
search_tool_menu.remove(child)
self._create_application_actions()
def _create_statusbar(self):
statusbar = ShellStatusbar(self)
# Set the initial text, the currently logged in user and the actual
# branch and station.
user = api.get_current_user(self.store)
station = api.get_current_station(self.store)
status_str = ' | '.join([
_("User: %s") % (user.get_description(),),
_("Computer: %s") % (station.name,),
"PID: %s" % (os.getpid(),)
])
statusbar.push(0, status_str)
return statusbar
def _osx_setup_menus(self):
self.Quit.set_visible(False)
self.HelpAbout.set_visible(False)
self.HelpAbout.set_label(_('About Stoq'))
self._osx_app.set_help_menu(
self.HelpMenu.get_proxies()[0])
self._osx_app.insert_app_menu_item(
self.HelpAbout.get_proxies()[0], 0)
self._osx_app.insert_app_menu_item(
gtk.SeparatorMenuItem(), 1)
self.Preferences.set_visible(False)
self._osx_app.insert_app_menu_item(
self.Preferences.get_proxies()[0], 2)
self._osx_app.ready()
def _launcher_ui_bootstrap(self):
self._restore_window_size()
self._update_toolbar_style()
self.hide_app(empty=True)
self._check_demo_mode()
self._check_online_plugins()
self._birthdays_bar = None
self._check_client_birthdays()
# json will restore tuples as lists. We need to convert them
# to tuples or the comparison bellow won't work.
actual_version = tuple(api.user_settings.get('actual-version', (0,)))
if stoq.stoq_version > actual_version:
api.user_settings.set('last-version-check', None)
self._display_changelog_message()
# Display the changelog message only once. Most users will never
# click on the "See what's new" button, and that will affect its
# visual identity.
api.user_settings.set('actual-version', stoq.stoq_version)
self._check_information()
if not stoq.stable and not api.is_developer_mode():
self._display_unstable_version_message()
if not self.in_ui_test:
# Initial fullscreen state for launcher must be handled
# separate since the window is not realized when the state loading
# is run in hide_app() the first time.
window = self.get_toplevel()
window.realize()
self.ToggleFullscreen.set_active(
self._app_settings.get('show-fullscreen', False))
self.ToggleFullscreen.notify('active')
toplevel = self.get_toplevel()
toplevel.connect('configure-event', self._on_toplevel__configure)
toplevel.connect('delete-event', self._on_toplevel__delete_event)
toplevel.add_accel_group(self.uimanager.get_accel_group())
# A GtkWindowGroup controls grabs (blocking mouse/keyboard interaction),
# by default all windows are added to the same window group.
# We want to avoid setting modallity on other windows
# when running a dialog using gtk_dialog_run/run_dialog.
window_group = gtk.WindowGroup()
window_group.add_window(toplevel)
def _check_online_plugins(self):
# For each online plugin, try to download and install it.
# Otherwise warn him
online_plugins = InstalledPlugin.get_pre_plugin_names(self.store)
if not online_plugins:
return
successes = []
manager = get_plugin_manager()
for plugin_name in online_plugins:
success, msg = manager.download_plugin(plugin_name)
successes.append(success)
if success:
manager.install_plugin(self.store, plugin_name)
online_plugins.remove(plugin_name)
if all(successes):
return
# Title
title = _('You have pending plugins.')
# Description
url = 'https://stoq.link/?source=stoq-plugin-alert&hash={}'.format(
api.sysparam.get_string('USER_HASH'))
desc = _(
'The following plugins need to be enabled: <b>{}</b>.\n\n'
'You can do it by registering on <a href="{}">Stoq.link</a>.'
).format(', '.join(online_plugins), url)
msg = '<b>%s</b>\n%s' % (title, desc)
self.add_info_bar(gtk.MESSAGE_WARNING, msg)
def _check_demo_mode(self):
if not api.sysparam.get_bool('DEMO_MODE'):
return
if api.user_settings.get('hide-demo-warning'):
return
button_label = _('Use my own data')
title = _('You are using Stoq with example data.')
desc = (_("Some features are limited due to fiscal reasons. "
"Click on '%s' to remove the limitations.")
% button_label)
msg = '<b>%s</b>\n%s' % (api.escape(title), api.escape(desc))
button = gtk.Button(button_label)
button.connect('clicked', self._on_enable_production__clicked)
self.add_info_bar(gtk.MESSAGE_WARNING, msg, action_widget=button)
def _check_client_birthdays(self):
if not api.sysparam.get_bool('BIRTHDAY_NOTIFICATION'):
return
# Display the info bar once per day
date = api.user_settings.get('last-birthday-check')
last_check = date and datetime.datetime.strptime(date, '%Y-%m-%d').date()
if last_check and last_check >= datetime.date.today():
return
# Only display the infobar if the user has access to calendar (because
# clicking on the button will open it) and to sales (because it
# requires that permission to be able to check client details)
user = api.get_current_user(self.store)
if not all([user.profile.check_app_permission(u'calendar'),
user.profile.check_app_permission(u'sales')]):
return
branch = api.get_current_branch(self.store)
clients_count = ClientWithSalesView.find_by_birth_date(
self.store, datetime.datetime.today(), branch=branch).count()
if clients_count:
msg = stoqlib_ngettext(
_("There is %s client doing birthday today!"),
_("There are %s clients doing birthday today!"),
clients_count) % (clients_count, )
button = gtk.Button(_("Check the calendar"))
button.connect('clicked', self._on_check_calendar__clicked)
self._birthdays_bar = self.add_info_bar(
gtk.MESSAGE_INFO,
"<b>%s</b>" % (glib.markup_escape_text(msg), ),
action_widget=button)
def _check_information(self):
"""Check some information with Stoq Web API
- Check if there are new versions of Stoq Available
- Check if this Stoq Instance uses Stoq Link (and send data to us if
it does).
"""
# Check version
self._version_checker = VersionChecker(self.store, self)
self._version_checker.check_new_version()
def _display_changelog_message(self):
msg = _("Welcome to Stoq version %s!") % stoq.version
button = gtk.Button(_("See what's new"))
button.connect('clicked', self._on_show_changelog__clicked)
self._changelog_bar = self.add_info_bar(gtk.MESSAGE_INFO, msg,
action_widget=button)
def _display_unstable_version_message(self):
msg = _(
'You are currently using an <b>unstable version</b> of Stoq that '
'is under development,\nbe aware that it may behave incorrectly, '
'crash or even loose your data.\n<b>Do not use in production.</b>')
self.add_info_bar(gtk.MESSAGE_WARNING, msg)
def _save_window_size(self):
if not hasattr(self, '_width'):
return
# Do not save the size of the window when we are in fullscreen
window = self.get_toplevel()
window = window.get_window()
if window.get_state() & gtk.gdk.WINDOW_STATE_FULLSCREEN:
return
d = api.user_settings.get('launcher-geometry', {})
d['width'] = str(self._width)
d['height'] = str(self._height)
d['x'] = str(self._x)
d['y'] = str(self._y)
def _restore_window_size(self):
d = api.user_settings.get('launcher-geometry', {})
try:
width = int(d.get('width', -1))
height = int(d.get('height', -1))
x = int(d.get('x', -1))
y = int(d.get('y', -1))
except ValueError:
pass
# Setup the default window size, for smaller sizes use
# 75% of the height or 600px if it's higher than 800px
if height == -1:
screen = gtk.gdk.screen_get_default()
height = min(int(screen.get_height() * 0.75), 600)
toplevel = self.get_toplevel()
toplevel.set_default_size(width, height)
if x != -1 and y != -1:
toplevel.move(x, y)
def _read_resource(self, domain, name):
from stoqlib.lib.kiwilibrary import library
# On development, documentation resources (e.g. COPYING) will
# be located directly on the library's root
devpath = os.path.join(library.get_root(), name)
if os.path.exists(devpath):
with open(devpath) as f:
return f.read()
return environ.get_resource_string('stoq', domain, name)
def _update_toolbar_style(self):
toolbar = self.uimanager.get_widget('/toolbar')
if not toolbar:
return
style_map = {'icons': gtk.TOOLBAR_ICONS,
'text': gtk.TOOLBAR_TEXT,
'both': gtk.TOOLBAR_BOTH,
'both-horizontal': gtk.TOOLBAR_BOTH_HORIZ}
# We set both horizontal as default to improve usability,
# it's easier for the user to know what some of the buttons
# in the toolbar does by having a label next to it
toolbar_style = api.user_settings.get('toolbar-style',
'both-horizontal')
value = style_map.get(toolbar_style)
if value:
toolbar.set_style(value)
def _run_about(self):
info = get_utility(IAppInfo)
about = gtk.AboutDialog()
about.set_name(info.get("name"))
about.set_version(info.get("version"))
about.set_website(stoq.website)
release_date = stoq.release_date
about.set_comments(_('Release date: %s') %
datetime.datetime(*release_date).strftime('%x'))
about.set_copyright('Copyright (C) 2005-2012 Async Open Source')
about.set_logo(render_logo_pixbuf('about'))
# License
if locale.getlocale()[0] == 'pt_BR':
filename = 'COPYING.pt_BR'
else:
filename = 'COPYING'
data = self._read_resource('docs', filename)
about.set_license(data)
# Authors & Contributors
data = self._read_resource('docs', 'AUTHORS')
lines = data.split('\n')
lines.append('') # separate authors from contributors
data = self._read_resource('docs', 'CONTRIBUTORS')
lines.extend([c.strip() for c in data.split('\n')])
about.set_authors(lines)
about.set_transient_for(get_current_toplevel())
about.run()
about.destroy()
def _hide_current_application(self):
if not self.current_app:
return False
if self._shutdown_application():
self.hide_app()
return True
def _show_uri(self, uri):
toplevel = self.get_toplevel()
open_browser(uri, toplevel.get_screen())
def _get_action_group(self, name):
action_group = self._action_groups.get(name)
if action_group is None:
action_group = gtk.ActionGroup(name)
self.uimanager.insert_action_group(action_group, 0)
self._action_groups[name] = action_group
return action_group
def _update_toggle_actions(self, app_name):
self._current_app_settings = d = self._app_settings.setdefault(app_name, {})
self.ToggleToolbar.set_active(d.get('show-toolbar', True))
self.ToggleStatusbar.set_active(d.get('show-statusbar', True))
self.ToggleToolbar.notify('active')
self.ToggleStatusbar.notify('active')
def _empty_message_area(self):
area = self.statusbar.message_area
for child in area.get_children()[1:-1]:
child.destroy()
def _shutdown_application(self, restart=False, force=False):
"""Shutdown the application:
There are 3 possible outcomes of calling this function, depending
on how many windows and applications are open::
* Hide application, save state, show launcher
* Close window, save state, show launcher
* Close window, save global state, terminate application
This function is called in the following situations:
* When closing a window (delete-event)
* When clicking File->Close in an application
* When clicking File->Quit in the launcher
* When clicking enable production mode (restart=True)
* Pressing Ctrl-w/F5 in an application
* Pressing Ctrl-q in the launcher
* Pressing Alt-F4 on Win32
* Pressing Cmd-q on Mac
:returns: True if shutdown was successful, False if not
"""
log.debug("Shutting down launcher")
# Ask the application if we can close, currently this only happens
# when trying to close the POS app if there's a sale in progress
current_app = self.current_app
if current_app and not current_app.can_close_application():
return False
# We can currently only close a window if the currently active
# application is the launcher application, unless we force it
# (e.g. when enabling production mode)
if current_app and current_app.app_name != 'launcher' and not force:
return True
# Here we save app specific state such as object list
# column position/ordering
if current_app and current_app.search:
current_app.search.save_columns()
self._save_window_size()
self.shell.close_window(self)
# If there are other windows open, do not terminate the application, just
# close the current window and leave the others alone
if self.shell.windows:
return True
self.shell.quit(restart=restart)
def _create_ui(self):
if self._osx_app:
self._osx_setup_menus()
self._create_shared_ui()
toplevel = self.get_toplevel().get_toplevel()
add_current_toplevel(toplevel)
def _load_shell_app(self, app_name):
user = api.get_current_user(self.store)
# FIXME: Move over to domain
if (app_name != 'launcher' and
not user.profile.check_app_permission(app_name)):
error(_("This user lacks credentials \nfor application %s") %
app_name)
return None
module = __import__("stoq.gui.%s" % (app_name, ),
globals(), locals(), [''])
attribute = app_name.capitalize() + 'App'
shell_app_class = getattr(module, attribute, None)
if shell_app_class is None:
raise SystemExit("%s app misses a %r attribute" % (
app_name, attribute))
shell_app = shell_app_class(window=self,
store=self.store)
shell_app.app_name = app_name
return shell_app
#
# Public API
#
[docs] def show_app(self, app, app_window, **params):
app_window.reparent(self.application_box)
self.application_box.set_child_packing(app_window, True, True, 0,
gtk.PACK_START)
# Default action settings for applications
self.Print.set_visible(True)
self.Print.set_sensitive(False)
self.ExportSpreadSheet.set_visible(True)
self.ExportSpreadSheet.set_sensitive(False)
self.ChangePassword.set_visible(False)
self.SignOut.set_visible(False)
self.Close.set_sensitive(True)
self.HomeToolItem.set_sensitive(True)
# We only care about Quit on OSX
self.Quit.set_visible(bool(self._osx_app))
self.NewToolItem.set_tooltip("")
self.NewToolItem.set_sensitive(True)
self.SearchToolItem.set_tooltip("")
self.SearchToolItem.set_sensitive(True)
self._update_toggle_actions(app.app_name)
self.get_toplevel().set_title(app.get_title())
self.application_box.show()
app.toplevel = self.get_toplevel()
if app.app_name != 'launcher':
self.application_actions[app.app_name].set_visible(False)
if self._birthdays_bar is not None:
if app.app_name in ['launcher', 'sales']:
self._birthdays_bar.show()
else:
self._birthdays_bar.hide()
# StartApplicationEvent must be emitted before calling app.activate(),
# so that the plugins can have the chance to modify the application
# before any other event is emitted.
StartApplicationEvent.emit(app.app_name, app)
app.activate(**params)
self.uimanager.ensure_update()
self.current_app = app
self.current_widget = app_window
if not self.in_ui_test:
while gtk.events_pending():
gtk.main_iteration()
app_window.show()
app.setup_focus()
[docs] def hide_app(self, empty=False):
"""
Hide the current application in this window
:param bool empty: if ``True``, do not add the default launcher application
"""
self.application_box.hide()
if self.current_app:
if self.current_app.app_name != 'launcher':
self.application_actions[self.current_app.app_name].set_visible(True)
inventory_bar = getattr(self.current_app, 'inventory_bar', None)
if inventory_bar:
inventory_bar.hide()
if self.current_app.search:
self.current_app.search.save_columns()
self.current_app.deactivate()
if self._help_ui:
self.uimanager.remove_ui(self._help_ui)
self._help_ui = None
self.current_widget.destroy()
StopApplicationEvent.emit(self.current_app.app_name,
self.current_app)
self.current_app = None
self._empty_message_area()
for item in self.tool_items:
item.destroy()
self.tool_items = []
self._update_toggle_actions('launcher')
if not empty:
self.run_application(app_name=u'launcher')
[docs] def add_info_bar(self, message_type, label, action_widget=None):
"""Show an information bar to the user.
:param message_type: message type, a gtk.MessageType
:param label: label to display
:param action_widget: optional, most likely a button
:returns: the infobar
"""
infobar = MessageBar(label, message_type)
if action_widget:
infobar.add_action_widget(action_widget, 0)
action_widget.show()
infobar.show()
self.main_vbox.pack_start(infobar, False, False, 0)
self.main_vbox.reorder_child(infobar, 2)
return infobar
[docs] def add_ui_actions(self, ui_string,
actions,
name='Actions',
action_type='normal',
filename=None,
instance=None):
if instance is None:
instance = self
ag = self._get_action_group(name)
to_add = [entry[0] for entry in actions]
for action in ag.list_actions():
if action.get_name() in to_add:
ag.remove_action(action)
if action_type == 'normal':
ag.add_actions(actions)
elif action_type == 'toggle':
ag.add_toggle_actions(actions)
elif action_type == 'radio':
ag.add_radio_actions(actions)
else:
raise ValueError(action_type)
if filename is not None:
ui_string = environ.get_resource_string('stoq', 'uixml', filename)
ui_id = self.uimanager.add_ui_from_string(ui_string)
self.action_permissions.update(self.common_action_permissions)
pm = PermissionManager.get_permission_manager()
for action in ag.list_actions():
action_name = action.get_name()
setattr(instance, action_name, action)
# Check permissions
key, required = instance.action_permissions.get(action_name,
(None, pm.PERM_ALL))
if not pm.get(key) & required:
action.set_visible(False)
# Disable keyboard shortcut
path = action.get_accel_path()
gtk.accel_map_change_entry(path, 0, 0, True)
return ui_id
[docs] def set_help_section(self, label, section):
def on_HelpHelp__activate(action):
show_section(section)
ui_string = """<ui>
<menubar action="menubar">
<menu action="HelpMenu">
<placeholder name="HelpPH">
<menuitem action="HelpHelp"/>
</placeholder>
</menu>
</menubar>
</ui>"""
help_help_actions = [
("HelpHelp", None, label,
get_accel('app.common.help'),
_("Show help for this application"),
on_HelpHelp__activate),
]
self._help_ui = self.add_ui_actions(
ui_string,
help_help_actions, 'HelpHelpActions')
[docs] def add_debug_ui(self):
ui_string = """<ui>
<menubar name="menubar">
<menu action="DebugMenu">
<menuitem action="Introspect"/>
<menuitem action="RemoveSettingsCache"/>
</menu>
</menubar>
</ui>"""
actions = [
('DebugMenu', None, _('Debug')),
('Introspect', None, _('Introspect slaves')),
('RemoveSettingsCache', None, _('Remove settings cache')),
]
self.add_ui_actions(ui_string, actions, 'DebugActions')
[docs] def add_new_items(self, actions):
self.tool_items.extend(
self.NewToolItem.add_actions(self.uimanager, actions))
[docs] def add_search_items(self, actions):
self.tool_items.extend(
self.SearchToolItem.add_actions(self.uimanager, actions))
[docs] def new_window(self):
"""
Creates a new shell window, with an application selector in it
"""
shell_window = self.shell.create_window()
shell_window.run_application(u'launcher')
shell_window.show()
[docs] def close(self):
"""
Closes this window
"""
self.hide_app(empty=True)
self.toplevel.destroy()
self.hide()
[docs] def switch_application(self, app_name, **params):
params['hide'] = True
self.run_application(app_name, **params)
[docs] def run_application(self, app_name, **params):
"""
Load and show an application in a shell window.
:param ShellWindow shell_window: shell window to run application in
:param str appname: the name of the application to run
:param params: extra arguments passed to the application
:returns: the shell application or ``None`` if the user doesn't have
access to open the application
:rtype: ShellApp
"""
# FIXME: Maybe we should really have an app that would be responsible
# for doing administration tasks related to stoqlink here? Right now
# we are only going to open the stoq.link url
if app_name == 'link':
toplevel = self.get_toplevel()
user_hash = api.sysparam.get_string('USER_HASH')
url = 'https://stoq.link?source=stoq&hash={}'.format(user_hash)
open_browser(url, toplevel.get_screen())
return
if params.pop('hide', False):
self.hide_app(empty=True)
shell_app = self._load_shell_app(app_name)
if shell_app is None:
return None
# Set the icon for the application
app_icon = get_application_icon(app_name)
toplevel = self.get_toplevel()
icon = toplevel.render_icon(app_icon, gtk.ICON_SIZE_MENU)
toplevel.set_icon(icon)
# FIXME: We should remove the toplevel windows of all ShellApp's
# glade files, as we don't use them any longer.
shell_app_window = shell_app.get_toplevel()
self.show_app(shell_app, shell_app_window.get_child(), **params)
shell_app_window.hide()
return shell_app
[docs] def get_available_applications(self):
user = api.get_current_user(self.store)
permissions = user.profile.get_permissions()
descriptions = get_utility(IApplicationDescriptions).get_descriptions()
available_applications = []
# sorting by app_full_name
for name, full, icon, descr in locale_sorted(
descriptions, key=operator.itemgetter(1)):
# FIXME: The delivery app is still experimental. Remove this
# once it is considered stable enough for our users
if name == 'delivery' and not api.is_developer_mode():
continue
if permissions.get(name):
available_applications.append(
Application(name, full, icon, descr))
return available_applications
#
# Kiwi callbacks
#
[docs] def key_F5(self):
# Backwards-compatibility
if self.current_app and self.current_app.can_change_application():
self.hide_app()
return True
def _on_osx__block_termination(self, app):
return not self._shutdown_application()
def _on_show_changelog__clicked(self, button):
show_section('changelog')
self._changelog_bar.hide()
def _on_check_calendar__clicked(self, button):
self.switch_application(u'calendar')
api.user_settings.set('last-birthday-check',
datetime.date.today().strftime('%Y-%m-%d'))
self._birthdays_bar.hide()
self._birthdays_bar = None
def _on_toplevel__configure(self, widget, event):
window = widget.get_window()
rect = window.get_frame_extents()
self._x = rect.x
self._y = rect.y
self._width = event.width
self._height = event.height
def _on_toplevel__delete_event(self, *args):
if self._hide_current_application():
return True
self._shutdown_application()
[docs] def on_uimanager__connect_proxy(self, uimgr, action, widget):
tooltip = action.get_tooltip()
if not tooltip:
return
if isinstance(widget, gtk.MenuItem):
widget.connect('select', self._on_menu_item__select, tooltip)
widget.connect('deselect', self._on_menu_item__deselect)
elif isinstance(widget, gtk.ToolItem):
child = widget.get_child()
if child is None:
return
child.connect('enter-notify-event',
self._on_tool_item__enter_notify_event, tooltip)
child.connect('leave-notify-event',
self._on_tool_item__leave_notify_event)
[docs] def on_uimanager__disconnect_proxy(self, uimgr, action, widget):
tooltip = action.get_tooltip()
if not tooltip:
return
if isinstance(widget, gtk.MenuItem):
try:
widget.disconnect_by_func(self._on_menu_item__select)
widget.disconnect_by_func(self._on_menu_item__deselect)
except TypeError:
# Maybe it was already disconnected
pass
elif isinstance(widget, gtk.ToolItem):
child = widget.get_child()
try:
child.disconnect_by_func(
self._on_tool_item__enter_notify_event)
child.disconnect_by_func(
self._on_tool_item__leave_notify_event)
except TypeError:
pass
def _on_menu_item__select(self, menuitem, tooltip):
self.statusbar.push(0xff, tooltip)
def _on_menu_item__deselect(self, menuitem):
self.statusbar.pop(0xff)
def _on_tool_item__enter_notify_event(self, toolitem, event, tooltip):
self.statusbar.push(0xff, tooltip)
def _on_tool_item__leave_notify_event(self, toolitem, event):
self.statusbar.pop(0xff)
def _on_enable_production__clicked(self, button):
if not self.current_app.can_close_application():
return
if not yesno(_(u"This will enable production mode and finish the "
u"demonstration. Are you sure?"),
gtk.RESPONSE_NO,
_(u"Enable production mode"), _(u"Continue testing")):
return
api.config.set('Database', 'enable_production', 'True')
api.config.flush()
self._shutdown_application(restart=True, force=True)
# File
[docs] def on_NewWindow__activate(self, action):
self.new_window()
[docs] def on_Print__activate(self, action):
if self.current_app:
self.current_app.print_activate()
else:
print('FIXME')
[docs] def on_ExportSpreadSheet__activate(self, action):
if self.current_app:
self.current_app.export_spreadsheet_activate()
else:
print('FIXME')
[docs] def on_Close__activate(self, action):
self._hide_current_application()
[docs] def on_ChangePassword__activate(self, action):
from stoqlib.gui.slaves.userslave import PasswordEditor
store = api.new_store()
user = api.get_current_user(store)
retval = run_dialog(PasswordEditor, self, store, user)
store.confirm(retval)
store.close()
[docs] def on_SignOut__activate(self, action):
from stoqlib.lib.interfaces import ICookieFile
get_utility(ICookieFile).clear()
self._shutdown_application(restart=True)
[docs] def on_Quit__activate(self, action):
if self._hide_current_application():
return
self._shutdown_application()
self.get_toplevel().destroy()
# Edit
def _on_ToggleToolbar__notify_active(self, action, pspec):
toolbar = self.uimanager.get_widget('/toolbar')
toolbar.set_visible(action.get_active())
self._current_app_settings['show-toolbar'] = action.get_active()
def _on_ToggleStatusbar__notify_active(self, action, pspec):
self.statusbar.set_visible(action.get_active())
self._current_app_settings['show-statusbar'] = action.get_active()
def _on_ToggleFullscreen__notify_active(self, action, spec):
window = self.get_toplevel()
if not window.get_realized():
return
is_active = action.get_active()
window = window.get_window()
is_fullscreen = window.get_state() & gtk.gdk.WINDOW_STATE_FULLSCREEN
if is_active != is_fullscreen:
if is_active:
window.fullscreen()
else:
window.unfullscreen()
# This is shared between apps, since it's weird to change fullscreen
# between applications
self._app_settings['show-fullscreen'] = is_active
# View
[docs] def on_Preferences__activate(self, action):
with api.new_store() as store:
run_dialog(PreferencesEditor, self, store)
self._update_toolbar_style()
# Help
[docs] def on_HelpContents__activate(self, action):
show_contents()
[docs] def on_HelpTranslate__activate(self, action):
self._show_uri("https://www.transifex.com/projects/p/stoq")
[docs] def on_HelpChat__activate(self, action):
self._show_uri("http://chat.stoq.com.br/")
[docs] def on_HelpSupport__activate(self, action):
self._show_uri("http://www.stoq.com.br/suporte")
[docs] def on_HelpAbout__activate(self, action):
self._run_about()
# Debug
[docs] def on_Introspect__activate(self, action):
window = self.get_toplevel()
introspect_slaves(window)
[docs] def on_RemoveSettingsCache__activate(self, action):
keys = ['app-ui', 'launcher-geometry']
keys.append('search-columns-%s' % (
api.get_current_user(api.get_default_store()).username, ))
for key in keys:
try:
api.user_settings.remove(key)
except KeyError:
pass
[docs]class VersionChecker(object):
DAYS_BETWEEN_CHECKS = 1
#
# Private API
#
def __init__(self, store, window):
self.store = store
self.window = window
def _display_new_version_message(self, latest_version):
# Only display version message in admin app
if 'AdminApp' not in self.window.__class__.__name__:
return
button = gtk.LinkButton(
'http://www.stoq.com.br/novidades',
_(u'Learn More...'))
msg = _('<b>There is a new Stoq version available (%s)</b>') % (
latest_version, )
self.window.add_info_bar(gtk.MESSAGE_INFO, msg, action_widget=button)
def _check_details(self, latest_version):
current_version = tuple(stoq.version.split('.'))
if tuple(latest_version.split('.')) > current_version:
self._display_new_version_message(latest_version)
else:
log.debug('Using latest version %r, not showing message' % (
stoq.version, ))
def _download_details(self):
log.debug('Downloading new version information')
webapi = WebService()
webapi.version(self.store, stoq.version,
callback=self._on_response_done)
def _on_response_done(self, response):
if response.status_code != 200:
return
details = response.json()
self._check_details(details['version'])
api.user_settings.set('last-version-check',
datetime.date.today().strftime('%Y-%m-%d'))
api.user_settings.set('latest-version', details['version'])
#
# Public API
#
[docs] def check_new_version(self):
if api.is_developer_mode():
return
log.debug('Checking version')
date = api.user_settings.get('last-version-check')
if date:
check_date = datetime.datetime.strptime(date, '%Y-%m-%d')
diff = datetime.date.today() - check_date.date()
if diff.days >= self.DAYS_BETWEEN_CHECKS:
return self._download_details()
else:
return self._download_details()
latest_version = api.user_settings.get('latest-version')
if latest_version:
self._check_details(latest_version)