Completed
Push — develop ( 574d21...ee6fd4 )
by Jace
19s queued 10s
created

Application.create_reference_widget()   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 15
ccs 0
cts 0
cp 0
rs 9.9
c 0
b 0
f 0
cc 1
nop 2
crap 2
1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3 1
4
"""Graphical interface for Doorstop."""
5 1
6 1
import argparse
7
import functools
8
import logging
9
import os
10
import sys
11
from itertools import chain
12
from unittest.mock import Mock
13
14
from doorstop import common, settings
15 1
from doorstop.common import DoorstopError, HelpFormatter, WarningFormatter
16 1
from doorstop.core import builder, vcs
17 1
from doorstop.gui import utilTkinter, widget
18 1
19 1
try:
20
    import tkinter as tk
21 1
    from tkinter import ttk
22 1
    from tkinter import filedialog
23 1
except ImportError as _exc:
24 1
    sys.stderr.write("WARNING: {}\n".format(_exc))
25 1
    tk = Mock()
26
    ttk = Mock()
27 1
28
29
log = common.logger(__name__)
30 1
31
32 1
def main(args=None):
33
    """Process command-line arguments and run the program."""
34
    from doorstop import GUI, VERSION
35 1
36 1
    # Shared options
37 1
    debug = argparse.ArgumentParser(add_help=False)
38
    debug.add_argument('-V', '--version', action='version', version=VERSION)
39 1
    debug.add_argument(
40 1
        '-v', '--verbose', action='count', default=0, help="enable verbose logging"
41
    )
42
    shared = {'formatter_class': HelpFormatter, 'parents': [debug]}
43 1
    parser = argparse.ArgumentParser(prog=GUI, description=__doc__, **shared)
44
45
    # Build main parser
46
    parser.add_argument(
47 1
        '-j', '--project', metavar="PATH", help="path to the root of the project"
48
    )
49
50 1
    # Parse arguments
51
    args = parser.parse_args(args=args)
52
53 1
    # Configure logging
54 1
    _configure_logging(args.verbose)
55 1
56 1
    # Run the program
57 1
    try:
58 1
        success = run(args, os.getcwd(), parser.error)
59 1
    except KeyboardInterrupt:
60
        log.debug("program interrupted")
61 1
        success = False
62 1
    if success:
63
        log.debug("program exited")
64
        return 0
65 1
    else:
66
        log.debug("program exited with error")
67
        return 1
68 1
69 1
70 1
def _configure_logging(verbosity=0):
71 1
    """Configure logging using the provided verbosity level (0+)."""
72 1
    # Configure the logging level and format
73 1
    if verbosity == 0:
74 1
        level = settings.VERBOSE_LOGGING_LEVEL
75 1
        default_format = settings.VERBOSE_LOGGING_FORMAT
76
        verbose_format = settings.VERBOSE_LOGGING_FORMAT
77 1
    elif verbosity == 1:
78 1
        level = settings.VERBOSE2_LOGGING_LEVEL
79 1
        default_format = settings.VERBOSE_LOGGING_FORMAT
80
        verbose_format = settings.VERBOSE_LOGGING_FORMAT
81
    else:
82 1
        level = settings.VERBOSE2_LOGGING_LEVEL
83 1
        default_format = settings.TIMED_LOGGING_FORMAT
84 1
        verbose_format = settings.TIMED_LOGGING_FORMAT
85
86
    # Set a custom formatter
87 1
    logging.basicConfig(level=level)
88
    formatter = WarningFormatter(default_format, verbose_format)
89
    logging.root.handlers[0].setFormatter(formatter)
90
91
92
def run(args, cwd, error):
93
    """Start the GUI.
94
95 1
    :param args: Namespace of CLI arguments (from this module or the CLI)
96
    :param cwd: current working directory
97 1
    :param error: function to call for CLI errors
98 1
99
    """
100
    from doorstop import __project__, __version__
101
102
    # Exit if tkinter is not available
103
    if isinstance(tk, Mock) or isinstance(ttk, Mock):
104
        return error("tkinter is not available")
105
106
    else:
107
108
        root = widget.Tk()
109
        root.title("{} ({})".format(__project__, __version__))
110
111
        from sys import platform as _platform
112
113
        # Load the icon
114
        if _platform in ("linux", "linux2"):
115
            # Linux
116
            from doorstop.gui import resources
117
118
            root.tk.call(
119
                # pylint: disable=protected-access
120
                'wm',
121
                'iconphoto',
122
                root._w,
123
                tk.PhotoImage(data=resources.b64_doorstopicon_png),
124
            )
125
        elif _platform == "darwin":
126
            # macOS
127
            pass  # TODO
128
        elif _platform in ("win32", "win64"):
129
            # Windows
130
            from doorstop.gui import resources
131
            import base64
132
            import tempfile
133
134
            try:
135
                with tempfile.TemporaryFile(
136
                    mode='w+b', suffix=".ico", delete=False
137
                ) as theTempIconFile:
138
                    theTempIconFile.write(
139
                        base64.b64decode(resources.b64_doorstopicon_ico)
140
                    )
141
                    theTempIconFile.flush()
142
                root.iconbitmap(theTempIconFile.name)
143
            finally:
144
                try:
145
                    os.unlink(theTempIconFile.name)
146
                except Exception:  # pylint: disable=W0703
147
                    pass
148
149
        app = Application(root, cwd, args.project)
150
151
        root.update()
152
        root.minsize(root.winfo_width(), root.winfo_height())
153
        app.mainloop()
154
155
        return True
156
157
158
def _log(func):
159
    """Log name and arguments."""
160
161
    @functools.wraps(func)
162
    def wrapped(self, *args, **kwargs):
163
        sargs = "{}, {}".format(
164
            ', '.join(repr(a) for a in args),
165
            ', '.join("{}={}".format(k, repr(v)) for k, v in kwargs.items()),
166
        )
167
        msg = "log: {}: {}".format(func.__name__, sargs.strip(", "))
168
        if not isinstance(self, ttk.Frame) or not self.ignore:
169
            log.debug(msg.strip())
170
        return func(self, *args, **kwargs)
171
172
    return wrapped
173
174
175
class Application(ttk.Frame):
176
    """Graphical application for Doorstop."""
177
178
    def __init__(self, root, cwd, project):
179
        ttk.Frame.__init__(self, root)
180
181
        # Create Doorstop variables
182
        self.cwd = cwd
183
        self.tree = None
184
        self.document = None
185
        self.item = None
186
187
        # Create string variables
188
        self.stringvar_project = tk.StringVar(value=project or '')
189
        self.stringvar_project.trace('w', self.display_tree)
190
        self.stringvar_document = tk.StringVar()
191
        self.stringvar_document.trace('w', self.display_document)
192
193
        # The stringvar_item holds the uid of the main selected item (or empty string if nothing is selected).
194
        self.stringvar_item = tk.StringVar()
195
        self.stringvar_item.trace('w', self.display_item)
196
197
        self.stringvar_text = tk.StringVar()
198
        self.stringvar_text.trace('w', self.update_item)
199
        self.intvar_active = tk.IntVar()
200
        self.intvar_active.trace('w', self.update_item)
201
        self.intvar_derived = tk.IntVar()
202
        self.intvar_derived.trace('w', self.update_item)
203
        self.intvar_normative = tk.IntVar()
204
        self.intvar_normative.trace('w', self.update_item)
205
        self.intvar_heading = tk.IntVar()
206
        self.intvar_heading.trace('w', self.update_item)
207
        self.stringvar_link = tk.StringVar()  # no trace event
208
        self.stringvar_ref = tk.StringVar()
209
        self.stringvar_ref.trace('w', self.update_item)
210
        self.stringvar_extendedkey = tk.StringVar()
211
        self.stringvar_extendedkey.trace('w', self.display_extended)
212
        self.stringvar_extendedvalue = tk.StringVar()
213
        self.stringvar_extendedvalue.trace('w', self.update_item)
214
215
        # Create widget variables
216
        self.combobox_documents = None
217
        self.text_items = None
218
        self.text_item = None
219
        self.listbox_links = None
220
        self.combobox_extended = None
221
        self.text_extendedvalue = None
222
        self.text_parents = None
223
        self.text_children = None
224
225
        # Initialize the GUI
226
        self.ignore = False  # flag to ignore internal events
227
        frame = self.init(root)
228
        frame.pack(fill=tk.BOTH, expand=1)
229
230
        # Start the application
231
        root.after(500, self.find)
232
233
    def init(self, root):
234
        """Initialize and return the main frame."""
235
        # pylint: disable=attribute-defined-outside-init
