New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[next_release] removeAllWidgets() doesn't include toolbar. #339
Comments
Yeah, I think at the moment it won't remove any of the bars - toolbar, menubar, or statusbar - they're not really classed as widgets behind the scenes. I don't really think I want it to either... I think it should stay just being widgets, and instead there could be separate functions to remove each of the bars. There could be a new function, that removes absolutely everything from the GUI - widgets, bars, etc... However - having two toolbar buttons with the same text looks wrong! I'm not sure that should be possible... |
or perhaps two functions, |
I tried to recreate this so I could easily remove the toolbar, but it seems addButtons completely ignores any sticky/stretch options :( |
There are functions to |
@jarvisteach Yep the problem is I'd like to be able to generate a different menu on the fly depending on what 'mode' the app is in as well as prevent the user from trying to switch to another during a network action. |
Three new functions: * `.addToolbarButton()` - adds a single button to the toolbar * `.removeToolbarButton()` - removes the named button from the toolbar * `.removeToolbar()` - removes all buttons from the toolbar Also: * moved declaration of `self.pinBut` to constructor - it was in `.addToolbar()` * moved packing of toolbar from constructor to `.addToolbar()` so that it gets shown again when new buttons added, also gets rid of toolbar from other GUIs * updated docs & testing
@mpmc - I've added new functions to remove toolbars, see the commit above. |
Three new functions: * `.removeStatusbarField(field)` - removes the specified field from the statusbar * `.removeStatusbar()` - removes the statusbar Also: * added check to stop multiple statusbars being added * updated docs & testing
New functions to remove statusbars |
Maybe you didn't get the latest code? When I run this:
Pressing any of the buttons on the toolbar removes them all, and pressing the ADD button brings them back. If I press the ADD button a second time, I get the following error: |
Your code works but mine doesn't 😕 Here's the full code that'll run without hs602 :) for you to try :) The installed appjar version is @next_release 4a3d44c #!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# gui.py
#
# Copyright 2018 Mark Clarkstone <git@markclarkstone.co.uk>
#
# 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., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
import gettext
import traceback
import appJar
gettext.install('Hs602util')
class Gui(appJar.gui):
"""Simple HS602 utility using appJar."""
def __init__(self, *args):
# Initialise parent.
super().__init__()
self.title = _('HS602 Utility')
# GUI Settings.
self.setTitle('{}'.format(self.title))
# Theme.
#self.useTtk()
#self.setTtkTheme('clam')
# HS602.
#self.controller = hs602.Controller()
self.controller_cache = {}
# Launch!
self.launch(*args)
# Helper methods.
def widget_defaults(self, **kwargs):
"""Set widget defaults.
:param inside: Set inside padding instead?
:param sticky: Sticky value (default nesw).
:param stretch: Stretch value (default both).
:param expand: Expand value (default none).
"""
# Outside the widget.
out_x = 5
out_y = 5
# Inside the widget.
in_x = 5
in_y = 5
# Get the values.
stretch, inside, sticky, expand = [
kwargs.get('stretch', 'both'),
kwargs.get('inside', False),
kwargs.get('sticky', 'nesw'),
kwargs.get('expand'),
]
# Call!
self.setSticky(sticky)
self.setStretch(stretch)
if expand:
self.setExpand(expand)
if inside is True:
self.setInPadding(in_x, in_y)
else:
self.setPadding(out_x, out_y)
def error(self, exc):
"""Display an error."""
# Properties.
# Label.
label_text = _('Sorry there\'s a problem, the details of '
'which are below.\nIf the problem persists, '
'please report it.\n\nThank you.')
# Button.
button_text = _('Quit')
button_tooltip = _('Close the utility')
# Textarea.
textarea_text = _('No traceback available')
# Attempt to get the traceback.
try:
textarea_text = ''.join(traceback.format_tb(
exc.__traceback__))
except AttributeError:
pass
# Format.
textarea_text = _('{}\n\n{}').format(exc, textarea_text)
label_text = '{}'.format(label_text)
button_text = '{}'.format(button_text)
def show():
"""Display the error."""
# Reset the GUI!
self.removeAllWidgets()
self.widget_defaults(sticky='ew', stretch='none')
# Add widgets - error label (top).
self.addLabel(label_text, label_text, 0, 0)
self.setLabelAlign(label_text, 'left')
# Quit button.
self.addButton(button_text, self.stop, 2, 0)
self.setButtonTooltip(button_text,
'{}'.format(button_tooltip))
# Text area.
self.widget_defaults()
self.addScrolledTextArea(textarea_text, 1, 0)
self.setTextArea(textarea_text, textarea_text)
self.disableTextArea(textarea_text)
self.addMenuEdit(inMenuBar=False)
# To prevent weird things happening, queue.
self.queueFunction(show)
def interlude(self, text=None):
"""Clear the widgets and a show message - used during callbacks.
text: Optional message to show.
"""
text = text or _('\u231B Please wait..')
text = '{}'.format(text)
# Clear and display the message.
self.removeAllWidgets()
self.widget_defaults()
self.addLabel(text, text)
self.setLabelAlign(text, 'center')
def callback(self, method, callback=None, *args, **kwargs):
"""Simple (queued) callback.
:param method: Method to thread.
:param callback: Optional method to receive the result.
:param args: Positional arguments (for the threaded method).
:param kwargs: Keyword arguments (for the threaded method).
"""
text = _('Callback "{}" isn\'t callable').format(callback)
def run():
"""Run the method in a new thread, catch errors."""
try:
ret = method(*args, **kwargs)
# Queue the run_callback!
self.queueFunction(run_callback, ret)
except Exception as exc:
self.error(exc)
def run_callback(result):
"""Run the callback, pass the result, catch errors."""
try:
# Only run if callable!
if callable(callback):
callback(result)
else:
self.warn(text)
except Exception as exc:
self.error(exc)
# Run!
self.thread(run)
def discover(self, *args, **kwargs):
"""Discover devices."""
# Interlude message.
interlude = _('\u23F2 Probing devices, please wait..')
# No devices found prompt.
nodev_title = _('No Devices')
nodev_text = _('No devices were found on your network. :(\n'
'Scan again (Retry) or connect manually '
'(Cancel)?')
# Frame title.
frame = _('Available Devices')
# Info label.
label = _('Which device do you want to connect to?')
# Connect button.
connect = _('\u260D Connect')
# Re-scan button.
rescan = _('\u26B2 Re-scan')
# Manual button.
manual = _('\u25C9 Manual connection')
def probe():
"""Probe for devices."""
return ['test', 'test2']
def show(result):
"""Show devices."""
# No devices?
if len(result) == 0:
question = self.retryBox('{}'.format(nodev_title),
'{}'.format(nodev_text))
if question in ['yes', True, _('yes')]:
self.discover()
else:
self.manual()
return
# Which do we connect to?
self.removeAllWidgets()
# These buttons are outside the frame.
self.widget_defaults(sticky='nesw')
self.addButton('{}'.format(rescan), self.discover, 2, 0, 1)
self.addButton('{}'.format(manual), self.manual, 2, 1, 1)
# Display the address.
with self.labelFrame('{}'.format(frame), 0, 0, 2):
self.widget_defaults()
# Info label.
info = '{}'.format(label)
self.addLabel(info, info, 0, 0, 2)
self.setLabelAlign(info, 'center')
# Option box & connect button.
self.addOptionBox('addr', result, 1, 0, 1)
self.addButton('{}'.format(connect), self.connect,
1, 1, 1)
self.interlude(interlude)
self.callback(probe, show)
def launch(self, *args):
"""Start the app - this just launches discover for now."""
self.discover()
def values(self, **kwargs):
"""Get all values, merge, convert & return as dictionary.
kwargs will be _appended_ to the input list.
:param convert: Convert.
"""
merge = dict()
convert = kwargs.get('convert', True)
# All available inputs.
inputs = [
self.getAllOptionBoxes(),
self.getAllEntries(),
self.getAllCheckBoxes(),
self.getAllRadioButtons(),
self.getAllProperties(),
self.getAllScales(),
self.getAllMeters(),
self.getAllSpinBoxes(),
self.getAllListBoxes(),
self.getAllDatePickers(),
kwargs,
]
for dictionary in inputs:
merge.update(dictionary)
for key, value in merge.items():
# Remove start/end spaces from values.
if isinstance(value, str):
merge[key] = value.strip()
if convert is True:
if isinstance(value, float):
merge[key] = int(round(value))
return merge
def manual(self, btn=None):
"""Manual connection.
:param btn: The button that called this method (not used).
"""
rescan = _('\u2753 Re-scan')
connect = _('\u260D Connect')
frame = _('Manual Connection')
address = _('Address')
tcp = _('TCP')
timeout = _('Timeout')
info = _('\u2139 Just an (IP) address is required, leave the '
'rest blank to use the defaults.')
# Reset to defaults.
self.removeAllWidgets()
self.widget_defaults()
# Buttons!
self.addButton('{}'.format(rescan), self.discover, 5, 0, 1)
self.addButton('{}'.format(connect), self.connect, 5, 1, 1)
# Frame - before the above buttons.
with self.labelFrame('{}'.format(frame), 0, 0, 2):
self.widget_defaults()
# Entry.
self.addEntry('addr', 0, 1)
self.addNumericEntry('tcp', 1, 1)
self.addNumericEntry('timeout', 3, 1)
self.setFocus('addr')
self.setEntryMaxLength('tcp', 5)
self.setEntryMaxLength('timeout', 3)
# Labels.
self.addLabel('addr', '{}'.format(address), 0, 0)
self.addLabel('tcp', '{}'.format(tcp), 1, 0)
self.addLabel('to', '{}'.format(timeout), 3, 0)
self.addLabel('info', '{}'.format(info), 4, 0, 2)
self.setLabelAlign('info', 'center')
self.setLabelAlign('addr', 'center')
self.setLabelAlign('tcp', 'center')
self.setLabelAlign('to', 'center')
self.addMenuEdit(inMenuBar=False)
def connect(self, *args, **kwargs):
"""Connect to the device."""
# Interlude message.
interlude = _('\u260D Connecting, please wait..')
# Address.
address_title = _('Oops!')
address_text = _('An address is required/Address is '
'Invalid!')
# Invalid response.
settings_title = _('Sorry!')
settings_text = _('There was a problem getting settings! :(\n'
'This may indicate that your device has '
'crashed, or you have a network issue.\n\n'
'- If you entered an address, check it\'s '
'valid.\n'
'- Check your firewall isn\'t blocking '
'ports.\n'
'- As a last resort, reboot the device.\n\n'
'Try again (Yes) or view the debug log (No)?')
def connect(**kwargs):
"""Get device information.
:param kwargs: Connection info.
"""
try:
self.controller.__init__(**kwargs)
# Cache the data & return
return {'demo':'demo'}
except Exception as exc:
return exc
def result(result):
"""Check the result before calling the main window.
:param result: response from the (threaded) method.
"""
if not result or not isinstance(result, dict):
# Do we try again or show the result as an error?
question = self.questionBox('{}'.format(settings_title),
'{}'.format(settings_text))
if question in ['yes', True, _('yes')]:
return self.manual()
# If we hit here, the user pressed no.
return self.error(result)
# Call the main window.
self.main()
# First get & merge all user-input sources - inc keywords.
values = self.values(**kwargs)
addr = values.get('addr', '')
tcp = values.get('tcp', 0)
timeout = values.get('timeout', 0)
# Check the address.
if not len(addr) > 0:
self.errorBox('{}'.format(address_title),
'{}'.format(address_text))
# This is a bit ugly, but at least the user can enter info.
# And it prevents getting stuck in an interlude.
return self.manual()
try:
# Just remove invalid tcp/timeout.
if tcp not in range(1, 65535):
values.pop('tcp')
if timeout not in range(1, 120):
values.pop('timeout')
except KeyError:
pass
# Display the main window!
self.interlude(interlude)
self.callback(connect, result, **values)
def window_toolbar(self):
"""Generic window toolbar."""
# Toolbar titles & methods.
toolbar = {
_('Status'): 'main',
_('Settings'): 'settings',
_('About'): 'about',
}
# Combined toolbar.
toolbar_list = [name for name, method in toolbar.items()]
def menu_action(button=None):
button = '{}'.format(toolbar.get(button))
method = getattr(self, button)
method()
# Add toolbar.
self.removeToolbar()
self.addToolbar(toolbar_list, menu_action)
def main(self):
"""The status window - this is the default."""
# Clear all widgets.
self.removeAllWidgets()
# Add toolbar.
self.window_toolbar()
def settings(self):
"""The settings window."""
# Clear all widgets.
self.removeAllWidgets()
# Add toolbar.
self.window_toolbar()
def main(args):
app = Gui(*args)
app.go()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv)) |
Ah, I think I worked it out - I'll clean it up. |
@mpmc - Actually, this is going to be a bit more work than I thought. A short-term fix would be to remove the toolbar before removing all widgets. |
A RootPage group isn't created, so this wasn't necessary. |
Covered by issue #462 |
While tinkering I came across an interesting development, removeAllWidgets() doesn't remove toolbars so you end up with something like this..
WIP code
The text was updated successfully, but these errors were encountered: