309 lines
14 KiB
Python
309 lines
14 KiB
Python
#!/usr/bin/env python
|
|
|
|
# $LicenseInfo:firstyear=2016&license=internal$
|
|
#
|
|
# Copyright (c) 2016, Linden Research, Inc.
|
|
#
|
|
# The following source code is PROPRIETARY AND CONFIDENTIAL. Use of
|
|
# this source code is governed by the Linden Lab Source Code Disclosure
|
|
# Agreement ("Agreement") previously entered between you and Linden
|
|
# Lab. By accessing, using, copying, modifying or distributing this
|
|
# software, you acknowledge that you have been informed of your
|
|
# obligations under the Agreement and agree to abide by those obligations.
|
|
#
|
|
# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
|
|
# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
|
|
# COMPLETENESS OR PERFORMANCE.
|
|
# $LicenseInfo:firstyear=2013&license=viewerlgpl$
|
|
# Copyright (c) 2013, Linden Research, Inc.
|
|
# $/LicenseInfo$
|
|
|
|
"""
|
|
@file InstallerUserMessage.py
|
|
@author coyot
|
|
@date 2016-05-16
|
|
"""
|
|
|
|
"""
|
|
This does everything the old updater/scripts/darwin/messageframe.py script did and some more bits.
|
|
Pushed up the manager directory to be multiplatform.
|
|
"""
|
|
|
|
import os
|
|
import Queue
|
|
import threading
|
|
import time
|
|
import Tkinter as tk
|
|
import ttk
|
|
|
|
|
|
class InstallerUserMessage(tk.Tk):
|
|
#Goals for this class:
|
|
# Provide a uniform look and feel
|
|
# Provide an easy to use convenience class for other scripts
|
|
# Provide windows that automatically disappear when done (for differing notions of done)
|
|
# Provide a progress bar that isn't a glorified spinner, but based on download progress
|
|
#Non-goals:
|
|
# No claim to threadsafety is made or warranted. Your mileage may vary.
|
|
# Please consult a doctor if you experience thread pain.
|
|
|
|
#Linden standard green color, from Marketing
|
|
linden_green = "#487A7B"
|
|
|
|
def __init__(self, text="", title="", width=500, height=200, icon_name = None, icon_path = None):
|
|
tk.Tk.__init__(self)
|
|
self.grid()
|
|
self.title(title)
|
|
self.choice = tk.BooleanVar()
|
|
self.config(background = 'black')
|
|
# background="..." doesn't work on MacOS for radiobuttons or progress bars
|
|
# http://tinyurl.com/tkmacbuttons
|
|
ttk.Style().configure('Linden.TLabel', foreground=InstallerUserMessage.linden_green, background='black')
|
|
ttk.Style().configure('Linden.TButton', foreground=InstallerUserMessage.linden_green, background='black')
|
|
ttk.Style().configure("black.Horizontal.TProgressbar", foreground=InstallerUserMessage.linden_green, background='black')
|
|
|
|
#This bit of configuration centers the window on the screen
|
|
# The constants below are to adjust for typical overhead from the
|
|
# frame borders.
|
|
self.xp = (self.winfo_screenwidth() / 2) - (width / 2) - 8
|
|
self.yp = (self.winfo_screenheight() / 2) - (height / 2) - 20
|
|
self.geometry('{0}x{1}+{2}+{3}'.format(width, height, self.xp, self.yp))
|
|
|
|
#find a few things
|
|
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
self.icon_dir = os.path.abspath(os.path.join(self.script_dir, 'icons'))
|
|
|
|
#finds the icon and creates the widget
|
|
self.find_icon(icon_path, icon_name)
|
|
|
|
#defines what to do when window is closed
|
|
self.protocol("WM_DELETE_WINDOW", self._delete_window)
|
|
|
|
#callback id
|
|
self.id = -1
|
|
|
|
def _delete_window(self):
|
|
#capture and discard all destroy events before the choice is set
|
|
if not ((self.choice == None) or (self.choice == "")):
|
|
try:
|
|
#initialized value. If we have an outstanding callback, kill it before killing ourselves
|
|
if self.id != -1:
|
|
self.after_cancel(self.id)
|
|
self.destroy()
|
|
except:
|
|
#tk may try to destroy the same object twice
|
|
pass
|
|
|
|
def set_colors(self, widget):
|
|
# #487A7B is "Linden Green"
|
|
widget.config(foreground = InstallerUserMessage.linden_green)
|
|
widget.config(background='black')
|
|
|
|
def find_icon(self, icon_path = None, icon_name = None):
|
|
#we do this in each message, let's do it just once instead.
|
|
if not icon_path:
|
|
icon_path = self.icon_dir
|
|
icon_path = os.path.join(icon_path, icon_name)
|
|
if os.path.exists(icon_path):
|
|
icon = tk.PhotoImage(file=icon_path)
|
|
self.image_label = tk.Label(image = icon)
|
|
self.image_label.image = icon
|
|
else:
|
|
#default to text if image not available
|
|
self.image_label = tk.Label(text = "Second Life")
|
|
|
|
def auto_resize(self, row_count = 0, column_count = 0, heavy_row = None, heavy_column = None):
|
|
#auto resize window to fit all rows and columns
|
|
#"heavy" gets extra weight
|
|
for x in range(column_count):
|
|
if x == heavy_column:
|
|
self.columnconfigure(x, weight = 2)
|
|
else:
|
|
self.columnconfigure(x, weight=1)
|
|
|
|
for y in range(row_count):
|
|
if y == heavy_row:
|
|
self.rowconfigure(y, weight = 2)
|
|
else:
|
|
self.rowconfigure(x, weight=1)
|
|
|
|
def basic_message(self, message):
|
|
#message: text to be displayed
|
|
#icon_path: directory holding the icon, defaults to icons subdir of script dir
|
|
#icon_name: filename of icon to be displayed
|
|
self.choice.set(True)
|
|
self.text_label = tk.Label(text = message)
|
|
self.set_colors(self.text_label)
|
|
self.set_colors(self.image_label)
|
|
#pad, direction and weight are all experimentally derived by retrying various values
|
|
self.image_label.grid(row = 1, column = 1, sticky = 'W')
|
|
self.text_label.grid(row = 1, column = 2, sticky = 'W', padx =100)
|
|
self.auto_resize(row_count = 1, column_count = 2)
|
|
self.mainloop()
|
|
|
|
def binary_choice_message(self, message, true = 'Yes', false = 'No'):
|
|
#true: first option, returns True
|
|
#false: second option, returns False
|
|
#usage is kind of opaque and relies on this object persisting after the window destruction to pass back choice
|
|
#usage:
|
|
# frame = InstallerUserMessage.InstallerUserMessage( ... )
|
|
# frame = frame.binary_choice_message( ... )
|
|
# (wait for user to click)
|
|
# value = frame.choice.get()
|
|
|
|
self.text_label = tk.Label(text = message)
|
|
#command registers the callback to the method named. We want the frame to go away once clicked.
|
|
#button 1 returns True/1, button 2 returns False/0
|
|
self.button_one = ttk.Radiobutton(text = true, variable = self.choice, value = True,
|
|
command = self._delete_window, style = 'Linden.TButton')
|
|
self.button_two = ttk.Radiobutton(text = false, variable = self.choice, value = False,
|
|
command = self._delete_window, style = 'Linden.TButton')
|
|
self.set_colors(self.text_label)
|
|
self.set_colors(self.image_label)
|
|
#pad, direction and weight are all experimentally derived by retrying various values
|
|
self.image_label.grid(row = 1, column = 1, rowspan = 3, sticky = 'W')
|
|
self.text_label.grid(row = 1, column = 2, rowspan = 3)
|
|
self.button_one.grid(row = 1, column = 3, sticky = 'W', pady = 40)
|
|
self.button_two.grid(row = 2, column = 3, sticky = 'W', pady = 0)
|
|
self.auto_resize(row_count = 2, column_count = 3, heavy_column = 3)
|
|
#self.button_two.deselect()
|
|
self.update()
|
|
self.mainloop()
|
|
|
|
def trinary_choice_message(self, message, one = 1, two = 2, three = 3):
|
|
#one: first option, returns 1
|
|
#two: second option, returns 2
|
|
#three: third option, returns 3
|
|
#usage is kind of opaque and relies on this object persisting after the window destruction to pass back choice
|
|
#usage:
|
|
# frame = InstallerUserMessage.InstallerUserMessage( ... )
|
|
# frame = frame.binary_choice_message( ... )
|
|
# (wait for user to click)
|
|
# value = frame.choice.get()
|
|
|
|
self.text_label = tk.Label(text = message)
|
|
#command registers the callback to the method named. We want the frame to go away once clicked.
|
|
self.button_one = ttk.Radiobutton(text = one, variable = self.choice, value = 1,
|
|
command = self._delete_window, style = 'Linden.TButton')
|
|
self.button_two = ttk.Radiobutton(text = two, variable = self.choice, value = 2,
|
|
command = self._delete_window, style = 'Linden.TButton')
|
|
self.button_three = ttk.Radiobutton(text = three, variable = self.choice, value = 3,
|
|
command = self._delete_window, style = 'Linden.TButton')
|
|
self.set_colors(self.text_label)
|
|
self.set_colors(self.image_label)
|
|
#pad, direction and weight are all experimentally derived by retrying various values
|
|
self.image_label.grid(row = 1, column = 1, rowspan = 4, sticky = 'W')
|
|
self.text_label.grid(row = 1, column = 2, rowspan = 4, padx = 5)
|
|
self.button_one.grid(row = 1, column = 3, sticky = 'W', pady = 5)
|
|
self.button_two.grid(row = 2, column = 3, sticky = 'W', pady = 5)
|
|
self.button_three.grid(row = 3, column = 3, sticky = 'W', pady = 5)
|
|
self.auto_resize(row_count = 3, column_count = 3, heavy_column = 3)
|
|
#self.button_two.deselect()
|
|
self.update()
|
|
self.mainloop()
|
|
|
|
def progress_bar(self, message = None, size = 0, interval = 100, pb_queue = None):
|
|
#Best effort attempt at a real progress bar
|
|
# This is what Tk calls "determinate mode" rather than "indeterminate mode"
|
|
#size: denominator of percent complete
|
|
#interval: frequency, in ms, of how often to poll the file for progress
|
|
#pb_queue: queue object used to send updates to the bar
|
|
self.text_label = tk.Label(text = message)
|
|
self.set_colors(self.text_label)
|
|
self.set_colors(self.image_label)
|
|
self.image_label.grid(row = 1, column = 1, sticky = 'NSEW')
|
|
self.text_label.grid(row = 2, column = 1, sticky = 'NSEW')
|
|
self.progress = ttk.Progressbar(self, style = 'black.Horizontal.TProgressbar', orient="horizontal", length=100, mode="determinate")
|
|
self.progress.grid(row = 3, column = 1, sticky = 'NSEW')
|
|
self.value = 0
|
|
self.progress["maximum"] = size
|
|
self.auto_resize(row_count = 1, column_count = 3)
|
|
self.queue = pb_queue
|
|
self.check_scheduler()
|
|
|
|
def check_scheduler(self):
|
|
if self.value < self.progress["maximum"]:
|
|
self.check_queue()
|
|
self.id = self.after(100, self.check_scheduler)
|
|
else:
|
|
#prevent a race condition between polling and the widget destruction
|
|
self.after_cancel(self.id)
|
|
|
|
def check_queue(self):
|
|
while self.queue.qsize():
|
|
try:
|
|
msg = float(self.queue.get(0))
|
|
#custom signal, time to tear down
|
|
if msg == -1:
|
|
self.choice.set(True)
|
|
self.destroy()
|
|
else:
|
|
self.progress.step(msg)
|
|
self.value = msg
|
|
except Queue.Empty:
|
|
#nothing to do
|
|
return
|
|
|
|
class ThreadedClient(threading.Thread):
|
|
#for test only, not part of the functional code
|
|
def __init__(self, queue):
|
|
threading.Thread.__init__(self)
|
|
self.queue = queue
|
|
|
|
def run(self):
|
|
for x in range(1, 90, 10):
|
|
time.sleep(1)
|
|
print "run " + str(x)
|
|
self.queue.put(10)
|
|
#tkk progress bars wrap at exactly 100 percent, look full at 99%
|
|
print "leftovers"
|
|
self.queue.put(9)
|
|
time.sleep(5)
|
|
# -1 is a custom signal to the progress_bar to quit
|
|
self.queue.put(-1)
|
|
|
|
if __name__ == "__main__":
|
|
#When run as a script, just test the InstallUserMessage.
|
|
#To proceed with the test, close the first window, select on the second. The third will close by itself.
|
|
import sys
|
|
import tempfile
|
|
|
|
def set_and_check(frame, value):
|
|
print "value: " + str(value)
|
|
frame.progress.step(value)
|
|
if frame.progress["value"] < frame.progress["maximum"]:
|
|
print "In Progress"
|
|
else:
|
|
print "Over now"
|
|
|
|
#basic message window test
|
|
frame2 = InstallerUserMessage(text = "Something in the way she moves....", title = "Beatles Quotes for 100", icon_name="head-sl-logo.gif")
|
|
frame2.basic_message(message = "...attracts me like no other.")
|
|
print "Destroyed!"
|
|
sys.stdout.flush()
|
|
|
|
#binary choice test. User destroys window when they select.
|
|
frame3 = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 200", icon_name="head-sl-logo.gif")
|
|
frame3.binary_choice_message(message = "And all I have to do is think of her.",
|
|
true = "Don't want to leave her now", false = 'You know I believe and how')
|
|
print frame3.choice.get()
|
|
sys.stdout.flush()
|
|
|
|
#progress bar
|
|
queue = Queue.Queue()
|
|
thread = ThreadedClient(queue)
|
|
thread.start()
|
|
print "thread started"
|
|
|
|
frame4 = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 300", icon_name="head-sl-logo.gif")
|
|
frame4.progress_bar(message = "You're asking me will my love grow", size = 100, pb_queue = queue)
|
|
print "frame defined"
|
|
frame4.mainloop()
|
|
|
|
#trinary choice test. User destroys window when they select.
|
|
frame3a = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 200", icon_name="head-sl-logo.gif")
|
|
frame3a.trinary_choice_message(message = "And all I have to do is think of her.",
|
|
one = "Don't want to leave her now", two = 'You know I believe and how', three = 'John is Dead')
|
|
print frame3a.choice.get()
|
|
sys.stdout.flush()
|