236
237
        # Shared arguments
238
        width_text = 30
239
        height_text = 10
240
        height_ext = 5
241
242
        # Shared keyword arguments
243
        kw_f = {'padding': 5}  # constructor arguments for frames
244
        kw_gp = {'padx': 2, 'pady': 2}  # grid arguments for padded widgets
245
        kw_gs = {'sticky': tk.NSEW}  # grid arguments for sticky widgets
246
        kw_gsp = dict(
247
            chain(kw_gs.items(), kw_gp.items())
248
        )  # grid arguments for sticky padded widgets
249
250
        root.bind_all("<Control-minus>", lambda arg: widget.adjustFontSize(-1))
251
        root.bind_all("<Control-equal>", lambda arg: widget.adjustFontSize(1))
252
        root.bind_all("<Control-0>", lambda arg: widget.resetFontSize())
253
254
        # Configure grid
255
        frame = ttk.Frame(root, **kw_f)
256
        frame.rowconfigure(0, weight=0)
257
        frame.rowconfigure(1, weight=1)
258
        frame.columnconfigure(0, weight=2)
259
        frame.columnconfigure(1, weight=1)
260
        frame.columnconfigure(2, weight=1)
261
        frame.columnconfigure(3, weight=2)
262
263
        # Create widgets
264
        def frame_project(root):
265
            """Frame for the current project."""
266
            # Configure grid
267
            frame = ttk.Frame(root, **kw_f)
268
            frame.rowconfigure(0, weight=1)
269
            frame.columnconfigure(0, weight=0)
270
            frame.columnconfigure(1, weight=1)
271
272
            # Place widgets
273
            widget.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
274
            widget.Entry(frame, textvariable=self.stringvar_project).grid(
275
                row=0, column=1, **kw_gsp
276
            )
277
278
            return frame
279
280
        def frame_tree(root):
281
            """Frame for the current document."""
282
            # Configure grid
283
            frame = ttk.Frame(root, **kw_f)
284
            frame.rowconfigure(0, weight=1)
285
            frame.columnconfigure(0, weight=0)
286
            frame.columnconfigure(1, weight=1)
287
288
            # Place widgets
289
            widget.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
290
            self.combobox_documents = widget.Combobox(
291
                frame, textvariable=self.stringvar_document, state="readonly"
292
            )
293
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
294
295
            return frame
296
297
        def frame_document(root):
298
            """Frame for current document's outline and items."""
299
            # Configure grid
300
            frame = ttk.Frame(root, **kw_f)
301
            frame.rowconfigure(0, weight=0)
302
            frame.rowconfigure(1, weight=5)
303
            frame.rowconfigure(2, weight=0)
304
            frame.rowconfigure(3, weight=0)
305
            frame.columnconfigure(0, weight=0)
306
            frame.columnconfigure(1, weight=0)
307
            frame.columnconfigure(2, weight=0)
308
            frame.columnconfigure(3, weight=0)
309
            frame.columnconfigure(4, weight=1)
310
            frame.columnconfigure(5, weight=1)
311
312
            @_log
313
            def treeview_outline_treeviewselect(event):
314
                """Handle selecting an item in the tree view."""
315
                if self.ignore:
316
                    return
317
                thewidget = event.widget
318
                curselection = thewidget.selection()
319
                if curselection:
320
                    uid = curselection[0]
321
                    self.stringvar_item.set(uid)
322
323
            @_log
324
            def treeview_outline_delete(event):  # pylint: disable=W0613
325
                """Handle deleting an item in the tree view."""
326
                if self.ignore:
327
                    return
328
                self.remove()
329
330
            # Place widgets
331
            widget.Label(frame, text="Outline:").grid(
332
                row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp
333
            )
334
            widget.Label(frame, text="Items:").grid(
335
                row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp
336
            )
337
            c_columnId = ("Id",)
338
            self.treeview_outline = widget.TreeView(frame, columns=c_columnId)
339
            for col in c_columnId:
340
                self.treeview_outline.heading(col, text=col)
341
342
            # Add a Vertical scrollbar to the Treeview Outline
343
            treeview_outline_verticalScrollBar = widget.ScrollbarV(
344
                frame, command=self.treeview_outline.yview
345
            )
346
            treeview_outline_verticalScrollBar.grid(
347
                row=1, column=0, columnspan=1, **kw_gs
348
            )
