peterbrittain / asciimatics

A cross platform package to do curses-like operations, plus higher level APIs and widgets to create text UIs and ASCII art animations
Apache License 2.0
3.63k stars 237 forks source link

How to update a label... or even better... a title? #83

Closed zeluspudding closed 7 years ago

zeluspudding commented 7 years ago

Shoot. I accidentally posted too soon. then tried to delete the issue. Just learned "ISSUE DELETE" doesn't exist. Bare with me as I flesh out the details

zeluspudding commented 7 years ago

Is it possible to update the text in a label or title? I have a "single page" (form) terminal application I'm making that basically "allows me to go left or right" (via buttons) through different "slides". Really it's just redrawing the same form with different data. It would be nice to know which "slide" I'm on by dynamically updating the title or a label. How would I do that?

zeluspudding commented 7 years ago

Also, I don't know how I feel being the 666th person staring your repo :/ :) just kidding

zeluspudding commented 7 years ago

Here's some example code:

from asciimatics.widgets import Frame, TextBox, Layout, Label, Divider, Text, \
    RadioButtons, Button, PopUpDialog
from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.exceptions import ResizeScreenError, NextScene, StopApplication, \
    InvalidFields
import sys
import pandas
import os
import time

class Model(object):
    def __init__(self, db_path = None):
        def load_database(self):
            if self._db_path == None:
                print('need path to db')
            else:
                return pandas.read_pickle(self._db_path)
        self._db_path = db_path
        self._db = load_database(self)  # object created by the class
        self.length = len(self._db)

    def __str__(self):
        return str(self._db.tail(1))

    def update_contract(self, row):        
        self._db.update(row)

    def get_contract(self, index):
        def stringify_values(dict_):
            return {k: str(v) if not isinstance(v, str)
                            else v for k, v in dict_.items()}
        # Create wrap around index
        if index < 0:
            self.index = self.length + index
        elif index >= self.length:
            self.index = index - self.length
        else:
            self.index = index

        return stringify_values(dict(self._db.loc[self.index]))

    def get_index_range(self):
        return len(self._db)

class DemoFrame(Frame):
    def __init__(self, screen, model):
        super(DemoFrame, self).__init__(screen,
                                        int(screen.height),
                                        int(screen.width * 2 // 3),
                                        data=model.get_contract(-1),
                                        title="My Form",
                                        reduce_cpu=True)
        # Save off the model that accesses the contacts database.
        self._model = model
        layout = Layout([10, 30, 1])
        self.add_layout(layout)
        layout.add_widget(Label("File %s / 9441" %(model.index+1)), 0)
        layout.add_widget(
            Text(label="principal:",
                 name="principal",
                 on_change=self._on_change), 1)

        layout.add_widget(
            Text(label="lender:",
                 name="lender",
                 on_change=self._on_change), 1)
        layout.add_widget(
            Text(label="lender_address1:",
                 name="lender_address1",
                 on_change=self._on_change), 1)
        layout.add_widget(RadioButtons([("Yes: ", 1),
                                        ("No: ", 2)],
                                       label="Is this a valid contract?",
                                       name="Valid Mortgage",
                                       on_change=self._on_change), 1)
        layout.add_widget(Divider(height=3), 1)
        layout2 = Layout([1, 1, 1])
        self.add_layout(layout2)
        layout2.add_widget(Button("Next", self._next), 0)
        layout2.add_widget(Button("Previous", self._previous), 1)
        layout2.add_widget(Button("Quit", self._quit), 2)
        self.fix()

    def _on_change(self):
        self.save()

    def _next(self):
        # Save table
        # Build new scene?
        self.data = self._model.get_contract(self._model.index + 1)

    def _previous(self):
        # Save table
        self.data = self._model.get_contract(self._model.index - 1)

    def _quit(self):
        self._scene.add_effect(
            PopUpDialog(self._screen,
                        "Quit?",
                        ["No", "Yes"],
                        on_close=self._quit_on_yes))

    @staticmethod
    def _quit_on_yes(selected):
        # Yes is the second button
        if selected == 1:
            raise StopApplication("User requested exit")

def demo(screen, scene):
    screen.play([Scene([DemoFrame(screen, db)], -1)], stop_on_resize=True, start_scene=scene)

base_path = r'path\to\pandas_data'
mortgage_db = os.path.join(base_path, r'code\post_process_pdfs\data_entry_db.pkl')

db = Model(mortgage_db)
last_scene = None
while True:
    try:
        Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) # Catch interrupt turns off quiting via CTRL+C
        sys.exit(0)
    except ResizeScreenError as e:
        last_scene = e.scene
peterbrittain commented 7 years ago

Thanks for the vote... Hopefully it'll move off 666 soon! ;-)

peterbrittain commented 7 years ago

In answer to your question, there is no API to change the title or a label at the moment. While you can update the internal properties and force an update, it is not "the done thing". I'd be happy for them to be exposed in future releases, though. In the meantime, I suggest you use a disabled Text or TextBox widget instead of the Label. See the _header field in https://github.com/peterbrittain/asciimatics/blob/master/samples/top.py for an example of how to do it.

zeluspudding commented 7 years ago

Hmmm. I've tried your suggestion and while a TextBox appears to get placed in my form (and is even usable if I enable it) I can't seem to get it to render pre-assigned text via .value. What am I missing?

The _header string I'm trying to render is "*****This doesn't show up****" below.

