Open mpmc opened 6 years ago
I've spent soooo much time trying to get window centring right, and still haven't managed it!
One issue is simply that every platform handles it differently, the second issue is that sometimes the GUI doesn't know how big it will be until it's been drawn.
When the GUI is initially placed on the screen - it should identify the middle of the screen, calculate the GUI's required width/height, do a bit of math, and then position itself appropriately.
This doesn't seem to be happening in the above example - it looks like it calculates it's position with no text - so slightly off centre - is the text added after app.go()
is called?
If a GUI then has widgets added to it, the GUI won't move, instead it should simply stretch to accommodate the widgets - this is happening in the above, as the GUI's top left corner doesn't move.
It would be possible to have the GUI stick to the middle, by recalculating its position every time it is reconfigured, but that sounds painful, and not common to most apps.
Instead, you could call the CENTER()
function when the gui is changed, which might adjust its position appropriately. (although I'm not sure I've tested this...)
This doesn't seem to be happening in the above example - it looks like it calculates it's position with no text - so slightly off centre - is the text added after app.go() is called?
Yes, I'm queuing, dropping of the trousers for you to see what I'm doing :laughing:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# gui6.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 socket
import gettext
import traceback
import appJar
import hs602
gettext.install('Hs602util')
class Gui(appJar.gui):
"""Simple HS602 utility using appJar."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = '{}'.format(_('HS602 Utility'))
self.addMenuEdit(False)
self.controller = hs602.Callback()
self.start()
# Helper methods.
@staticmethod
def trace(exc):
"""Try and get traceback info.
:param exc: Exception to get the trace for.
"""
trace = ''
try:
trace = ''.join(traceback.format_tb(exc.__traceback__))
except AttributeError:
pass
return trace
def removeAllWidgets(self, *args, **kwargs):
"""This overrides the default removeAllWidgets so that the
default padding can be reset properly."""
super().removeAllWidgets(*args, **kwargs)
self.padding = 8, 8
self.inpadding = 5, 5
def start(self, btn=None):
"""Trigger the default window."""
self.trigger_discovery()
def queueFunction(self, method, *args, **kwargs):
"""This overrides the default queueFunction so that we can
catch Exceptions.
:param method: Method to queue.
:param args: Arguments to pass to the method.
:param kwargs: Keywords arguments to pass to the method.
"""
# Raise these rather than showing the error dialog to prevent
# a loop.
raise_only = [self.error, self.queueFunction]
def run(*args, **kwargs):
try:
method(*args, **kwargs)
except Exception as exc:
if method not in raise_only:
print(exc)
self.error(exc)
else:
raise exc
super().queueFunction(run, *args, **kwargs)
def values(self, **kwargs):
"""Get all values, merge & return as dictionary.
:param kwargs: Optional keyword arguments - appended to the
end of the input list.
"""
# All available inputs.
inputs = filter(None, [
self.getAllEntries(),
self.getAllOptionBoxes(),
self.getAllSpinBoxes(),
self.getAllListBoxes(),
self.getAllProperties(),
self.getAllCheckBoxes(),
self.getAllRadioButtons(),
self.getAllScales(),
self.getAllMeters(),
self.getAllDatePickers(),
kwargs,
])
result = data = dict()
for pairs in inputs:
for key, val in pairs.items():
# Try and strip values.
try:
val = val.strip()
except AttributeError:
pass
try:
# Skip if value is empty or if key already exists.
if not val or result[key]:
continue
except KeyError:
pass
result[key] = val
return result
# User interface methods.
def error(self, exc):
"""Display an error."""
label = {
'title': 'Message',
'value': '{}'.format(_('Sorry there\'s a problem, the '
'details of which are below.\nIf '
'the problem persists, please '
'report it.\n\nThank you!')),
'pos': (0, 0),
'sticky': 'nw',
'stretch': 'none',
}
close = {
'title': '{}'.format('Quit!'),
'value': self.stop,
'pos': (2, 0),
'sticky': 'nesw',
'stretch': 'none',
'tip': '{}'.format(_('Close the Utility.')),
}
textarea = {
'title': 'Error details',
'value': '{}\n\n{}'.format(exc, self.trace(exc)),
'pos': (1, 0),
'sticky': 'nesw',
'stretch': 'both',
}
def show():
# Reset the GUI!
self.removeAllWidgets()
# Add widgets - error label (top).
self.label(**label)
self.setLabelAlign(label.get('title'), 'left')
# Quit/Go back button (bottom).
self.button(**close)
# Text area.
self.text(**textarea)
self.disableTextArea(textarea.get('title'))
# Show the error.
self.queueFunction(show)
def msg(self, text=None):
"""Clear the widgets and a show message - used during callbacks.
text: Optional message to show.
"""
label = {
'title': 'Information',
'value': '{}'.format(text or _('\u231B Action in-progress, '
'please wait..')),
}
def show():
# Clear and display the message.
self.removeAllWidgets()
self.label(**label)
self.setLabelAlign(label.get('title'), 'center')
self.queueFunction(show)
def trigger_discovery(self, *args, **kwargs):
"""Trigger discovery callback."""
msg = _('\u231B Searching for devices, please wait..')
# Register the callback.
self.controller.register('devices', callback=self.discovery)
# Let the user know what's going on.
self.msg(msg)
def discovery(self, result=None):
"""Discovery and connection screen.
:param result: Result of the callback (if one).
"""
frame = {
'title': '{}'.format(_('\u2605 Connection')),
'sticky': 'nesw',
'stretch': 'both',
'padding': (5, 5),
}
info = {
'title': 'Information',
'value': '{}'.format(_('\u2139 To connect, select a device '
'from the list (if shown) or enter '
'an address.')),
'pos': (0, 0, 2),
'sticky': 'nw',
'stretch': 'none',
}
device_list = {
'title': 'addr',
'pos': (1, 0, 2),
'value': result,
'sticky': 'nesw',
'stretch': 'both',
}
connection_info = {
'title': 'Connection Information',
'value': '{}'.format('Just an address is required, the '
'rest can be left blank.\nNote: '
'If you enter a hostname and have '
'slow DNS, connecting might take a '
'while!'),
'pos': (2, 0, 2),
'sticky': 'nw',
'stretch': 'none',
}
addr = {
'title': 'addr',
'pos': (3, 0),
'default': '{}'.format(_('Device address')),
'sticky': 'nesw',
'stretch': 'both',
'tip': '{}'.format(_('Enter a device address (usually an '
'IP address).')),
}
timeout = {
'title': 'timeout',
'default': '{}'.format(_('Timeout')),
'pos': (3, 1),
'limit': 3,
'kind': 'numeric',
'sticky': 'nesw',
'stretch': 'both',
'tip': '{}'.format(_('Connection timeout (enter a value '
'between 3-120 (seconds).')),
}
tcp = {
'title': 'tcp',
'pos': (4, 0),
'default': '{}'.format(_('TCP port')),
'limit': 5,
'kind': 'numeric',
'sticky': 'nesw',
'stretch': 'both',
'tip': '{}'.format(_('Device command port, leave blank '
'to use default (8087).')),
}
udp = {
'title': 'udp',
'pos': (4, 1),
'default': '{}'.format(_('UDP port')),
'limit': 5,
'kind': 'numeric',
'sticky': 'nesw',
'stretch': 'both',
'tip': '{}'.format(_('Device broadcast port, leave blank '
'to use default (8086).')),
}
scan = {
'title': '{}'.format(_('\u26B2 Scan')),
'value': self.trigger_discovery,
'pos': (5, 0),
'sticky': 'nesw',
'stretch': 'both',
'tip': '{}'.format('Scan your network.')
}
connect = {
'title': '{}'.format(_('\u2713 Connect')),
'value': self.connect,
'pos': (5, 1),
'sticky': 'nesw',
'stretch': 'both',
'focus': True,
'tip': '{}'.format(_('Connect to the device!')),
}
def show():
# Reset the window before displaying any widgets.
self.removeAllWidgets()
with self.labelFrame(**frame):
self.label(**info)
self.label(**connection_info)
self.entry(**addr)
self.entry(**timeout)
self.entry(**tcp)
self.entry(**udp)
self.button(**scan)
self.button(**connect)
if result:
self.option(**device_list)
# Bail if not a list.
if not isinstance(result, list):
return self.error(result)
self.queueFunction(show)
def connect(self, *args, **kwargs):
"""Initialise the connection to the device."""
addr = {
'title': '{}'.format(_('Error')),
'message': '{}'.format(_('A valid address is required!')),
'kind': 'error',
}
values = self.values(**kwargs)
# Bail if address isn't valid.
if not values.get('addr'):
self.queueFunction(self.popUp, **addr)
return
# Setup the controller, setup the callback (requesting all
# settings, and then show msg.
self.controller.__init__(**values)
self.controller.register('settings', callback=self.device)
self.msg()
def device(self, result):
"""The device screen - This is triggered via the callback.
:param args: This should be a list of device settings or an
exception.
"""
error_popup = {
'title': '{}'.format(_('Error')),
'kind': 'error',
'message': '{}\n\n{}\n\n{}'.format(
_('There was a problem connecting!\nDebug info:-'),
result,
_('You\'ll be returned to the connect screen.'),
),
}
# Bail if result is not a dictionary.
if not isinstance(result, dict):
# Alert & return to the discovery screen, hide the list.
self.queueFunction(self.popUp, **error_popup)
self.queueFunction(self.discovery, list())
return
print(result)
def main(args):
app = Gui(*args, useTtk=False)
app.go()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
I'd rather not have the window stuck, but to keep it centred unless the user moves or resizes. Hard nut to crack from the sounds of it!
I've gone back to the drawing board (again) :rofl: and tried to use the center option, but the log just spams CENTER() missing 1 required positional argument: 'win'
@mpmc - I've made some changes to setLocation & CENTER. If you now call setLocation('center')
it should work.
I've tried adding a flag that keeps the window centered - and it works, but it is super laggy. The only way to have it done right is to recenter every time a widget is added once go()
has been called - I don't think it's worthwhile keeping, so I've removed it.
@jarvisteach I shall give it a go when I rewrite the app again hehe, atm I'm looking into creating a web 'app' version of my hs602 client. I still fully intend to create a standalone app using appJar :) & I hope to help you out where I can, I have some ideas that I'd like to discuss with you as well.
@jarvisteach I had a think about this issue. A solution might be to add a flag that gives the option to trigger a call to setposition if the app starts with no widgets, and if widgets are added later, reset the position after they're drawn.
The only problem I can think of would be detecting when all widgets have been added/drawn. It's why I've done this..
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Defaults.
self.title = _('HS602 Utility') # Window title.
self.addMenuEdit(inMenuBar=False) # Right-click menus.
self.controller = hs602.Controller() # Controller object.
self.padding = self._padding = 5, 5 # Widget outer padding.
self.inPadding = self._inPadding = 5, 5 # Widget inner padding.
self._cache = None # settings cache.
# Start.
self.discovery()
# Correct initial positioning.
self.queueFunction(self.setLocation, 'center')
Something I've noticed, if you start with a blank app and add widgets to it later the window is never centred on the screen..
An example..
Not sure how this can be fixed, but it seems rather odd :p