349
            self.treeview_outline.configure(
350
                yscrollcommand=treeview_outline_verticalScrollBar.set
351
            )
352
353
            self.treeview_outline.bind(
354
                "<<TreeviewSelect>>", treeview_outline_treeviewselect
355
            )
356
            self.treeview_outline.bind("<Delete>", treeview_outline_delete)
357
            self.treeview_outline.grid(row=1, column=1, columnspan=3, **kw_gsp)
358
            self.text_items = widget.noUserInput_init(
359
                widget.Text(frame, width=width_text, wrap=tk.WORD)
360
            )
361
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
362
            self.text_items_hyperlink = utilTkinter.HyperlinkManager(self.text_items)
363
            widget.Button(frame, text="<", width=0, command=self.left).grid(
364
                row=2, column=0, sticky=tk.EW, padx=(2, 0)
365
            )
366
            widget.Button(frame, text="v", width=0, command=self.down).grid(
367
                row=2, column=1, sticky=tk.EW
368
            )
369
            widget.Button(frame, text="^", width=0, command=self.up).grid(
370
                row=2, column=2, sticky=tk.EW
371
            )
372
            widget.Button(frame, text=">", width=0, command=self.right).grid(
373
                row=2, column=3, sticky=tk.EW, padx=(0, 2)
374
            )
375
            widget.Button(frame, text="Add Item", command=self.add).grid(
376
                row=2, column=4, sticky=tk.W, **kw_gp
377
            )
378
            widget.Button(frame, text="Remove Selected Item", command=self.remove).grid(
379
                row=2, column=5, sticky=tk.E, **kw_gp
380
            )
381
382
            return frame
383
384
        def frame_item(root):
385
            """Frame for the currently selected item."""
386
            # Configure grid
387
            frame = ttk.Frame(root, **kw_f)
388
            frame.rowconfigure(0, weight=0)
389
            frame.rowconfigure(1, weight=4)
390
            frame.rowconfigure(2, weight=0)
391
            frame.rowconfigure(3, weight=1)
392
            frame.rowconfigure(4, weight=1)
393
            frame.rowconfigure(5, weight=1)
394
            frame.rowconfigure(6, weight=1)
395
            frame.rowconfigure(7, weight=0)
396
            frame.rowconfigure(8, weight=0)
397
            frame.rowconfigure(9, weight=0)
398
            frame.rowconfigure(10, weight=0)
399
            frame.rowconfigure(11, weight=4)
400
            frame.columnconfigure(0, weight=1, pad=kw_f['padding'] * 2)
401
            frame.columnconfigure(1, weight=1)
402
403
            @_log
404
            def text_focusin(_):
405
                """Handle entering a text field."""
406
                self.ignore = True
407
408
            @_log
409
            def text_item_focusout(event):
410
                """Handle updated text text."""
411
                self.ignore = False
412
                thewidget = event.widget
413
                value = thewidget.get('1.0', tk.END)
414
                self.stringvar_text.set(value)
415
416
            @_log
417
            def text_extendedvalue_focusout(event):
418
                """Handle updated extended attributes."""
419
                self.ignore = False
420
                thewidget = event.widget
421
                value = thewidget.get('1.0', tk.END)
422
                self.stringvar_extendedvalue.set(value)
423
424
            # Selected Item
425
            widget.Label(frame, text="Selected Item:").grid(
426
                row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp
427
            )
428
            self.text_item = widget.Text(
429
                frame, width=width_text, height=height_text, wrap=tk.WORD
430
            )
431
            self.text_item.bind('<FocusIn>', text_focusin)
432
            self.text_item.bind('<FocusOut>', text_item_focusout)
433
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
434
435
            # Column: Properties
436
            self.create_properties_widget(frame).grid(
437
                row=2, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
438
            )
439
440
            # Column: Links
441
            self.create_links_widget(frame).grid(
442
                row=4, rowspan=3, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
443
            )
444
445
            # External Reference
446
            self.create_reference_widget(frame).grid(
447
                row=7, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
448
            )
449
450
            widget.Label(frame, text="Extended Attributes:").grid(
451
                row=9, column=0, columnspan=3, sticky=tk.W, **kw_gp
452
            )
453
            self.combobox_extended = widget.Combobox(
454
                frame, textvariable=self.stringvar_extendedkey
455
            )
