summaryrefslogtreecommitdiff
path: root/indra/viewer_components/manager/InstallerUserMessage.py
blob: bef2bf28a66bf78b535be3e4b4dd68e4f3bcfb79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#!/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.
    def __init__(self, text="", title="", width=500, height=200, fillcolor='#487A7B', *args, **kwargs):
        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='#487A7B', background='black')
        ttk.Style().configure('Linden.TButton', foreground='#487A7B', background='black')
        ttk.Style().configure("black.Horizontal.TProgressbar", foreground='#487A7B', background='black')

        # 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))
        self.script_dir = os.path.dirname(os.path.realpath(__file__))
        self.icon_dir = os.path.abspath(os.path.join(self.script_dir, 'icons'))
        #defines what to do when window is closed
        self.protocol("WM_DELETE_WINDOW", self._delete_window)

    def _delete_window(self):
        #capture and discard all destroy events before the choice is set
        if not ((self.choice == None) or (self.choice == "")):
            try:
                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 = '#487A7B')
        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, icon_path = None, icon_name = None):
        #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.find_icon(icon_path, icon_name)
        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(1, 2)
        self.mainloop()

    def binary_choice_message(self, message, icon_path = None, icon_name = None, one = 'Yes', two = 'No'):
        #one: first option, returns True
        #two: 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.find_icon(icon_path, icon_name)
        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 = one, variable = self.choice, value = True, 
            command = self._delete_window, style = 'Linden.TButton')
        self.button_two = ttk.Radiobutton(text = two, 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 progress_bar(self, message = None, icon_path = None, icon_name = 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.find_icon(icon_path, icon_name)
        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(1, 3)
        self.queue = pb_queue
        self.check_scheduler()

    def check_scheduler(self):
        if self.value < self.progress["maximum"]:
            self.check_queue()
            self.after(100, self.check_scheduler)

    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:
                pass

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.
    from contextlib import closing
    from multiprocessing import Process

    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")
    frame2.basic_message(message = "...attracts me like no other.", icon_name="head-sl-logo.gif")
    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")
    frame3.binary_choice_message(message = "And all I have to do is think of her.", icon_name="head-sl-logo.gif", 
        one = "Don't want to leave her now", two = '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")
    frame4.progress_bar(message = "You're asking me will my love grow", icon_name="head-sl-logo.gif", size = 100, pb_queue = queue)
    print "frame defined"
    
    frame4.mainloop()