from asciimatics.widgets import Frame, TextBox, Layout, Label, Divider, Text, \
    RadioButtons, Button, PopUpDialog
from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.exceptions import ResizeScreenError, NextScene, StopApplication, \
    InvalidFields
import sys
import pandas
import os
import time

class Model(object):
    def __init__(self, db_path = None):
        def load_database(self):
            if self._db_path == None:
                print('need path to db')
            else:
                return pandas.read_pickle(self._db_path)
        self._db_path = db_path
        self._db = load_database(self)  # object created by the class
        self.length = len(self._db)

    def __str__(self):
        return str(self._db.tail(1))

    def update_contract(self, row):        
        self._db.update(row)

    def get_contract(self, index):
        def stringify_values(dict_):
            return {k: str(v) if not isinstance(v, str)
                            else v for k, v in dict_.items()}
        # Create wrap around index
        if index < 0:
            self.index = self.length + index
        elif index >= self.length:
            self.index = index - self.length
        else:
            self.index = index

        return stringify_values(dict(self._db.loc[self.index]))

    def get_index_range(self):
        return len(self._db)

class DemoFrame(Frame):
    def __init__(self, screen, model):
        super(DemoFrame, self).__init__(screen,
                                        int(screen.height),
                                        int(screen.width * 2 // 3),
                                        data=model.get_contract(0),
                                        reduce_cpu=True,
                                        name="My Form")
        # Save off the model that accesses the contacts database.
        self._model = model
        layout = Layout([1, 30, 1])
        self.add_layout(layout)
        self._header = TextBox(1, as_string=True)
        self._header.disabled = True
        self._header.custom_colour = "label"
#        self._header.value = "File %s / 9441" % self._model.index
        self._header.value = "*****This doesn't show up****"
        layout.add_widget(self._header, 1)
        layout.add_widget(
            Text(label="Principal:",
                 name="principal",
                 on_change=self._on_change), 1)
        layout.add_widget(
            Text(label="Lender:",
                 name="lender",
                 on_change=self._on_change), 1)
        layout.add_widget(
            Text(label="Lender Address:",
                 name="lender_address1",
                 on_change=self._on_change), 1)
        layout.add_widget(
            Text(label="Maturity Date:",
                 name="maturity_date",
                 on_change=self._on_change), 1)
        layout.add_widget(
            Text(label="Interest Rate [%]:",
                 name="interest_rate",
                 on_change=self._on_change), 1)
        layout.add_widget(RadioButtons([("No: ", 1),
                                        ("Yes: ", 2)],
                                       label="Remove this record?",
                                       name="Valid Mortgage",
                                       on_change=self._on_change), 1)
        layout.add_widget(Divider(height=3), 1)
        layout2 = Layout([1, 1, 1])
        self.add_layout(layout2)
        layout2.add_widget(Button("Next", self._next), 0)
        layout2.add_widget(Button("Previous", self._previous), 1)
        layout2.add_widget(Button("Quit", self._quit), 2)
        self.fix()

    def _on_change(self):
#        changed = False
        self.save()
#        for key, value in self.data.items():
#            if key not in form_data or form_data[key] != value:
#                changed = True
#                break

    def _next(self):
        # Save table
        self._header.value = "Form %s / 9441" %(self._model.index + 1)       
        self.data = self._model.get_contract(self._model.index + 1)

    def _previous(self):
        # Save table
        self.data = self._model.get_contract(self._model.index - 1)

#        try:
#            self.save(validate=True)
#            message = "Values entered are:\n\n"
#            for key, value in self.data.items():
#                message += "- {}: {}\n".format(key, value)
#        except InvalidFields as exc:
#            message = "The following fields are invalid:\n\n"
#            for field in exc.fields:
#                message += "- {}\n".format(field)
#        self._scene.add_effect(
#            PopUpDialog(self._screen, message, ["OK"]))

    def _quit(self):
        self._scene.add_effect(
            PopUpDialog(self._screen,
                        "Quit?",
                        ["No", "Yes"],
                        on_close=self._quit_on_yes))

    @staticmethod
    def _quit_on_yes(selected):
        # Yes is the second button
        if selected == 1:
            raise StopApplication("User requested exit")

def demo(screen, scene):
    screen.play([Scene([DemoFrame(screen, db)], -1)], stop_on_resize=True, start_scene=scene)

base_path = r'path_to_db'
mortgage_db = os.path.join(base_path, r'code\post_process_pdfs\data_entry_db.pkl')

db = Model(mortgage_db)
last_scene = None
while True:
    try:
        Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) # Catch interrupt turns off quiting via CTRL+C
        sys.exit(0)
    except ResizeScreenError as e:
        last_scene = e.scene
peterbrittain commented 7 years ago

What version are you using?

peterbrittain commented 7 years ago

I suspect that you're running the v1.8.0 or earlier. There is a patch to the latest version of the code that allows you to override the value of a Widget during construction. When I run your code with the latest version on master, you see the "*This doesn't show up" Label.

zeluspudding commented 7 years ago

Ahh yes. I am running v1.8.0. I'll update. Thank you so much again!

... I'd just like to compliment that I think asciimatics, in particular your contacts.py example, is an excellent introduction to Separated Presentation. Thank you so much for taking the time to whip up useful examples and documentation!

peterbrittain commented 7 years ago

OK - I've now exposed these in the latest build.