456
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
457
            self.text_extendedvalue = widget.Text(
458
                frame, width=width_text, height=height_ext, wrap=tk.WORD
459
            )
460
            self.text_extendedvalue.bind('<FocusIn>', text_focusin)
461
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
462
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)
463
464
            return frame
465
466
        def frame_family(root):
467
            """Frame for the parent and child document items."""
468
            # Configure grid
469
            frame = ttk.Frame(root, **kw_f)
470
            frame.rowconfigure(0, weight=0)
471
            frame.rowconfigure(1, weight=1)
472
            frame.rowconfigure(2, weight=0)
473
            frame.rowconfigure(3, weight=1)
474
            frame.columnconfigure(0, weight=1)
475
476
            # Place widgets
477
            widget.Label(frame, text="Linked To:").grid(
478
                row=0, column=0, sticky=tk.W, **kw_gp
479
            )
480
            self.text_parents = widget.noUserInput_init(
481
                widget.Text(frame, width=width_text, wrap=tk.WORD)
482
            )
483
            self.text_parents_hyperlink = utilTkinter.HyperlinkManager(
484
                self.text_parents
485
            )
486
            self.text_parents.grid(row=1, column=0, **kw_gsp)
487
            widget.Label(frame, text="Linked From:").grid(
488
                row=2, column=0, sticky=tk.W, **kw_gp
489
            )
490
            self.text_children = widget.noUserInput_init(
491
                widget.Text(frame, width=width_text, wrap=tk.WORD)
492
            )
493
            self.text_children_hyperlink = utilTkinter.HyperlinkManager(
494
                self.text_children
495
            )
496
            self.text_children.grid(row=3, column=0, **kw_gsp)
497
498
            return frame
499
500
        # Place widgets
501
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
502
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
503
        frame_document(frame).grid(row=1, column=0, **kw_gs)
504
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
505
        frame_family(frame).grid(row=1, column=3, **kw_gs)
506
507
        return frame
508
509
    @_log
510
    def find(self):
511
        """Find the root of the project."""
512
        if not self.stringvar_project.get():
513
            try:
514
                path = vcs.find_root(self.cwd)
515
            except DoorstopError as exc:
516
                log.error(exc)
517
            else:
518
                self.stringvar_project.set(path)
519
520
    @_log
521
    def browse(self):
522
        """Browse for the root of a project."""
523
        path = filedialog.askdirectory()
524
        log.debug("path: {}".format(path))
525
        if path:
526
            self.stringvar_project.set(path)
527
528
    @_log
529
    def display_tree(self, *_):
530
        """Display the currently selected tree."""
531
        # Set the current tree
532
        self.tree = builder.build(root=self.stringvar_project.get())
533
        log.info("displaying tree...")
534
535
        # Display the documents in the tree
536
        values = [
537
            "{} ({})".format(document.prefix, document.relpath)
538
            for document in self.tree
539
        ]
540
        self.combobox_documents['values'] = values
541
542
        # Select the first document
543
        if len(self.tree):  # pylint: disable=len-as-condition
544
            self.combobox_documents.current(0)
545
        else:
546
            logging.warning("no documents to display")
547
548
    @_log
549
    def display_document(self, *_):
550
        """Display the currently selected document."""
551
        # Set the current document
552
        index = self.combobox_documents.current()
553
        self.document = list(self.tree)[index]
554
        log.info("displaying document {}...".format(self.document))
555
556
        # Record the currently opened items.
557
        c_openItem = []
558
        for c_currUID in utilTkinter.getAllChildren(self.treeview_outline):
559
            if self.treeview_outline.item(c_currUID)["open"]:
560
                c_openItem.append(c_currUID)
561
562
        # Record the currently selected items.
563
        c_selectedItem = self.treeview_outline.selection()
564
565
        # Clear the widgets
566
        self.treeview_outline.delete(*self.treeview_outline.get_children())
567
        widget.noUserInput_delete(self.text_items, '1.0', tk.END)
568
        self.text_items_hyperlink.reset()
569
570
        # Display the items in the document
571
        c_levelsItem = [""]
572
        for item in self.document.items:
573
            theParent = next(
574
                iter(reversed([x for x in c_levelsItem[: item.depth]])), ""
575
            )
576
577
            while len(c_levelsItem) < item.depth:
578
                c_levelsItem.append(item.uid)
579
            c_levelsItem = c_levelsItem[: item.depth]
