39
39
from pyrevit .userconfig import user_config
40
40
from pyrevit import DB
41
41
42
-
43
- #pylint: disable=W0703,C0302,C0103
44
42
mlogger = logger .get_logger (__name__ )
45
43
46
-
47
44
DEFAULT_STYLESHEET_NAME = 'outputstyles.css'
48
45
49
46
50
- def docclosing_eventhandler (sender , args ): #pylint: disable=W0613
47
+ def docclosing_eventhandler (sender , args ):
51
48
"""Close all output window on document closing."""
52
49
ScriptConsoleManager .CloseActiveOutputWindows ()
53
50
@@ -97,7 +94,10 @@ def reset_stylesheet():
97
94
98
95
class PyRevitOutputWindow (object ):
99
96
"""Wrapper to interact with the output window."""
100
-
97
+
98
+ def __init__ (self ):
99
+ self ._table_counter = 0
100
+
101
101
@property
102
102
def window (self ):
103
103
"""``PyRevitLabs.PyRevit.Runtime.ScriptConsole``: Return output window object."""
@@ -551,16 +551,170 @@ def print_md(md_str):
551
551
if PY3 :
552
552
sys .stdout .flush ()
553
553
554
- def print_table (self , table_data , columns = None , formats = None ,
555
- title = '' , last_line_style = '' ):
556
- """Print provided data in a table in output window.
554
+
555
+ def table_html_header (self , columns , table_uid , border_style ):
556
+ """Helper method for print_table() method
557
+
558
+ Return html <thead><tr><th> row for the table header
559
+
560
+ Args:
561
+ columns (list[str]): list of column names
562
+ table_uid (str): a unique ID for this table's CSS classes
563
+ border_style (str): CSS border style string for table cells
564
+
565
+ Returns:
566
+ str: HTML string containing the table header row
567
+
568
+ Examples:
569
+ ```python
570
+ output = pyrevit.output.get_output()
571
+
572
+ # Basic usage - called internally by print_table()
573
+ columns = ["Name", "Age", "City"]
574
+ table_uid = 1
575
+ border_style = "border: 1px solid black;"
576
+ header_html = output.table_html_header(
577
+ columns, table_uid, border_style)
578
+ # Returns: "<thead><tr style='border: 1px solid black;'>" \
579
+ # "<th class='head_title-1-0' align='left'>Name</th>" \
580
+ # "<th class='head_title-1-1' align='left'>Age</th>" \
581
+ # "<th class='head_title-1-2' align='left'>City</th>" \
582
+ # "</tr></thead>"
583
+
584
+ # Without border style
585
+ header_html = output.table_html_header(
586
+ columns, table_uid, "")
587
+ # Returns: "<thead><tr>" \
588
+ # "<th class='head_title-1-0' align='left'>Name</th>" \
589
+ # "<th class='head_title-1-1' align='left'>Age</th>" \
590
+ # "<th class='head_title-1-2' align='left'>City</th>" \
591
+ # "</tr></thead>"
592
+ ```
593
+ """
594
+ html_head = "<thead><tr {}>" .format (border_style )
595
+ for i , c in enumerate (columns ):
596
+ html_head += \
597
+ "<th class='head_title-{}-{}' align='left'>{}</th>" .format (
598
+ table_uid , i , c )
599
+ # pyRevit original print_table uses align='left'.
600
+ # This is now overridden by CSS if specified
601
+ html_head += "</tr></thead>"
602
+
603
+ return html_head
604
+
605
+
606
+ def table_check_input_lists (self ,
607
+ table_data ,
608
+ columns ,
609
+ formats ,
610
+ input_kwargs ):
611
+ """Helper method for print_table() method
612
+
613
+ Check that the table_data is present and is a list
614
+ Check that table_data rows are of the same length
615
+ Check that all print_table() kwargs of type list are of correct length
616
+
617
+ Args:
618
+ table_data (list[list[Any]]): The whole table data as 2D list
619
+ columns (list[str]): list of column names
620
+ formats (list[str]): list of format strings for each column
621
+ input_kwargs (list[list[Any]]): list of additional argument lists
622
+
623
+ Returns:
624
+ tuple: (bool, str) - (True/False, message) indicating result
625
+
626
+ Examples:
627
+ ```python
628
+ output = pyrevit.output.get_output()
629
+
630
+ # Valid table data
631
+ table_data = [["John", 25, "NYC"], ["Jane", 30, "LA"]]
632
+ columns = ["Name", "Age", "City"]
633
+ formats = ["", "{} years", ""]
634
+ input_kwargs = [["left", "center", "right"],
635
+ ["100px", "80px", "120px"]]
636
+
637
+ is_valid, message = output.table_check_input_lists(
638
+ table_data, columns, formats, input_kwargs)
639
+ # Returns: (True, "Inputs OK")
640
+
641
+ # Invalid - mismatched column count
642
+ table_data = [["John", 25], ["Jane", 30, "LA"]] # Inconsistent
643
+ is_valid, message = output.table_check_input_lists(
644
+ table_data, columns, formats, input_kwargs)
645
+ # Returns: (False, "Not all rows of table_data are of "
646
+ # "equal length")
647
+
648
+ # Invalid - wrong number of columns
649
+ columns = ["Name", "Age"] # Only 2 columns but data has 3
650
+ is_valid, message = output.table_check_input_lists(
651
+ table_data, columns, formats, input_kwargs)
652
+ # Returns: (False, "Column head list length not equal "
653
+ # "to data row")
654
+
655
+ # Invalid - empty table data
656
+ is_valid, message = output.table_check_input_lists(
657
+ [], columns, formats, input_kwargs)
658
+ # Returns: (False, "No table_data list")
659
+ ```
660
+ """
661
+
662
+ # First check positional and named keyword args
663
+ if not table_data :
664
+ return False , "No table_data list"
665
+ if not isinstance (table_data , list ):
666
+ return False , "table_data is not a list"
667
+ # table_data is a list. The first sublist must also be a list
668
+ first_data_row = list (table_data [0 ])
669
+ if not isinstance (first_data_row , list ):
670
+ return False , "table_data's first row is not a list or tuple ({})" .format (type (first_data_row ))
671
+ len_data_row = len (first_data_row )
672
+ if not all (len (row ) == len_data_row for row in table_data ):
673
+ return False , "Not all rows of table_data are of equal length"
674
+
675
+ if columns and len_data_row != len (columns ): # columns is allowed to be None
676
+ return False , "Column head list length not equal to data row"
677
+
678
+ if formats and len_data_row != len (formats ): # formats is allowed to be None
679
+ return False , "Formats list length not equal to data row"
680
+
681
+ # Next check **kwargs
682
+ # Loop through the lists and return if not a list or len not equal
683
+ for kwarg_list in input_kwargs :
684
+ if not kwarg_list : # No kwarg is OK beacause they are optional
685
+ continue
686
+ if not isinstance (kwarg_list , list ):
687
+ return False , "One of the print_table kwargs that should be a list is not a list ({})" .format (kwarg_list )
688
+ if len (kwarg_list ) != len_data_row :
689
+ return False , "print_table kwarg list length problem (should match {} columns)" .format (len_data_row )
690
+
691
+ return True , "Inputs OK"
692
+
693
+
694
+ def print_table (self ,
695
+ table_data ,
696
+ columns = None ,
697
+ formats = None ,
698
+ title = '' ,
699
+ last_line_style = '' ,
700
+ ** kwargs ):
701
+ """Print provided data in a HTML table in output window.
702
+ The same window can output several tables, each with their own formatting options.
557
703
558
704
Args:
559
705
table_data (list[iterable[Any]]): 2D array of data
560
706
title (str): table title
561
707
columns (list[str]): list of column names
562
- formats (list[str]): column data formats
563
- last_line_style (str): css style of last row
708
+ formats (list[str]): column data formats using python string formatting
709
+ last_line_style (str): css style of last row of data (NB applies to all tables in this output)
710
+ column_head_align_styles (list[str]): list css align-text styles for header row
711
+ column_data_align_styles (list[str]): list css align-text styles for data rows
712
+ column_widths (list[str]): list of CSS widths in either px or %
713
+ column_vertical_border_style (str): CSS compact border definition
714
+ table_width_style (str): CSS to use for width for the whole table, in either px or %
715
+ repeat_head_as_foot (bool): Repeat the header row at the table foot (useful for long tables)
716
+ row_striping (bool): False to override the default white-grey row stripes and make all white)
717
+
564
718
565
719
Examples:
566
720
```python
@@ -573,56 +727,125 @@ def print_table(self, table_data, columns=None, formats=None,
573
727
title="Example Table",
574
728
columns=["Row Name", "Column 1", "Column 2", "Percentage"],
575
729
formats=['', '', '', '{}%'],
576
- last_line_style='color:red;'
730
+ last_line_style='color:red;',
731
+ col_head_align_styles = ["left", "left", "center", "right"],
732
+ col_data_align_styles = ["left", "left", "center", "right"],
733
+ column_widths = ["100px", "100px", "500px", "100px"],
734
+ column_vertical_border_style = "border:black solid 1px",
735
+ table_width_style='width:100%',
736
+ repeat_head_as_foot=True,
737
+ row_striping=False
738
+
577
739
)
740
+ Returns:
741
+ Directly prints:
742
+ print_md of the title (str):
743
+ print_html of the generated HTML table with formatting.
578
744
```
579
745
"""
580
- if not columns :
581
- columns = []
582
- if not formats :
583
- formats = []
584
746
585
- if last_line_style :
586
- self .add_style ('tr:last-child {{ {style} }}'
587
- .format (style = last_line_style ))
747
+ # Get user formatting options from kwargs
748
+ column_head_align_styles = kwargs .get ("column_head_align_styles" , None )
749
+ column_data_align_styles = kwargs .get ("column_data_align_styles" , None )
750
+ column_widths = kwargs .get ("column_widths" , None )
751
+ column_vertical_border_style = kwargs .get ("column_vertical_border_style" , None )
752
+ table_width_style = kwargs .get ("table_width_style" , None )
753
+ repeat_head_as_foot = kwargs .get ("repeat_head_as_foot" , False )
754
+ row_striping = kwargs .get ("row_striping" , True )
755
+
756
+
757
+ # Get a unique ID for each table from _table_counter
758
+ # This is used in HTML tags to define CSS classes for formatting per table
759
+ self ._table_counter += 1
760
+ table_uid = self ._table_counter
761
+
762
+ # Validate input arguments should be lists:
763
+ input_kwargs = [column_head_align_styles , column_data_align_styles , column_widths ]
764
+ inputs_ok , inputs_msg = self .table_check_input_lists (table_data ,
765
+ columns ,
766
+ formats ,
767
+ input_kwargs )
768
+
769
+ if not inputs_ok :
770
+ self .print_md ('### :warning: {} ' .format (inputs_msg ))
771
+ return
772
+
773
+
774
+ if not row_striping :
775
+ # Override default original pyRevit white-grey row striping. Makes all rows white.
776
+ self .add_style ('tr.data-row-{} {{ {style} }}' .format (table_uid , style = "background-color: #ffffff" ))
588
777
589
- adjust_base_col = '|'
590
- adjust_extra_col = ':---|'
591
- base_col = '|'
592
- extra_col = '{data}|'
593
-
594
- # find max column count
595
- max_col = max ([len (x ) for x in table_data ])
778
+ if last_line_style :
779
+ # Adds a CCS class to allow a last-row format per table (if several in the same output)
780
+ self .add_style ('tr.data-row-{}:last-child {{ {style} }}' .format (table_uid , style = last_line_style ))
781
+
782
+ if column_head_align_styles :
783
+ for i , s in enumerate (column_head_align_styles ):
784
+ self .add_style ('.head_title-{}-{} {{ text-align:{style} }}' .format (table_uid , i , style = s ))
785
+
786
+ if column_data_align_styles :
787
+ for i , s in enumerate (column_data_align_styles ):
788
+ self .add_style ('.data_cell-{}-{} {{ text-align:{style} }}' .format (table_uid , i , style = s ))
789
+
790
+ if table_width_style :
791
+ self .add_style ('.tab-{} {{ width:{} }}' .format (table_uid , table_width_style ))
792
+
793
+
794
+ # Open HTML table and its CSS class
795
+ html = "<table class='tab-{}'>" .format (table_uid )
796
+
797
+ # Build colgroup if user argument specifies column widths
798
+ if column_widths :
799
+ COL = "<col style='width: {}'>"
800
+ html += '<colgroup>'
801
+ for w in column_widths :
802
+ html += COL .format (w )
803
+ html += "</colgroup>"
804
+
805
+ if column_vertical_border_style :
806
+ border_style = "style='{}'" .format (column_vertical_border_style )
807
+ else :
808
+ border_style = ""
596
809
597
- header = ''
810
+ # Build header row (column titles) if requested
598
811
if columns :
599
- header = base_col
600
- for idx , col_name in zip_longest (range (max_col ), columns , fillvalue = '' ): #pylint: disable=W0612
601
- header += extra_col .format (data = col_name )
602
-
603
- header += '\n '
604
-
605
- justifier = adjust_base_col
606
- for idx in range (max_col ):
607
- justifier += adjust_extra_col
608
-
609
- justifier += '\n '
610
-
611
- rows = ''
612
- for entry in table_data :
613
- row = base_col
614
- for idx , attrib , attr_format \
615
- in zip_longest (range (max_col ), entry , formats , fillvalue = '' ):
616
- if attr_format :
617
- value = attr_format .format (attrib )
618
- else :
619
- value = attrib
620
- row += extra_col .format (data = value )
621
- rows += row + '\n '
622
-
623
- table = header + justifier + rows
624
- self .print_md ('### {title}' .format (title = title ))
625
- self .print_md (table )
812
+ html_head = self .table_html_header (columns , table_uid , border_style )
813
+ html += html_head
814
+ else :
815
+ html_head = ""
816
+ repeat_head_as_foot = False
817
+
818
+
819
+ # Build body rows from 2D python list table_data
820
+ html += "<tbody>"
821
+ for row in table_data :
822
+ # Build an HTML data row with CSS class for this table
823
+ html += "<tr class='data-row-{}'>" .format (table_uid )
824
+ for i , cell_data in enumerate (row ):
825
+
826
+ # Slight workaround to be backwards compatible with pyRevit original print_table
827
+ # pyRevit documentation gives the example: formats=['', '', '', '{}%'],
828
+ # Sometimes giving an empty string, sometimes a placeholder with string formatting
829
+ if formats : # If format options provided
830
+ format_placeholder = formats [i ] if formats [i ] else "{}"
831
+
832
+ cell_data = format_placeholder .format (cell_data ) # Insert data with or without formatting
833
+
834
+ html += "<td class='data_cell-{}-{}' {}>{}</td>" .format (table_uid , i , border_style , cell_data )
835
+ html += "</tr>"
836
+
837
+ # Re-insert header row at the end, if requested and if column titles provided
838
+ if repeat_head_as_foot :
839
+ html += html_head
840
+
841
+
842
+ html += "</tbody>"
843
+ html += "</table>" # Close table
844
+
845
+ if title :
846
+ self .print_md ('### {title}' .format (title = title ))
847
+ self .print_html (html )
848
+
626
849
627
850
def print_image (self , image_path ):
628
851
r"""Prints given image to the output.
0 commit comments