580
            for x in range(item.depth):
581
                c_levelsItem.append(item.uid)
582
583
            # Add the item to the document outline
584
            self.treeview_outline.insert(
585
                theParent,
586
                tk.END,
587
                item.uid,
588
                text=item.level,
589
                values=(item.uid,),
590
                open=item.uid in c_openItem,
591
            )
592
593
            # Add the item to the document text
594
            widget.noUserInput_insert(
595
                self.text_items, tk.END, "{t}".format(t=item.text or item.ref or '???')
596
            )
597
            widget.noUserInput_insert(self.text_items, tk.END, " [")
598
            widget.noUserInput_insert(
599
                self.text_items,
600
                tk.END,
601
                item.uid,
602
                self.text_items_hyperlink.add(
603
                    # pylint: disable=unnecessary-lambda
604
                    lambda c_theURL: self.followlink(c_theURL),
605
                    item.uid,
606
                    ["refLink"],
607
                ),
608
            )
609
            widget.noUserInput_insert(self.text_items, tk.END, "]\n\n")
610
611
        # Set tree view selection
612
        c_selectedItem = [
613
            x
614
            for x in c_selectedItem
615
            if x in utilTkinter.getAllChildren(self.treeview_outline)
616
        ]
617
        if c_selectedItem:
618
            # Restore selection
619
            self.treeview_outline.selection_set(c_selectedItem)
620
        else:
621
            # Select the first item
622
            for uid in utilTkinter.getAllChildren(self.treeview_outline):
623
                self.stringvar_item.set(uid)
624
                break
625
            else:
626
                logging.warning("no items to display")
627
                self.stringvar_item.set("")
628
629
    @_log
630
    def display_item(self, *_):
631
        """Display the currently selected item."""
632
        try:
633
            self.ignore = True
634
635
            # Fetch the current item
636
            uid = self.stringvar_item.get()
637
            if uid == "":
638
                self.item = None
639
            else:
640
                try:
641
                    self.item = self.tree.find_item(uid)
642
                except DoorstopError:
643
                    pass
644
            log.info("displaying item {}...".format(self.item))
645
646
            if uid != "":
647
                if uid not in self.treeview_outline.selection():
648
                    self.treeview_outline.selection_set((uid,))
649
                self.treeview_outline.see(uid)
650
651
            # Display the item's text
652
            self.text_item.replace(
653
                '1.0', tk.END, "" if self.item is None else self.item.text
654
            )
655
656
            # Display the item's properties
657
            self.stringvar_text.set("" if self.item is None else self.item.text)
658
            self.intvar_active.set(False if self.item is None else self.item.active)
659
            self.intvar_derived.set(False if self.item is None else self.item.derived)
660
            self.intvar_normative.set(
661
                False if self.item is None else self.item.normative
662
            )
663
            self.intvar_heading.set(False if self.item is None else self.item.heading)
664
665
            # Display the item's links
666
            self.listbox_links.delete(0, tk.END)
667
            if self.item is not None:
668
                for uid in self.item.links:
669
                    self.listbox_links.insert(tk.END, uid)
670
            self.stringvar_link.set('')
671
672
            # Display the item's external reference
673
            self.stringvar_ref.set("" if self.item is None else self.item.ref)
674
675
            # Display the item's extended attributes
676
            values = None if self.item is None else self.item.extended
677
            self.combobox_extended['values'] = values or ['']
678
            if self.item is not None:
679
                self.combobox_extended.current(0)
680
681
            # Display the items this item links to
682
            widget.noUserInput_delete(self.text_parents, '1.0', tk.END)
683
            self.text_parents_hyperlink.reset()
684
            if self.item is not None:
685
                for uid in self.item.links:
686
                    try:
687
                        item = self.tree.find_item(uid)
688
                    except DoorstopError:
689
                        text = "???"
690
                    else:
691
                        text = item.text or item.ref or '???'
692
                        uid = item.uid
693
694
                    widget.noUserInput_insert(
695
                        self.text_parents, tk.END, "{t}".format(t=text)
696
                    )
697
                    widget.noUserInput_insert(self.text_parents, tk.END, " [")
698
                    widget.noUserInput_insert(
699
                        self.text_parents,
700
                        tk.END,
701
                        uid,
702
                        self.text_parents_hyperlink.add(
703
                            # pylint: disable=unnecessary-lambda
704
                            lambda c_theURL: self.followlink(c_theURL),
705
                            uid,
706
                            ["refLink"],
707
                        ),
708
                    )
709
                    widget.noUserInput_insert(self.text_parents, tk.END, "]\n\n")
710
711
            # Display the items this item has links from
712
            widget.noUserInput_delete(self.text_children, '1.0', 'end')
713
            self.text_children_hyperlink.reset()
714
            if self.item is not None:
715
                for uid in self.item.find_child_links():
716
                    item = self.tree.find_item(uid)
717
                    text = item.text or item.ref or '???'
718
                    uid = item.uid
719
720
                    widget.noUserInput_insert(
721
                        self.text_children, tk.END, "{t}".format(t=text)
722
                    )
723
                    widget.noUserInput_insert(self.text_children, tk.END, " [")
724
                    widget.noUserInput_insert(
725
                        self.text_children,
726
                        tk.END,
727
                        uid,
728
                        self.text_children_hyperlink.add(
729
                            # pylint: disable=unnecessary-lambda
730
                            lambda c_theURL: self.followlink(c_theURL),
731
                            uid,
732
                            ["refLink"],
733
                        ),
734
                    )
735
                    widget.noUserInput_insert(self.text_children, tk.END, "]\n\n")
736
        finally:
737
            self.ignore = False
738
739
    @_log
740
    def display_extended(self, *_):
741
        """Display the currently selected extended attribute."""
742
        try:
743
            self.ignore = True
744
745
            name = self.stringvar_extendedkey.get()
746
            log.debug("displaying extended attribute '{}'...".format(name))
747
            self.text_extendedvalue.replace('1.0', tk.END, self.item.get(name, ""))
748
        finally:
749
            self.ignore = False
750
751
    @_log
752
    def update_item(self, *_):
753
        """Update the current item from the fields."""
754
        if self.ignore:
755
            return
756
        if not self.item:
757
            logging.warning("no item selected")
758
            return
759
760
        # Update the current item
761
        log.info("updating {}...".format(self.item))
762
        self.item.auto = False
763
        self.item.text = self.stringvar_text.get()
764
        self.item.active = self.intvar_active.get()
765
        self.item.derived = self.intvar_derived.get()
766
        self.item.normative = self.intvar_normative.get()
767
        self.item.heading = self.intvar_heading.get()
768
        self.item.links = self.listbox_links.get(0, tk.END)
769
        self.item.ref = self.stringvar_ref.get()
770
        name = self.stringvar_extendedkey.get()
771
        if name:
772
            self.item.set(name, self.stringvar_extendedvalue.get())
773
        self.item.save()
774
775
        # Re-select this item
776
        self.display_document()
777
778
    @_log
779
    def left(self):
780
        """Dedent the current item's level."""
781
        self.item.level <<= 1
782
        self.document.reorder(keep=self.item)
783
        self.display_document()
784
785
    @_log
786
    def down(self):
787
        """Increment the current item's level."""
788
        self.item.level += 1
789
        self.document.reorder(keep=self.item)
790
        self.display_document()
791
792
    @_log
793
    def up(self):
794
        """Decrement the current item's level."""
795
        self.item.level -= 1
796
        self.document.reorder(keep=self.item)
797
        self.display_document()
798
799
    @_log
800
    def right(self):
801
        """Indent the current item's level."""
802
        self.item.level >>= 1
803
        self.document.reorder(keep=self.item)
804
        self.display_document()
805
806
    @_log
807
    def add(self):
808
        """Add a new item to the document."""
809
        logging.info("adding item to {}...".format(self.document))
810
        if self.item:
811
            level = self.item.level + 1
812
        else:
813
            level = None
814
        item = self.document.add_item(level=level)
815
        logging.info("added item: {}".format(item))
816
        # Refresh the document view
817
        self.display_document()
818
        # Set the new selection
819
        self.stringvar_item.set(item.uid)
820
821
    @_log
822
    def remove(self):
823
        """Remove the selected item from the document."""
824
        newSelection = ""
825
        for c_currUID in self.treeview_outline.selection():
826
            # Find the item which should be selected once the current selection is removed.
827
            for currNeighbourStrategy in (
828
                self.treeview_outline.next,
829
                self.treeview_outline.prev,
830
                self.treeview_outline.parent,
831
            ):
832
                newSelection = currNeighbourStrategy(c_currUID)
833
                if newSelection != "":
834
                    break
835
            # Remove the item
836
            item = self.tree.find_item(c_currUID)
837
            logging.info("removing item {}...".format(item))
838
            item = self.tree.remove_item(item)
839
            logging.info("removed item: {}".format(item))
840
            # Set the new selection
841
            self.stringvar_item.set(newSelection)
842
            # Refresh the document view
843
            self.display_document()
844
845
    @_log
846
    def link(self):
847
        """Add the specified link to the current item."""
848
        # Add the specified link to the list
849
        uid = self.stringvar_link.get()
850
        if uid:
851
            self.listbox_links.insert(tk.END, uid)
852
            self.stringvar_link.set('')
853
854
            # Update the current item
855
            self.update_item()
856
857
    @_log
858
    def unlink(self):
859
        """Remove the currently selected link from the current item."""
860
        # Remove the selected link from the list
861
        index = self.listbox_links.curselection()
862
        self.listbox_links.delete(index)
863
864
        # Update the current item
865
        self.update_item()
866
867
    @_log
868
    def followlink(self, uid):
869
        """Display a given uid."""
870
        # Update the current item
871
        self.ignore = False
872
        self.update_item()
873
874
        # Load the good document.
875
        document = self.tree.find_document(uid.prefix)
876
        index = list(self.tree).index(document)
877
        self.combobox_documents.current(index)
878
        self.display_document()
879
880
        # load the good Item
881
        self.stringvar_item.set(uid)
882
883
    def create_properties_widget(self, parent):
884
        frame = ttk.Frame(parent)
885
886
        frame.columnconfigure(0, weight=1)
887
        frame.rowconfigure(0, weight=1)
888
        frame.rowconfigure(1, weight=1)
889
        frame.rowconfigure(2, weight=1)
890
        frame.rowconfigure(3, weight=1)
891
        frame.rowconfigure(4, weight=1)
892
893
        widget.Label(frame, text="Properties:").grid(row=0, column=0, sticky=tk.NW)
894
        widget.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(
895
            row=1, column=0, sticky=tk.NW
896
        )
897
        widget.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(
898
            row=2, column=0, sticky=tk.NW
899
        )
900
        widget.Checkbutton(
901
            frame, text="Normative", variable=self.intvar_normative
902
        ).grid(row=3, column=0, sticky=tk.NW)
903
        widget.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(
904
            row=4, column=0, sticky=tk.NW
905
        )
906
907
        return frame
908
909
    def create_links_widget(self, parent):
910
        frame = ttk.Frame(parent)
911
912
        frame.columnconfigure(0, weight=1)
913
        frame.columnconfigure(1, weight=1)
914
        frame.columnconfigure(2, weight=0)
915
        frame.columnconfigure(3, weight=0)
916
        frame.rowconfigure(0, weight=1)
917
        frame.rowconfigure(1, weight=1)
918
        frame.rowconfigure(2, weight=1)
919
920
        width_uid = 10
921
        widget.Label(frame, text="Links:").grid(
922
            row=0, column=0, columnspan=1, sticky=tk.NW
923
        )
924
        widget.Entry(frame, textvariable=self.stringvar_link).grid(
925
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
926
        )
927
        widget.Button(frame, text="+", command=self.link).grid(
928
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
929
        )
930
        widget.Button(frame, text="-", command=self.unlink).grid(
931
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
932
        )
933
        self.listbox_links = widget.Listbox(frame, width=width_uid)
934
        self.listbox_links.grid(
935
            row=2,
936
            column=0,
937
            rowspan=2,
938
            columnspan=4,
939
            padx=(3, 0),
940
            pady=(3, 0),
941
            sticky=tk.NSEW,
942
        )
943
944
        return frame
945
946
    def create_reference_widget(self, parent):
947
        frame = ttk.Frame(parent)
948
949
        frame.columnconfigure(0, weight=1)
950
        frame.rowconfigure(0, weight=1)
951
        frame.rowconfigure(1, weight=1)
952
953
        widget.Label(frame, text="External Reference:").grid(
954
            row=0, column=0, sticky=tk.W
955
        )
956
        widget.Entry(frame, textvariable=self.stringvar_ref).grid(
957
            row=1, column=0, sticky=tk.NSEW
958
        )
959
960
        return frame
961
962
963
if __name__ == '__main__':
964
    sys.exit(main())
965